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]>
This commit is contained in:
2026-09-27 11:33:44 -05:00
co-authored by Claude Opus 5.5
parent 2333d837e2
commit fd72d26899
9 changed files with 1421 additions and 20 deletions
+285
View File
@@ -0,0 +1,285 @@
# 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
```sh
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
```
@@ -137,6 +137,10 @@ Changed:
`packages/ledger`, so A leaves a one-line hand-written pointer to the
weekly routine below the markers. E moves the routine into
`packages/ledger/README.md` and removes the pointer.
*Amended by lead decision 40 (2026-09-27):* row 7 stays in the queue
until Jason closes it, because its gate is his. E still writes the
weekly routine into the ledger README. A left no pointer below the
markers, so E has none to remove.
- `docs/TOOLS.md`: usage lines. This file carries other owners' uncommitted
changes; add a scoped patch the way #1511 did.
@@ -167,6 +171,8 @@ Changed:
that was row 7).
- One more Gitea call, `state=open`, one page; a full page fails as today
(Q7). Referenced issues absent from that list count as closed.
*Superseded by 8.10:* absent issues are looked up, and a full page is
handled as lead decision 40 rules.
- Reads `docs/plans/queue.json` through the queue validator by relative
import (`../../queue/src/queue.mjs`). There are no workspaces, so a bare
`@mosaic/queue` import would not resolve (R14).
@@ -371,6 +377,10 @@ items have no owner or issue.
*Decided:* row 7 moves to `packages/ledger/README.md` as the weekly
routine (sequenced across A and E; see 2.A). The parked items become rows
with state `parked`, owner `unassigned` and issue null.
*Amended by lead decision 40 (2026-09-27):* row 7 stays until Jason
closes it. Its gate is Jason's, and closing a Jason-gated row needs his
cited approval, so the lead can't retire it alone. E writes the weekly
routine into `packages/ledger/README.md` anyway.
## 5. Where the brief contradicts the code or itself
@@ -1741,7 +1751,7 @@ source is unchanged.
| Calls | Purpose |
|---|---|
| 1 | Metrics, unchanged. |
| 1 | Queue checks: `state=open`, limit 50. A full page makes the queue issue checks "incomplete". |
| 1 | Queue checks: `state=open`, limit 50. A full page makes the queue issue checks "incomplete" only when some issue in a row's `closes` is left without a known state (lead decision 40). |
| up to 10 | Queue checks: `GET issues/N` for issues in some row's `closes` that are neither in the open list nor shown closed in the metric page. Issues beyond 10 are "unknown (budget)", and the run is incomplete. |
That is at most 12 calls. `--no-issues` makes 0 calls and prints `queue
@@ -1784,8 +1794,9 @@ days (legacy lower bound)". Until then its age is undecidable.
`missing`, `invalid` or `pid-gone` owner, and any issue or age violation.
- `incomplete`: no known violation, but something couldn't be decided. That
covers:
- an issue check that was `unknown (budget)`, hit a full page, or was not
run;
- an issue check that was `unknown (budget)`, or was not run;
- a full open page while some issue in a row's `closes` has no known
state;
- a `pid-unknown` owner;
- an undecidable legacy age.
- `reduced pass`: nothing known and nothing undecided. The liveness evidence
@@ -2411,3 +2422,10 @@ The round-2 modifications:
with no reviewer approvals, so a Jason-gated row could close without its
reviewers. That move now carries the in-review→done checks on a comment
round; the table and 8.9's receipts paragraph say so.
- 2026-09-27: amended for lead decision 40 (Piece E questions). A full
`state=open` page makes the queue issue checks incomplete only when some
issue in a row's `closes` is left without a known state. An issue off
the page is looked up or left `unknown (budget)`, so truncation can't
hide a violation. 8.10's budget table and result list say so. Row 7
stays in the queue until Jason closes it; Q9 and 2.A say so. Section 2's
E text points to 8.10.
@@ -0,0 +1,168 @@
# Queue Piece E review, round 1 (#1508, row 13)
Filbert, 2026-09-27. Plan: `queue-as-data-plan-2026-09-26.md` §8.10,
amended for lead decision 40 (plan sha256 68a25ffe…, uncommitted).
## Verdict
**Changes requested, one item (C1).** The review covers the amended
round-1 candidate: `build.patch` sha256
02a01c291890626f58ba103d1c65e853b042b609887868bf59dea7f9b50b227d at
2333d837, with `build-manifest.sha256` cad51929… and `build.md` d3bdb826….
I had also reviewed the first version (patch 78445322…, manifest
8f0e4fea…) in full. The amendment changes only the full-page rule, its
tests and the README, so everything I checked on the first version still
holds.
C1 is a real bug in the age check, and it's small. Everything else is
ready, including the decision-40 change.
## C1. The age check never flags a row made required after genesis
**Where:** `packages/ledger/src/queue-checks.mjs`, line 200 in the amended
file:
```js
const days = ageDays(Date.parse(`${r.requiredSince}T00:00:00Z`), now);
if (days > AGE_LIMIT_DAYS) add(violations, 'age', ...
```
**What happens.** The code assumes `requiredSince` is a date. Genesis
rows carry dates (rows 9, 10, 11 and 13 have `2026-09-13`). But the queue
writes a full ISO time for any row made required later:
`set <id> required true` sets `requiredSince = entry.at`
(`packages/queue/src/queue.mjs:860`), and `add --required` does the same
(line 904). The validator accepts both forms (line 322, `checkTime(…,
{ date: true })`). For an ISO time the template gives
`2026-09-01T12:00:00.000ZT00:00:00Z`, `Date.parse` returns NaN, `days`
is NaN, and `NaN > 14` is false. The row is never an age violation, and
nothing is reported as undecided either.
**Reproduced** in-process at 2026-09-27T12:00Z on one required,
in-progress row:
- `requiredSince: "2026-09-01"` → violations `owner-invalid, age`, result
`fail`;
- `requiredSince: "2026-09-01T12:00:00.000Z"` → violations `owner-invalid`
only. The age violation is missing.
(`owner-invalid` comes from passing no seats; it doesn't matter here.)
**Why it matters.** Today's rows all have dates, so Monday's run is right.
The first row made required through the queue CLI would slip past the
14-day gate without a word. A check that goes quiet is the failure the
ledger's result levels exist to prevent.
**Fix.**
- A `DATE_RE` value parses as `T00:00:00Z`; anything else goes to
`Date.parse` as it stands.
- If the result is NaN, fail closed: an `age` undecided item naming the
row, or a violation. Never silence. (The validator should make this
unreachable, but the check shouldn't depend on that.)
- Tests: an ISO `requiredSince` 15 days old fails and one 14 days old
doesn't, next to the existing date cases. Your fixtures build
`requiredSince` with `since(n)`, so an ISO variant fits in the same test.
- Mutant: restore the old template. The new test should kill it.
- The README's age paragraph can say that `requiredSince` is a date for
genesis rows and an ISO time for later ones.
## The full page (lead decision 40)
I agree with the ruling, and the amendment implements it correctly. An
issue is never treated as closed because it's missing from the open list.
It's looked up or left `unknown (budget)`, and the unknown state already
makes the run incomplete. A server that caps pages below 50 would make
`full` meaningless anyway, so dropping it as a standalone signal costs
nothing.
What I checked on the delta:
- `open-list-full` is added only when the page is full and some issue in
`wanted` is `unknown`. `wanted` is the union of every row's `closes`,
including rows that aren't done. An unknown issue on a pending row can
only affect a disposition, but it still keeps the page undecided. That
errs toward `incomplete`, and it matches the decision's wording ("some
wanted issue"). Keep it.
- The new end-to-end test drives the fake helper through `issueStates` and
`queueChecks` for all three cases: resolved, open off the page, and past
the budget.
- The README's result list and call table match the code.
- My plan now says the same thing: 8.10's budget table and result list,
a pointer from section 2's E text, and Q9 and 2.A for row 7.
## What I checked
All of this ran in scratch clones with push disabled: `/tmp/fqe` at
f304eaa5 for the first version, and `/tmp/fqe2` at 2333d837 for the
amendment. The inputs were frozen as 0444 copies.
- **Manifest and suites.** The amended manifest checks 5/5. `node --test
packages/ledger/tests/` passes 76/76, and `packages/queue/tests` with
`packages/seat/tests` passes 161/161. The first version had passed
75/75 and 161/161.
- **Source.** I read `queue-checks.mjs` in full, and the diffs to
`cli.mjs`, the README and both test files. I compared the amendment with
the first version file by file. Only `queue-checks.mjs` (the rule and its
comment), the README (two passages) and `queue-checks.test.mjs` changed.
- **Local run** on the first version, for 2026-09-27 with `--no-issues
--no-t3` and three seats declared: 0 violations, result `incomplete`
(issues not run), 18 protected changes.
- **Mutations.** I wrote 21 mutants of my own against the first version
(E1–E21), apart from Darkwing's 37. The suite kills 19. The two
survivors are equivalent:
- E2 checks the metric page before the open list. Both are current
sources, so they can't disagree about an issue.
- E8 applies the age check to rows that aren't required. The validator
refuses `requiredSince` on such a row, so the mutant changes nothing.
I wrote five more against the amendment (F1–F5), and the suite kills
all five:
- F1: a full page is always undecided;
- F2: an unknown issue makes `open-list-full` without a full page;
- F3: any state other than open counts as unresolved;
- F4: only `unknown (budget)` counts, so a failed lookup doesn't;
- F5: it takes two unknown issues.
None of my 26 mutants touches the age parsing. The C1 bug is in an input
shape the fixtures don't use.
## Darkwing's choices
I agree with choices 1 to 11 in `build.md`. Two of them depart from the
plan's table, and in both cases the change fails closed:
- 4: a malformed pid is `invalid`, not `pid-unknown`, so it's a violation
rather than undecided;
- 5: a registration for another checkout is `missing`, because it matches
the seat name but not the root.
Choice 11 (protected changes listed, never checked) is what R4, R10 and J2
asked for.
## Non-blocking
- **n1. The call deadline kills the helper, not curl.** `execFileSync`
with a 60-second `timeout` sends SIGTERM to `gitea-api.sh` only. I
checked the behaviour with a script that runs a child `sleep`:
`execFileSync` throws `ETIMEDOUT` at the deadline, so the ledger's wall
time stays bounded, but the child keeps running as an orphan. For the
ledger that means a stuck curl can outlive the run, and the helper's
response file may be left in `/tmp`. It's a GET, so nothing is written
to Gitea. D wrapped its helper call in `timeout -s KILL` for this.
Doing the same here is a small change. It can go in this round or
later.
- **n2. `closedInMetric` could also check `state === 'closed'`.** It uses
`closed_at` alone. I haven't checked whether Gitea clears `closed_at`
when an issue is reopened. If it doesn't, a reopened issue that is off
the open page but on the metric page would count as closed. The open
page is always full now, so that case can happen. Checking `state` as
well settles it either way.
- **n3. The unknown-budget message doubles a word.** The test pins
`unknown (unknown (budget))`. It would read better as `unknown (budget)`
or `unknown (lookup budget)`. Cosmetic.
- **n4. `--unsupported-runtime` is self-declared.** The flag is a
statement from whoever runs the ledger, not evidence. It's printed on
every run, which is what 8.10 asks for. Nothing to change.
## For Darkwing and Sage
Fix C1 with its test and mutant, then send the patch and manifest again.
I'll review round 2 against C1 only, plus n1 if it's in. My plan
amendment (68a25ffe…) goes into E's commit with this review.
@@ -0,0 +1,101 @@
# Queue Piece E review, round 2 (#1508, row 13)
Filbert, 2026-09-27. Round 1: `queue-e-review-r1-2026-09-27.md`
(sha256 81f26f2e…). This round checks C1, n1, n2 and n3.
## Verdict
**Approved.** The review covers `build.patch` sha256
ab1f12cad711284f8a722ea51fa73cd8e344c703701f8b76957ae33de091ae84 at
2333d837, with `build-manifest.sha256` 0b20bbca… and `build.md`
75571f0b…. C1 is fixed, and so are n1, n2 and n3. Two of my mutants
survive. Neither hides a defect; see the notes below. Nothing needs a
round 3.
## What I checked
All of this ran in a scratch clone, `/tmp/fqe3`, at 2333d837 with push
disabled, on frozen 0444 copies of the three inputs. Darkwing's `r1/`
copies still match the hashes I reviewed in round 1.
- **Manifest and suites.** The manifest checks 5/5. `node --test
packages/ledger/tests/` passes 78/78, and `packages/queue/tests` with
`packages/seat/tests` passes 161/161.
- **What changed.** I compared all five files with the round-1 candidate.
`cli.mjs` and `ledger.test.mjs` are unchanged. `queue-checks.mjs`, the
README and `queue-checks.test.mjs` change only for C1, n1, n2 and n3.
- **C1.**
- `requiredDay` floors `Date.parse` to 00:00Z of its UTC day. A bare date
parses as 00:00Z, so the separate date branch I suggested would add
nothing. Dropping it is right.
- Counting an ISO time from its UTC day rather than its hour is a change
from my fix, and I agree with it. Both forms age in whole UTC days. A
row can be flagged up to a day early, never late.
- A value that doesn't parse is an `age-invalid` violation, so the run
fails. Any finding in `violations` counts toward the result, and no
other code matches on the check name, so the new name needs no other
wiring.
- The tests cover an ISO time at 15 days (fails), at 14 days (passes),
and at 23:59Z fifteen days back (fails, which needs the floor), and
month 13 (`age-invalid`, result fail).
- **n1.** Each call runs as `timeout -s KILL 60 gitea-api.sh GET …`. Without
`--foreground`, GNU timeout signals the whole process group, which kills
curl too. The new test starts a helper with a hanging child and a 1 s
deadline, then checks that both pids are gone. Adding `--foreground`
leaves the child alive, and the test catches it (R7).
- **n2.** Metric-page evidence needs `state === 'closed'` and a
`closed_at`. The metric call returns the raw Gitea records, filtered but
not mapped (`ledger.mjs` `readIssues`), so `state` is there in real runs.
The fixture covers a reopened entry (`closed_at` only) and one with
`state` only. Both are looked up.
- **n3.** The message reads `is unknown (over the lookup budget)`.
- **Mutations.** I wrote 13 mutants of my own for this round. The suite
kills 11:
- R1: no floor on `requiredDay`;
- R2: the NaN guard removed;
- R3: `age-invalid` counted as undecided;
- R4: metric evidence on `closed_at` alone;
- R5: metric evidence on `state` alone;
- R6: the default TERM signal in place of KILL (caught by the message);
- R7: `--foreground` (caught by the orphan check);
- R8: a kill recognised only by exit 137;
- R10: exit 127 no longer read as "unavailable";
- R11: `ceil` in place of `floor`;
- R12: a signal-killed call treated as success.
Two survive:
- **R9**, a kill recognised only by `r.signal === 'SIGKILL'`. Without
`--foreground`, GNU timeout sends KILL to its own group and dies with
it, so `spawnSync` sees the signal, not exit 137. The `status === 137`
branch is defensive and can't be reached in this setup. The mutant is
equivalent.
- **R13**, `if (r.error) throw r.error;` removed. With `timeout` missing
from PATH, the call still fails, but the message says "credential or
Gitea request failure" rather than "the timeout command is
unavailable". That's still a refusal (exit 2); only the wording is
wrong. See n1.
## Non-blocking
- **n1. The missing-`timeout` message has no test (R13).** A test could
point `PATH` at an empty directory for one call and give the helper by
absolute path. That's optional. The failure is closed either way.
- **n2. A day past the end of the month doesn't parse as invalid.** V8
turns `2026-02-30` and `2026-02-30T00:00:00.000Z` into 2026-03-02. It
returns NaN only for values like month 13. So `age-invalid` catches some
impossible dates but not all. A row whose date overflows ages from the
wrong day and gets no warning. This belongs with Darkwing's follow-up
(a): the queue validator checks shape, not calendar. A calendar check
there, such as a round trip through `toISOString`, closes both. The CLI
never writes such a value, and readQueue refuses hand edits, so it can't
happen today.
- **Darkwing's follow-ups.** I agree with both:
- (a), above;
- (b), the metric call's orphan curl in `ledger.mjs`, the same fix as
n1 in round 1.
Neither is E's scope.
## For Darkwing and Sage
E is approved as it stands. My plan amendment (68a25ffe…) and both review
files go into E's commit.