Review coverage steady state¶
Ryll is about to reach 100% human-reviewed under the code review
tracking system (docs/code-review-tracking.md). This plan covers
the steady state that follows: automatically pruning review marks
that go stale as PRs merge, and alerting when the review backlog
grows large enough that a review session is warranted.
Situation¶
The review tracking tooling (scripts/review-tracking.py) is
deliberately manual today: prune is run by hand after a pull as
part of the review session discipline. That was the right call for
the build-up phase, where every session started with a prune
anyway. In steady state it has two gaps:
- Between review sessions, nothing prunes.
REVIEWS.mdand the coverage number on ryll's main are only accurate as of the last session, and quietly overstate coverage as PRs merge. - Nothing tells Mikal that the backlog has grown. The whole point of steady state is to come back and re-review changed files, but there is no signal for when.
Decisions already made in discussion (2026-08-02):
- Threshold is absolute, not percentage: alert when 5 or more in-scope files need review. A percentage threshold at ryll's size (145 files) implied a ~14-file buffer, which is too large; "how much review work has piled up" is naturally an absolute number and survives repo-size changes and adoption by differently-sized repos.
- Alerting rides the existing consistency audit rather than a
new bespoke workflow:
scripts/audit-check.pyalready runs daily over a repo matrix including ryll, andscripts/audit-manage-issues.pyalready creates, updates, dedupes, and closes issues. - Coverage is computed against HEAD, not trusted from committed state: the audit check must count marks whose stamped blob SHA still matches HEAD, independently of whether prune has run. This decouples alerting from pruning — if the prune automation ever breaks, the coverage number cannot be silently inflated.
- Gating is structural: the check applies only to repos where
the review tooling is deployed, detected by the presence of
.vscode/review-scope.toml. Currently that is only ryll. Kerbside is the likely next adopter, but only after the ryll experiment stabilises — nothing in this plan should hardcode ryll. - Prune is triggered by pushes to the default branch, not a cron: staleness only ever appears when a commit lands there. The daily audit acts as a backstop signal even if a prune run is missed.
- Prune commits directly to main (as the bot, unsigned) rather
than raising a PR. Prune is fail-safe by construction — it can
only remove marks, never add or refresh them — and the
attestations live in the signed stamp commits already in
history, which a later unsigned commit cannot retroactively
forge. This matches the existing pattern of the consistency
audit's "Regenerate audit compliance tables." auto-commits. An
automated PR was considered and rejected: it leaves
REVIEWS.mdwrong while waiting for merge, and an open PR touching.vscode/*.weaudit*conflicts with any review session committed in the meantime.
Mission¶
When this plan is complete:
- A merge to ryll's default branch (develop) that changes
reviewed files results, within minutes, in an automated commit
pruning the stale marks and regenerating
REVIEWS.md. - The daily consistency audit computes ryll's effective review
backlog against HEAD, and opens (or updates) a
Consistency: Human review coverageissue on ryll when 5 or more in-scope files need review, closing it again when the backlog drops below 5. The issue body lists the files needing review, split into stale and never-reviewed, as a session work queue. - Repos without
.vscode/review-scope.tomlreport the check as not applicable and are otherwise unaffected. - The documentation (
docs/code-review-tracking.md, a newaudits/review-coverage.md) describes the steady state, including why an unsigned prune commit does not weaken the attestation story.
Design¶
Phase 1: a status subcommand for review-tracking.py¶
Add a read-only status subcommand to
scripts/review-tracking.py alongside stamp/prune/regen/
next. It computes, without mutating any state:
- the set of in-scope tracked files (existing
load_scope()/in_scope()/tracked_files()helpers); - for each, whether it carries a currently-valid full-file
review mark: marked in a
.weauditstate file, stamped in the sidecar, and the stamped SHA equalsblob_sha('HEAD:path'). This is exactly the staleness predicatecmd_prune()uses; - the files needing review, categorised:
- stale: marked reviewed, but the stamp is missing or no longer matches HEAD (a mark without a stamp cannot be verified, so it is conservatively treated as needing review);
- never reviewed: in scope with no full-file mark at all.
Partial (region) marks do not count as reviewed, consistent
with how
generate_reviews_md()counts coverage.
Output: human-readable summary by default; --json emits a
machine-readable object for the audit check, e.g.:
{
"in_scope": 145,
"reviewed": 140,
"needing_review": 5,
"stale": ["ryll/src/app.rs", "..."],
"never_reviewed": ["ryll/src/new_thing.rs"]
}
Notes:
statusdiffers from theREVIEWS.mdheader line ("N of M in scope files are currently reviewed"), which counts marks without checking them against HEAD and is therefore only accurate immediately after a prune. That difference is the point — see the design decision above. Do not change theREVIEWS.mdcomputation.- Files with a stamp but no longer tracked at HEAD (deleted) simply drop out of both the denominator and the needing-review list; prune cleans up their marks as it does today.
load_scope()importstomllib, sostatus(like the other subcommands) needs Python 3.11+. The static self-hosted runners are Debian 12 (Python 3.11), so this is already satisfied, but verify during implementation.
Tests go in scripts/test_review_tracking.py, following its
existing fixture patterns: valid mark counted, stale mark counted
as needing review, unstamped mark counted as needing review,
partial mark not counted as reviewed, scope filtering, JSON
output shape.
Phase 2: the review-coverage audit check¶
In scripts/audit-check.py:
- New
check_review_coverage(repo_path, props): - If
.vscode/review-scope.tomldoes not exist in the target repo:not_applicable, details "Human review tracking not deployed (no .vscode/review-scope.toml)". - Otherwise run
review-tracking.py status --jsonas a subprocess withcwd=repo_path, locating the script relative to__file__(the audit always runs from a development-repo checkout, so the script is a sibling). Do not go through the target repo'stools/review-tracking.shwrapper — the wrapper searches for a development clone the runner does not have in the expected places. failwhenneeding_review >= REVIEW_BACKLOG_THRESHOLD(a module-level constant, 5, with a comment recording that the value is a tuning knob agreed 2026-08-02).passotherwise. Details always include the counts, e.g. "3 of 145 in-scope files need review (threshold 5)".- On failure, include a
missingkey listing the files needing review, stale first, each prefixed with its category (e.g.stale: ryll/src/app.rs).build_issue_body()inaudit-manage-issues.pyalready rendersmissingas a bullet list, so the issue body becomes the session work queue with no changes to the issue machinery. - Register the check in
run_all_checks().
The depth-1 clone made by the audit workflow contains the full
tree and blobs at HEAD, which is all status reads — no workflow
change is needed.
In scripts/audit_common.py:
AUDIT_METADATA['review-coverage'] = {'spec': 'audits/review-coverage.md', 'template': None}.ISSUE_TITLES['review-coverage'] = 'Human review coverage'.
New audits/review-coverage.md, modelled on the existing single-
check specs (e.g. audits/delete-branch-on-merge.md):
- What we check: repos with review tracking deployed must have
fewer than 5 in-scope files needing review, computed against
HEAD via
review-tracking.py status. - How to become compliant: run a review session (link to
docs/code-review-tracking.md). - The
<!-- consistency-audit:begin/end -->marker block soaudit-update-docs.pymaintains the compliance table. Expected steady state: ryll compliant or non-compliant as backlog moves, every other repo N/A.
Also add the new audit to the criteria lists in
PROJECT-CONSISTENCY-AUDITS.md and PLAN-consistency.md,
matching how the other audits are described there.
Tests in scripts/test_audit_check.py, following its fixture
patterns: not_applicable without a scope config; pass/fail either
side of the threshold (fixture repo needs git init, a scope
config, weaudit state, sidecar, and committed files so blob SHAs
resolve); missing list contents and ordering.
Behavioural notes, to verify rather than assume during implementation:
audit-manage-issues.pyupdates an existing open issue rather than filing duplicates, and closes it when the check passes. This gives the desired open/update/close lifecycle with no code changes. Expect routine churn: a single feature PR can easily touch 5 in-scope files, so the issue will often open shortly after a merge and close after the next session. That is accepted behaviour (a standing work-queue nudge); if it proves noisy the threshold is one constant, and hysteresis (open at 5, close below some smaller number) is noted as future work.- The full needing-review list goes in the issue uncapped. At ryll's scale the worst case (~145 lines) is an acceptable issue body; revisit if a much larger repo adopts the tooling.
Phase 3: documentation (development repo)¶
docs/code-review-tracking.md:
- Rewrite the "run by hand -- deliberately not from git hooks" framing: manual invocation remains the story for stamp (and for prune during review sessions), but prune now also runs from CI on pushes to main in adopting repos. The objection to git hooks (state changing mid-operation in a developer's clone) does not apply to a CI run against the repo's own main.
- New "Steady state" section covering: the prune workflow (trigger, what it commits, loop safety); the coverage audit and its threshold; and the attestation argument — prune only removes marks, attestation lives in the signed commits that introduced the stamps, and verifying a mark means verifying the signed commit that introduced it, so an unsigned automation commit removing marks weakens nothing.
- Session-discipline note: automated prunes now land on origin/main, so pulling (and reloading the weAudit view) before marking is load-bearing, not just hygiene.
- Adoption section: deploying to a new repo (kerbside next, after ryll stabilises) now also means copying the prune workflow and its tools/ script.
Update AGENTS.md / ARCHITECTURE.md in the development repo if
their descriptions of the audit or review tooling enumerate
checks or subcommands.
Phase 4: the prune workflow (ryll repo, separate PR)¶
This phase lands in shakenfist/ryll, referencing this plan. Note
ryll's default branch (and the branch carrying review state) is
develop, not main -- discovered during implementation; the
workflow and script below target develop.
.github/workflows/prune-reviews.yml:on: push: branches: [develop]plusworkflow_dispatch.- Top-level
permissions: {}; job-levelpermissions: contents: write(workflow-standards audit). runs-on: [self-hosted, static].- A
concurrencygroup (e.g.prune-reviews) so overlapping merges serialise rather than race the push. - Steps: checkout ryll; clone shakenfist/development at depth 1
into a temp path and export
SHAKENFIST_DEVELOPMENT; runtools/ci-prune-reviews.sh. tools/ci-prune-reviews.sh(scripts over five lines do not live inline in workflow steps), mirroring the development repo'sscripts/commit-audit-docs.sh:./tools/review-tracking.sh prune- if
git diff --quiet -- .vscode/ REVIEWS.mdshows no changes, exit 0; -
otherwise commit as shakenfist-bot (
user.name 'shakenfist-bot',user.email 'bot@shakenfist.com') with message: -
git pull --rebase origin developthengit push origin develop(same landing pattern as commit-audit-docs.sh). - Loop safety: pushes made with the default
GITHUB_TOKENdo not trigger workflows; even if the trigger changed, a second prune run is a no-op and commits nothing. - Interaction with review sessions: a push of session commits (stamps) triggers a prune run that finds nothing stale and exits quietly. No conflict.
- Update ryll's
tools/review-tracking.shheader comment and ryll'sAGENTS.md(andARCHITECTURE.mdif it mentions the review tooling) to note that prune now also runs automatically on main.REVIEWS.mdneeds no change (generated). - Shellcheck the new script (
tools/run-shellcheck.sh/ pre-commit), and actionlint via pre-commit for the workflow.
Phase 5: end-to-end verification¶
After the development PR merges:
workflow_dispatchthe consistency audit; confirm ryll's row inaudits/review-coverage.mdregenerates, every other repo shows N/A, and no issue is filed while the backlog is under 5.
After the ryll PR merges:
- The merge itself should trigger the first prune run (the PR
touches in-scope files:
tools/,AGENTS.md), pruning their marks and committing. Verify the bot commit and regeneratedREVIEWS.mdlook right, and that no workflow loop occurs. - Simulate backlog: after enough merges (or by temporarily lowering the threshold in a test run), confirm the issue is created with the file list, and closes after a review session restores coverage.
- Confirm a normal review session still works end to end: pull (picking up bot prunes), review, stamp, signed commit, push.
Execution¶
One commit per logical change; the development-repo work is one PR on this branch, the ryll work a separate PR.
| Step | Repo | Description | Status |
|---|---|---|---|
| 1 | development | status subcommand + tests |
Done |
| 2 | development | check_review_coverage + registration + tests |
Done |
| 3 | development | audit_common.py metadata + audits/review-coverage.md |
Done |
| 4 | development | PROJECT-CONSISTENCY-AUDITS.md + PLAN-consistency.md entries |
Done |
| 5 | development | docs/code-review-tracking.md steady-state rewrite |
Done |
| 6 | ryll | prune workflow + tools/ci-prune-reviews.sh + docs |
Done |
| 7 | both | end-to-end verification (phase 5) | Blocked on merge |
pre-commit run --all-files must pass before each commit is
proposed; the Python follows the house style (single quotes,
120-column wrap).
Success criteria¶
review-tracking.py statusreports correct effective coverage on a fixture repo and on ryll itself, and mutates nothing.scripts/test_review_tracking.pyandscripts/test_audit_check.pypass, with new cases covering the scenarios listed above.- A daily audit run on the current matrix produces: ryll pass (or fail with a correct file list), all other repos N/A, and a correctly-managed issue lifecycle.
- A merge to ryll main is followed by exactly one bot prune commit when marks went stale, and none when they did not.
- The attestation verification story in
docs/code-review-tracking.mdremains true end to end.
Future work¶
- Adopt the tooling (and prune workflow) in kerbside once the ryll experiment stabilises.
- A consistency check that repos with
.vscode/review-scope.tomlalso carry the prune workflow, so the two halves of the steady state cannot drift apart as more repos adopt. - Hysteresis on the threshold (open at 5, close lower) if the issue churn from routine merges proves noisy.
- Revisit capping the issue-body file list if a repo much larger than ryll adopts the tooling.