Summary
npm run lint:py runs ruff check only. The CI job runs ruff check and ruff format --check. A locally green Python change can therefore still fail CI. This actually happened during PR #2599 and cost a build cycle plus two incorrect "validation green" reports.
Correcting the original framing
This was first recorded as "unpinned ruff>=0.15 means a formatter release can break CI with no code change." That is wrong, and re-verification on main shows why:
.github/workflows/python-lint.yml:62 installs with uv sync --locked.
- Every skill's
uv.lock pins an exact ruff version, for example powerpoint 0.15.4, demo-video 0.15.20, adr-author 0.15.12.
- Dependabot already covers the
uv ecosystem weekly.
CI is deterministic. The lockfile is authoritative. Nobody should spend time "fixing" a determinism problem that does not exist.
The actual defect
|
npm run lint:py |
CI Python Lint job |
ruff check |
✅ |
✅ |
ruff format --check |
❌ |
✅ |
scripts/linting/Invoke-PythonLint.ps1 documents this deliberately: it defaults to read-only ruff check for CI gating, and applies ruff format only under -Fix. The intent is reasonable — the default lane should not mutate source. The problem is that the local lane's name and output imply coverage it does not have.
Options
- Add a non-mutating
ruff format --check to the default lint:py path so it matches the CI job. Most direct; keeps the lane read-only.
- Add a separate
lint:py:format script and reference it from the contributing docs.
- Document the divergence only, and leave the lanes as they are.
Option 1 seems right — a local lint lane that does not reproduce its CI counterpart is a trap — but the maintainer should decide, since it may be a deliberate command-taxonomy choice under .github/instructions/ci-owned-validation.instructions.md. Read that before changing package.json.
Secondary: ruff version drift
Separate and lower value. Ruff has drifted across 11 skills, so formatting behaviour is not uniform repository-wide even though each skill is individually reproducible:
| Version |
Skills |
| 0.15.4 |
powerpoint |
| 0.15.6 |
jira, gitlab |
| 0.15.10 |
tts-voiceover |
| 0.15.11 |
customer-card-render |
| 0.15.12 |
mural, adr-author |
| 0.15.15 |
vally-tests |
| 0.15.17 |
accessibility |
| 0.15.20 |
demo-video, vex |
Also note project-planning/adr-author declares ruff>=0.6 in pyproject.toml while every other skill declares >=0.15. Worth aligning the declared floor even if the locks stay independent.
Converging these is optional. Do not let a version bump silently reformat source as a side effect; if a newer ruff wants formatting changes, surface that as its own decision.
Local Ruff resolution parity
The local runner currently resolves Ruff from each project's .venv, then falls back to a global Ruff on PATH. CI instead runs uv sync --locked followed by uv run ruff, so the local and CI commands can use different Ruff versions and produce different formatter verdicts. This is observable today: some projects use the global fallback, and an unsynchronized environment can cross the Ruff 0.15/0.16 boundary where Markdown code blocks enter formatter scope.
Resolve this as part of this issue rather than a separate follow-up. Reuse the lock-bound uv execution pattern already used by the local Python test runner, while retaining a clear and tested fallback when uv is unavailable. Account explicitly for the large scripts/evals/moderation environment so the fix does not introduce an unexplained 935 MB sync cost into the ordinary lint path.
Acceptance Criteria
Verified against main at 278eb128.
Summary
npm run lint:pyrunsruff checkonly. The CI job runsruff checkandruff format --check. A locally green Python change can therefore still fail CI. This actually happened during PR #2599 and cost a build cycle plus two incorrect "validation green" reports.Correcting the original framing
This was first recorded as "unpinned
ruff>=0.15means a formatter release can break CI with no code change." That is wrong, and re-verification onmainshows why:.github/workflows/python-lint.yml:62installs withuv sync --locked.uv.lockpins an exact ruff version, for example powerpoint0.15.4, demo-video0.15.20, adr-author0.15.12.uvecosystem weekly.CI is deterministic. The lockfile is authoritative. Nobody should spend time "fixing" a determinism problem that does not exist.
The actual defect
npm run lint:pyruff checkruff format --checkscripts/linting/Invoke-PythonLint.ps1documents this deliberately: it defaults to read-onlyruff checkfor CI gating, and appliesruff formatonly under-Fix. The intent is reasonable — the default lane should not mutate source. The problem is that the local lane's name and output imply coverage it does not have.Options
ruff format --checkto the defaultlint:pypath so it matches the CI job. Most direct; keeps the lane read-only.lint:py:formatscript and reference it from the contributing docs.Option 1 seems right — a local lint lane that does not reproduce its CI counterpart is a trap — but the maintainer should decide, since it may be a deliberate command-taxonomy choice under
.github/instructions/ci-owned-validation.instructions.md. Read that before changingpackage.json.Secondary: ruff version drift
Separate and lower value. Ruff has drifted across 11 skills, so formatting behaviour is not uniform repository-wide even though each skill is individually reproducible:
Also note
project-planning/adr-authordeclaresruff>=0.6inpyproject.tomlwhile every other skill declares>=0.15. Worth aligning the declared floor even if the locks stay independent.Converging these is optional. Do not let a version bump silently reformat source as a side effect; if a newer ruff wants formatting changes, surface that as its own decision.
Local Ruff resolution parity
The local runner currently resolves Ruff from each project's
.venv, then falls back to a global Ruff onPATH. CI instead runsuv sync --lockedfollowed byuv run ruff, so the local and CI commands can use different Ruff versions and produce different formatter verdicts. This is observable today: some projects use the global fallback, and an unsynchronized environment can cross the Ruff 0.15/0.16 boundary where Markdown code blocks enter formatter scope.Resolve this as part of this issue rather than a separate follow-up. Reuse the lock-bound
uvexecution pattern already used by the local Python test runner, while retaining a clear and tested fallback whenuvis unavailable. Account explicitly for the largescripts/evals/moderationenvironment so the fix does not introduce an unexplained 935 MB sync cost into the ordinary lint path.Acceptance Criteria
ci-owned-validation.instructions.mdwas consulted before changing anypackage.jsonscript.npm run validate:skillsand the dependency-pinning validation pass.uv.lock, using the project's locked Ruff version rather than an arbitrary.venvor global binary.uvis unavailable is explicit, documented, and covered by tests.scripts/evals/moderationwithout an unexplained full-environment sync cost.Verified against
mainat278eb128.