Plan: restructure the audit scripts into a package of check classes¶
Prompt¶
Before executing any part of this plan, read the code it moves.
scripts/audit-check.py is 6,657 lines and its test file is 6,148;
neither can be held in your head, and this plan's central risk is a
line that changes while looking like it did not.
Read docs/consistency-audits.md first -- it is the reference for
what a daily run does and how a criterion is added -- then
AGENTS.md for the invariants that are not visible in the code, and
ARCHITECTURE.md for how the pieces fit. PUSH-AUDIT.md is the
pre-push runbook this plan's last phase runs, and its "Duplicated
logic" bullet currently tells reviewers something this plan changes.
The two standing constraints from PLAN-TEMPLATE.md bind hard here.
The blast radius is other people's repositories: this tooling files
and closes GitHub issues fleet-wide every morning, so a regression
does not produce a red build. And this repository is inside its own
audit matrix, so it is measured by the tooling it is changing while
the change is in flight.
Ground every claim in the code. Where this plan quotes a number --
45 checks, 21 gh calls, 20 untested checks -- re-derive it before
relying on it; the tree moves.
Situation¶
scripts/ is the whole of this repository's code, grown by
accretion one criterion at a time, and the growth has landed almost
entirely in two files:
| File | Lines | What is in it |
|---|---|---|
audit-check.py |
6,657 | 45 check functions, ~90 helpers, ~40 module-level constant and regex blocks |
test_audit_check.py |
6,148 | 45 TestCase classes, 107 temporary-directory sites, ~40 per-class _repo/_check helpers |
test_audit_update_docs.py |
786 | |
test_review_tracking.py |
736 | |
review-tracking.py |
636 | a separate CLI, cmd_* dispatch |
test_issue_fix_extraction.py |
530 | |
audit-update-docs.py |
379 | |
audit-manage-issues.py |
330 | |
audit_common.py |
323 | AUDIT_METADATA, ISSUE_TITLES -- data only |
test_check_audit_smoke.py |
132 | |
check-audit-smoke.py |
90 | |
commit-audit-docs.sh |
31 |
Size on its own is not a reason to move code, and PUSH-AUDIT.md
says so to reviewers today: "audit-check.py is 5,000 lines of
check functions that resemble each other by design. Flag duplication
only where a helper already exists and was not used." That is right
about the check bodies and wrong about what surrounds them. Three
specific things are wrong, and each is measurable.
Twenty of the forty-five checks have no direct test. Counting
call sites of audit_check.check_*( in test_audit_check.py:
| Coverage | Checks |
|---|---|
| No direct test | default-branch-naming, github-security, delete-branch-on-merge, merge-queue-config, export-repo-config, devpi-fallback, devpi-stale-ip, flake8wrap, llm-tooling, pre-commit-config, pyproject-usage, readme-absolute-links, release-process, rust-unwrap-lint, secret-scanning-ci, static-runner-tags, version-file-gitignore, workflow-permissions (plus the two check_file_* helpers) |
| One or two | 21 checks |
| Three or more | 6 checks |
The untested set is not random. Every check that queries the GitHub
API is in it, because there is nothing to fake: audit-check.py
makes 21 direct subprocess.run(['gh', ...]) calls, each carrying
its own copy of the timeout and FileNotFoundError handling. The
workaround already exists, applied by hand and exactly once --
evaluate_merge_queue_rules() was split out as a pure function so
MergeQueueConfigTest could test the ruleset logic, and the gh
wrapper around it stayed untested. That is the right instinct with
nowhere to put itself.
The check id is a string literal repeated two to nine times per
check. 'id': 'python-version-targeting' appears nine times;
'id': 'pyproject-usage' eight. Roughly 180 literals that must
agree with each other and with the id registered in
check_calls(). A test asserts the last of those agreements,
because that is the one that would make a check unschedulable; the
rest are unguarded.
A criterion spans four files, and the sync is enforced by tests
rather than by structure. AGENTS.md opens with it and
docs/consistency-audits.md spells it out over five numbered steps,
noting that step 4 "is the one that bites" -- its absence broke the
2026-08-12 run, which rewrote every docs/audits/*.md before
crashing without committing any. The tests that now guard it are
good tests. They are also evidence that the shape is wrong: an
invariant needs a test because the structure cannot state it.
Underneath all three is the same absence. There is no Check type,
no repository type and no GitHub client type, so there is nowhere
for an id, a spec path, an issue title, a cached file read or a fake
API response to live.
Mission and problem statement¶
Turn audit-check.py into a package where a criterion is a class in
one module beside its siblings, every check is testable including
the ones that talk to GitHub, and the id is written once. Adding a
criterion should touch a check module and a spec page, not four
files kept in step by tests.
The measure of success is negative: the audit must say exactly what it said yesterday about the real fleet, at every commit along the way.
Deliberately out of scope:
- Changing what any criterion means. No check gets stricter,
looser or newly applicable. A
detailsstring that is genuinely wrong changes in its own commit, with the compliance-page effect stated in the message. review-tracking.py. It shares a directory with the checker and nothing else. Recorded under Future work.- New dependencies. The scripts are stdlib-only and stay that way.
- Moving the entry point.
scripts/audit-check.pykeeps its path, its arguments and its JSON.
Open questions¶
- Does
audit_common.pysurvive as a module? This plan keeps the name and its two exports so thataudit-manage-issues.pyandaudit-update-docs.pyare untouched, and makes the exports derived views. Folding it intoscripts/audit/registry.pywould be tidier and would spread the change into two more scripts. Default if nobody answers: keep the module. - Is one pull request per phase right for phase 3? Eight commits
in one pull request is a large review; eight pull requests is
eight mornings of a half-migrated scheduler. Default: one pull
request, eight commits, with the phase not complete until
check_calls()is empty. - Should the snapshot harness live in
tools/permanently, or be deleted with the plan? It is useful beyond this work -- any change to a check wants it -- so the default is to keep it and document it indocs/consistency-audits.md.
Decisions¶
D1. Classes at three seams, and nowhere else¶
Three types earn their existence:
Check-- an abstract base holdingid,spec,template,issue_titleand an optionalcolumnas class attributes, withapplies(repo)returning a skip reason orNone, and an abstractrun(repo)returning aResult.self.ok(),self.fail()andself.skip()build the result dict, so the id is written once.Repo-- replaces the(repo_path, props, repo_name, org)tuple threaded through every signature, with cached accessors:exists(),read(),workflows(),docs_markdown(). Caching is not premature:list_workflow_files()is called from 14 places and there are 41open()sites, so a daily run re-reads every workflow file a dozen times.GitHubClient-- a protocol with aghCLI implementation and a fake. This is the one that buys the missing coverage; the other two are tidying.
Everything else stays a module-level function. mask_source(),
evaluate_merge_queue_rules(), split_runner_labels(),
blank_generated_blocks() and their kin are already the most
testable code in the file. They move to new homes unchanged. A
refactor that turns pure functions into methods to make a diagram
tidier makes the tests worse, and we would notice only afterwards.
D2. Modules by spec family, not one file per check¶
Forty-five files would trade one unnavigable file for a directory
nobody can hold in their head, and it would fight the code: the plan
checks share their PLAN_* regexes, the runner checks share label
parsing, the LLM-doc checks share heading iteration. Eight modules
of four to seven hundred lines, grouped the way the specifications
are:
scripts/audit/
check.py Check, Result, the status vocabulary
repo.py Repo, detect_repo_properties, REPO_OVERRIDES
github.py GitHubClient, GhCli, FakeGitHub
registry.py CHECKS, and the metadata views derived from it
text/
markdown.py links, fences, headings, code stripping
workflows.py job blocks, runs-on labels, concurrency
python_source.py mask_source, class parsing, entry points
shared_blocks.py block extraction and validation
checks/
llm_docs.py ci_workflows.py runners.py packaging.py
docs_content.py plans.py review.py github_config.py
scripts/audit-check.py a CLI shim: argument parsing, JSON out
The text/ split is where the ~40 module-level regex blocks go, and
it is the part with real reuse in it today. It also ends the
importlib.util.spec_from_file_location dance the tests need,
because audit-check.py has a hyphen and cannot be imported.
D3. The registry becomes the source of truth, and the metadata is derived¶
registry.py holds CHECKS, a list of instances.
audit_common.AUDIT_METADATA and ISSUE_TITLES become views
computed from it, and audit-update-docs.py's COLUMN_NAMES
derives from Check.column. Neither audit-manage-issues.py nor
audit-update-docs.py changes: they keep importing the same two
names from the same module and get the same dicts.
Adding a criterion then means a check class and a spec page. Steps 2
and 4 of docs/consistency-audits.md disappear, and the tests that
guard them become unnecessary rather than merely passing.
ISSUE_TITLES is an idempotency key, not a label. A renamed
entry orphans every open issue for that check across the fleet,
which is why AGENTS.md calls it an interface. Deriving it puts 45
strings within reach of an accidental edit, so the derivation is
pinned: a test holds today's AUDIT_METADATA and ISSUE_TITLES as
literal frozen dicts and asserts the derived views equal them
exactly. That is not maintenance -- a new check adds a line, and a
changed line is supposed to be hard.
D4. audit-check.py keeps its path and its output¶
The daily workflow, ci.yml, README.md and three documentation
pages name the script by path. Making the package importable does
not require moving the entry point, and moving it would spread this
change into the workflows for no gain. It becomes a shim that parses
arguments, builds a Repo, runs the registry and prints the same
JSON.
The JSON is a contract in a stronger sense than most: details
strings are rendered into docs/audits/compliance.md and into issue
bodies filed fleet-wide. A reworded detail is a diff on the
compliance page and, worse, is invisible in review.
D5. The regression net is a local before-and-after diff, not a committed fixture¶
Correctness here means "the audit says exactly what it said yesterday about the real fleet". Synthetic fixtures cannot show that; the real clones can.
So the net is tools/audit-snapshot.sh <clones-dir> <out-dir>,
which runs the checker over every clone and writes one JSON file
each, plus a comparison run after each phase. The snapshots are not
committed. Generated JSON in scripts/ or docs/ would land in
review scope -- .vscode/review-scope.toml includes *.json -- and
would either need an exclusion argued for or would sit permanently
stale in the review queue. A repeatable command in tools/ and a
scratch directory get the same guarantee without that.
Two wrinkles, both real:
- The
timestampfield moves on every run and is stripped before comparison. - Six checks reach the network and so are not deterministic across
two runs: a repository setting can genuinely change in between.
They are
default-branch-naming,github-security,delete-branch-on-merge,merge-queue-config,merge-group-cancellation(throughmerge_queue_is_serial) andsfui-vendor(which clones). Until theGitHubClientseam exists they are advisory in the diff. From phase 2 onward a recording implementation captures theghresponses on the "before" run and replays them on the "after" run, so the five API checks can be made exact.
sfui-vendor cannot, and stays advisory permanently. It does not
make a request and read a response; it clones the canonical
repository into a temporary directory and runs that repository's
own tools/vendor.sh against the working tree it produced.
Recording a working tree is not the same problem as recording a
response, and it already has the seam it needs: canonical_url is
a parameter, and SfuiVendorTest drives it against a local fixture
repository. It is among the better tested checks here, not one of
the gaps.
Note that this set is not the github_config family of phase 3.
export-repo-config makes no API call, while
merge-group-cancellation and sfui-vendor sit in other families
that migrate earlier -- which is why phase 2 routes every gh
call through GhCli rather than leaving it to the family that
looks like it owns them.
D6. applies() is separate from run() so scoping stays cheap¶
check_calls() returns deferred lambdas for a stated reason: a
repository scoped with only_checks must be able to skip a check
without paying for it, because several checks query the GitHub API
and those queries fail on a private repository for reasons that have
nothing to do with compliance. Splitting the cheap applicability
test from the expensive body preserves that: the scheduler consults
only_checks, then applies(), and calls run() only if both let
it through. It also lifts the not_applicable preamble out of most
of the 45 function bodies, which is where a good deal of the
repetition lives.
D7. Tests move with their checks, on a shared fixture base¶
scripts/tests/ mirrors scripts/audit/, one test module per check
module. Two pieces of shared machinery replace what is copied per
class today:
FixtureRepo--write(),workflow(),git(),commit(), wrapping the temporary directory. There are 107 temporary-directory sites and about 40 near-identical_repo/_checkhelpers.CheckTestCase--assert_pass(),assert_fail(containing=...),assert_skip().
And one thing that does not exist today: contract tests parametrized over the whole registry. Every check has a spec file that exists on disk; is registered exactly once; returns one of the three statuses; and survives an empty repository, a docs-only repository and a directory that is not a git checkout without raising. That last group is the one with real value, and it is only writable once the checks are uniform.
The GIT_* environment scrub at the top of test_audit_check.py
moves to the shared base. It exists because the pre-commit hook runs
these tests during git commit, when git exports GIT_INDEX_FILE
to hooks and the fixture subprocesses inherit it and write to the
real index. Losing it in the move would corrupt the index of
whoever commits next.
D8. Sequencing against the human review of scripts/¶
A whole-file review mark attests to content by blob SHA and is
discarded when the file changes, so this restructure discards every
mark on every file it moves. A review of scripts/ is in flight
right now -- origin/reviews carries a fresh mark on
scripts/audit_common.py, dated 2026-09-01 -- and audit_common.py
is precisely the file phase 4 turns into derived views.
The restructure goes first. Attesting to 6,657 lines that are about to be split into eight modules spends the scarcest thing here on content that will not survive, and the review is worth more against the structure we intend to keep. Two consequences to accept openly:
- Review coverage in
REVIEWS.mddrops when this lands, and thereview-coveragecriterion may file an issue against this repository. That is the check working. It is not silenced. - Findings from the in-flight review are not wasted and must not be dropped. Any still unaddressed when phase 3 starts are carried into "Bugs fixed during this work" and fixed in the module they land in.
Execution¶
Work happens on audit-scripts-restructure off main; this plan
file lands with the change. There is no develop branch here --
REPO_OVERRIDES exempts this repository from the default-branch
criterion because it publishes no releases.
Phases are the sections below rather than separate files, following this repository's convention.
| Phase | Status | Merged |
|---|---|---|
| 1. Freeze today's behaviour | Complete | 76975d9..42565c2 |
| 2. Introduce the three seams | Complete | 76975d9..42565c2 |
| 3. Migrate the checks, one family per commit | Complete | 76975d9..42565c2 |
| 4. Make the registry the source of truth | Complete | 76975d9..42565c2 |
| 5. Close the coverage gap | Complete | 76975d9..42565c2 |
| 6. Push audit | Complete | 76975d9..42565c2 |
All six phases ship as a single pull request, so the Merged record
is one entry rather than six. The merge commit's SHA cannot be known
before the merge, and the plan is being closed with the work rather
than reopened afterwards to write it down, so the range is recorded
in the form plan-push-audit-phase allows for a phase that landed
directly: 76975d9..42565c2, seventeen commits, the whole of this
branch. git log --first-parent 76975d9~1..42565c2 is what a later
audit reads.
If the branch is squashed or rebased on the way in, those SHAs stop resolving. Say so on the pull request if that happens, and replace them with the merge commit -- that is the one edit worth a follow-up, and it is a one-line one.
The branch also carries a merge of origin/main, taken after the push
audit because main moved while this was in flight and REVIEWS.md
conflicted. That is outside the audited range deliberately: the merge
resolves one generated file by regenerating it, and re-auditing to
cover a rendering of the review marks would say nothing. The audited
range is the one recorded above.
Phases 1 and 2 are small and ship together as one pull request. Phase 3 is the bulk of the work and ships as its own, one commit per check family, because a reviewer can read a family in one sitting and cannot read eight. Phases 4 and 5 ship together: phase 4 deletes the sync tests and phase 5 replaces the coverage they were standing in for, and landing the deletion alone would leave the repository briefly worse.
Phase 3 has a hard rule: it does not end with the registry and
check_calls() both populated. A half-migrated scheduler is the
failure mode this plan is most likely to produce, and the audit runs
every morning regardless of what we have finished.
1. Freeze today's behaviour¶
tools/audit-snapshot.sh(new). Takes a directory of clones and an output directory, runsscripts/audit-check.pyover each clone, writesaudit-result-<repo>.jsonper repository, and strips thetimestampfield. A--diff <old> <new>mode reports per-check differences, listing the six network-dependent checks separately as advisory until phase 2. The advisory list is re-derived fromaudit-check.pyby a test, so a check that grows aghcall cannot leave it stale.docs/consistency-audits.mdgains a short subsection under "Testing a change" describing the before-and-after procedure, so it is available to work after this plan as well.- Capture the baseline over the fleet clones under
~/src/shakenfist/and keep it for the duration.
Nothing else changes. This phase is the reason the rest can be judged.
2. Introduce the three seams¶
scripts/audit/check.py--Result, the status vocabulary, and theCheckbase withapplies()/run()and theok/fail/skipconstructors.scripts/audit/repo.py--Repowith the cached accessors, plusdetect_repo_properties()andREPO_OVERRIDESmoved verbatim.Repo.propskeeps the existing dict so migrated and unmigrated checks read the same thing.scripts/audit/github.py--GitHubClient,GhCliholding the timeout andFileNotFoundErrorhandling once,FakeGitHubtaking a dict of responses and able to inject failures, andRecordingGitHubClientfor the snapshot harness.scripts/audit/registry.py--CHECKS, empty, and a scheduler that runs the registry and then the survivingcheck_calls()entries.scripts/audit-check.pybecomes the shim.run_all_checks()keeps its name and signature;test_check_audit_smoke.pyandci.ymlboth drive it.
Behaviour is unchanged and the snapshot must be identical. From here the API checks replay recorded responses, so the diff is exact for all 45.
3. Migrate the checks, one family per commit¶
Eight commits, in ascending order of risk, so the pattern is settled before it meets the checks that matter most:
llm_docs.py--llm-tooling,llm-doc-structure,llm-context-lint,llm-context-lint-ci.docs_content.py--readme-structure,readme-absolute-links,docs-external-links,diagram-format,mermaid-lint-ci.plans.py--plan-index,plan-template,plan-source-references,plan-phase-references,push-audit.packaging.py--pyproject-usage,version-file-gitignore,python-version-targeting,pin-indirect-dependencies,dependency-name-normalization,renovate,release-process,rust-unwrap-lint,flake8wrap,console-logging,header-sanitization.runners.py--self-hosted-runners,static-runner-tags,vm-runner-size.ci_workflows.py--workflow-permissions,pre-commit-config,expensive-lane-path-filter,merge-group-cancellation,ci-review-automation,secret-scanning-ci,devpi-fallback,devpi-stale-ip.review.py--review-coverage,review-scope-completeness,review-marks-pre-commit,sfui-vendor.github_config.py--default-branch-naming,github-security,delete-branch-on-merge,merge-queue-config,export-repo-config. Last, because all five are untested and four of them query the GitHub API, so by now there is a fake to write them against.export-repo-configis in the family for what it is about rather than for how it works; it reads the filesystem.
Each commit moves the checks, moves their shared constants into
text/, moves their tests to scripts/tests/, converts those tests
to the shared fixture base, and empties the corresponding entries
from check_calls(). Each commit runs pre-commit run --all-files
and the snapshot diff, and each is expected to produce an empty
diff.
Detail strings are copied, not rewritten.
4. Make the registry the source of truth¶
AUDIT_METADATAandISSUE_TITLESbecome derived views inaudit_common.py, computed fromregistry.CHECKS. The module keeps its name and its exports.COLUMN_NAMESinaudit-update-docs.pyderives fromCheck.column.column_name()keeps its warning-and-ugly-heading fallback: a run that publishes a bad label still beats a run that publishes nothing.- A frozen-snapshot test pins both derived dicts to today's values.
- The cross-file sync tests that are now structurally impossible to break are deleted, and the ones that are not are kept and said to be kept.
- Documentation:
docs/consistency-audits.md"Adding a criterion" becomes two steps;AGENTS.mdloses the four-files paragraph and keeps theISSUE_TITLESwarning, repointed;ARCHITECTURE.mdgets the new module inventory;README.md's link toscripts/audit-check.pystill resolves and stays;PUSH-AUDIT.md's "Duplicated logic" bullet is rewritten, because it currently tells reviewers that these functions resemble each other by design.
5. Close the coverage gap¶
Write the missing tests, against the seams rather than around them.
The 18 checks with no test get one each at minimum -- pass, fail and
not-applicable. The five that call the GitHub API additionally get
the failure paths that have never been exercised: API error,
timeout, gh absent, private repository. sfui-vendor is not among
them -- it reaches the network but already has its own seam and its
own tests.
Then the registry contract tests from D7.
The measure is not a coverage percentage. It is that every check in
CHECKS appears in a test module, asserted by a contract test, so
the next check cannot arrive untested.
6. Push audit¶
Run PUSH-AUDIT.md over the accumulated diff of phases 1 to 5
against main, not phase by phase. Findings land as their own pull
request; the plan is not complete until each is resolved or declined
in writing, with the reason recorded here. If the audit finds
nothing, say so in one sentence.
The brief worth stating in advance: this is a move of roughly 13,000
lines, and the failure mode of a move is a line that changed while
looking like it did not. The audit should read for behaviour drift
in detail strings and in regex constants above everything else, and
should check that no check lost its applies() guard on the way
into a class.
Outcome. Wave 1 passed. pre-commit clean; no line over 120
characters; no import outside the standard library and this package;
no edit to docs/audits/compliance.md; no generated block in a
specification; templates/shared-blocks/ untouched, so nothing is
newly non-compliant fleet-wide. Two greps hit and both were looked
at: ISSUE_TITLES changed, which is phase 4's derivation and is
pinned by scripts/tests/test_metadata.py; and two TODO matches
are pre-existing fixture text moved verbatim rather than new debt.
REVIEWS.md is current and every mark on an edited file was pruned
rather than re-stamped.
The brief's own question was answered directly rather than by
reading. Seven criteria have neither an applies() method nor a
skip() path -- llm-tooling, renovate, ci-review-automation,
pre-commit-config, export-repo-config, github-security and
delete-branch-on-merge. Checking each against the phase 1 tree:
none of the seven ever reported not_applicable, so no guard was
lost. They are pass-or-fail criteria by design.
One finding, fixed here rather than deferred because this work
introduced it: docs/audits/README.md still told a reader that
adding a criterion touches four files. Every other document that
said so was updated in phase 4; that one was missed because it
describes the specification directory rather than the tooling.
Two things the audit surfaced that are not findings against this
work, recorded so the next reader does not re-derive them.
scripts/check-audit-smoke.py fails locally because skillsaw is
not installed outside the audit's venv, which is the smoke checker
working as designed -- llm-context-lint reporting not_applicable
for want of its tool is exactly what it exists to catch. And
PUSH-AUDIT.md still writes every diff against main...HEAD; this
audit was run against origin/main...HEAD because local main was
four commits stale, which would have silently widened the diff. That
is the correction already recorded under Future work in
PLAN-audit-compliance-split.md and it is still owed.
Risks and mitigations¶
- The daily run files and closes real issues fleet-wide. A
regression does not produce a red build; it produces issues in
other people's repositories, or silently closes ones that should
be open. Mitigations: the snapshot diff is required to be empty at
every commit;
audit-manage-issues.pyis run only with--dry-runby hand; each pull request lands with a morning between it and the next, and the first 06:00 UTC run after each landing is checked before the next phase starts. - Review marks on
scripts/are discarded. Accepted deliberately in D8. Coverage drops, an issue may be filed, and the re-review is worth more against the new structure. Not silenced. ISSUE_TITLESdrifts and orphans open issues. The frozen snapshot test in phase 4, and the fact that a title change must edit a literal dict to pass.- Detail strings reworded by accident. The snapshot compares them byte-for-byte, which is the only reason it is worth running against real clones rather than fixtures.
- The migration stalls half-done. The most likely bad outcome: a
hybrid scheduler is a working state, so nothing forces the second
half. Phase 3 is defined as not complete until
check_calls()is empty, and ships as one pull request, so a stall is an unlanded branch rather than a landed half-measure. - The changed reviewer guidance.
PUSH-AUDIT.mdcurrently instructs reviewers to expect resemblance between check functions. Phase 4 updates it; until then a reviewer working from the old wording will argue with the refactor, and phase 3's pull request description says so.
Agent guidance¶
Execution model¶
The standard model from PLAN-TEMPLATE.md: implementation by
sub-agents, review in the management session, read the files rather
than trusting the summary.
Two adjustments for this plan. Phase 3's commits are mechanical in shape and subtle in consequence, so the management session's review is not "does it look right" but "does the snapshot diff empty" -- run it, do not accept a sub-agent's report of it. And phases 2, 4 and 6 stay in the management session's hands for the decisions inside them, even where a sub-agent does the typing.
Planning effort¶
The master plan is high effort. Per phase:
- High: phases 2 and 3. Phase 2 is the design; phase 3 carries the fleet-wide risk across 45 checks.
- Medium: phases 1, 4 and 5. Mechanical, or following the pattern phase 2 establishes.
- Phase 6 takes its effort from
PUSH-AUDIT.md.
Step-level guidance¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 1a | medium | sonnet | none | Write tools/audit-snapshot.sh: run scripts/audit-check.py over each clone in a directory, write one JSON per repo with timestamp stripped, and a --diff mode reporting per-check differences with the six network-dependent checks listed separately, plus a test re-deriving that list from the checker |
| 1b | low | haiku | none | Add the before-and-after procedure to docs/consistency-audits.md under "Testing a change" |
| 2a | high | opus | worktree | Create scripts/audit/ with check.py, repo.py, github.py, registry.py per D1 and D6. No checks move. run_all_checks() keeps its name and signature and runs the empty registry then check_calls(); snapshot must be identical |
| 2b | high | opus | worktree | Add FakeGitHub and RecordingGitHubClient, and route all 21 gh calls through GhCli without moving the checks that make them |
| 3a-3h | high | opus | worktree | One per family in the order listed under phase 3. Move the checks to classes, their constants to text/, their tests to scripts/tests/ on the shared base, and empty their check_calls() entries. Detail strings copied verbatim. pre-commit run --all-files and an empty snapshot diff before handing back |
| 4a | medium | sonnet | none | Derive AUDIT_METADATA, ISSUE_TITLES and COLUMN_NAMES from the registry; add the frozen-snapshot test pinning the first two to today's literal values |
| 4b | medium | sonnet | none | Update docs/consistency-audits.md, AGENTS.md, ARCHITECTURE.md and PUSH-AUDIT.md per phase 4 |
| 5a | medium | sonnet | none | Tests for the 18 checks with none: pass, fail, not-applicable each |
| 5b | medium | opus | none | Failure-path tests for the five checks that call the GitHub API, and the registry contract tests from D7 |
| 6a | high | opus | none | Run PUSH-AUDIT.md over the accumulated diff of phases 1-5 against main |
A brief that says "move the plan checks" is not enough. Name the check ids, the destination module, the constants that move with them, and the test classes that follow -- the ids are listed per family under phase 3 precisely so a brief can quote them.
Model choice¶
Opus for anything that moves a check or designs a seam; the risk is a silent behaviour change, and that is exactly what a lighter model misses while producing plausible-looking code. Sonnet for the documentation and the mechanical test-writing in phases 4 and 5. Haiku only for 1b.
The project-specific checks for this plan are:
-
pre-commit run --all-filespasses. -
tools/audit-snapshot.sh --diffbetween the phase 1 baseline and the current tree reports no differences. -
python3 scripts/audit-check.py --repo-path . --repo-name developmentreports what it reported before, allowing for the knownreview-coveragemovement from D8. -
audit-manage-issues.pyhas been run with--dry-runonly.
Management session review checklist¶
The standard checklist from PLAN-TEMPLATE.md, plus:
- The snapshot diff was run by the management session, not reported by the sub-agent.
- No
detailsstring changed. Where one did, it is its own commit and the message says what moves on the compliance page. - Every check that had an
applies()-equivalent early return still has one; a lostnot_applicableguard reads as a new failure across the fleet. -
check_calls()is smaller than it was, and empty by the end of phase 3.
Administration and logistics¶
Success criteria¶
We will know when this plan has been successfully implemented because the following statements will be true:
scripts/audit/holds the package described in D2, andscripts/audit-check.pyis a shim under 100 lines with unchanged arguments and JSON.check_calls()no longer exists;registry.CHECKSschedules all 45 checks.AUDIT_METADATAandISSUE_TITLESare derived and pinned by a frozen-snapshot test.- Every check in
CHECKShas a test module entry, asserted by a contract test; the 18 previously untested checks have pass, fail and not-applicable cases; the five checks that call the GitHub API have their failure paths. tools/audit-snapshot.sh --diffover the fleet clones reports no differences between the phase 1 baseline and the final tree.pre-commit run --all-filespasses.- One 06:00 UTC run has completed after the final landing, and
docs/audits/compliance.mdis unchanged by it apart from its timestamp. docs/consistency-audits.md,AGENTS.md,ARCHITECTURE.mdandPUSH-AUDIT.mddescribe the structure that exists.- No script has acquired a dependency outside the standard library.
- Phase 6's findings are resolved or declined in writing.
Documentation index maintenance¶
This plan carries a row in docs/plans/index.md -- date, link,
one-line intent, status. The index row is the whole-plan status and
only reaches Complete once every phase in the Execution table has
been completed, abandoned or superseded. Update it as the plan
progresses, not only at the end.
Future work¶
review-tracking.py. 636 lines, a cleancmd_*dispatch, and it would read better with aScopeand aReviewStateobject behind the commands. It is not hurting anyone today and it shares nothing with the audit checker but a directory, so it is a separate change. Its tests are also the ones most entangled with real git fixtures, and theFixtureRepohelper from D7 is what it would want first.check_review_coveragecan put a subprocess traceback into a detail string. Noted already under Future work inPLAN-audit-compliance-split.md. Oncedetailsconstruction goes throughResult, capping and sanitising it is a change in one place, which is the argument for doing it after this plan rather than during.- Adopt
Repo.read()in the checks. The content cache exists and is tested, but production does not reach it: the check modules make 35 directopen()calls and none go throughread(), so the only caller isworkflow(), which only the seam tests use. The directory-listing cache argued for in D2 was realised; the read cache was not. Converting the 21 repository-relative sites is mechanical --exists()thenopen()collapses into oneread()returningNone-- but it is 21 behaviour-preserving edits across eight modules, which wants its own snapshot run rather than being bolted onto this one. Raised in review of PR #79. - Parallelising the checks. Uniform
Checkobjects with a declared GitHub dependency make it possible to run the filesystem checks concurrently. The daily run is not slow enough to justify it, and concurrency would make the snapshot diff harder to trust. Recorded because it becomes cheap, not because it is wanted. - A check with no spec page, or a spec page with no check. The
contract tests in phase 5 catch the first;
unmeasured_specs()already does the second. Worth folding together once both live in the same place. -
PLAN-TEMPLATE.mdarrived with this plan. It was written because this repository holds the fleet to theplan-templatecriterion and had no template of its own, sodevelopmentwasN/Aon a standard it enforces. Nothing else here depends on it. -
Shared blocks propagate to this repository first and lag everywhere else. Both places a block is required show the same pattern, from
docs/audits/compliance.mdrather than from local clones, which are stale enough to give the opposite answer: -
push-audit: all eight repositories that carry aPUSH-AUDIT.mdare non-compliant, and every one of them is missingdiagram-discipline-- added here on 2026-08-29 and nowhere else yet. Five also lackpath-traversal-review,python-version-disciplineandfunctional-test-coverage; occystrap and sfui additionally do not referencePUSH-AUDIT.mdfromAGENTS.md, so their audit is one nothing points at. plan-template: three of the seven repositories with a template are non-compliant -- divergulent, occystrap and shakenfist -- all missingplan-push-audit-phase.
Every one of these already has an open tracking issue, so the criteria are working as designed and this is a backlog rather than a discovery. It is worth recording here only because adding a required block is a fleet-wide edit that lands as one commit in this repository and eight pull requests elsewhere, and the lag between the two is invisible from inside this repository. Whether the block-refresh should be templated or scripted is a separate question from this plan.
Bugs fixed during this work¶
To be filled in as the work proceeds. Anything still open from the
in-flight review of scripts/ on origin/reviews when phase 3
starts is carried here, per D8.
Back brief¶
Before phase 1 begins, the executing session back briefs Mikal on
its understanding of this plan. Three things are worth confirming
before any code moves, because they are cheap now and expensive
later: the module grouping in D2, the decision in D3 to derive
AUDIT_METADATA and ISSUE_TITLES rather than keep them
hand-written, and the sequencing against the human review in D8.