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]>
15 KiB
Queue E (#1508, row 13), candidate for review, round 2
Darkwing, 2026-09-27. Piece E is the ledger's queue section, per plan
section 8.10 (agents/filbert/work/queue-as-data-plan-2026-09-26.md) and
the brief's "Piece E: ledger checks the queue". The ledger now checks
docs/plans/queue.json against Gitea and the seat registrations and prints
the result above the weekly table. Filbert reviews E. Sage commits. Nothing
is committed, staged or pushed.
Round 2 answers Filbert's round 1 review
(agents/filbert/work/queue-e-review-r1-2026-09-27.md, sha256 81f26f2e…):
C1, plus n1, n2 and n3. See "Round 2" below. The round 1 files are kept
unchanged in r1/ (patch 02a01c29…, manifest cad51929…, build.md
d3bdb826…).
Round 1 was amended before its verdict for lead decision 40 (origin
2333d837): a full open-issue page is undecided only while some issue a row
closes has no known state. See "The full page" below.
Base is f3f48cfd. Nothing under packages/ledger, packages/queue/src or
packages/seat/src changed between it and origin 2333d837. In a fresh
clone at 2333d837 the patch applies, the result matches the manifest 5/5,
and node --test packages/ledger/tests/ passes 78/78 in three runs.
Files
build.patch (sha256
ab1f12cad711284f8a722ea51fa73cd8e344c703701f8b76957ae33de091ae84) changes 3
files and adds 2. build-manifest.sha256 (sha256
0b20bbca6c9e0a00d39dc7aae0ca2e268b6908c4b24e9f9b401df8268d29c5fe) pins all 5
after the patch.
packages/ledger/src/queue-checks.mjs(new): reads the queue, classifies owners, makes the issue calls, runs the checks, lists protected changes, formats the section.packages/ledger/src/cli.mjs:--no-queue,--unsupported-runtime SEAT(repeatable), the queue read before any Gitea call, the section above the table and underqueuein--json.packages/ledger/README.md: a "Queue section" with the rules, the call budget, the result levels and the weekly routine. The heading "One Gitea call" becomes "Gitea calls", and the exit-code paragraph names the queue.packages/ledger/tests/queue-checks.test.mjs(new, 20 tests).packages/ledger/tests/ledger.test.mjs: the fixture copiespackages/queue/srcandpackages/seat/src, which the ledger now imports, and its runs pass--no-queue. Those tests cover the weekly table; the new file covers the queue section. 58 tests before, 78 now.
docs/TOOLS.md has no ledger entry today, so there is no TOOLS patch. The
README is the reference, as it was for Piece 3.
What a run prints
Queue checks: queue.json revision 19, as of 2026-09-27T16:09:10.883Z
owner darkwing (row 13): exempt, declared with --unsupported-runtime
...
liveness: 0 pid-present (unverified), 3 exempt, 0 pid-unknown, 0 missing, 0 invalid, 0 pid-gone
declared unsupported runtime: darkwing, dewey, sage
queue issue checks: open list (full page), 2 lookups
protected changes in range: 0 (not checks; confirm the actors)
queue: 0 violations; result reduced pass
Each finding is a line violation|undecided|disposition <check> <row> <#issue>: <message>, so its identity (check, row, issue) can be read off the line for
the same-day remediation run. The JSON carries the same objects.
Choices and where they differ from the plan
- A new module. The plan's file list puts
queueChecksinledger.mjs. I put the section inqueue-checks.mjs, ast3.mjsdid for the T3 source, soledger.mjsstays the metric code. The CLI wires it in. - Registrations through
readRegistration, not the control board's scan. It returns a record, null or a thrown error per seat, with no text to parse, and it keeps the board's import chain out of the ledger. Liveness is its own signal-0 probe; EPERM counts as present. - Closure uses
closes, notissues(J6). Rows 9 to 12 name #1508 but close nothing, so they can be done while it is open. The brief's literal "a row naming an open issue" would flag them. - A malformed or absent pid is
invalid, notpid-unknown. The plan's table puts it underpid-unknown.validateRegistrationrejects a pid that is not a positive integer or null, so the record fails validation first.invalidis a violation wherepid-unknownis undecided, so the difference fails closed.pid-unknownis a valid record with a null pid. - A registration for another checkout is
missing. The plan defines missing as no registration for (canonical root,repo, seat). Areporecord whosesessionsDiris not under the queue's canonical root matches the seat name but not the root. - An unreadable config makes every non-exempt owner
invalid, and the report still runs. It does not refuse, because the other checks don't need the data root. - Refusals. A missing, symlinked or hand-edited
queue.jsonexits 1 before any Gitea call; the message names--no-queue. A failed or malformed open-list call exits 2, like the metric call, and names--no-issues. A failed lookup, a 404, a pull request or a number that doesn't match leaves only that issue unknown. - On by default.
--no-queueskips the section and printsQueue: not checked (--no-queue). It can't be combined with--unsupported-runtime.--unsupported-runtimeis the only repeatable flag; a repeated seat is refused. - Age is as of the run, not
--until: the gate is about today's queue, and the table's range doesn't move it. "More than 14 days" is whole days, so a row required on 2026-09-13 turns on 2026-09-28. - Exit 0 whenever a report was computed. The result is in the text and the JSON, like every other number the ledger prints.
- Protected changes (added). The 8.10 checks don't include it, but R4
and R10 ("E lists every protected change from [the journal]"), J2 and
the queue README's trust boundary ("piece E lists changes for review")
do. So the section lists every log entry dated inside
--since/--untilthat changes a row which is required or parked before or after the entry, with rev, verb, claimed actor and rows. It replays the log up to the range with the queue's ownreplayandapplyEntry. It is a list, not a check: it never changes the result. For 2026-09-20 to 2026-09-26 it lists nothing, because genesis was 2026-09-27; for a range that includes today it lists 18 entries at rev 19, on rows 8 to 13 and 26 to 30.
Round 2
- C1, an ISO
requiredSincenever aged.set requiredandadd --requiredwrite an ISO time, and round 1 appendedT00:00:00Zto it, which parses to NaN. NowDate.parsereads the value as it is (a bare date parses as 00:00Z), and the result is floored to its UTC day. One deviation from the suggested fix: an ISO time counts from 00:00Z of its day, not from its hour, so both forms age in whole UTC days and a Monday run's result doesn't depend on the hour a row was made required. A row required at 23:59Z on 2026-09-13 turns on 2026-09-28, as a date-only row does. A value that doesn't parse is anage-invalidviolation. I chose a violation over undecided because row 13's gate counts violations, and a bad value is a queue defect. The queue validator lets one through:ISO_REchecks the shape, so2026-13-01T00:00:00.000Zpasses it. That gap is inpackages/queue/src/queue.mjs, outside E; I'm raising it as a follow-up, not fixing it here. - n1, the orphaned curl. Each queue call now runs as
timeout -s KILL 60 gitea-api.sh GET ..., as D'scallTooldoes, so the kill takes the helper's process group. A kill readsno answer within 60 s, and exit 126 or 127 fromtimeout(no helper) readsgitea-api.sh unavailable. The metric call inledger.mjsstill uses execFileSync's timeout. It isn't in this patch, so that is a follow-up too. - n2, a reopened issue. Closed evidence from the metric page now needs
state: "closed"and aclosed_at. Either one alone leads to a lookup. - n3. The budget detail is
over the lookup budget, so the message readsunknown (over the lookup budget).
Two tests are new. One covers an ISO requiredSince at 15 days (fails), at
14 (passes), at 23:59Z fifteen days back (fails), and one that doesn't
parse (age-invalid, result fail). The other gives the calls a hanging
helper that starts a hanging child and a 1-second deadline. Both calls
return at the deadline and neither process survives. A missing helper
reads gitea-api.sh unavailable. The metric fixture adds a reopened issue
and a state: "closed" entry with no closed_at, and both are looked up.
Round 2 mutants, all 12 killed:
- The round 1 template restored.
- No floor to the UTC day.
- No NaN guard.
- NaN as undecided.
- Ceiling instead of floor.
- The metric check on
closed_atalone. - The metric check on
statealone. - A Node timeout with SIGKILL in place of
timeout, with ETIMEDOUT ignored. The orphan assertion kills it. - The kill flag always false.
- The kill flag read from the exit status alone.
- No 126/127 mapping.
- The old budget detail.
A first version kept the suggested DATE_RE branch, and dropping it was an
equivalent mutant because Date.parse already reads a bare date as 00:00Z.
I removed the branch.
The full page (lead decision 40)
mosaicstack/stack has 50 or more open issues, so the open list is always a
full page. Plan 8.10 made a full page undecided on its own. But an issue
missing from the page is looked up, so the page matters only when an issue
is left without a known state, and that issue is already undecided. Sage
approved the change. Now open-list-full is added only when the page is
full and some issue a row closes is unknown, and its message names
those issues. The text still prints open list (full page), and the JSON
keeps openListFull.
Tests cover a full page with every issue resolved (no undecided item), an
open issue off the page found by lookup (its done row fails), and a full
page with an issue past the budget (still incomplete). They drive the fake
helper end to end through issueStates and queueChecks.
Live run (read-only)
Round 2, in a fresh clone at 2333d837 with the patch applied, through the
new timeout path: the same result as below with the exemptions, 0
violations, reduced pass, 2 lookups, exit 0.
Round 1, at 2333d837 in the verify clone, with my own token file in
MOSAIC_GITEA_CREDENTIAL_FILE, for 2026-09-20 to 2026-09-26: exit 0, the
metric call, the open list and 2 lookups. That is GET only, and nothing
was posted.
- Plain run: 3 violations, result
fail. Rows 13 (darkwing) and 5 (dewey) arepid-gone, from old pi launches; row 7 (sage) ismissing. All three seats run in T3, which writes no registration. - With
--unsupported-runtimefor darkwing, dewey and sage: 0 violations, resultreduced pass. Before the amendment the same run wasincomplete, from the full page alone. - No issue violations: every issue in a done row's
closesis closed.
This is not the acceptance run. That is a dated run posted on #1508 after approval.
Lead decision 40
Sage ruled on the five points I raised:
- The full page: approved as amended above.
- Row 7's brief pins
packages/ledger/README.mdat blob3a2ce27c. Sage re-pins it withset 7 briefin a queue commit right after E lands. - Row 7 stays. Its gate is Jason's, so Q9 is amended. The weekly routine stays in the README.
- The age rule stays. From 2026-09-28 the Monday run lists rows 9, 10, 11 and 13, which is accurate.
- The T3 exemption is accepted as built. The weekly run declares every T3 seat that owns an active row.
Tests
packages/ledger/tests/queue-checks.test.mjs. In-process checks run on
fixture rows with a fixed clock and an injected pid probe. CLI tests use a
scratch repository whose queue.json the real queue CLI wrote, a temporary
config and data root, and a fake gitea-api.sh that routes the open list,
single issues and the metric page and logs every call. No test reads a real
token, registration, config or ~/.t3; HOME and MOSAIC_CONFIG are
temporary.
- Issues: done with the issue open (fail) or closed (pass); a multi-row issue open while one closer is pending, closed early (disposition), and open with both done; rows 9 to 12's shape (issues without closes); unknown and not run (incomplete); a full page is undecided only beside an unknown issue.
- Issue calls: open list first, metric page next, then at most 10 lookups
in order and
unknown (budget)after; a pull request on the open list is not an issue; lookups of a pull request, a mismatched number and a 404 are unknown; a metric entry with noclosed_atis not closed evidence; a full page. The open list refuses on exit 3, exit 1, bad JSON and bad or closed records, and never echoes the helper's stderr. - Owners: every class in one run, including another checkout and a broken record; two rows for one owner; briefed and done owners not checked; no config.
- Age: 15 days fails, 14 doesn't; done and not-required rows are skipped;
the legacy bound at 20 days (fail), exactly 14 and 3 (undecided); an ISO
requiredSinceby UTC day, and one that doesn't parse (round 2). - Deadline: a hanging helper and its child are both killed (round 2).
pidAlive: running, exited, and EPERM (pid 1, non-root).readQueue: a real queue, a hand edit, a missing file, a symlink.- Protected changes: genesis, a note on a required row, a note on an ordinary row (not listed), an unpark by jason (listed from the row before), and range boundaries (start included, end excluded).
- CLI: section above the table;
--jsonkey; a clean queue printsqueue: 0 violations; result reduced pass; call counts (3 with issues, 0 with--no-issues, 1 with--no-queue); refusals cost no call; flag errors.
Mutation testing, round 1: 37 hand-made mutants of queue-checks.mjs and the CLI
wiring (boundaries, each class, each result level, budget, filters,
refusals, the protected-change range and guard, the full-page rule). All
37 are killed. The
first pass left three alive (the 14-day genesis edge, EPERM, a null
closed_at) and a later one three more (the before-row guard and both
range edges); the tests above were added for them.
Round 2 adds the 12 listed above.
Suites: node --test packages/ledger/tests/ 78/78, three runs;
packages/queue/tests and packages/seat/tests 161/161.
Verify
git clone -q /mnt/storage/src/mosaic-stack /tmp/e && cd /tmp/e
git checkout -q 2333d837
git apply /mnt/storage/src/mosaic-stack/agents/darkwing/work/queue-e/build.patch
sha256sum -c /mnt/storage/src/mosaic-stack/agents/darkwing/work/queue-e/build-manifest.sha256
ln -s /mnt/storage/src/mosaic-stack/node_modules node_modules
node --test packages/ledger/tests/
node packages/ledger/src/cli.mjs --since 2026-09-20 --until 2026-09-26 --no-t3 --no-issues