# Ledger Read-only counts from local `refactor` commit subjects, one Gitea issue-list request through `scripts/gitea-api.sh`, and repo seats' Pi session logs. No board changes, data-root writes, fleet reads, transcript output, or scheduler. ```sh node packages/ledger/src/cli.mjs --since 2026-09-06 --until 2026-09-12 node packages/ledger/src/cli.mjs --since 2026-09-06 --until 2026-09-12 --json node packages/ledger/src/cli.mjs --since 2026-09-06 --no-issues node --test packages/ledger/tests/ ``` Dates include both endpoints in UTC. Omitted `--until` means today in UTC. The checkout containing this package supplies the sources, not the shell's cwd. No install, build, service restart, or configuration change is needed. ## Counting rules - Git uses committer timestamps and every commit reachable from local `refactor`, including merges. It reads subjects only. A repeated `#N` in one subject counts once. A commit naming two issues counts for both rows but once in total commits. Untagged commits do not count. Issue numbers are literal references to `mosaicstack/stack`, with no attempt to remap archived repositories' numbers. This can associate historical references with unrelated same-number issues. - Table 1 includes issues with a tagged commit or a `closed_at` in range. Opened is `created_at`; hours open is `closed_at - created_at`, rounded to one decimal, or `open` if not closed. It is not age as of `--until`. Reopen history is unavailable from the issue-list response. Median hours open uses only issues closed in range and rounds after computing the median. - Follow-ups are `max(commits in range - 1, 0)` per issue, not a lifetime count and not a quality assessment. Follow-ups per issue divides their sum by all Table 1 rows, including close-only rows. - Table 2 counts user-message entries in `.pi/state//sessions/*.jsonl` where `` is a real directory in `agents/`. All matching files count; duplicated entries in copied logs are not deduplicated. No transcript content leaves the parser. Assistant messages and logs outside repo seats do not count. Symlink source directories are refused and symlink files are not followed. - The first text line alone classifies a message. A bracketed addressing preamble whose source session is `control-board` is board; any other valid addressing preamble is agent; otherwise human. This is a format count, not proof of who typed the message. Text blocks are joined with newlines. The entry timestamp is used, falling back to the message timestamp. - Seats with no in-range user messages are omitted. Issue seats come from `#N` mentions anywhere in in-range user text, including quoted text. - Human messages per closed issue divides Table 2's human sum by issues closed in range. A zero denominator with human messages is `unknown`; a truly empty report has zero totals. JSON keeps numeric values as numbers; text displays ratios and durations with one decimal. Titles truncate to 48 characters in text only. Missing evidence is the literal string `unknown`. ## One Gitea call and missing evidence The client requests issues updated since the start date, all states, first page, limit 50. This includes issues closed in range, even if later updated. Gitea caps responses at 50; a full page fails rather than silently reporting partial totals. Use a narrower range or `--no-issues`, not hidden pagination. A commit-linked issue not returned by the updated-since query still has a row, with unknown metadata. This is the cost of the brief's one-call boundary. Exit 0 means a report was computed. Exit 1 means bad arguments or unreadable git or session evidence. Malformed JSONL, including a partially written last line, refuses the report; rerun after the seat finishes writing. Exit 2 means issue credentials, API, payload, or completeness failure. The CLI never prints API error bodies or reads authentication files itself. `--no-issues` makes no API call, keeps commit-derived rows, and shows unknown issue metadata, closed counts, median duration, and human-per-closed ratio. It cannot invent close-only rows. For fixtures, a fake `gitea-api.sh` can be placed first on PATH. Otherwise the repository scripts directory is appended to PATH for the issue request. Tests use only temporary repositories, logs, and fake API tools, with no real credentials or network. The helper regression stubs Node before any credential read and checks successful GET, successful POST, and failed HTTP status. ## Acceptance Gate D is Jason's: run the requested week, choose a number to move next week, and write the sentence and number into `docs/plans/CURRENT.md`. Automated tests and publication do not pass that gate. Remove the package to stop using it; there is no persistent ledger state to migrate or restore.