feat(queue): Piece D, reviews as issue comments, raw per-seat token helper (row 12, #1508)

queue move ID in-review posts the review request as a Gitea comment and
review record reads verdicts back, so reviews stop being files in
docs/plans/reviews/. On a comment round, in-review to waiting-on-jason
now needs every listed reviewer's approval for the current round, the
same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads
the raw per-seat token files (lead decisions 37 to 39): config built and
checked before curl starts, export attribute cleared, fixed base URL.
test-queue.sh skips its live checks outside the canonical root.

Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302)
and corrected the plan (293747cd). Rocko reviewed the helper (e896192f,
2096b0a3), and Sage's lead check passed under decision 38. Manifest
b402fb38, 19 files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 10:07:29 -05:00
co-authored by Claude Opus 5.5
parent cdcedb2741
commit f539466fcb
19 changed files with 2541 additions and 78 deletions
+86 -10
View File
@@ -33,6 +33,8 @@ scripts/test-queue.sh
| `sync [--op ID]` | yes | fsyncs and the witness; logs nothing |
| `snapshot --out DIR` | yes | copies of both files into an empty DIR outside the repository |
| `unlock [--check-gate]` | no; takes the unlock gate | removes a dead or mismatched lock |
| `review request`, `review resolve`, `review abandon`, `review record` | yes | one log entry, the witness, the table; a request also posts one comment and logs its outcome |
| `review verify-commit ID REF` | no | nothing; reads the queue and git objects |
Every change takes `--op ID` (8 to 72 characters of `[a-z0-9._-]`,
starting with a letter or digit, not ending in `.outcome`) and an actor from `--by NAME` or
@@ -52,12 +54,13 @@ or genesis from the map), it keeps the part still among the new issues, and
the receipt ends in `(kept narrowed)`.
Exit codes: 0 ok; 1 the operation failed; 2 invalid data or refused;
3 uncertain (visible or durable, not acknowledged); 4 usage.
3 uncertain (visible or durable, not acknowledged; for a review request,
not known to be posted); 4 usage.
Until piece D, `move ID in-review` needs `--candidate`: an existing file is
read as a manifest (one `<sha256> <path>` line per file), anything else as a
commit reachable from `refs/heads` or `refs/tags`. The candidate is frozen
for the round.
`move ID in-review` needs `--candidate`. An existing file is read as a
manifest (one `<sha256> <path>` line per file), anything else as a commit
reachable from `refs/heads` or `refs/tags`. The candidate is frozen for the
round.
The review's issue follows lead decision 23. A row with no issues can't
request review. A row with one issue uses it. A row with several needs
@@ -66,10 +69,72 @@ unless `--issue` names another; if the row no longer lists the kept issue,
the request refuses until `--issue` names one. Each round records the issue
it used (`review.rounds[].issue`).
`move ID done` from in-review needs `--evidence
comment=<id>,round=<n>,candidate=<digest>`. The round must be the current
one and the digest its candidate's, so a comment from an earlier round
can't close a later one, even when the candidate is the same.
A row with reviewers opens a request round and posts the review request
comment; see "Review requests". A row with no reviewers opens a round that
posts nothing. For that kind of round, `move ID done` from in-review needs
`--evidence comment=<id>,round=<n>,candidate=<digest>`. The round must be
the current one and the digest its candidate's, so a comment from an
earlier round can't close a later one, even when the candidate is the same.
## Review requests
Piece D, section 8.9 of the plan. `move ID in-review` from in-progress, on
a row with reviewers, logs the new round and a request attempt in the `requesting`
state, then posts one comment on the round's issue as the acting seat and
logs what the transport said as a second entry, `<op>.outcome`. `review
request ID` makes a new attempt in the current round after a failed or
abandoned one.
- **Credentials.** The request posts with the acting seat's own token file,
named by `MOSAIC_GITEA_CREDENTIAL_FILE`. It must end in
`/agents/<login>/secrets/gitea-mosaicstack-<login>.token`, be a regular
file (no symlink, and its real path ends the same way), belong to this
user and be mode 0600. The lead's login is `jarvis`. The shared
`~/secrets/mosaic.gitea.json` refuses. The queue checks the file with
`lstat` and never opens it; `scripts/gitea-api.sh` reads it. Before the
POST, `GET user` must name the seat's login.
- **Outcome.** HTTP 201 with a comment id is `posted` (exit 0). HTTP 400,
401, 403, 404 or 422, or a failed pre-send check, is `failed` (exit 1).
Anything else is `uncertain` (exit 3): another status, 201 without an id,
a transport error, or no answer before the 30-second deadline, which kills
the helper and curl with it. The log records the status and a fixed
detail, never the response body.
- **Retries never post.** Running the same `--op` again prints what the log
holds and sends nothing. A request left `requesting` (the process died
after the log entry) or `uncertain` needs a person to look on the issue
for a comment carrying `<!-- mosaic-queue-op: OP -->`. If it is there,
the owner or a privileged actor runs `review resolve ID OP --comment N`.
The queue fetches that comment and refuses unless it is on the round's
issue, written by the requester's login, and carries both markers for
this attempt, round and candidate. If it is not there, a privileged actor
runs `review abandon ID OP --reason TEXT --yes`. An abandon sets the
round's `duplicateRisk`, because the comment may still land.
- **Late outcomes.** An outcome that arrives after a resolve with the same
comment id keeps `posted`. One that disagrees, or a posted outcome after
an abandon, makes the attempt `conflict`, which a resolve settles. The
outcome entry is the one entry a done row still accepts, so a late answer
is recorded even after the row closed.
- **Unresolved requests** (`requesting`, `uncertain`, `conflict`) block a
new request, a new round, a move to `waiting-on-jason` and `done`.
- **Verdicts.** Each listed reviewer other than the owner posts a comment
on the issue, then runs `review record ID --verdict approve|changes
--comment N --candidate DIGEST`, once per round. DIGEST must be the
round's candidate. On a request round, `move ID done` and
`move ID waiting-on-jason` each need an approval recorded by every
reviewer in the current round, so a Jason-gated row reaches Jason only
after its reviewers approved. `done` also refuses `--evidence`. The queue
doesn't fetch the verdict comment; `record` is cooperative like every
other actor claim.
- **`review verify-commit ID REF`** checks a prospective commit or tree
against the current round's candidate. For a manifest, every listed path
must hash to its digest. For a commit candidate, every path the candidate
changed from its first parent must match in REF by blob and mode, and
every path it deleted must be absent.
Each log entry records the `semantics` it was written under. Entries from
semantics 1, before piece D, replay under the old rules: a move to
in-review opens a round that posts nothing. Review entries need
semantics 2.
## Where the files live
@@ -223,6 +288,12 @@ reporting, so a write in progress is never reported as lost history.
witness continuity. Canonical `verify`, under the lock, does that.
- **Commit candidates** stay retrievable only while some ref keeps the
commit. The queue keeps the manifest text, not the source bytes.
`review verify-commit` on a commit candidate needs that commit.
- **Post to outcome.** A kill after the POST and before the outcome entry
leaves the attempt `requesting` with a comment on the issue. The queue
never posts on its own again; `review resolve` records the comment.
- **Verdict comments** are not fetched. `review record` trusts the
reviewer's comment id the way the queue trusts `--by`.
- **Locks** are never removed for their age. `unknown` and `invalid` locks
wait for a person. A host rename makes old locks `unknown`.
- `queue unlock --check-gate` classifies a stale unlock gate. Remove the
@@ -264,7 +335,7 @@ then `queue render`.
`node --test packages/queue/tests/` runs everything in scratch repositories
under the system temp directory; nothing touches this checkout's `.git`.
Faults reach the code only through options the tests pass in (`io`, `proc`,
`hook`, `now`, `readOrder`, `lockWaitMs`); the CLI passes none.
`hook`, `now`, `readOrder`, `lockWaitMs`, `deadlineMs`); the CLI passes none.
- `data.test.mjs`: serialization, replay, the transition matrix, `next`
ordering, render.
@@ -280,3 +351,8 @@ Faults reach the code only through options the tests pass in (`io`, `proc`,
`map-check.mjs` reports each kind of drift.
- `commit.test.mjs`: `queue-commit.sh` and the guard, through PATH shims
that run an action at an exact point in the procedure.
- `review.test.mjs`: review requests against `fixtures/fake-gitea.mjs`,
installed as the scratch repository's `scripts/gitea-api.sh`, with dummy
token files. Every transport answer, the deadline, kills at each step,
a held lock at the outcome, late outcomes, resolve checks, credential
checks, verdicts across rounds, `verify-commit` and semantics 1 replay.