Consistency audit phase 3: skillsaw CI detection, upstream¶
Master plan: PLAN-consistency-audit.md
Planning effort: medium. The change is small and the argument for it is strong, but it lands in another repository, in a checker that audits nineteen repositories at once. The care is in not fixing kerbside's finding by loosening a check everyone else relies on.
Scope¶
In scope:
- Issue #359, by changing
check_llm_context_lint_ci()inshakenfist/development'sscripts/audit-check.pyso a workflow that demonstrably runs skillsaw satisfies the CI half, however the linter was installed. - Tests in
scripts/test_audit_check.pycovering kerbside's shape, and covering the cases the widening must not let through. - A regression sweep of every sibling repository, so the widening is shown to change exactly one verdict.
Out of scope:
- Any change to kerbside itself. This phase's only kerbside
artefact is this plan file.
functional-tests.ymlalready runs the linter correctly and must not be rewritten to suit a checker. REPO_OVERRIDES. See decision 2.- Issues #370 and #381, the two new findings that arrived while phase 2 was in flight. They are local work and are now registered as phase 5.
What the survey found¶
The master plan's phase 3 sketch is accurate in every particular, which is worth stating plainly because the two previous phases both found their sketch stale. Every claim was re-verified against both trees today.
Kerbside does run skillsaw in CI.
.github/workflows/functional-tests.yml lines 267-272, in the
sanity_checks job:
- name: Lint agent context with skillsaw
run: |
uv pip install skillsaw==0.18.0
skillsaw --no-custom-rules .
The checker cannot see it, and the reason is precise.
scripts/audit-check.py line 5844 sets
SKILLSAW_SOURCE = 'stbenjam/skillsaw', and
check_llm_context_lint_ci() (line 6020) satisfies its CI half
two ways only: a workflow that names that string outside a
comment (named_in_ci, line 6052), or a workflow running
pre-commit run while the pre-commit config names it
(via_pre_commit, line 6057). Kerbside installs from PyPI, so
the string appears in no workflow except a comment at line 250,
and file_mentions() (line 120) skips full-line comments
deliberately and for a good reason. No workflow runs
pre-commit run. Both routes fail; the pre-commit half passes,
because .pre-commit-config.yaml line 54 does name the upstream
repository.
The specification already says the checker is wrong.
docs/audits/llm-context-lint-ci.md requires that "A CI
workflow runs skillsaw", and states that "how skillsaw is
invoked is deliberately not pinned". It even anticipates this
class of error one paragraph earlier, about the pre-commit
route: "Requiring the linter to be named in a workflow as well
would report a repository non-compliant for a wiring that does
run it." That is exactly what is happening to kerbside, by a
different route. This phase is not asking for an exception, it
is asking the implementation to match its own written intent.
Two facts the sketch did not know. First,
scripts/test_audit_check.py exists and already carries eight
cases for this check (lines 4728-4822), so the new tests join a
suite rather than founding one; it runs as
python3 scripts/test_audit_check.py, wired as a local
pre-commit hook at .pre-commit-config.yaml line 107. Second,
file_matches() (line 5856) already exists with the same
comment-skipping contract as file_mentions(), so the widening
needs no new helper.
Phase 1 and 2 both landed and both closed their issues by audit run. #368 and #373 closed at 2026-08-30T11:36-37Z, #360 at 11:41Z. Two closeout notes:
- #370 is still open, and its body is misleading. It lists
the three shared blocks phase 1 added, all three of which are
present on develop today. The audit does not refresh an open
issue's body, so the text describes a fixed problem. #370 is
open only because
push-auditgained a new requirement,diagram-discipline, on 2026-08-29. Do not read it as evidence that phase 1 failed. - #360 was closed by hand, not by the audit run, two seconds
after PR #380 merged. The outcome is the same here because
ci-review-automationwas independently verified passing before the merge, but the plan's success criteria ask for closure by a passing run, and this one was not.
Decisions¶
1. Match an invocation, not a mention. The widening adds a
third route: a workflow line that runs skillsaw as a command,
anchored at the start of the line. Concretely a
SKILLSAW_RUN_RE = re.compile(r'^\s*skillsaw\b') used through
the existing file_matches(). This deliberately distinguishes
kerbside's two adjacent lines: skillsaw --no-custom-rules .
matches and counts, while uv pip install skillsaw==0.18.0 does
not. Installing a linter is not running it, and a check that
could not tell those apart would be worse than the one being
replaced.
2. Fix the checker, do not exempt the repository. A
REPO_OVERRIDES entry for kerbside would close #359 today and
leave the next repository that installs skillsaw from PyPI to
rediscover the same wall. The overrides table is for properties
that cannot be detected from a clone -- that a repo is docs-only,
that it is not really a Python project -- and this is not one of
those. It is a detection gap. Overrides are the fallback if the
change is rejected, not the remedy.
3. Prove the blast radius rather than reason about it. The checker audits nineteen repositories, and the failure mode of a widened check is silent: a repository that stops being reported looks the same as one that was always fine. Step 3b therefore runs the checker over every sibling checkout before and after and diffs the verdicts. The expected diff is exactly one line. This is the step most likely to be skipped as ceremony and it is the one carrying the actual risk.
4. The plan lives here, the change lands there. This file is
committed to kerbside because that is where the master plan is
and where the issue was raised, but the code change is a
separate pull request against shakenfist/development. The two
are linked by referencing each other, not by any shared branch.
Kerbside's own tree does not change in this phase, so its
audit-check.py result will not move until the upstream change
merges and the next daily audit runs.
Step plan¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 3a | medium | sonnet | worktree | In the shakenfist/development repository at /srv/kasm_profiles/mikal/vscode/src/shakenfist/development, create a worktree off the default branch and widen check_llm_context_lint_ci() in scripts/audit-check.py (line 6020). Today its CI half is satisfied only by named_in_ci (a workflow naming SKILLSAW_SOURCE, line 6052) or via_pre_commit (a workflow running pre-commit run with the hook configured, line 6057). Add a third route: a workflow that runs skillsaw as a command. Define SKILLSAW_RUN_RE = re.compile(r'^\s*skillsaw\b') beside SKILLSAW_SOURCE (line 5844) and test it with the existing file_matches() helper (line 5856), which already skips full-line comments. The anchor is load-bearing: skillsaw --no-custom-rules . must match and uv pip install skillsaw==0.18.0 must not, because installing is not running. Update the function's docstring to say all three routes count. Do not touch docs/audits/llm-context-lint-ci.md -- the specification already describes the intended behaviour and this change brings the code to it, which is the argument for the change and should be stated in the commit message. |
| 3b | medium | sonnet | none | Add tests to scripts/test_audit_check.py for the widened check, joining the eight cases already at lines 4728-4822; follow their construction exactly. Cover four cases: (1) kerbside's shape passes -- pre-commit config naming stbenjam/skillsaw, plus a workflow whose run block installs skillsaw from PyPI on one line and invokes skillsaw --no-custom-rules . on the next; (2) a workflow that only installs it (pip install skillsaw) and never invokes it still fails; (3) a workflow mentioning skillsaw only in a full-line comment still fails; (4) a workflow that invokes skillsaw while the pre-commit config does not configure the hook still fails, on the pre-commit half. Case 2 is the one that proves decision 1 and must not be dropped. Run python3 scripts/test_audit_check.py and report the result. |
| 3c | medium | sonnet | none | Prove the blast radius. The sibling repository checkouts live under /srv/kasm_profiles/mikal/vscode/src/shakenfist/ and /srv/kasm_profiles/mikal/vscode/src/mach33labs/. For every one that is a git checkout, run python3 scripts/audit-check.py --repo-path <path> --repo-name <name> twice -- once from the unmodified checker at the default branch, once from the 3a worktree -- capture the llm-context-lint-ci verdict for each, and diff the two sets. The expected difference is exactly one repository: kerbside, fail to pass. Report the full before/after table, not just the diff. If any other repository changes verdict, stop and report it without adjusting anything; that means the regex is matching something unintended and the design needs revisiting, not tuning. If the audit script is refused by a sandbox permission classifier, report that as blocked rather than as a result. |
| 3d | low | sonnet | none | Prepare the upstream pull request against shakenfist/development from the 3a worktree, but do not create it -- print the proposed title and body and stop. The body should lead with the specification quote that makes the case ("how skillsaw is invoked is deliberately not pinned", and the paragraph about not reporting a repository non-compliant for a wiring that does run it), state that kerbside is the repository that surfaced it, show the two adjacent lines that must be told apart, and summarise 3c's regression table. Reference kerbside issue #359 as context. Michael creates pull requests himself; offering one is the job here. |
Each step is its own commit:
- 3a:
Detect skillsaw invoked from a workflow.(in development) - 3b:
Test skillsaw CI detection.(in development) - 3c: no commit (verification)
- 3d: no commit (prepares a pull request body)
Risks and mitigations¶
| Risk | Mitigation |
|---|---|
| The widened regex quietly passes a repository that installs skillsaw but never runs it, converting a real finding into a false pass across the fleet. | The anchor ^\s*skillsaw\b distinguishes invocation from installation, 3b case 2 asserts it directly, and 3c's before/after sweep would show any repository whose verdict moved. |
| A repository names a file or directory called skillsaw at the start of an indented line and accidentally satisfies the check. | 3c's sweep over every sibling checkout is exactly the test for this. The expected diff is one line, and any second line stops the phase rather than being explained away. |
The change is made in kerbside instead, by rewriting functional-tests.yml to name stbenjam/skillsaw so the existing checker sees it. |
Explicitly out of scope. The workflow already runs the linter correctly and the PyPI pin is deliberately kept in lockstep with the pre-commit rev; rewriting working CI to satisfy a checker's implementation detail is the compliance theatre this master plan declined once already. |
| The upstream change is rejected, and #359 stays open indefinitely. | Decision 2 names the fallback: record the divergence in the master plan and ask for a REPO_OVERRIDES entry with a documented reason. That is a worse outcome, not a failure of the phase. |
| Kerbside's audit result does not move when this phase is done, and the phase looks unfinished. | It cannot move: nothing in kerbside changes. #359 closes only after the upstream pull request merges and the next daily audit runs, which is a separate event on a separate timetable. The Definition of done is written against the upstream repository for this reason. |
Definition of done¶
-
check_llm_context_lint_ci()inshakenfist/developmentrecognises a workflow that invokes skillsaw, however it was installed, and its docstring says so. -
python3 scripts/test_audit_check.pypasses, including a new case asserting that installing skillsaw without invoking it still fails. - Running the modified checker across every sibling
checkout changes exactly one verdict:
llm-context-lint-cifor kerbside, fail to pass. The before/after table is in the phase's report, not merely asserted. -
pre-commit run --all-filesis clean in the development worktree. - No file in the kerbside repository has changed except
this plan, the master plan and
docs/plans/index.md.git diff --stat developproves it. - A pull request body against
shakenfist/developmenthas been drafted and shown to Michael, who creates it. - Issue #359 closes from a passing audit run after that pull request merges, not by hand.
Back brief¶
This is the first phase whose work lands outside kerbside, so the reporting matters more than usual: the kerbside tree will look untouched and the audit numbers will not move, and both of those are correct rather than evidence of nothing happening.
Two gates:
- Gate 1, after 3c and before 3d. The regression table is the whole safety argument for changing a checker that judges nineteen repositories. Show it in full. A one-line diff is the pass condition; anything else stops the phase.
- Gate 2, at 3d. The pull request is not created by the
session. The drafted title and body go to Michael, who decides
whether and when to open it against
shakenfist/development.
One inherited item to carry, not to fix here: #370 remains open
with a body describing an already-solved problem, because the
audit does not refresh issue bodies. Phase 5 owns it. Anyone
reading #370 before then should check PUSH-AUDIT.md rather
than trust the issue text.