Review import¶
Prompt¶
Before responding to questions or discussion points in this document, explore this repository thoroughly. Read the relevant files and ground your answers in what they actually say. Do not speculate about the repository when you could read it instead. Flag any uncertainty explicitly rather than guessing.
There is no application code here. The artifacts are the audit
specifications in docs/audits/, the tooling in scripts/ that
measures them, the templates in templates/ that the rest of the
fleet copies, and the workflows that run all of it every morning
against every Shaken Fist repository.
Consult AGENTS.md for the conventions and the invariants that
are not visible in the code, and ARCHITECTURE.md for the shape
of the system. docs/consistency-audits.md is the reference for
what a daily run does, how to add a criterion, how to bring a
repository into scope, and how to test a change before it reaches
the fleet -- read it before changing anything under scripts/ or
docs/audits/. docs/code-review-tracking.md covers the review
tooling, and PUSH-AUDIT.md is the pre-push review runbook that
every plan's final phase runs.
Two things make planning here different from planning in a repository that holds a product, and both should shape any plan written from this template:
- The blast radius is other people's repositories. The daily
workflow files and closes GitHub issues fleet-wide. A change
that is merely wrong does not produce a red build; it produces
issues in ten repositories, or silently closes ones that should
have stayed open. Always pass
--dry-runwhen runningaudit-manage-issues.pyby hand. - This repository is in its own audit matrix. A standard we exempt ourselves from is a standard we stop noticing the cost of. A change to a criterion is a change we are measured against the next morning, so a plan should say how many repositories -- including this one -- it newly fails.
Plan file conventions (shared block; do not edit -- the canonical
copy lives in shakenfist/development at
templates/shared-blocks/plan-file-conventions.md):
- All planning documents live in
docs/plans/. - Detailed planning gets one plan file per phase. Phase files are
named for their master plan, sit in the same directory as it,
and append
-phase-NN-descriptivebefore the.mdextension. - The master plan tracks its phases in a table under its Execution section:
| Phase | Plan | Status |
|---|---|---|
| 1. Schema migration | PLAN-thing-phase-01-schema.md | Not started |
| 2. Public API | PLAN-thing-phase-02-api.md | Not started |
- One commit per logical change, and at minimum one commit per phase. Unrelated changes are not batched into a single commit. Each commit is self-contained: it builds, passes tests, and has a message explaining what changed and why.
In this repository. Plans here keep their phases as sections
inside the master plan rather than as separate phase files, and
the Execution table's Plan column is dropped accordingly;
docs/plans/index.md says so. The shared convention above is the
fleet default, and a plan large enough to want phase files should
use them rather than argue with the block.
Situation¶
Whole-file human review (docs/code-review-tracking.md) is
deployed in five repositories: this one, actions, hunkydory,
kerbside and ryll. Much of what churns the review queue in the
other four is not their own code. It is files this repository
manages -- workflow templates, helper scripts, the exported GitHub
configuration -- which are written and reviewed here, copied out
by standards-alignment rollouts, and then have to be reviewed
again, file by file, in every repository that received them. A
template change touching five files costs five re-reviews here and
up to twenty more across the fleet, of content that is often
byte-for-byte what was already read here.
A review mark attests to a blob SHA, not a path, and two files
with the same blob SHA have the same bytes. So a review of a blob
here is equally a review of an identical blob anywhere else. The
tooling does not know that: prune in a target repository only
ever compares a target's own stamps against its own HEAD.
Measured on 2026-09-24 against local clones (one to five days
old), counting target files whose blob at HEAD matches a blob
this repository has stamped as fully reviewed at any point in the
sidecar's history:
| Repository | In-scope files needing review | Byte-identical to a reviewed blob here |
|---|---|---|
| actions | 14 | 0 |
| hunkydory | 2 | 5 |
| kerbside | 140 | 4 |
| ryll | 37 | 4 |
The matches are export-repo-config.yml (three repositories),
.github/exported-config/repository-settings.json (three),
tools/mermaid-lint.sh (two), and one each of
codeql-analysis.yml, pr-re-review.yml, pr-retest.yml,
mermaid-lint.yml and pin-indirect-dependencies.sh. Counting
only the stamps current at this repository's HEAD drops ryll
from 4 to 2 and kerbside from 4 to 3: a target copy that lags a
template this repository has since changed and re-reviewed is the
common case during a rollout, and only the sidecar's history still
records the review of the older blob.
Import on its own therefore buys a handful of files per repository today. The near misses are the larger opportunity. Most template-derived files have drifted from their template, and the drift is of four kinds, which want different treatment:
- Deliberate parameters.
templates/renovate/renovate.ymlcarries{{GITHUB_REPO_NAME}}inRENOVATE_AUTODISCOVER_FILTER, so every copy differs by one line by construction, although${{ github.repository }}would supply the same value at run time.mermaid-lint.ymlexpects a repository with its own exclusions to add a!path/**pair, and says so. - Copies lagging the template. kerbside's and ryll's
mermaid-lintfiles are blob-identical to older versions of the template;pr-re-review.ymlin three repositories predates the template's merge-ref hardening. (A local clone of ryll also showed a runner label lagging, and Renovate pins can drift the same way, but step 3a's fresh clones found neither.) - Local improvements never upstreamed. actions'
renovate.ymlhas aconcurrencyblock the template lacks; ryll'scodeql-analysis.ymlhas the review-statepaths-ignoreblock thatdocs/code-review-tracking.mdstep 8 asks every adopted repository to carry. - Genuinely per-repository files.
release.yml(155 to 529 differing lines) and the review automation workflows (240 and more) are adapted substantially per repository, and may be better not thought of as copies at all.
The review tracking CI machinery is itself in the second and
third categories without being a template: prune-reviews.yml,
tools/ci-prune-reviews.sh and tools/review-tracking.sh exist
in four divergent hand-made copies. actions' ci-prune-reviews.sh
has a three-attempt landing retry and a git status --porcelain
check the other three lack; the workflows differ in default branch
(main against develop), in whether they carry the
workflow_dispatch ref guard (kerbside does not), and in how
ryll's checkout authenticates (DEPENDENCIES_TOKEN, because its
ruleset requires a pull request and the Actions app cannot bypass
it).
Two constraints the design has to respect:
- The prune commit's attestation argument. The bot's
Prune stale review marks.commits are unsigned, anddocs/code-review-tracking.mdjustifies that on the grounds that prune can only remove marks. An import adds marks from an unsigned bot commit, so the argument has to be rebuilt rather than inherited: an imported mark must point at the signed commit in this repository that introduced the stamp, so that it is a pointer to an attestation rather than an attestation. - Amplification. Today an unsigned commit that forges a stamp
on this repository's
mainmarks files reviewed in this repository only. With import it marks them reviewed in five. The import is therefore the right place to check signatures rather than merely record where they are.
Mission and problem statement¶
Make a review done here count wherever the same bytes appear in another adopted repository, and make the same bytes appear more often.
When this plan is complete:
review-tracking.py import, run in an adopted repository, marks as reviewed every in-scope file whose blob atHEADthis repository has stamped under a full-file mark in a signed commit, recording in the target's sidecar where the attestation lives.REVIEWS.mdshows which reviews were imported and from where.- The review tracking CI machinery is a template in
templates/review-tracking/, copied byte-for-byte into every adopted repository, runningpruneand thenimporton every push to the default branch and daily. - The templates whose copies have drifted from them in the adopted repositories have been triaged, the drift parameterised away or upstreamed where that is possible, and the copies re-synchronised.
docs/code-review-tracking.mddescribes import and its attestation argument.
It deliberately does not cover shared blocks. They are spliced
into files such as AGENTS.md, so the enclosing file's blob never
matches anything reviewed here, and a partial-file import would
need region marks that survive edits elsewhere in the file, which
the tooling deliberately does not trust. Nor does it cover moving
templates to reusable workflows, or an audit criterion enforcing
template conformance; both are recorded as future work.
Open questions¶
Each question carries the default the plan takes if nobody answers.
- Where do imported marks live? Decided 2026-09-24: only
in
.vscode/imports.weaudit-shas.json, a sidecar-shaped file with no.weauditfile beside it, and never in a human reviewer's state file. The boundary between what a human did in this repository and what an automated process brought in is then explicit, and a reviewer working in VSCode is never shown a tick for a file they have not read here.
A .weaudit file was the first choice and step 1a ruled it out.
weAudit loads every .vscode/*.weaudit file and decorates a
file as audited from its path alone, whatever file or author the
entry came from (src/codeMarker.ts:626-628 in
trailofbits/vscode-weaudit), and hiding another file's marks is
a per-session toggle that is not persisted and only works when
the entries' author equals the file's basename. Worse, weAudit
writes an auditedFiles change to <author>.weaudit
(codeMarker.ts:559-586, :931), so an imported entry authored
by the original reviewer would, once un-ticked, be written into
that reviewer's own state file. weAudit only globs *.weaudit,
so it never reads the sidecar-shaped file.
The name also matches the .gitignore exception and the
paths-ignore entry (.vscode/*.weaudit-shas.json) that step 8
of the adoption procedure already put in every adopted
repository, so an import commit neither needs a new exception
nor triggers the expensive CI lanes.
2. What triggers an import after a review lands here? Default:
add a daily schedule trigger to prune-reviews.yml. A rollout
usually reaches the targets before the template is re-reviewed
here, and a push-only trigger would then never look again. The
alternative -- this repository dispatching prune-reviews in
every adopted repository when its own review state changes --
imports within minutes but needs a token with actions: write
across the fleet.
3. Should the source be restricted to templates/? Default:
no. Any path fully reviewed here is eligible, because identical
bytes need no context the file does not carry, and whole-file
review never attested to context anyway: a native mark does not
go stale when a script the file calls changes either. The
matches above show why the restriction would cost something:
ryll's tools/mermaid-lint.sh matches this repository's
tools/mermaid-lint.sh, not the template.
4. Should this repository import from itself? Default: no.
Identical copies within this repository (a template and this
repository's own instance of it) could cross-import, but the
case is rare, and running import against its own clone is a
no-op worth making explicit rather than an edge worth
supporting.
5. Is gitsign available on the static runners? Answered by
step 2a, 2026-09-24: no. The static runners are provisioned by
mach33labs/33fl/static_runner.yml with a minimal base package
set. The workflow downloads a pinned sigstore/gitsign release
binary and checks it against the release's checksums.txt, the
same way other workflows fetch pinned release tarballs; baking
it into the runner image can follow once the workflow has proven
out. Verification also needs egress to Rekor
(rekor.sigstore.dev) and Sigstore's TUF root, which nothing in
the fleet yet proves the runners have; the first scheduled run
in phase 2 is the test. If verification cannot run there, import
records provenance without verifying it and says so on stderr on
every run. Settled 2026-09-30: the first runs after the
rollout imported 16 entries across the four repositories (actions
3, hunkydory 6, kerbside 4, ryll 3), every one verified: true,
so the static runners reach Rekor and the TUF root.
6. The checkout token. Answered by step 2a, 2026-09-24:
DEPENDENCIES_TOKEN is a per-repository secret, present in
exactly the three adopted repositories whose develop ruleset
requires a pull request (hunkydory, kerbside, ryll) and absent
from the two whose main rulesets do not (actions, this
repository). The template's checkout uses
${{ secrets.DEPENDENCIES_TOKEN || github.token }}, which
reproduces today's behaviour in all five from one file.
Execution¶
| Phase | Status | Merged |
|---|---|---|
1. import subcommand |
Complete | 54c7ecc (#177) |
| 2. Review tracking CI template and rollout | Complete | 02d6dbb (#186); actions 74a33c9 (#111); hunkydory f24d413 (#57); kerbside 62ccbcb (#504); ryll ce3ebb3 (#418) |
| 3. Template convergence | Complete | b520c22 (#203); actions 89717fa (#122), e05bbb1 (#134); hunkydory 8730d75 (#63), c22059e (#66); kerbside 700102e (#511), ede2d7d (#519); ryll 79721f3 (#425), f8a4e2f (#455) |
| 4. Verification | Complete | bdc52ab (#249) |
| 5. Push audit | Complete | 99158da (#251), 728f0ae (#254); actions e3e6155 (#157), 68dc806 (#160); hunkydory e5a9b0e (#74), e8c62e5 (#77); kerbside c608d2c (#563); ryll 60990bb (#530) |
Plan status vocabulary (shared block; do not edit -- the canonical
copy lives in shakenfist/development at
templates/shared-blocks/plan-status-vocabulary.md):
A status cell -- in the master plan's own Execution phase table, and
in the row docs/plans/index.md carries for the plan -- holds
exactly one of these terms and nothing else:
Proposed-- written down as a concept, not yet scheduled.Not started-- scheduled, but no work has begun.In progress-- work has begun and has not finished.Blocked-- cannot proceed until something outside the plan changes. Say what, in the plan.Complete-- the work is done.Abandoned-- deliberately dropped without being done.Superseded-- replaced by another plan, which the plan names.
The term is the whole cell. No dates, no phase arithmetic, no parenthetical qualifiers, no summary of what happened: a status is read to decide whether a plan still wants attention, and prose in that column has repeatedly grown until it could no longer be read either by a person scanning the table or by tooling. Detail belongs in the plan file, and a one-line summary belongs in the index's own Intent column.
Matching is case-insensitive, so In Progress is accepted, but the
spelling above is the one to write.
In this repository. The same term is written twice: once in
this plan's own phase table, and once in the row the plan carries
in docs/plans/index.md. The index row is the whole-plan status,
so it only reaches Complete once every phase has been
completed, abandoned or superseded. The plan-index criterion
reads that table, and this repository is inside its own audit
matrix, so a status that drifts out of the vocabulary fails our
own tooling before it fails anybody else's.
Push audit phase (shared block; do not edit -- the canonical
copy lives in shakenfist/development at
templates/shared-blocks/plan-push-audit-phase.md):
- Every master plan ends with a phase that runs the repository's
PUSH-AUDIT.mdover the whole plan's work. It is the last row of the Execution table and it is not optional. The rule binds every plan that carries the phase, which is decidable from the plan file alone: a plan that is alreadyComplete,AbandonedorSupersededand does not carry the phase is not reopened to acquire one, and a plan that has the phase runs it even if it reachesCompletebefore the phase does. - That phase audits the accumulated diff of every phase in the plan against the default branch, not the diff of the last phase alone. Auditing one phase at a time would miss what the phases did to each other -- the duplicated helper that only exists once phases three and six have both landed, the doc page that phase two made wrong and phase five never revisited.
- Once the plan's phases have merged, a diff against the default
branch is empty and would read as a clean audit. The range is not
reliably derivable after the fact either: unrelated work lands on
the default branch between phases, so anything anchored on "since
the plan file appeared" is far too wide. It has to be recorded. As
each phase lands, what put it on the default branch goes into the
plan: the merge commit of its pull request, whose diff against its
first parent is the whole of what landed, or -- where the phase
landed directly -- every commit of the phase, or its
first..lastrange. A single commit is only ever enough when it is a merge commit. - Where the Execution phases are a table, that record is a
Mergedcolumn, added last so that a row which omits it still reachesStatus; where they are prose sections it is aMerged:line in the phase's own section. TheStatuscolumn keeps its single vocabulary term and nothing else (seeplan-status-vocabulary). A phase that landed in another repository records<repo> <sha> (#pr)and is audited against that repository's default branch, as part of the pull request that lands it; the plan's own push-audit phase cites that audit rather than re-running it. - Phases that landed before the plan started recording them are
reconstructed rather than left blank. Recover what you can from
gh pr list --state mergedandgit rev-list --first-parent, and say in the plan that the range was reconstructed. Do not trust a path-filteredgit logon its own: it lists the commits that touched a path without saying which arrived directly and which arrived inside a pull request, and recording a commit that came in under a merge audits one commit of that pull request rather than the pull request. A reconstructed record may be a summary table in the audit phase's own section rather than a column or a line in the Execution table, which keeps retrospective archaeology out of a table that tracks live status. Where a phase accreted over months of unrelated commits and no range is recoverable, say that instead and name the paths the audit read -- an audit that says what it could not scope is a result; one that silently audits nothing is not. - Findings land as their own pull request against the default branch, and the plan is not complete until they are resolved or explicitly declined in writing. A finding that is declined says why, in the plan, where the next reader will find it.
- Where the audit finds nothing, record that in the plan in one sentence. It is a real result, and a run of them is the evidence for making the phase conditional rather than mandatory.
- A repository with no
PUSH-AUDIT.mdstill carries the phase, and the phase says that the runbook does not exist yet and what was done instead. Silently omitting it is what let the audit go untriggered for as long as it did.
In this repository. PUSH-AUDIT.md exists at the repository
root and is referenced from AGENTS.md, so the final phase runs
it rather than explaining its absence. Note that every diff
command in it is written against main...HEAD: a stale local
main silently widens the audit to unrelated history, so fetch
before starting, or read it as origin/main...HEAD.
Phases 2 and 3 land partly in actions, hunkydory, kerbside and ryll. Each of those pull requests was to be audited against its own repository's default branch as it landed; phase 5 found none had been, and audited those repositories itself (phase 5, decision 2).
Phase landing (shared block; do not edit -- the canonical copy
lives in shakenfist/development at
templates/shared-blocks/plan-phase-landing.md):
A plan's status and a repository's review state both live in files that every branch would otherwise rewrite. Left alone, that turns each of them into a merge-conflict hot spot, and it spends a pull request and a full CI run on a change that is entirely prose. Three rules keep them out of the way.
-
A phase is closed out in the first commit of the next phase, not in a pull request of its own. By the time the next phase branches, the previous one has merged, so its merge commit is known and its
Mergedcell can record the thing the push-audit phase actually needs. This is the only ordering that works: a phase cannot record its own merge commit, and a separate close-out pull request buys that record at the price of a round trip. The close-out sets the finished phase'sStatusandMergedcells and the plan's row indocs/plans/index.md, and it is committed before the next phase's own work, so that the branch never claims the plan is further along than the default branch is. -
The last phase closes itself out. The push-audit phase is the last row of every plan, so no next phase will carry its close-out. Where the audit raises findings, the plan is not complete until they are resolved or declined, and those land as their own pull request after the audit phase has merged -- so that pull request is the carrier, and it can record the audit phase's merge commit, which by then is known. Where the audit finds nothing there is no carrier, and no follow-up pull request is opened for the sake of one cell: the phase sets its own
Status, and the plan's index row, toCompletein its own pull request, and records noMergedcell. It is the only row permitted to omit one. The column exists so that the push-audit phase can reconstruct what to audit; the audit phase is last, so nothing ever reads its own row. -
REVIEWS.mdis not pruned or regenerated in a pull request that changes code or documentation. Editing a reviewed file stales its mark, and adding or removing an in-scope file moves the header count, but neither is the landing pull request's business.pruneregenerates the file whether or not it dropped anything, so theprune-reviewsworkflow heals both on the next push to the default branch. Pruning from a branch is also wrong more often than it is right, though not for the reason it first appears:prunecompares each stamp againstHEAD, which on a branch is the branch tip, so it drops the marks for the files the pull request itself touched while keeping marks the default branch has already pruned. Committing that state merges a review file computed from a stale tree, and can resurrect marksprune-reviewshas already removed. Accumulated staleness is reported by thereview-coverageaudit, which recomputes coverage againstHEADand raises an issue once the backlog is worth a review session.
A review session is the exception, and it is not optional
tidiness: stamp regenerates REVIEWS.md as well as writing the
marks, and the rows, the sidecars and the marks are committed
together (see docs/code-review-tracking.md). Where a repository
requires a pull request to reach its default branch, that is how
a review session lands, so "not in a pull request" is about the
kind of change, not the mechanism.
These rules assume phases land one after another. Where two phase branches are open at once, each closes out only the phase it directly follows.
In this repository. The plan index is docs/plans/index.md.
review-tracking-tests deliberately does not assert the REVIEWS.md
header count, so a phase that adds or removes an in-scope file needs
no regeneration commit; prune-reviews corrects the count on the
next push to main.
One exception matters to this plan. Phase 1 changes how
REVIEWS.md is rendered, and review-tracking-tests asserts that
every row of this repository's REVIEWS.md is reproducible from
the committed state. Phase 1's pull request therefore has to
regenerate this repository's REVIEWS.md in the same commit as
the rendering change, or its own CI fails. That is a rendering
change, not a prune, and does not break the rule above.
Phase 1: import subcommand¶
Planning effort: high. This phase decides what an imported mark means and how it is verified.
Add an import subcommand to scripts/review-tracking.py, kept
separate from prune so that prune stays remove-only -- the
property the existing attestation argument rests on -- and the one
subcommand that adds marks without a human can be read, tested
and switched off on its own.
Source. The source is the clone of this repository the script
itself lives in (the parent of scripts/), which is where the
target's wrapper already found it. import refuses to run when
the target's top level is that same clone (open question 4).
What counts as a reviewed blob. Walk the history of every
.vscode/*.weaudit-shas.json in the source, oldest first. At each
commit, a sidecar entry path -> sha counts only if the same
commit's .vscode/<reviewer>.weaudit has a full-file
auditedFiles entry for path that is not a derived directory
entry. The sidecar alone is not enough: stamp also stamps files
that carry only partial (region) marks, and a partial review must
never be imported as a full one. The first commit at which a
(reviewer, sha) pair qualifies is the commit that introduced the
attestation; record it. Build a map from blob SHA to
(reviewer, source path, introducing commit, stamp date). Where a
blob was reviewed more than once, prefer the earliest.
Verification. For each introducing commit that is about to be
used, verify its signature with gitsign against the identity and
issuer in docs/code-review-tracking.md ("Commit signing"). A
commit that fails verification is skipped with a warning naming
it. --no-verify disables this, and when verification is disabled
or gitsign is not installed, every run says so on stderr. Record
the verification outcome per import in the run's output.
What gets imported. For each in-scope tracked file in the
target (existing load_scope(), in_scope(), tracked_files())
whose HEAD blob is in the map, which does not already carry a
valid full-file mark in a reviewer's state file, and which no
import-exclude pattern matches (below), add an entry to
.vscode/imports.weaudit-shas.json (open question 1):
{
"version": 1,
"files": {
"<target path>": {
"sha": "<blob sha>",
"date": "<source stamp date>",
"imported": {
"repo": "shakenfist/development",
"reviewer": "<reviewer>",
"path": "<source path>",
"commit": "<introducing commit>",
"verified": true
}
}
}
}
import writes nothing else: no .weaudit file, and never a
human reviewer's state file or sidecar. The date is the source
stamp's date, not today, because it records when the content was
read. verified records whether the introducing commit's
signature was checked.
A native review supersedes an import: when a reviewer's state file
carries a valid full-file mark for a file the imports file also
lists, import removes its own entry, so REVIEWS.md shows one
row per file and the human's is the one kept.
A file carrying a stale native mark is not a candidate until
prune has removed that mark, which is why the CI script runs
prune first. A file carrying only a partial native mark can be
imported; the partial row stays as well.
Rejecting an import. An import has no tick in weAudit, so
there is nothing there to un-tick, and deleting the entry by hand
would only see it re-imported on the next scheduled run. So
.vscode/review-scope.toml gains an optional import-exclude
list of fnmatch patterns with the same semantics as exclude
(including ! re-includes). import never adds a matching file
and removes any existing imported entry that matches. Like the
other exclusions in that file, an entry should say why in a
comment.
Interaction with the other subcommands. The imports file is a
source of marks for every read path, and is a separate code path
from state_files(), which keeps globbing *.weaudit only.
prunedrops an imported entry whose sha no longer matchesHEAD, exactly as it does a native stamp; the blob SHA is what the review attests to. It removes the imports file when it empties.stampignores the imports file entirely. Nothing in it was marked in this clone.statuscounts an imported entry whose sha matchesHEADas a valid review. It does not verify provenance: native marks are not signature-checked bystatuseither, and the audit workflow's depth-1 checkout of this repository has no history to check against.status --jsongains animportedcount so the audit can report it.nexttreats imported files as reviewed and does not offer them.render_reviews_md()counts imported files toward the header's reviewed count and gives them rows in the reviewed-files table, with a newSourcecolumn:-for a native review,development@<12-char commit>for an imported one, and theReviewercell from the entry'simported.reviewer. Regenerate this repository'sREVIEWS.mdin the same commit (see the landing note above).
Output and exit status. Print one line per import naming the
target path, the source path and the short commit, and a summary
count. Exit zero whether or not anything was imported; the CI
script decides whether to commit by looking at the tree, as it
does for prune.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 1a | medium | sonnet | none | Before any code, read the weAudit extension source (trailofbits.weaudit, on GitHub) and report only: whether it shows audited-file ticks from a .vscode/*.weaudit file that is not the current user's (here .vscode/imports.weaudit), whether that can be turned off in the UI or settings, whether it ever writes to a state file other than the current user's, and which top-level keys a state file needs for weAudit to load it without error. Settles the part of open question 1 that is still open. |
| 1b | high | opus | worktree | Implement import in scripts/review-tracking.py exactly as the phase 1 section of docs/plans/PLAN-review-import.md describes: history walk with full-mark filtering, gitsign verification with --no-verify, candidate selection, writes confined to .vscode/imports.weaudit-shas.json (no .weaudit file is ever written or read for imports), native marks superseding imports, the import-exclude list in load_scope()'s config, the self-import refusal, prune, stamp, status, next and render_reviews_md() treating the imports file as described, status --json imported count, and the Source column in render_reviews_md(). Follow the file's existing style (module-level helpers, git() wrapper, load_json/write_json preserving trailing newlines, single quotes, 120 columns). Update the module docstring's subcommand list. Regenerate this repository's REVIEWS.md. |
| 1c | high | opus | none | Tests in scripts/tests/test_review_tracking.py, following its existing fixture repos: a source fixture with signed-commit verification stubbed out, covering full-mark import, partial-mark-only source not imported, earliest review preferred, lagging copy imported from history, already-marked target untouched, stale target mark not replaced before prune, out-of-scope target file ignored, nothing ever written to any .weaudit file or a human reviewer's sidecar, prune dropping a stale import and removing an emptied imports file, native mark superseding an import, import-exclude preventing and removing an import, self-import refused, stamp ignoring the imports file, next not offering an imported file, status counting imports, and the Source column. |
| 1d | medium | sonnet | none | Document import in docs/code-review-tracking.md: a new subsection under "Steady state" covering what is imported, why imports live in a sidecar-shaped file weAudit never reads (with the weAudit behaviour from open question 1 that ruled out a .weaudit file), rejecting an import with import-exclude, the provenance record, why verification happens at import time (amplification), and a rewrite of the "prune only removes marks" argument so it is scoped to prune and a parallel argument covers import. Update the subcommand list near the top of the file. |
Phase 2: review tracking CI template and rollout¶
Planning effort: medium. The pattern is established; the work is reconciling four copies.
Create templates/review-tracking/ holding prune-reviews.yml,
ci-prune-reviews.sh and review-tracking.sh, with a README in
the shape of the other template directories. Each must be copyable
byte-for-byte, so that the copies are themselves importable:
- The default branch comes from
${{ github.event.repository.default_branch }}in the workflow (including the ref guard) and is passed to the script in the environment, instead of being written in. - The script takes actions' landing retry and
git status --porcelaincheck, which are improvements the other three lack. - The script runs
prune, thenimport, and commits both with a message that names both (Prune and import review marks.). - The workflow gains a daily
scheduletrigger (open question 2), atimeout-minutes, clones this repository with full history (it is 16 MB) instead of--depth 1, and installs or locates gitsign (open question 5). - The checkout token follows open question 6.
- The comments are rewritten for the template rather than
inherited from ryll's, and link to
docs/code-review-tracking.mdby absolute URL.
Replace this repository's own prune-reviews.yml and
ci-prune-reviews.sh with copies. Its review-tracking.sh stays
different -- docs/code-review-tracking.md step 5 explains why it
must not search for a sibling clone -- and the README says so.
Then, one pull request per repository, replace the copies in
actions, hunkydory, kerbside and ryll, and check each repository's
first scheduled run. Update step 7 of "Adopting a repository" to
point at the template, and the review-tracking-adoption skill to
match.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 2a | medium | sonnet | none | Survey and report only: is gitsign installable on the [self-hosted, static] runners (check the image definitions in shakenfist/images and any existing gitsign use in the fleet), and is DEPENDENCIES_TOKEN an organisation secret visible to actions, hunkydory, kerbside and ryll (gh secret list --org shakenfist and per repo). Settles open questions 5 and 6. |
| 2b | high | opus | none | Write templates/review-tracking/ as the phase 2 section describes, starting from actions' tools/ci-prune-reviews.sh and ryll's .github/workflows/prune-reviews.yml, and replace this repository's two copies. Run actionlint and shellcheck via pre-commit run --all-files. |
| 2c | medium | sonnet | none | Update docs/code-review-tracking.md step 7 and the steady-state description of the workflow, and .claude/skills/review-tracking-adoption/SKILL.md, to point at the template. |
| 2d | medium | sonnet | worktree | In each of actions, hunkydory, kerbside and ryll, one branch and pull request per repository replacing the three files with byte-identical copies of the template; confirm with git hash-object that each copy matches. Do not merge. |
Outcome. All four rollout pull requests merged on 2026-09-28,
and each repository's copies match the template. Two things came up
that the steps above did not anticipate. actions carried a test
(PruneGuardTest) asserting the old workflow's literal
refs/heads/main guard, updated in its rollout pull request. And
hunkydory had never done adoption step 8: its expensive workflows
ran in full on review-only changes, and since Build, test and
lint, Pre-commit hooks and Analyze are required checks there,
its rollout pull request added a job-level check_paths skip rather
than a trigger-level paths-ignore. Step 8 and the adoption skill
still describe only the trigger-level form, and phase 3 picks that
up.
Phase 3: template convergence¶
Planning effort: high. Each template needs a judgement about which kind of drift it has, and some of the answers change the template every other repository receives.
For each template directory under templates/ with a copy in an
adopted repository, classify the difference between template and
copy into the four kinds in "Situation", and act:
- Parameters are replaced with values the runtime supplies
(
${{ github.repository }}for renovate's autodiscover filter) where that is possible, so the file can be copied verbatim. A parameter that cannot be removed stays, and the README says the copy is not expected to be importable. - Lagging copies are re-synchronised.
- Local improvements are upstreamed into the template, then
every adopted copy re-synchronised -- including in repositories
outside the review-tracking set when
standards-alignmentnext runs there; this phase does not chase them. - Genuinely per-repository files are recorded as such in the template's README, so that nobody spends effort converging them.
Record the classification as a table in this section: template, repositories, kind, action taken. The survey in "Situation" is the starting point but was taken from local clones, so re-measure.
What the survey found (step 3a, re-run 2026-09-30). The 2026-09-24 survey had drifted in six days. That is the argument for this phase in miniature: copies change under the plan as well as under the template. Re-measured against the default branches of all five repositories:
- 5 of 29 copies are byte-identical, down from 6. The
denominator excludes
review-tracking(phase 2 made all 12 of its copies identical) andrelease-automation(per-repository; counting it gives 5 of 34). - pr-re-review in kerbside and ryll was re-synchronised by other work on 2026-09-27: +2.
- Template commit 488a2b0 (2026-09-25) added a comment to
export-repo-config.yml, so the kerbside and ryll copies now lag: -2. - hunkydory#57 gave hunkydory's
codeql-analysis.ymla job-level review-only skip, so it no longer matches: -1. - Four claims in the earlier table were false by 2026-09-30:
- actions'
pr-re-review.ymlalready haspersist-credentials: false. Its only divergence is two comments that saypr-auto-review.ymlis "alongside this file". - kerbside's and ryll's
pr-retest.ymlalready have the bot guard, and all four copies carry the current template body. What remains is the dispatched-workflow parameter. - The template's
renovate.jsonno longer says "Automatically merge" (fixed in 488a2b0). The hunkydory and ryll copies still do. export-repo-config.ymlis identical in only one of three repositories, not three.- Four claims still hold:
- The mermaid-lint copies in kerbside and ryll lag.
- actions'
renovate.ymlhastimeout-minutesandconcurrencythat the template lacks. - kerbside's and ryll's CodeQL copies still check out
HEAD^2. - kerbside's
pin-indirect-dependencies.ymladds apt packages. - Three things the plan did not know:
- kerbside's
pr-retest.ymlnotes that dispatchingfunctional-tests.ymlruns the merge tier, not the lanes that gate a pull request. - ryll's CodeQL cancels in-progress push and scheduled runs as well as pull request runs.
- actions' CodeQL
paths-ignorelacks the review paths, so it misses adoption step 8. - Decision 3 as first written counted only the five
review-tracking repositories, which made
ci.ymllook like the majority default; across the fleet it isfunctional-tests.yml. - Required checks: only hunkydory has status checks CodeQL
would affect (
Analyze). kerbside and ryll require only their merge-queue gates; actions and this repository require none. - Audit criteria: none of those under
scripts/audit/checks/inspects anything the actions below change.CiReviewAutomationdoes require pr-re-review'srev-parse HEAD^2confirm step and the fork gates on both review automation files, and the template change keeps both. - Step 8 of "Adopting a repository" and the
review-tracking-adoptionskill describe only the trigger-levelpaths-ignore. Both say a skipped required check "sits expected forever", which is true of a workflow that never starts but not of a job skipped byif:, which satisfies the check. Neither offers the job-level skip that hunkydory, kerbside and actions already use for required checks.
This section's table and decisions are corrected to match. The "Situation" measurements are left as the 2026-09-24 baseline that phase 4 re-measures against.
Classification and actions, in order of copies gained per unit of work:
| Order | Template | Kind | Action | Copies gained |
|---|---|---|---|---|
| 1 | mermaid-lint (.sh and .yml) |
lagging | Re-sync kerbside and ryll verbatim; no template change | 4 |
| 2 | export-repo-config.yml |
lagging | Re-sync kerbside and ryll verbatim; the template README says actions' file at that path is the reusable workflow, not a copy (likewise issue-link-check.yml) |
2 |
| 3 | renovate/renovate.yml |
parameter, local improvement | Filter becomes ${{ github.repository }}; upstream actions' timeout-minutes and concurrency; re-sync all four and this repository |
4 |
| 4 | ci-review-automation/pr-retest.yml |
parameter | Dispatch ${{ vars.RETEST_WORKFLOW \|\| 'functional-tests.yml' }} with neutral wording; re-sync all four and this repository; actions, development, hunkydory and ryll set RETEST_WORKFLOW=ci.yml |
4 |
| 5 | ci-review-automation/pr-re-review.yml |
parameter (comment only) | Neutral wording for where pr-auto-review.yml lives; re-sync actions |
1 |
| 6 | codeql/codeql-analysis.yml |
local improvement, lagging | Job-level review-only skip (decision 2) and the rest of decision 2; re-sync hunkydory and ryll; kerbside and actions stay per-repository | 2 |
| 7 | pin-indirect-dependencies/pin-indirect-dependencies.yml |
parameter, local improvement | {{PROJECT_NAME}} goes: the checkout moves to the workspace root (not ${{ github.event.repository.name }}, which is empty on the daily schedule run); extra apt packages move to a tracked file the workflow reads if present; re-sync kerbside |
1 |
| -- | release-automation/*, renovate/renovate.json |
per-repo | Record as per-repo in the READMEs; upstream kerbside's vm sign-tag label; fix the "Automatically merge" description in the hunkydory and ryll copies |
0 |
That takes the identical count from 5 to 23 of 29.
Decisions.
- The denominator is 29.
review-trackingis phase 2's and already converged;release-automationis per-repository. Phase 4 reports both totals. - CodeQL takes hunkydory's job-level skip. This settles the earlier "decision needed before item 5". The skip works whatever a repository requires, is proven in hunkydory, and a trigger-level filter cannot be safe in a template some copies have as a required check. Around it:
- the filter also excludes
docs/**, as three of four copies do; - a fork guard;
- actions' pull-request-only
cancel-in-progress, which fixes ryll cancelling push and scheduled runs; timeout-minutes;branches: [main, develop], which names branches but stays identical across the fleet, the mermaid-lint precedent;- the obsolete
HEAD^2checkout goes.
kerbside pins languages because autobuild fails on its C file,
and actions runs a language matrix. Both stay documented
per-repository copies rather than growing a parameter for two
repositories.
3. pr-retest reads its target from a repository variable. The
default is functional-tests.yml: it is the fleet majority (nine
repositories, against six on ci.yml and one on
unit-tests.yml) and what the template dispatched before, so most
repositories are unaffected on their next sync. The variable is
set to ci.yml in actions, development, hunkydory and ryll; each
is a change to that repository's settings made by Michael, and
development's must be set before this phase's pull request merges.
kerbside needs none. kerbside's caveat -- the dispatched workflow
is not what gates a pull request there -- is a design question
about the retest bot, recorded as future work rather than solved
here.
4. This repository's own copies converge with the fleet's
where a template change makes that possible: renovate.yml,
pr-retest.yml and codeql-analysis.yml.
5. Step 8 and the adoption skill document the job-level skip
as the answer for a required check, and correct the "sits
expected forever" wording. actions' CodeQL gains the review paths
in its re-sync pull request, closing its step 8 gap.
6. Pull requests and review. The template changes land in this
repository first, as one pull request with one commit per
template. The copies follow as one pull request per repository
after it merges, so that each copy matches what landed here. A
copy is only importable once the changed template has been
re-reviewed and stamped here, so the re-review is part of the
phase, not an afterthought.
The decision most open to argument is 2's handling of kerbside and
actions. A languages parameter (${{ vars.CODEQL_LANGUAGES }},
empty meaning autodetect) would make kerbside's copy importable
too, at the cost of every repository's CodeQL depending on an
unset variable behaving like an absent input. That is not verified,
and verifying it on a security scanner is the wrong place to learn
it.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 3a | high | opus | none | Done 2026-09-30; the findings are above. |
| 3b | medium | sonnet | none | Items 1, 2 (README only), 3, 5 and 7 and the per-repo README notes. Per template, one commit in this repository: change the template as the table says, update its README, and where this repository carries its own copy (.github/workflows/renovate.yml) re-sync it. Keep the CiReviewAutomation properties (rev-parse HEAD^2 confirm step, fork gates; see scripts/audit/checks/ci_workflows.py:932). For item 7, the apt list is a tracked tools/pin-indirect-dependencies-apt.txt, read only if present. Run pre-commit run --all-files and python3 scripts/audit-check.py --repo-path . --repo-name development. |
| 3c | high | opus | none | Items 4 and 6: pr-retest's vars.RETEST_WORKFLOW and the CodeQL template of decision 2, starting from hunkydory's codeql-analysis.yml (origin/develop, commit cd098f5) and actions' fork guard and concurrency. Walk every event (review-only pull request, code pull request, fork pull request, push, schedule, dispatch) and state each job's outcome in the commit message. Re-sync this repository's copies. Keep test_registry.py's ci-review-automation tests passing. |
| 3d | medium | sonnet | none | Rewrite step 8 of "Adopting a repository" in docs/code-review-tracking.md and steps 2-3 of .claude/skills/review-tracking-adoption/SKILL.md to give the job-level check_paths skip as the answer for a required check, with hunkydory's shape as the worked example. |
| 3e | medium | sonnet | worktree | After the 3b-3d pull request merges: one branch and pull request each in actions, hunkydory, kerbside and ryll re-syncing that repository's copies from the table, verified with git hash-object (for kerbside's pin-indirect-dependencies that is the workflow and the new tools/pin-indirect-dependencies-apt.sh, plus a tools/pin-indirect-dependencies-apt.txt carrying its mysqlclient build dependencies), plus the renovate.json description fix (hunkydory, ryll) and the review paths in actions' CodeQL. Do not push or open pull requests until asked. RETEST_WORKFLOW=ci.yml is set by Michael in actions, hunkydory and ryll before each pull request merges (development's is set before the 3b-3d pull request merges); kerbside needs none. |
Risks.
- A converged template breaks a copy that relied on its local difference. kerbside's CodeQL and pr-retest are the known cases, and both are handled above. Each 3e pull request runs that repository's own CI, and the management session reads the diff of every copy against its old version before opening it.
- The CodeQL skip hides a real change. The filter fails open
(
|| 'true',!cancelled()), and step 3c's event walk is the check. - The imports this phase is for do not arrive. A re-synchronised copy imports only after the new template is reviewed here. Michael re-reviews the changed templates before 3e opens its pull requests; phase 4 counts the result.
Definition of done.
- For every row in the table above,
git hash-objectof each copy on each repository's default branch equals the template's, or the template's README names that copy as per-repository. A script run by phase 4 checks it; step 3a's survey is its first draft. grep -n 'sits "expected" forever' docs/code-review-tracking.md .claude/skills/review-tracking-adoption/SKILL.mdfinds nothing, and both describecheck_paths.- A review-only pull request in hunkydory merges with
Analyzereported as skipped. pre-commit run --all-filespasses here, andaudit-check.pyreports no new failure for any of the five repositories.
Back brief. Before 3c edits anything, the management session agrees the CodeQL template's event table with Michael. It is the one change here that alters a security scanner's coverage in every copy.
Outcome. Re-measured 2026-10-13 against each repository's
default branch: every copy the classification table converges is
byte-identical to its template, stamped here, and reviewed in its
repository (by import or by a direct stamp). The three copies that
differ are the ones the table keeps per-repository: CodeQL in
actions and kerbside, and actions' reusable export-repo-config.yml.
A review-only hunkydory pull request (#68) merged with Analyze
reported as skipped.
Convergence did not hold on its own. The first-round re-sync pull
requests merged on 09-30 (actions#122, hunkydory#63), 10-01
(kerbside#511) and 10-03 (ryll#425). Meanwhile 5e9a4b3 (#204, on
main 10-01) changed both bot workflow templates, 3b5142c (#212,
on main 10-03) changed pr-re-review.yml again, and Renovate
bumped renovate.yml on 10-02. actions' and hunkydory's copies
lagged within a day of merging; kerbside's and ryll's
pr-re-review.yml were already behind when they merged. Eight bot
workflow copies lagged until a second round of re-sync pull
requests (actions#134, hunkydory#66, kerbside#519, ryll#455)
merged on 10-03 and 10-04. Renovate's bumps converge by
themselves, because it makes the same edit in every repository; a
hand edit to a template does not. Phase 4 takes this as its main
input.
audit-check.py against the five default branches shows no failure
this phase caused. hunkydory's ci-review-automation failure
predates it: hunkydory's ci.yml has never called the automatic
reviewer, and this phase did not touch that file.
Phase 4: verification¶
Planning effort: medium. Nothing here changes what a criterion
means or touches audit-check.py's scheduler; the one judgement
call (decision 3) is recorded, not implemented.
Scope. In: measure what phases 1 to 3 bought, against the
2026-09-24 "Situation" baseline; test that the documentation is
enough to verify an imported mark by hand, and fix it where it is
not; record the evidence that prune removes an imported mark; and
put the drift phase 3 hit in front of the decision about a template
conformance criterion. Out: building that criterion, re-syncing any
copy that has drifted since 2026-10-13 (that is ordinary
standards-alignment work), and anything under Future work.
What the survey found (2026-10-13). The section as first written was mostly right. Corrected here, in place of the old text:
- "Collect the
prune-reviewsrun logs" is the wrong source. Actions logs expire after 90 days; the record that lasts is the git history of each repository's.vscode/imports.weaudit-shas.json, which every prune-and-import commit updates. In actions it shows five commits from 2026-09-28 to 2026-10-09, each adding or removing entries. 4a reads that history instead. - The third bullet has already happened and needs recording, not
watching for. In actions, 00642fd dropped the imported
renovate.ymlmark after a Renovate bump, and 953a997 droppedpr-re-review.ymland replacedpr-retest.ymlafter the 10-01 template change. 7e77693 then re-importedrenovate.ymlat the new blob once it was stamped here. - The documentation has no hand procedure for an imported mark.
docs/code-review-tracking.mdgives thegitsign verifycommand for a commit ("Commit signing") and explains why an imported entry is a pointer to an attestation, but not the steps a reader takes to follow that pointer. Those are: verify the entry'scommithere, and confirm that commit's sidecar records the same blob SHA at the entry'spathwith no partial marks. 4b tests whether a reader can get there from the text alone; the expectation is that it cannot, and the fix is a short procedure in the doc. - Drift returned within days (see phase 3's Outcome), and
Renovate bumps converge by themselves while hand edits do not.
The Future work item "A template conformance criterion" asked to
revisit after this phase; development#151 asks the same
question for
templates/mermaid-lint/. - gitsign is installed on this host (
/usr/bin/gitsign), so the hand verification needs no setup.
Decisions.
- The measurement is a script in this plan, not a new tool. Phase 3's Definition of done promised "a script run by phase 4". It lives in 4a's brief, below, so it costs no review here and no maintenance; if the conformance criterion is ever built, it is that criterion's first draft.
- The hand verification is done by an agent working only from
docs/, then repeated by Michael on one entry. The agent is the better test of whether the documentation is enough, because it cannot fall back on knowing the code; Michael's run checks the agent did not. - No conformance criterion in this plan. The data argues for one: import made drift visible (files fell out of review in each repository), but nothing said why or which template to re-sync from, and a person measuring by hand found it. That change is high effort here (a new criterion that files issues in other repositories), and #151 already frames it more broadly than this plan can, across every template, not just the ones import touches. 4c adds this plan's drift data to #151 and points the Future work item at it.
- A doc fix is the only change to shipped content. If 4b finds the procedure gap, the fix lands in this phase's pull request. Anything else 4b finds is recorded and filed, not fixed.
The decision most open to argument is 3. The drift cost a second round of four pull requests, and the next hand edit to a template will cost another; a reviewer could fairly say the criterion is the actual deliverable. The reason to defer it is that the right design (shared-block-style version markers, a hash comparison in the audit, or development opening the re-sync pull requests itself) is the question #151 already asks, and answering it inside a verification phase would be the scope creep this plan's phases have avoided.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 4a | medium | sonnet | none | Measure, write nothing to any repository. For each of development (main), actions (main), hunkydory, kerbside and ryll (develop), make a detached worktree of the freshly fetched default branch in the scratchpad (local clones live beside this one under src/shakenfist/), and report: (1) in-scope files needing review, from tools/review-tracking.sh status (python3 scripts/review-tracking.py status here), against the "Situation" table; (2) how many in-scope files are byte-identical to a blob this repository has stamped as fully reviewed, at HEAD and anywhere in the sidecar's history, the same two counts "Situation" gives; (3) entries in .vscode/imports.weaudit-shas.json now, all with verified: true or not; (4) from git log -p of that file, every entry added and removed, with dates. Then run the template copy check below and report it as a table. Remove the worktrees afterwards. |
| 4b | medium | sonnet | none | Using ONLY docs/code-review-tracking.md and docs/ (do not read scripts/review-tracking.py), verify one imported entry by hand in each of actions, hunkydory, kerbside and ryll: pick an entry from its .vscode/imports.weaudit-shas.json, and establish from the doc's instructions that the named commit in shakenfist/development is signed by the named reviewer's identity and records a full review of that blob at that path. Report each step taken, where the doc did not say what to do next, and the result. If the doc lacks the procedure, draft the shortest addition that would have been enough, under "Importing reviews from this repository", for the management session to review. Michael then repeats the check on one entry from the drafted text. |
| 4c | low | sonnet | none | Write the results into this section as an Outcome paragraph: the re-measured "Situation" table beside the baseline, the imports per repository and the prune removals (00642fd, 953a997 in actions, plus any 4a found elsewhere), 4b's result, and the copy check. Comment on development#151 with this plan's drift data (templates changed after convergence, the copies that lagged and for how long, and that Renovate bumps converged by themselves), and change the Future work item "A template conformance criterion" to point at #151. |
The template copy check, for 4a (run from src/shakenfist/, after
fetching each clone):
declare -A B=([actions]=main [hunkydory]=develop [kerbside]=develop [ryll]=develop)
pairs="mermaid-lint/mermaid-lint.sh:tools/mermaid-lint.sh
mermaid-lint/mermaid-lint.yml:.github/workflows/mermaid-lint.yml
export-repo-config/export-repo-config.yml:.github/workflows/export-repo-config.yml
renovate/renovate.yml:.github/workflows/renovate.yml
ci-review-automation/pr-retest.yml:.github/workflows/pr-retest.yml
ci-review-automation/pr-re-review.yml:.github/workflows/pr-re-review.yml
codeql/codeql-analysis.yml:.github/workflows/codeql-analysis.yml
pin-indirect-dependencies/pin-indirect-dependencies.yml:.github/workflows/pin-indirect-dependencies.yml
pin-indirect-dependencies/pin-indirect-dependencies.sh:tools/pin-indirect-dependencies.sh
pin-indirect-dependencies/pin-indirect-dependencies-apt.sh:tools/pin-indirect-dependencies-apt.sh
review-tracking/prune-reviews.yml:.github/workflows/prune-reviews.yml
review-tracking/ci-prune-reviews.sh:tools/ci-prune-reviews.sh
review-tracking/review-tracking.sh:tools/review-tracking.sh"
for r in actions hunkydory kerbside ryll; do
for p in $pairs; do
t=${p%%:*}; c=${p#*:}
th=$(git -C development rev-parse origin/main:templates/$t)
ch=$(git -C $r rev-parse -q --verify origin/${B[$r]}:$c) || { echo "MISSING $r $c"; continue; }
[ "$th" = "$ch" ] && echo "same $r $c" || echo "DIFF $r $c"
done
done
On 2026-10-13 it prints DIFF for exactly the three per-repository
copies (CodeQL in actions and kerbside, actions'
export-repo-config.yml) and same for the other 36. It also
prints 13 MISSING lines, for copies those repositories never had:
mermaid-lint exists only in kerbside and ryll, and
pin-indirect-dependencies only in kerbside, the one application
among the four. A new MISSING line would mean a copy was deleted.
Risks.
- 4b's agent reads the code anyway, and the test proves nothing.
The management session checks the agent's transcript lists only
docs/reads, and Michael's repeat from the drafted text is the backstop. - The re-measured counts are not comparable with the baseline. The baseline used local clones one to five days old; 4a uses fresh default branches. The Outcome says so beside the table rather than presenting the difference as all import's doing.
- A copy has drifted again by the time 4a runs. That is a result, not a failure: the Outcome reports it, and it goes into the #151 comment.
Definition of done.
- The phase 4 Outcome has a "Situation" table re-measured on a stated date, beside the 2026-09-24 baseline, with imports per repository.
docs/code-review-tracking.mdhas a hand procedure for an imported entry (grep -n 'imports.weaudit-shas.json'finds it next to agitsign verifycommand), or 4b's report shows the doc was already enough and the Outcome says so.- Michael has verified one imported entry from the doc text, and the Outcome names it.
- development#151 has a comment with this plan's drift data, and the Future work item links #151.
pre-commit run --all-filespasses, andaudit-check.pyfor this repository reports what it reported before.
Back brief. None needed beyond the plan's: no step here is expensive to redo.
Outcome. Measured 2026-10-13 on fresh default branches.
"Needing review" is from status; "byte-identical" means
identical to a blob this repository has stamped as fully
reviewed, both at this repository's HEAD and anywhere in its
sidecar's history:
| Repository | In scope | Needing review 09-24 → 10-13 | Byte-identical (history) 09-24 → 10-13 | Byte-identical (at HEAD) 09-24 → 10-13 | Imported now |
|---|---|---|---|---|---|
| actions | 147 | 14 → 5 | 0 → 6 | 0 → 6 | 5 |
| hunkydory | 40 | 2 → 0 | 5 → 9 | 5 → 9 | 7 |
| kerbside | 258 | 140 → 130 | 4 → 12 | 3 → 12 | 5 |
| ryll | 245 | 37 → 101 | 4 → 10 | 2 → 10 | 3 |
Notes on the table:
- Every imported entry is
verified: true. - The at-
HEADand history counts are now equal in all four repositories: no copy lags a template. - The byte-identical counts exceed the imported counts because a native mark on the same blob pre-empts import.
- ryll's rise is new Rust code entering scope
(
shakenfist-spice-protocol/,shakenfist-spice-renderer/,ryll/src/app/), not import. - The baseline used local clones one to five days old, so the difference above is not all import's doing (see the risk above).
Import history, from each repository's own
.vscode/imports.weaudit-shas.json git log:
- actions: 6e0aa3e (09-28) added three review tracking files;
017a178 (09-30) added
pr-re-review.yml,pr-retest.ymlandrenovate.yml; 00642fd (10-03) droppedrenovate.ymlafter the v46.3.6 Renovate bump; 953a997 (10-03) droppedpr-re-review.ymland replacedpr-retest.ymlafter the template change in 5e9a4b3; 7e77693 (10-09) importedrenovate.ymlat the v46.3.7 blob. - hunkydory: fff62e1 (09-28) added six; 0d24140 (09-30) added
codeql-analysis.ymlandrenovate.yml; 1cc5885/0e3e3fe (10-03) droppedrenovate.yml(v46.3.6 bump) andpr-re-review.yml; cfc85d2 (10-10) importedrenovate.ymlat the v46.3.7 blob. - kerbside: 6df1ee3 (09-28) added four; 233eb40 (09-30) dropped
pr-re-review.ymlandprune-reviews.yml; f126408 (10-01) added eight; fddd582/89ac333 (10-03) droppedrenovate.yml(v46.3.6 bump) andpr-re-review.yml(template change); e425a42 (10-05) droppedexport-repo-config.yml,mermaid-lint.yml,pin-indirect-dependencies.ymlandpr-retest.ymlafter native re-stamps; efc652b (10-11) importedrenovate.ymlat the v46.3.7 blob. - ryll: 6d5a3da (09-28) added three; 13fa6e9 (10-03) added
seven; 41541fb (10-04) dropped
pr-re-review.yml(template change) and 09ffde3 (10-04) droppedrenovate.yml(v46.3.6 bump, merged in ryll#440 on 10-03); 3dc0ce4 (10-04) imported both at their new blobs, stamped here hours earlier, and droppedprune-reviews.yml, which had gained a native mark; c494993 (10-05) dropped seven after native re-stamps; ebca209 (10-10) importedrenovate.ymlat the v46.3.7 blob.
Prune of an imported mark when its file changes is confirmed
in every repository. The v46.3.6 Renovate bump (10-02) dropped the
imported renovate.yml in actions (00642fd), hunkydory (1cc5885)
and kerbside (fddd582) on 10-03, and in ryll (09ffde3) on 10-04,
after its bump merged. Only ryll re-imported that blob (3dc0ce4, the
same day); the other three were re-stamped natively before an
import could happen. For the v46.3.7 bump (10-08), this repository
stamped the new blob on 10-09 (f2bc259), and all four repositories
imported it between 10-09 and 10-11: a lag of one to three days
from the bump.
Key finding: imported files were re-reviewed by hand anyway. kerbside's e425a42 and ryll's c494993 dropped imports even though the files had not changed. In both repositories a human review commit had just stamped the same blobs natively:
- kerbside 2dafb4e, "review: CI workflows", 2026-10-05, stamped
six files. Four of them (
export-repo-config.yml,mermaid-lint.yml,pin-indirect-dependencies.yml,pr-retest.yml) were still imported at blobs that had not changed; the other two,pr-re-review.ymlandrenovate.yml, had lost their imports on 10-03 when their blobs changed; - ryll 3d16282, "review: docs, with a lot of unrelated stamp updates too", 2026-10-05.
A native mark always takes precedence, so prune dropped the
now-redundant import entries. Import kept those files out of the
review queue for days, but they were still read again by a human.
The likely cause is that imported marks live only in
.vscode/imports.weaudit-shas.json and never in a .weaudit
file, so the weAudit editor shows those files as unreviewed and a
reviewer working through the tree finds and re-reads them anyway.
Not diagnosed further here; see Future work.
4b result. An agent working only from docs/ verified one
imported entry in each repository:
| Repository | Entry | Source commit |
|---|---|---|
| actions | pr-retest.yml |
3343336 |
| hunkydory | codeql-analysis.yml |
f82b09c |
| kerbside | tools/mermaid-lint.sh |
d7638dc |
| ryll | tools/ci-prune-reviews.sh |
d771816 |
All four signatures verify and all four are full auditedFiles
marks. The agent had to assemble the steps from three sections of
docs/code-review-tracking.md, and two facts it needed were not
written down anywhere in docs/: the .weaudit JSON shape for a
full versus a partial mark, and how to find a reviewer's signing
identity. "Verifying an imported entry by hand", added in this
phase, closes both gaps. Michael repeated the check from the
documented procedure on 2026-10-13, on ryll's imported
renovate.yml (source commit f2bc259, blob d63b3b2): the signature,
Rekor entry and certificate claims validate, the sidecar and
auditedFiles both record a full review of that blob, and ryll's
develop carries it. His one difficulty was that step 3 prints both
state files whole, hundreds of lines to find one path; the step now
filters to the path.
The converged renovate.yml also works in practice: ryll's Renovate
run on 2026-10-13 resolved the new ${{ github.repository }} filter
to shakenfist/ryll and autodiscovered exactly that repository
(shakenfist/ryll Actions run 38329074401).
The drift data below was posted to development#151 on 2026-10-13 (https://github.com/shakenfist/development/issues/151#issuecomment-6133037509). Dates in this plan are in Michael's local time (AEST/AEDT).
Template copy check. Unchanged from this phase's first run on
2026-10-13: three DIFF (CodeQL in actions and kerbside,
actions' export-repo-config.yml), all per-repository by design,
and 36 same.
Drift. The templates behind those copies changed again after phase 3 converged them:
- 5e9a4b3 (#204, on
main10-01) changed both bot workflow templates; - 3b5142c (#212, on
main10-03) changedpr-re-review.ymlagain; - Renovate bumped
renovate.ymlon 10-02 and 10-08.
Eight bot workflow copies lagged from 10-01 until a second round of re-sync pull requests (actions#134, hunkydory#66, kerbside#519, ryll#455) merged on 10-03 and 10-04: two to three days. Renovate's bumps converged by themselves, because it makes the same edit everywhere; the hand edits did not. Import made this drift visible only as files dropping out of review; nothing named the template to re-sync from.
Phase 5: push audit¶
Planning effort: medium. The runbooks already exist in all five repositories; the judgement here is in scoping (decision 2).
Scope. In: development's PUSH-AUDIT.md over the four merge
commits that landed phases 1 to 4 here, and each adopted
repository's own PUSH-AUDIT.md over the three merges that landed
phases 2 and 3 there. Findings are fixed or declined in writing.
Out: anything Future work already records, and work merged in
those repositories since that did not come from this plan.
What the survey found (2026-10-13).
- The out-of-repository audits this section planned to cite do
not exist. As first written, the section said to "cite the
per-repository audits for the pull requests that landed
elsewhere", and the
plan-push-audit-phaseshared block expects a phase landing in another repository to be audited as part of its pull request. None of the twelve was: no body or comment on actions#111, #122, #134, hunkydory#57, #63, #66, kerbside#504, #511, #519 or ryll#418, #425, #455 mentions a push audit. They are audited here instead (decision 2), and this section is corrected to say so. - The ranges are all recorded, every one as a merge commit, so none needs reconstructing. Sizes against first parent:
| Repository | Merge (PR) | Files | Insertions | Deletions |
|---|---|---|---|---|
| development | 54c7ecc (#177) | 11 | 2903 | 262 |
| development | 02d6dbb (#186) | 20 | 1427 | 163 |
| development | b520c22 (#203) | 29 | 1262 | 177 |
| development | bdc52ab (#249) | 2 | 383 | 20 |
| actions | 74a33c9 (#111), 89717fa (#122), e05bbb1 (#134) | 5, 6, 2 | 310, 111, 88 | 100, 48, 0 |
| hunkydory | f24d413 (#57), 8730d75 (#63), c22059e (#66) | 5, 5, 2 | 437, 115, 88 | 60, 41, 0 |
| kerbside | 62ccbcb (#504), 700102e (#511), ede2d7d (#519) | 3, 9, 2 | 310, 764, 88 | 53, 71, 0 |
| ryll | ce3ebb3 (#418), 79721f3 (#425), f8a4e2f (#455) | 3, 10, 2 | 305, 302, 88 | 68, 57, 0 |
- Most of the out-of-repository diff is template content
byte-identical to files development's own audit reads. What is
each repository's own: actions'
tests/test_workflow_references.py,docs/ci.md,.gitignoreand the CodeQLpaths-ignorelines; hunkydory'scheck_pathsjobs inci.ymland therenovate.jsondescription; kerbside'stools/pin-indirect-dependencies-apt.txt; ryll'sdocs/ci.md,MERGE-TEMPLATE.mdandrenovate.jsondescription. - All four adopted repositories have a
PUSH-AUDIT.md(563 to 702 lines), so none needs the "no runbook yet" fallback.
Decisions.
- Development's audit runs the runbook as written, with
AUDIT_RANGEset to each of the four merges in turn (<sha>^1..<sha>), and each wave 2 brief carries all four concrete ranges and the insertion totals above. The runbook's own guard applies: a report whose totals do not match is re-run. Findings are judged against currentmain. Phase 1's code was changed by later phases, and a finding about a line that no longer exists is resolved. - The adopted repositories are audited by their own runbooks,
scoped to their own content. For each file a merge touched,
the agent compares it against its template at the template
revision of the time. A byte-identical copy is recorded as
"audited as development's
templates/<path>" and not re-read; only the per-repository content listed above gets wave 2. This is the decision most open to argument. Each runbook is written for its repository's own risks (kerbside's for a Python service, ryll's for Rust), and a copy can be wrong for its repository even when its template is right. A reasonable reviewer could ask for full runs. The answer is that each re-sync agent already read every old copy against its template before replacing it, and reported every behaviour the copy lost (phases 2 and 3). Four full wave 2 runs over 90% shared bytes would spend most of their effort re-auditing development's templates four times. - One agent per adopted repository, running wave 1 and the scoped wave 2 together. Their diffs are a few hundred lines of their own content. Development's audit gets the runbook's four parallel wave 2 agents, because its diff is about 6,000 lines of Python, shell, workflow and documentation.
- Findings land where the code is. Development's findings land in this phase's pull request, which also closes the plan. Findings in an adopted repository land as one pull request there, opened only when Michael asks. This plan is not complete until each finding is fixed or declined in writing in the Outcome below.
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 5a | low | sonnet | none | Development wave 1 (PUSH-AUDIT.md, "Wave 1: Mechanical checks"), run once per merge with AUDIT_RANGE set explicitly to 54c7ecc^1..54c7ecc, 02d6dbb^1..02d6dbb, b520c22^1..b520c22, bdc52ab^1..bdc52ab, after git fetch origin. Report each range's git diff --shortstat (it must match the survey table), the pre-commit run --all-files result on current main, and every grep hit with file and line. Change nothing. |
| 5b | high | opus | none | Development wave 2a, code quality, per PUSH-AUDIT.md "2a". Read the four ranges from 5a; the substituted commands and the totals they must reproduce go in the brief. Concentrate on scripts/review-tracking.py's import path and templates/review-tracking/ci-prune-reviews.sh. Judge findings against current main. State the insertion and deletion totals measured. |
| 5c | high | sonnet | none | Development wave 2b, tests, per "2b": scripts/tests/test_review_tracking.py, scripts/tests/test_ci_prune_reviews.py and the registry and docs tests the four ranges touched. Mutate before believing a test guards something. Same range and totals rule. |
| 5d | medium | sonnet | none | Development wave 2c, documentation, per "2c": docs/code-review-tracking.md, the template READMEs, the adoption and standards-alignment skills, and AGENTS.md/ARCHITECTURE.md as the ranges changed them. Check that no fact is stated two ways, and that nothing in docs/ cites a plan phase. Same range and totals rule. |
| 5e | high | opus | none | Development wave 2d, security, per "2d". The import trust argument ("The prune commit's attestation argument" and "Amplification" in "Situation"), gitsign verification and its failure modes, the CI token scope, the gitsign download pin, and the CodeQL template's fork guard and fail-open filter. Same range and totals rule. |
| 5f | medium | sonnet | none | actions: its own PUSH-AUDIT.md, wave 1 and a scoped wave 2 over 74a33c9, 89717fa and e05bbb1 (<sha>^1..<sha>; totals in the survey table). For each touched file, hash it at the merge and compare with development's templates/ blob at the date of the merge (git log --before). List byte-identical copies as audited elsewhere; give wave 2 to the rest (decision 2's list for actions). Change nothing; report findings with file and line. |
| 5g | medium | sonnet | none | hunkydory: as 5f, over f24d413, 8730d75 and c22059e. The check_paths jobs in ci.yml gate required checks, so verify the fail-open (\|\| 'true', !cancelled()) and predicate-quantifier: 'every'. |
| 5h | medium | sonnet | none | kerbside: as 5f, over 62ccbcb, 700102e and ede2d7d. |
| 5i | medium | sonnet | none | ryll: as 5f, over ce3ebb3, 79721f3 and f8a4e2f. A Rust repository; do not install a toolchain. A wave 1 check that needs cargo is reported as skipped, not run. |
| 5j | medium | sonnet | none | Fix the development findings the management session accepts, one commit each. Write the Outcome below: every finding with its disposition (fixed in which commit, declined and why, or filed where), and one line per repository that had none. Then mark this phase and the plan Complete in the Execution table and in docs/plans/index.md; this phase records no Merged cell, per the shared block. (As it turned out, the audit's template findings needed re-sync pull requests in the four adopted repositories, so the close-out followed them and recorded their merge commits, as plan-phase-landing allows for a findings carrier.) |
Risks.
- An agent audits an empty diff and reports it clean. Every report states its measured totals, and the management session compares them with the survey table before reading the findings.
- Decision 2's scoping misses a defect in a copy. That would be a copy wrong for its repository but right as a template. The per-repository agents are told to flag any copy whose behaviour depends on something its repository lacks, such as a variable, a label or a file. The re-sync reports from phases 2 and 3 are the other check.
- A finding needs a template change. It lands here first and then in the copies, as phase 3 did. A fix that would re-open convergence is recorded and filed rather than made in this phase.
Definition of done.
- Each development report states totals that match the survey table for its ranges.
- Every finding has a disposition in the Outcome: a fixing commit, a written decline, or an issue link.
- Any adopted repository with findings has a pull request linked from the Outcome, or an explicit "no findings".
- The Execution table shows phase 5
Complete, and thedocs/plans/index.mdrow showsComplete. TheMergedcell is empty if the audit found nothing; this one found template findings, carried by re-sync pull requests that landed after it, so the cell records their merge commits (see plan-phase-landing above). pre-commit run --all-filespasses, andaudit-check.pyfor this repository reports what it reported before.
Back brief. Before 5j opens any pull request in an adopted repository, the management session lists the findings to Michael and he chooses which land.
Outcome.
1. Range check. All nine audit reports reproduced the survey table's totals exactly (5a-5i).
2. Methodology note. Finding the template revision at a
merge's date with git rev-list -1 --before=<date> origin/main
needs --first-parent. Without it, a commit from a merged side
branch can be chosen and give a false "differs". The actions and
hunkydory audits (5f, 5g) both hit this and re-derived the
comparison.
3. Adopted repositories: no findings in any of the four.
- actions: 452 tests, gitleaks clean, local
test/docs/.gitignore/CodeQL paths correct.
- hunkydory: the check_paths jobs fail open and keep the
required check names; RETEST_WORKFLOW and RENOVATE_TOKEN are
present.
- kerbside: all 11 copies identical; the apt list is right for
mysqlclient on Debian 13; pyproject.toml is at the root and
functional-tests.yml is dispatchable.
- ryll: wave 1 passed including cargo in Docker; the docs,
MERGE-TEMPLATE.md and renovate.json are accurate;
DEPENDENCIES_TOKEN and the ruleset are confirmed.
No pull requests were needed for audit findings.
4. Development findings and dispositions. Commit hashes are
taken from git log.
| Finding | Source | Disposition |
|---|---|---|
| 5b-1 | import walked the source's HEAD |
Fixed: it walks origin/main (59d2e02) |
| 5e-M1, first half | wrongly typed history crashed the walk | Fixed (05bb3e7) |
| 5e-M1, second half | an import failure lost prune's results | Fixed: prune's result lands, then the run fails (0aca89b) |
| 5b-2 | no per-run verification budget | Fixed (99a2783); timeout comment corrected (ce3b32d) |
| 5e-L2 | existing target entries never re-checked | Fixed (cfc232a) |
| 5b-3 | wrong exit-code docstring | Fixed (648db83) |
| 5e-M3 | residual trust in development's code and identity map | Documented (d0de9c3); Michael chose this over CODEOWNERS, as sole reviewer of development PRs |
| 5d | CodeQL example in adoption step 8 out of date | Fixed (89400f5) |
| 5e-L4 | silent DEPENDENCIES_TOKEN fallback |
Documented (944b61a) |
| 5e-M2 | push token and global git config on shared static runners | Fixed: config isolation, per-run TUF_ROOT, token scoped to the push, hooks off for the push (9556eba) |
| 5c, first | architecture refusal untested | Fixed (8a2a37a) |
| 5e-L1 | stamps not tied to content in the commit's ancestry | Declined, added to Future work: the exact-match fix is wrong, since 8 legitimate stamps survive rebases, and the ancestry check is a larger change |
| 5e-L3 | docs/** could skip executable docs code |
Declined: a template change for a speculative case; no adopted repository has code under docs/ |
| 5b-4 | every state file buffered in memory | Declined: about 8.5 MB today; stream if it ever matters |
| 5b-5 | partial-to-full upgrade without prune | Declined: no clean fix; stamp's stale ERROR is the defence |
| 5b-6 | published details string changed | No action: intentional |
| 5c, second | verified-to-unverified downgrade protection is incidental | No action: the property holds |
| 9556eba's report | the checkout's own .git/config on a reused workspace is outside the isolation |
Declined here, then fixed when kerbside#563's review of the re-sync showed hooks in the reused .git/hooks also ran on fetch, reset and commit: the job lands from a fresh clone and runs no git command in the workspace |
| #251 review, round one | an entry pointing at a later genuine introduction of a review was replaced as unsupported; malformed imports entries crashed import | Fixed: every introducing commit attests, and load_imports() checks the file's shape in one place (559e9ef) |
| #251 review, round two | default history simplification hid review branches whose merge matched main | Fixed: the walk uses --full-history; an invalid or non-object imports file is treated as empty (c61040f) |
5. Follow-up.
- The template change (new hashes: prune-reviews.yml 91ff178,
ci-prune-reviews.sh b4ce5b6) must be re-synced into actions,
hunkydory, kerbside and ryll. Script and workflow go together,
because the script now requires PUSH_TOKEN.
- Michael must re-stamp the changed files here before the copies
import. At close-out this had not happened: of the files #251 and
#254 changed, only scripts/tests/test_review.py is stamped at its
current content. That is ordinary review coverage, tracked by
#173, not a condition of this plan.
- The config isolation is untested on a real static runner until
development's own prune-reviews runs after this merges. Ignoring
the global configuration also drops any safe.directory, proxy or
CA setting the runners rely on; if that run fails with "dubious
ownership" or a TLS error, add the setting for the checkout through
GIT_CONFIG_COUNT rather than restoring the global file.
The re-sync pull requests open only after that run has succeeded,
so that a runner dependency is found in one repository, not five.
That run passed (38378512735, shakenfist-static-2) and the four
opened, but kerbside#563's review then found hooks and
configuration in the reused workspace's .git still reached the
job, and actions#157's that a hook could rewrite the script during
the checkout itself, so the workflow now fetches the script, and
the script lands, from fresh clones. actions#157 and hunkydory#74
merged before this did, carrying ci-prune-reviews.sh 883d3e9, and
their first runs (actions 38480219082 on shakenfist-static-2,
hunkydory 38480253693 on shakenfist-static-1) cloned shallowly
with no checkout and pushed, so the fresh-clone workflow has run on
both static runners. The final template differs from 883d3e9 only
in a comment, so those two need one more verbatim re-sync; kerbside
and ryll take the final hashes directly.
- The plan reaches Complete when the re-sync pull requests
merge. Until then the success criterion "the three review
tracking CI files are byte-identical across the four adopted
repositories" is false. So leave phase 5 and the index row In
progress in this commit, and say that a one-line close-out
follows the re-syncs. They merged as actions#157 (e3e6155) and
#160 (68dc806), hunkydory#74 (e5a9b0e) and #77 (e8c62e5),
kerbside#563 (c608d2c) and ryll#530 (60990bb), after #254 here
(728f0ae). At close-out prune-reviews.yml 91ff178,
ci-prune-reviews.sh b4ce5b6 and review-tracking.sh db83f95
hashed identically to their templates in all four, so phase 5 and
the plan are Complete, with a Merged cell recording those
commits.
Agent guidance¶
Execution model¶
Sub-agent execution model (shared block; do not edit -- the
canonical copy lives in shakenfist/development at
templates/shared-blocks/subagent-execution-model.md):
All implementation work is done by sub-agents, never in the management session. The management session is reserved for planning, review, and decision-making. This keeps the management context lean and avoids drowning it in implementation diffs.
The workflow is:
- Plan at high effort in the management session.
- Spawn a sub-agent for each implementation step with the brief from the plan, at the recommended effort level and model.
- Review the sub-agent's output in the management session. Check the actual files -- the sub-agent's summary describes what it intended, not necessarily what it did.
- Fix or retry if the output is wrong. Diagnose whether the brief was insufficient (improve it) or the model was too light (upgrade it), then re-run.
- Commit once the management session is satisfied.
This applies to all steps, including high-effort ones. If a sub-agent cannot succeed even with a detailed brief and the right model, that is a signal the brief needs improving, not that the management session should do the implementation itself.
Use isolation: "worktree" for sub-agents when the change is
risky or experimental; the worktree is discarded if the output is
unsatisfactory. For safe, well-understood changes, sub-agents can
work directly in the main tree.
Planning effort¶
Planning effort (shared block; do not edit -- the canonical copy
lives in shakenfist/development at
templates/shared-blocks/plan-planning-effort.md):
The master plan itself is always created at high effort -- it requires broad codebase understanding, cross-referencing several source files, and judgment calls about scope and sequencing.
Each phase plan states the recommended effort level for planning that phase. Phases that turn on design decisions, cross-component coordination, protocol changes, or subtle correctness questions should be planned at high effort. Phases that are mechanical, or that follow a pattern already established elsewhere in the codebase, can be planned at medium effort.
In this repository. High effort is anything that changes what
a criterion means, anything that touches the scheduler in
audit-check.py, and anything that reaches
audit-manage-issues.py -- those decide what the fleet is held
to and what lands in other people's issue trackers. Medium effort
covers adding a criterion that follows the shape of an existing
one, a documentation sweep, or a template change with a worked
example already in the tree.
Step-level guidance¶
Sub-agent step guidance (shared block; do not edit -- the
canonical copy lives in shakenfist/development at
templates/shared-blocks/subagent-step-guidance.md):
Each phase plan includes a table like this:
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 1a | medium | sonnet | none | One-sentence summary of what to do and which files to touch |
| 1b | high | opus | worktree | Why this needs high effort: requires understanding X to do Y |
Effort levels, from cheapest to most thorough:
- low -- Purely mechanical changes: rename, reformat, add a log line, regenerate generated code. The brief is a complete instruction.
- medium -- The plan provides enough context to follow a clear brief. The sub-agent may read a few files, but the approach is already decided.
- high -- Requires reading several files, making judgment calls, or understanding non-obvious invariants. The sub-agent needs to think about edge cases.
- xhigh -- The setting for hard coding and agentic steps: long-horizon changes, or steps where the sub-agent must both research and implement.
- max -- Correctness matters more than cost. Expect diminishing returns and occasional overthinking; reserve it for steps where a wrong answer would be expensive to detect.
Brief for sub-agent: this is the key field. Write it as if briefing a colleague who has never seen the codebase. Include what to change, which files to touch, what patterns to follow, and any non-obvious constraints.
A good brief front-loads the research the planner already did, so the implementing agent does not repeat it. Instead of "add storage functions for the new object", name the functions to add, the file they belong in, the existing equivalent to mirror (with line numbers), and any registration the change also needs.
The better the brief, the lower the effort level needed and the lighter the model that can succeed.
In this repository. The phase sections above carry the step
tables. Two facts every brief in this plan depends on: stamp
stamps files with partial marks as well as full ones
(scripts/review-tracking.py, cmd_stamp), so a sidecar entry
alone never proves a full review; and review-tracking-tests
compares every row of this repository's REVIEWS.md against a
fresh render, so any change to render_reviews_md() regenerates
the file in the same commit.
Model choice¶
Sub-agent model roster (shared block; do not edit -- the canonical
copy lives in shakenfist/development at
templates/shared-blocks/subagent-model-roster.md):
The planner recommends which model is best suited to each step. This is a judgment call, not a rigid rule -- the right model depends on what the step requires, not on whether it is "planning" or "implementation". The models available to sub-agents are:
- fable -- The most capable model available, for the hardest reasoning and the longest-horizon work: multi-step changes a single sub-agent must carry end to end, or steps whose correctness depends on holding a whole subsystem in mind at once. It costs materially more than opus, so reserve it for steps that have already defeated opus or are expected to.
- opus -- The default for steps needing deep reasoning, architectural understanding, subtle correctness judgment (locking, state machines, migrations), or intricate implementation that would be costly to debug if it were wrong.
- sonnet -- A good default for well-briefed implementation work. Faster and cheaper than opus, and effective when the plan front-loads the research and the brief leaves no broad judgment calls to make.
- haiku -- Suitable for purely mechanical tasks: search-and-replace, regenerating generated code, adding log lines, running commands. The brief must be a near-complete instruction.
Model choice interacts with effort level and brief quality. A detailed brief compensates for a lighter model -- sonnet at medium effort with a thorough brief often matches opus at medium effort with a vague brief. The planner's job is to write briefs good enough that the recommended model can succeed.
The model also determines the context window: fable, opus and sonnet have 1M tokens, haiku has 200K. A step that must hold many files in context at once may need one of the larger-context models for that reason alone, even when the reasoning itself is straightforward.
When in doubt, skew to the more capable model. Saving money only matters if the outcome is still acceptable. A failed or low-quality implementation wastes more time -- and therefore more money -- than the heavier model would have cost. Recommend a lighter model only when you are confident the brief is detailed enough for it to succeed.
In this repository. The project-specific checks referred to above are:
-
pre-commit run --all-filespasses. It runs actionlint, shellcheck, flake8, skillsaw and all five test suites, andci.ymlruns the same command on every pull request. -
python3 scripts/audit-check.py --repo-path . --repo-name developmentstill reports what it reported before the change, or the plan says why the verdict moved. - If the change touches issue filing, it was exercised with
--dry-runonly.
Management session review checklist¶
Management session review checklist (shared block; do not edit --
the canonical copy lives in shakenfist/development at
templates/shared-blocks/plan-review-checklist.md):
After a sub-agent completes, the management session verifies:
- The files that were supposed to change actually changed -- read them, do not trust the summary.
- No unrelated files were modified.
- The changes match the intent of the brief: not merely syntactically correct, but semantically right.
- The project's own pre-merge checks pass, including any generated code that has to be regenerated and committed (see the project-specific checks below).
- The commit message follows project conventions, including
the
Co-Authored-Byline recording model, context window, and effort level.
Administration and logistics¶
Success criteria¶
We will know when this plan has been successfully implemented because the following statements will be true:
pre-commit run --all-filespasses.scripts/audit-check.pyrun against this repository reports no new failures, or the plan states which verdicts moved and why.prunestill only ever removes marks, andimportis the only code path by which a mark is added without a human. It writes only to.vscode/imports.weaudit-shas.json; no automated process writes to any.weauditfile or a reviewer's sidecar.- Every imported mark names a signed commit in this repository that introduced a full-file review of the same blob, or the run that imported it said on stderr that it could not verify.
- The three review tracking CI files are byte-identical across the four adopted repositories (and this repository, for the two it shares).
- Anything under
templates/is judged as the code it will become in ten other repositories: placeholders consistent, no reference to paths that only exist here, and the README beside it saying what to substitute. - Python is wrapped at 120 characters, single quotes for strings and double quotes for docstrings, and no script has grown a dependency outside the standard library.
- Documentation in
docs/describes any user-visible change.AGENTS.mdchanges only if a convention changed;ARCHITECTURE.mdonly if the shape of the system changed;README.mdonly if the pitch, the install story or the documentation links changed.
Documentation index maintenance¶
When creating a new master plan from this template, add one row to
the table in docs/plans/index.md: the date the plan was written,
a link to it, a one-line intent, and its status from the
vocabulary above. Rows run oldest first. One row per master plan,
never one per phase -- the phases are tracked in the plan's own
Execution table, and duplicating them in the index is how the two
drift apart.
There is no phase-arithmetic column and no order.yml here; both
belong to repositories whose documentation is published through a
generated navigation. The plan-index criterion checks the
columns this index actually has.
The index row carries the whole-plan status, so it only reaches
Complete once every phase has been completed, abandoned or
superseded. Update it as the plan progresses, not only at the end.
Plan close-out sections (shared block; do not edit -- the
canonical copy lives in shakenfist/development at
templates/shared-blocks/plan-closeout-sections.md):
Future work¶
We should list obvious extensions, known issues, unrelated bugs we encountered, and anything else we should one day do but have chosen to defer to here, so that we do not forget them.
...
Bugs fixed during this work¶
This section should list any bugs we encounter during development that we fixed. You should also scan the project's issue tracker, where one exists, for directly related issues that we should either resolve as part of this master plan or at least be aware of while planning it.
...
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.
In this repository. Future work, as of writing:
- A
verifysubcommand that walks every mark in a repository to the signed commit that introduced its stamp -- following an imported mark's provenance into this repository -- and checks each signature. Today verification of native marks is a manual procedure too; this plan does not change that, but import makes it more worth automating. - Reusable workflows. Moving templates such as
export-repo-config.ymltoworkflow_callworkflows would remove the copies altogether, leaving small identical callers. It changes the trust model (a caller pinned by SHA, bumped by Renovate) and is a bigger decision than this plan should make. - A template conformance criterion. Tracked in development#151. Phase 4 found that templates changed again within days of phase 3's convergence. Renovate's bumps converged by themselves; the hand edits to the bot workflow templates (5e9a4b3, 3b5142c) needed a second round of four re-sync pull requests. #151 asks the same question across every template, not only the ones import touches.
- Imported marks are invisible in the editor. Phase 4 found
that kerbside (2dafb4e) and ryll (3d16282) both carried native
review commits that re-stamped files already covered by an
import, even though the imported blobs had not changed. The
likely cause is that an imported entry lives only in
.vscode/imports.weaudit-shas.json, never in a.weauditfile, so weAudit's own UI shows the file as unreviewed and a reviewer working through the tree re-reads it anyway. A possible direction is to show imported files as reviewed in weAudit, or to havenextand the reviewer's own workflow surface that a file is already covered by an import; the design is left open. - Shared blocks. See the mission statement for why they are out of scope.
- Upgrading unverified imports. An entry imported with
"verified": false(gitsign absent, or--no-verify) is never re-checked. If phase 2 finds the runners cannot reach Rekor, those entries stay unverified after the runners are fixed;importshould re-verify them when it can, and drop any that fail. - A failed verification hides later attestations. The history
walk records only the earliest commit that attests to a blob. If
that commit's signature does not verify, the blob is skipped even
when a later, signed commit attests to the same blob. Rare, since
review commits are signed, but falling back to the next
attestation would be more correct.
The same applies when the earliest attestation is by a reviewer
with no entry in
REVIEWER_IDENTITIES. - Split
scripts/review-tracking.py. Phase 1 took it from 636 to about 1,230 lines, past the ~800-line mark intemplates/shared-blocks/source-file-size.mdthat this repository publishes to the fleet. The file is under whole-file review, so every change to it costs a re-read of all of it. Raised by the automated review of #177 and deliberately not done there, where it would have buried the logic change in the diff. - Automate the gitsign bump.
GITSIGN_VERSIONandGITSIGN_SHA256intemplates/review-tracking/ci-prune-reviews.share bumped by hand. A Renovate regex manager can bump the version but not the binary's digest, so a half-automated bump would fail the checksum check; this wants either a Renovate post-upgrade task or a small scheduled job. Raised by the automated review of #186. - Stagger the daily run. Every adopted repository runs the
template's
23 4 * * *schedule at the same minute on the shared static runners. They queue rather than fail, so this is left alone unless phase 4 shows the queue matters; a per-repository cron would break byte-identity for that file. Raised by the automated review of #186. - What the retest bot should dispatch. kerbside's
pr-retest.ymlnotes that dispatchingfunctional-tests.ymlruns the merge tier, not the lanes that gate a pull request there. Phase 3 makes the target a repository variable; whether the bot should instead re-run a pull request's own gating lanes is a change to the review automation template, not to convergence. - Stamps are not tied to content in the commit's ancestry. Raised by phase 5's security audit (5e-L1). A stamp is matched to a file by an exact content match today, which is deliberate: 8 legitimate stamps survive rebases that an ancestry check would break. Tying a stamp to its blob's ancestry instead would catch a narrower class of forgery, but is a larger change than this plan should make.
Related issues: development#173 is this repository's own review-coverage issue; import does not affect it, because this repository does not import from itself.