Repository navigation
ci: add one local entry point that runs the lint and lint-2 hygiene gates against the diff #6008
Description
Activity
- addedneeds-triageNot yet classified. Floor until a type and one priority tier are set.Not yet classified. Floor until a type and one priority tier are set.
on Oct 3, 2026 This was generated by AI during triage.
Routing: human-gated, decision needed. Work class: structural (C4). Priority: medium.
Why a human decides: the repo already decided to defer exactly this.
README.md:145-163("Local pre-flight for hygiene gates", from the #3522 owner decision) maps every CI gate to its local command, and says "There is no single script that runs the whole hygiene set ... A pre-commit hook or a make target that wraps the runnable rows is a later tooling decision, not this record." This issue asks for that decision. The recommended option also moves the gate list out of the CI workflow and into a script that every hygiene job calls, which is a cross-cutting CI change (C4).Verification of the claims on main:
- Confirmed: no runner exists (
scripts/hasaggregate-hygiene-results.sh, which reads CI outcomes and runs no gates). There is nolefthook.yml,.pre-commit-config.yamlor.husky/. - Stale:
lintandlint-2no longer exist. The hygiene gates now run in four jobs,lint-repo,lint-shell,check-pluginsandcheck-skills, each ending in the aggregator (.github/workflows/ci.yml:978,:1230,:1611,:1936). The ci.yml has 69continue-on-error: truesteps. - Partial existing coverage: the README mapping table names a local command for a subset of gates, so part of the discoverability gap is already closed. The single command is not.
The decision:
- Recommended by the issue:
scripts/run-hygiene-gates.sh [--base <ref>]owns the gate list, and every hygiene job calls it, so the local and CI lists cannot drift. Cost: restructures the four hygiene jobs' gate steps. Per-gate step names and outcomes then come from the script rather than from separate workflow steps. - A local-only runner that mirrors the workflow steps: smaller, but a second list that drifts.
- A pre-push hook on top of option 1, opt-in.
What the answer unblocks: the brief, and whether #6009's annotation work should wait for the runner (#6009 is defaulted to its smaller option 1 now; whichever lands second rebases onto the other's gate steps).
Agent Brief (for after the decision, option 1):
- Summary: One command runs every in-repo hygiene gate against a base ref on a workstation, and CI runs the same gate list from the same source.
- Desired behavior: the command prints
<gate>: ok|FAILper gate plus the output of each failure, exits non-zero when any gate fails, and names each gate it cannot run locally (composite-action gates, missing tools) as "CI only" or "skipped: ". It never skips one silently. The hygiene jobs get their gate list from the same source, so adding a gate in one place adds it in both. - Acceptance criteria: documented in
AGENTS.md; re-running the failing gates from the issue body's examples on their failing heads reproduces each failure; CI's hygiene results are unchanged on a clean PR. - Out of scope: changing what any gate checks; the ci-workflows composite actions themselves.
- Confirmed: no runner exists (
- addedpriority: mediumReal value, no hard deadline; normal backlog flow.Real value, no hard deadline; normal backlog flow.needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.Human-in-the-loop required; autonomous sessions must not resolve items carrying this.status: needs-decisionAwaiting a human or maintainer judgment call.Awaiting a human or maintainer judgment call.work-class: structuralRefactors, migrations, contract changes; cross-cutting and hard to reverse.Refactors, migrations, contract changes; cross-cutting and hard to reverse.and removedneeds-triageNot yet classified. Floor until a type and one priority tier are set.Not yet classified. Floor until a type and one priority tier are set.
on Oct 4, 2026
Problem
The hygiene lane is 66
continue-on-errorgate steps spread over thelintandlint-2jobs in.github/workflows/ci.yml(lines 540 to 1734 on main at123c94b91). 35 of them call ascripts/gate directly; the rest call ci-workflows composite actions (exec-bit, machine-specific paths and others) or inline shell. No script, Makefile, package.json script, or git hook runs them together on a workstation:ls scriptshas no runner;package.jsonhas noscriptsblock.lefthook.yml,.pre-commit-config.yamlor.husky/. The only installed git hook is aprepare-commit-msgshim, and the lefthook binary it calls prints "No config files" on every commit.So the only way to learn which gates a change trips is to push and wait for CI.
Evidence
On #5992, these failed in CI and each needed a separate push and CI round:
exec-bit,spoke-plugin-root(scripts/check-spoke-plugin-root.sh),plugin-options-docs(scripts/sync-plugin-options-docs.py --check),shell-portability-lint(scripts/check-shell-portability.sh) on run 37094085450, fixed in78a01940c("fix: satisfy the exec-bit, plugin-root, options-docs and portability lint").changed-skills(scripts/check-changed-skills.sh),machine-specific-paths,stale-base-overlapandchangelog-parity-bumpon #5920, as reported by the authoring session (run IDs not re-checked here).Every one of these is a deterministic script that needs only the checkout and
origin/main.Impact
Each CI-only failure costs one push, one
cirun (PR median about 3.5 min after #5886) plustest-windows(about 11 to 13 min), and a fresh turn to read the log. While main merges about 3 PRs an hour, each extra round also raises the odds of a version collision and another base merge.Proposed fix options
scripts/run-hygiene-gates.sh [--base <ref>], called by CI as well. One script owns the gate list, runs each gate against the diff, prints<gate>: ok|FAILplus the gate's own output for failures, and exits non-zero on any failure. CI's two lint jobs call it (or call it per half) instead of listing steps inline, so the local and CI lists cannot drift. Composite-action gates either get a local equivalent or are listed as "CI only" in the script's output.ci.yml, but it is a second list that drifts the first time someone adds a step.origin/mainfetched and Python with the pinned requirements.Recommendation: option 1. One gate list serves both places, and the same script can print each failure's reason into the CI annotation (see the companion draft on
aggregate-hygiene-results.sh). Basis: the eight CI-only failures above;ci.ymlstep count fromgrep -c 'continue-on-error: true'over the lint jobs.Acceptance criteria
AGENTS.md, runs everyscripts/hygiene gate from both lint jobs againstorigin/mainon a workstation and exits non-zero when any fails.