Files
stack/agents/darkwing/work/queue-e/build.md
T
jason.woltjeandClaude Opus 5.5 fd72d26899 feat(ledger): Piece E, queue section in the weekly ledger (row 13, #1508)
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]>
2026-09-27 11:33:44 -05:00

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 under queue in --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 copies packages/queue/src and packages/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

  1. A new module. The plan's file list puts queueChecks in ledger.mjs. I put the section in queue-checks.mjs, as t3.mjs did for the T3 source, so ledger.mjs stays the metric code. The CLI wires it in.
  2. 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.
  3. Closure uses closes, not issues (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.
  4. A malformed or absent pid is invalid, not pid-unknown. The plan's table puts it under pid-unknown. validateRegistration rejects a pid that is not a positive integer or null, so the record fails validation first. invalid is a violation where pid-unknown is undecided, so the difference fails closed. pid-unknown is a valid record with a null pid.
  5. A registration for another checkout is missing. The plan defines missing as no registration for (canonical root, repo, seat). A repo record whose sessionsDir is not under the queue's canonical root matches the seat name but not the root.
  6. 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.
  7. Refusals. A missing, symlinked or hand-edited queue.json exits 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.
  8. On by default. --no-queue skips the section and prints Queue: not checked (--no-queue). It can't be combined with --unsupported-runtime. --unsupported-runtime is the only repeatable flag; a repeated seat is refused.
  9. 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.
  10. Exit 0 whenever a report was computed. The result is in the text and the JSON, like every other number the ledger prints.
  11. 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/--until that 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 own replay and applyEntry. 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 requiredSince never aged. set required and add --required write an ISO time, and round 1 appended T00:00:00Z to it, which parses to NaN. Now Date.parse reads 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 an age-invalid violation. 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_RE checks the shape, so 2026-13-01T00:00:00.000Z passes it. That gap is in packages/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's callTool does, so the kill takes the helper's process group. A kill reads no answer within 60 s, and exit 126 or 127 from timeout (no helper) reads gitea-api.sh unavailable. The metric call in ledger.mjs still 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 a closed_at. Either one alone leads to a lookup.
  • n3. The budget detail is over the lookup budget, so the message reads unknown (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:

  1. The round 1 template restored.
  2. No floor to the UTC day.
  3. No NaN guard.
  4. NaN as undecided.
  5. Ceiling instead of floor.
  6. The metric check on closed_at alone.
  7. The metric check on state alone.
  8. A Node timeout with SIGKILL in place of timeout, with ETIMEDOUT ignored. The orphan assertion kills it.
  9. The kill flag always false.
  10. The kill flag read from the exit status alone.
  11. No 126/127 mapping.
  12. 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) are pid-gone, from old pi launches; row 7 (sage) is missing. All three seats run in T3, which writes no registration.
  • With --unsupported-runtime for darkwing, dewey and sage: 0 violations, result reduced pass. Before the amendment the same run was incomplete, from the full page alone.
  • No issue violations: every issue in a done row's closes is 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:

  1. The full page: approved as amended above.
  2. Row 7's brief pins packages/ledger/README.md at blob 3a2ce27c. Sage re-pins it with set 7 brief in a queue commit right after E lands.
  3. Row 7 stays. Its gate is Jason's, so Q9 is amended. The weekly routine stays in the README.
  4. The age rule stays. From 2026-09-28 the Monday run lists rows 9, 10, 11 and 13, which is accurate.
  5. 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 no closed_at is 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 requiredSince by 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; --json key; a clean queue prints queue: 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