The ledger prints a queue section above the weekly table. It checks four things: - open issues named by done rows; - owner registrations for active rows; - closed issues for done rows; - the age of required rows. The result is fail, incomplete or reduced pass. It uses its own Gitea budget of the open list plus at most 10 lookups. A full open page counts only while an issue in some row's closes has no known state (lead decision 40). T3 seats are exempt per run with --unsupported-runtime. The weekly routine is in packages/ledger/README.md. Built by Darkwing (build.patch ab1f12ca, manifest 0b20bbca). Filbert reviewed it: round 1 81f26f2e asked for changes (C1, ISO requiredSince never aged); round 2 ce8ce150 approved. Also carries Filbert's plan amendment for decision 40 (68a25ffe). Co-Authored-By: Claude Opus 5.5 <[email protected]>
262 lines
15 KiB
Markdown
262 lines
15 KiB
Markdown
# Ledger
|
|
|
|
Read-only counts from local `refactor` commit subjects, one Gitea issue-list
|
|
request through `scripts/gitea-api.sh`, repo seats' Pi session logs, and T3's
|
|
thread messages in `~/.t3/userdata/state.sqlite`. Above the counts, a queue
|
|
section checks `docs/plans/queue.json` against Gitea and the seat
|
|
registrations (see "Queue section").
|
|
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 packages/ledger/src/cli.mjs --since 2026-09-06 --no-t3
|
|
node packages/ledger/src/cli.mjs --since 2026-09-06 --t3-db /tmp/fixture.sqlite
|
|
node packages/ledger/src/cli.mjs --since 2026-09-06 --no-queue
|
|
node packages/ledger/src/cli.mjs --since 2026-09-06 --unsupported-runtime dewey
|
|
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.
|
|
A line ends at `\n` only. A U+2028 or U+2029 inside a JSON string does not
|
|
split a record.
|
|
- Table 2 also counts T3 thread messages with role `user`. The T3 source
|
|
follows. A seat's row sums its Pi and T3 counts; the JSON keeps the split in
|
|
`pi` (Pi rows) and `t3.seats` (T3 rows).
|
|
- The first text line alone classifies a message. Two addressing forms count:
|
|
the tmux preamble `[host:session -> host:session]` that `agent-send.sh`
|
|
writes, and the T3 header `[from: role (thread-id) -> to: role (thread-id)]`
|
|
from `docs/guides/T3-AGENT-COMMS.md`. Either may carry ` class=<class>` before
|
|
the closing bracket. A preamble whose sender is `control-board` (tmux session
|
|
or T3 role) is board; any other valid preamble is agent; otherwise human.
|
|
The class may be in either case: seats send `class=DECISION`.
|
|
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`.
|
|
|
|
## T3 source
|
|
|
|
The rules come from `docs/plans/2026-09-26_ledger-t3-source.md` (Gate F).
|
|
The source is on by default. `--no-t3` skips it, and the report then says
|
|
`T3: not read (--no-t3)`. `--t3-db <path>` reads another database file with
|
|
the same checks. The JSON records the database path and whether it was the
|
|
default. When it wasn't, the text report prints the path, so a fixture result
|
|
can't pass for a live one. The two flags can't be combined.
|
|
|
|
The reader opens `state.sqlite` read-only through `node:sqlite` and reads no
|
|
other file in `~/.t3`. It runs every query in one read transaction with a 5 s
|
|
busy timeout. It never writes the main database file. Like any SQLite
|
|
connection it may create `-wal` and `-shm` beside it, so a directory that
|
|
isn't writable refuses when SQLite needs them.
|
|
|
|
- **Project.** Only threads in the one non-deleted T3 project whose
|
|
`workspace_root` equals this checkout's root byte for byte. The root is the
|
|
realpath of the package, so a project opened through the compatibility
|
|
symlink `~/src/mosaic-stack-dev-test` does not match, and the report refuses
|
|
with no project.
|
|
- **Thread to seat.** A thread belongs to seat `<s>` when `<s>` is a real
|
|
directory in `agents/` and the lower-cased title equals `<s>` or starts
|
|
with `<s>` and a space. "Dewey in Claude" maps to `dewey`; "Sagebrush" maps
|
|
to nothing. Several threads can map to one seat. Threads that map to no
|
|
seat share one row, `t3:unmapped`, so their human messages still reach the
|
|
totals. `t3.seats` and `t3.unmapped` in the JSON list the thread ids and
|
|
titles behind each row.
|
|
- **Titles are current state.** T3 titles an unnamed thread from its first
|
|
prompt, and a rename moves a thread's whole history to another row. This
|
|
moves counts between rows, never out of the totals.
|
|
- **Header check.** A user message whose T3 header is addressed to its own
|
|
thread id must name that thread's seat as the `to:` role (compared lower
|
|
case). In an unmapped thread the `to:` role must not be a seat. A conflict
|
|
exits 1 and names the thread, its title and both roles. A header addressed
|
|
to another thread isn't checked. The check misses a renamed thread that no
|
|
agent writes to. Such a thread can only add human counts to a row.
|
|
- **Excluded.** Imported threads (id prefix `import:`) are partial copies of
|
|
Claude Code sessions, not T3 traffic; every T3 event marked `historyImport`
|
|
sits in one today. Deleted threads don't count; archived threads do.
|
|
`t3.excluded` gives both thread counts.
|
|
- **Blind spot.** Threads in other T3 projects are not counted, even if they
|
|
worked on this repository. Live, there is a project at `/home/jwoltje` and
|
|
a deleted one at `/mnt/storage/src`.
|
|
- **Diagnostic.** `t3.diagnostic.humanSentThroughApi` counts in-range user
|
|
messages the header rule calls human that T3 recorded as sent through its
|
|
API (no `appVersion` in the event's origin). Those are seat messages whose
|
|
header the rule doesn't accept, such as the older free-text Discord Bot
|
|
headers, and would show the next format drift. `humanWithoutEvent` counts
|
|
human messages with no `thread.message-sent` event. This is T3's internal
|
|
metadata, so it feeds no table or total. If `orchestration_events` or a
|
|
column it needs is missing, or an event doesn't parse, both read `unknown`.
|
|
|
|
These refuse the report with exit 1, and the ones about the database name
|
|
`--no-t3`: a missing, unreadable or unopenable database (including a busy
|
|
lock past the timeout); a symlink at `~/.t3`, `~/.t3/userdata` or
|
|
`state.sqlite` (with `--t3-db`, the file or its directory); a missing table or
|
|
column the counts need; no project or more than one for this root; a message
|
|
in a counted thread with a role other than `user` or `assistant`, non-text
|
|
content, or a `created_at` that doesn't parse; a header conflict. A missing Pi
|
|
directory means no Pi seats ran here; a missing T3 database means the path or
|
|
T3 changed, so it refuses instead of counting zero. Error messages name ids
|
|
and paths, never message text.
|
|
|
|
## Queue section
|
|
|
|
The rules come from plan 8.10 in
|
|
`agents/filbert/work/queue-as-data-plan-2026-09-26.md` (Piece E, #1508). The
|
|
section is on by default and prints above the weekly table; `--json` puts it
|
|
under `queue`. `--no-queue` skips it and prints `Queue: not checked
|
|
(--no-queue)`. The ledger reads `docs/plans/queue.json` through the queue's own
|
|
validator before any Gitea call, so a missing, symlinked or hand-edited file
|
|
exits 1 and costs no call. It writes nothing, takes no queue lock and never
|
|
changes a row.
|
|
|
|
Four checks, each finding named by (check, row, issue):
|
|
|
|
- **Issues.** An issue is expected closed once every row whose `closes`
|
|
includes it is done (J6). Then an open issue is `issue-open` on each of
|
|
those rows. A row that names an issue in `issues` but not in `closes` is
|
|
never checked against it, so rows 9 to 12 can be done while #1508 is open.
|
|
A closed issue with a row that closes it still pending is printed as a
|
|
`disposition` for the lead. It is not a violation.
|
|
- **Owners.** Each owner of an `in-progress` or `in-review` row gets one
|
|
liveness class from its `repo` registration in `<dataRoot>/seats`:
|
|
- `exempt`: declared on this run with `--unsupported-runtime SEAT`.
|
|
- `missing`: no registration, or one written for another checkout.
|
|
- `invalid`: the registration fails validation, or the config that names
|
|
the data root can't be read. A malformed or absent pid fails validation,
|
|
so it lands here, not in `pid-unknown`.
|
|
- `pid-unknown`: a valid registration with a null pid.
|
|
- `pid-gone`: the recorded pid is not running.
|
|
- `pid-present`: the pid is running. A registration holds no process-start
|
|
identity and a reused pid looks the same, so this reads "pid present
|
|
(identity not verified)".
|
|
The coverage line counts them: `liveness: N pid-present (unverified), N
|
|
exempt, N pid-unknown, N missing, N invalid, N pid-gone`.
|
|
- **Age.** A required row that is not done and whose `requiredSince` is more
|
|
than 14 whole days before the run is listed by name. `requiredSince` is a
|
|
date at genesis and an ISO time once `set required` or `add --required`
|
|
writes it; both count from 00:00Z of their UTC day. A value that doesn't
|
|
parse as a date is an `age-invalid` violation. A legacy row with
|
|
`requiredSince: "unknown"` was required no later than genesis, so once
|
|
genesis is more than 14 days old it is listed as `age ≥ N days (legacy lower
|
|
bound)`. Before that its age is undecided. Age runs to the time of the run,
|
|
not to `--until`.
|
|
- **Protected changes.** Every log entry dated inside the report range that
|
|
changes a required or parked row is listed with its revision, verb, claimed
|
|
actor and rows. The queue trusts `--by` (its README, "Trust boundary"), so
|
|
this list is how a wrong claim gets seen. It is not a check and never
|
|
changes the result. Jason confirms the actors weekly.
|
|
|
|
Every run ends with `queue: N violations; result R`, where R is one of three:
|
|
|
|
- `fail`: any violation. That is an open issue, a `missing`, `invalid` or
|
|
`pid-gone` owner, or an age.
|
|
- `incomplete`: no violation, but something undecided. That is an issue
|
|
whose state is unknown, a full open-issue page with some issue left
|
|
unknown, issue checks not run, a
|
|
`pid-unknown` owner, or a legacy age before genesis is 14 days old.
|
|
- `reduced pass`: nothing known and nothing undecided. There is no full pass,
|
|
because no owner's process identity is ever verified.
|
|
|
|
The exit code stays 0 whenever a report was computed; the result is in the
|
|
text and the JSON.
|
|
|
|
### Queue issue calls
|
|
|
|
The metric call can't answer "is this issue closed", because an issue nobody
|
|
touched this week isn't in it. So the queue checks have their own budget, per
|
|
plan 8.10:
|
|
|
|
| Calls | Purpose |
|
|
|---|---|
|
|
| 1 | `state=open`, issues only, limit 50, one page. A full page may be short. Issues missing from it are looked up, so it makes the run `incomplete` only while some issue a row closes has no known state (lead decision 40). |
|
|
| up to 10 | `GET issues/N` for each issue in some row's `closes` that is neither on the open list nor closed on the metric page. Beyond 10, the issue's state is unknown, "over the lookup budget". |
|
|
|
|
With the metric call that is at most 12 calls. `--no-issues` makes none and
|
|
prints `queue issue checks: not run`. Closed needs positive evidence: an
|
|
entry on the metric page with state `closed` and a `closed_at`, or a lookup
|
|
returning a closed issue that isn't a pull request. A failed lookup, a 404, a
|
|
pull request or a mismatched number leaves the issue unknown. A failed or
|
|
malformed open-list call exits 2, like the metric call. Each queue call runs
|
|
under `timeout -s KILL 60`, which kills the helper and its curl together.
|
|
|
|
### Weekly routine
|
|
|
|
Run the ledger each Monday for the week that ended on Saturday, Sunday
|
|
through Saturday as row 7 counts it, with the queue section on and no
|
|
`--no-issues`:
|
|
|
|
```sh
|
|
node packages/ledger/src/cli.mjs --since 2026-09-20 --until 2026-09-26
|
|
```
|
|
|
|
Post the dated run on #1508 with its coverage line and result. Any seat
|
|
whose runtime writes no registration (T3 seats today) is passed with
|
|
`--unsupported-runtime SEAT`, which the output prints. A violation is
|
|
remediated when a second dated run on the same UTC day no longer reports its
|
|
(check, row, issue), and both runs are posted on #1508. A row edit alone is
|
|
not remediation, and a `note` can't clear an owner finding, because that
|
|
check reads registrations. Row 13's gate is one Monday run with zero
|
|
violations, or every violation remediated that day.
|
|
|
|
## Gitea calls and missing evidence
|
|
|
|
The metric 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, a queue.json
|
|
the validator refuses, or unreadable git, session or T3 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. The queue's open-list call
|
|
fails with exit 2 the same way. `--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, T3 databases, queue files, seat
|
|
registrations, configs and fake API tools, with no real credentials or network. Every CLI run in the tests sets `HOME` to
|
|
a temporary directory, so no test opens the real `~/.t3`. 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.
|