Files
stack/packages/ledger

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.

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/<seat>/sessions/*.jsonl where <seat> 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.