T3 messages with role reasoning and an id starting "reasoning:" are now skipped before any other check and counted as reasoningMessages. Any other unknown role, including reasoning with another id prefix, still refuses. Issues updated since the start date are read in pages of 50 until a short page, at most 10 pages. A full tenth page, a duplicate issue number across pages, a page over 50 rows or any failed page refuses with exit 2, so partial totals are never reported. The report prints how many pages it read. The README describes the skip, the page loop and its refusals. Candidate manifest 4eed1d6405a505e88cde5062c61f768639d899b8f301019b1403d68fb15d9d36 (Darkwing round 1, packetd6ea56831), BASEcc48b124f. Approved by Filbert (comment 28407). Sage gate rerun at644ef5310plus the candidate passed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
278 lines
16 KiB
Markdown
278 lines
16 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.
|
|
- **Reasoning summaries.** Since 2026-10-13, T3 stores reasoning summaries
|
|
as messages with role `reasoning` and an id starting `reasoning:summary:`.
|
|
A message in a counted thread with role `reasoning` and an id starting
|
|
`reasoning:` counts nowhere. The reader skips it before the text,
|
|
`created_at` and header checks, so its content can't refuse or change a
|
|
run. `t3.excluded` gives the imported and deleted thread counts and the
|
|
skipped reasoning messages, at any date.
|
|
- **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` (role
|
|
`reasoning` with an id that doesn't start `reasoning:` included), 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 pages can't answer "is this issue closed", because an issue nobody
|
|
touched this week isn't on them. 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 a metric page. Beyond 10, the issue's state is unknown, "over the lookup budget". |
|
|
|
|
With up to 10 metric pages that is at most 21 calls. `--no-issues` makes none and
|
|
prints `queue issue checks: not run`. Closed needs positive evidence: an
|
|
entry on a 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 a metric page. 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,
|
|
limit 50. This includes issues closed in range, even if later updated, so
|
|
`--until` can't narrow it. Gitea caps a page at 50. The client reads pages 1,
|
|
2, 3 and on, and stops at the first page with fewer than 50 issues. It reads
|
|
at most 10 pages, so a run makes up to 10 metric calls. A full page 10 exits 2
|
|
and says the 10-page ceiling was reached, rather than report partial totals;
|
|
narrow `--since` or use `--no-issues`. Every page gets the same record checks,
|
|
and a page with more than 50 issues refuses. An issue number seen on two
|
|
pages exits 2, because an update during the read can move an issue onto the
|
|
next page; rerun the report. A failed page refuses the whole read and names
|
|
the page, never its stderr or response body. The text report prints
|
|
`Issues: N pages read from Gitea` (`1 page` for one), and the JSON has
|
|
`issueSource: { read: true, pages: N }`, or
|
|
`{ read: false }` with `--no-issues`. A commit-linked issue not returned by the
|
|
updated-since query still has a row, with unknown metadata.
|
|
|
|
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.
|