Skip to content

Commit b3465a8

Browse files
authored
Merge pull request #6 from maybebyte/chore/dogfood-precommit
chore(pre-commit): dogfood the shipped pre-commit gate on the maintainer repo
2 parents 15e7c7e + e344f5d commit b3465a8

7 files changed

Lines changed: 645 additions & 0 deletions

File tree

.pre-commit-config.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
minimum_pre_commit_version: "4.0.0"
2+
default_install_hook_types: [pre-commit, pre-push]
3+
default_stages: [pre-commit]
4+
5+
repos:
6+
- repo: https://github.com/pre-commit/pre-commit-hooks
7+
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
8+
hooks:
9+
- id: check-merge-conflict
10+
args: [--assume-in-merge]
11+
- id: end-of-file-fixer
12+
exclude: '^template/'
13+
- id: trailing-whitespace
14+
exclude: '^template/'
15+
16+
- repo: local
17+
hooks:
18+
- id: ruff-check
19+
name: ruff check (--fix)
20+
entry: uv run ruff check --fix --force-exclude
21+
language: system
22+
types_or: [python, pyi]
23+
require_serial: true
24+
- id: ruff-format
25+
name: ruff format
26+
entry: uv run ruff format --force-exclude
27+
language: system
28+
types_or: [python, pyi]
29+
require_serial: true
30+
- id: basedpyright
31+
name: basedpyright (type check)
32+
entry: uv run basedpyright
33+
language: system
34+
types: [python]
35+
pass_filenames: false
36+
stages: [pre-push]
37+
- id: forbid-rej
38+
name: forbid copier .rej conflict files
39+
entry: "unresolved copier .rej conflict files present; resolve and delete them"
40+
language: fail
41+
files: '\.rej$'

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ just fmt-check # ruff format --check . (CI's format gate)
2727

2828
The maintainer harness runs the same full `select=["ALL"]` ruleset the template ships, scoped to `tests/` (the only Python surface; `template/` is Jinja). Every config-level ignore is load-bearing and audited — there are **no inline `# noqa`**. CI enforces `ruff check` + `ruff format --check` (the `lint` job in `.github/workflows/test-template.yml`).
2929

30+
## Pre-commit
31+
32+
```bash
33+
just setup # one-time: sync the venv and install the git hooks
34+
just precommit # run every hook (commit-stage + pre-push basedpyright) over the whole tree
35+
```
36+
37+
`pre-commit install` registers both git hooks (`default_install_hook_types: [pre-commit, pre-push]`). On commit: ruff-check `--fix`, ruff-format, end-of-file-fixer / trailing-whitespace (both `exclude: '^template/'` — template Jinja whitespace is deliberate), check-merge-conflict, forbid-rej. On push: end-of-file-fixer and trailing-whitespace also run (they inherit a `pre-push` stage from the pinned `pre-commit-hooks` manifest, on any changed non-`template/` text file), plus basedpyright when the push includes a `*.py` file.
38+
39+
This is a **local-only** gate: there is no pre-commit CI job, matching the template (whose downstream CI also never runs pre-commit). The ruff and basedpyright *substance* is enforced by the existing `lint` and `typecheck` CI jobs; the hygiene hooks (eof / trailing-whitespace / check-merge-conflict / forbid-rej) have **no** CI backstop and are a local convenience here. A bare `uv run pre-commit run --all-files` runs commit-stage hooks only — basedpyright fires on push or via `just typecheck`; `just precommit` runs both.
40+
41+
Deliberate divergences from `template/.pre-commit-config.yaml.jinja`: ruff runs via local `uv run ruff` hooks (locked 0.15.19) instead of the `astral-sh/ruff-pre-commit` repo (pinned 0.15.18) — the venv is always synced here, so there is no bootstrap reason to keep the isolated-env repo hook; the pytest hook is dropped (the maintainer's only suite is the heavy generation matrix — CI-only). The two configs share the SHA-pinned `pre-commit-hooks` block: **bump both `rev:` pins together** (v6.0.0 = `3e8a8703…`).
42+
3043
## Add a guardrail layer
3144

3245
1. Add an `enable_*` toggle to `copier.yml`.
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# Dogfood Pre-commit Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Wire pre-commit into the maintainer repo as a local-only gate that mirrors the guardrail the template ships, adapted to the maintainer's surface.
6+
7+
**Architecture:** Add `pre-commit` as a dev dependency, a root `.pre-commit-config.yaml` (hygiene hooks + local `uv run ruff`/`basedpyright` hooks + `forbid-rej`), `just setup`/`just precommit` recipes, and an AGENTS.md section. No CI job and no `test_generation.py` change — the substantive checks are already CI-enforced by the existing `lint`/`typecheck` jobs; hygiene is a local gate. Verification per task is running the relevant command and observing its output (there is no pytest surface for this layer).
8+
9+
**Tech Stack:** pre-commit (>=4,<5), uv, just, ruff (locked 0.15.19), basedpyright (1.39.8), `pre-commit/pre-commit-hooks` v6.0.0.
10+
11+
**Source spec:** `docs/superpowers/specs/2026-07-01-dogfood-precommit-design.md`
12+
13+
## Global Constraints
14+
15+
- Branch: work on `chore/dogfood-precommit` (already contains the spec commit `2cbacac`). Do not branch again.
16+
- `pre-commit>=4,<5` — mirrors `minimum_pre_commit_version: "4.0.0"`.
17+
- SHA-pin the `pre-commit-hooks` repo with its exact-tag comment: `rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0` (AGENTS.md NEVER rule: never change a pinned SHA without updating its exact-tag comment).
18+
- Hygiene hooks (`end-of-file-fixer`, `trailing-whitespace`) carry `exclude: '^template/'``template/` is Jinja and its whitespace is deliberate (`keep_trailing_newline: true`).
19+
- Local ruff hooks use `uv run ruff` (locked 0.15.19), NOT `astral-sh/ruff-pre-commit`.
20+
- No pytest hook; no CI pre-commit job; no `test_generation.py` change; no CHANGELOG `[Unreleased]` entry (CHANGELOG is downstream/template-scoped).
21+
- Commits: Conventional Commit format, no AI-attribution trailer, GPG-signed (automatic in this repo). Author is `Ashlen <[email protected]>`.
22+
- Every edited/added file outside `template/` must be hygiene-clean (no trailing whitespace, ends in a final newline) — the pre-commit hooks enforce this on the repo itself once installed.
23+
- First `pre-commit run` downloads the `pre-commit-hooks` environment at the pinned SHA (needs network); subsequent runs are cached.
24+
25+
---
26+
27+
### Task 1: Add pre-commit dev dependency
28+
29+
**Files:**
30+
- Modify: `pyproject.toml` (the `[dependency-groups] dev` array)
31+
- Modify: `uv.lock` (regenerated)
32+
33+
**Interfaces:**
34+
- Consumes: nothing.
35+
- Produces: `uv run pre-commit` becomes available for all later tasks.
36+
37+
- [ ] **Step 1: Add the dependency**
38+
39+
In `pyproject.toml`, add `"pre-commit>=4,<5",` to the `[dependency-groups] dev` array, alongside the existing dev dependencies. Keep the array's existing ordering/style.
40+
41+
- [ ] **Step 2: Lock and sync**
42+
43+
Run: `uv lock && uv sync`
44+
Expected: `uv.lock` updates to include `pre-commit` (and its deps: `cfgv`, `identify`, `nodeenv`, `virtualenv`, etc.); `uv sync` installs them into `.venv`.
45+
46+
- [ ] **Step 3: Verify pre-commit is runnable at the required major**
47+
48+
Run: `uv run pre-commit --version`
49+
Expected: prints `pre-commit 4.x.y` (major 4).
50+
51+
- [ ] **Step 4: Verify the lock recorded pre-commit**
52+
53+
Run: `grep -A1 'name = "pre-commit"' uv.lock | head -2`
54+
Expected: shows `name = "pre-commit"` and a `version = "4...."` line.
55+
56+
- [ ] **Step 5: Commit**
57+
58+
```bash
59+
git add pyproject.toml uv.lock
60+
git commit -m "build(deps): add pre-commit dev dependency"
61+
```
62+
63+
---
64+
65+
### Task 2: Add the root pre-commit config
66+
67+
**Files:**
68+
- Create: `.pre-commit-config.yaml`
69+
70+
**Interfaces:**
71+
- Consumes: `uv run pre-commit` (Task 1); `uv run ruff`, `uv run basedpyright` (already present).
72+
- Produces: a validated pre-commit config that later recipes/tasks invoke.
73+
74+
- [ ] **Step 1: Create `.pre-commit-config.yaml`**
75+
76+
```yaml
77+
minimum_pre_commit_version: "4.0.0"
78+
default_install_hook_types: [pre-commit, pre-push]
79+
default_stages: [pre-commit]
80+
81+
repos:
82+
- repo: https://github.com/pre-commit/pre-commit-hooks
83+
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0
84+
hooks:
85+
- id: check-merge-conflict
86+
args: [--assume-in-merge]
87+
- id: end-of-file-fixer
88+
exclude: '^template/'
89+
- id: trailing-whitespace
90+
exclude: '^template/'
91+
92+
- repo: local
93+
hooks:
94+
- id: ruff-check
95+
name: ruff check (--fix)
96+
entry: uv run ruff check --fix --force-exclude
97+
language: system
98+
types_or: [python, pyi]
99+
require_serial: true
100+
- id: ruff-format
101+
name: ruff format
102+
entry: uv run ruff format --force-exclude
103+
language: system
104+
types_or: [python, pyi]
105+
require_serial: true
106+
- id: basedpyright
107+
name: basedpyright (type check)
108+
entry: uv run basedpyright
109+
language: system
110+
types: [python]
111+
pass_filenames: false
112+
stages: [pre-push]
113+
- id: forbid-rej
114+
name: forbid copier .rej conflict files
115+
entry: "unresolved copier .rej conflict files present; resolve and delete them"
116+
language: fail
117+
files: '\.rej$'
118+
```
119+
120+
- [ ] **Step 2: Validate the config parses**
121+
122+
Run: `uv run pre-commit validate-config .pre-commit-config.yaml`
123+
Expected: no output, exit code 0 (invalid YAML/schema would print an error).
124+
125+
- [ ] **Step 3: Stage the new file so `--all-files` sees it, then run commit-stage hooks**
126+
127+
`--all-files` operates on `git ls-files`; an unstaged new file is invisible.
128+
129+
Run: `git add .pre-commit-config.yaml && uv run pre-commit run --all-files`
130+
Expected: `check-merge-conflict`, `end-of-file-fixer`, `trailing-whitespace`, `ruff-check`, `ruff-format` each print `Passed` (the tree is already clean, so the fixer hooks make no changes and exit 0); `forbid-rej` prints `(no files to check) Skipped` — a `language: fail` hook only runs (and can only ever fail) when a matching `.rej` file is present. First run also prints `[INFO] Initializing environment...` lines while it fetches the pinned hooks repo.
131+
132+
- [ ] **Step 4: Run the pre-push stage hook**
133+
134+
Run: `uv run pre-commit run --all-files --hook-stage pre-push`
135+
Expected: `end-of-file-fixer`, `trailing-whitespace`, and `basedpyright` all print `Passed` — the two fixers inherit a `pre-push` stage from the pinned pre-commit-hooks v6.0.0 manifest (which overrides `default_stages`), so they run here too. `check-merge-conflict`, `ruff-check`, `ruff-format`, and `forbid-rej` are stage-filtered out at pre-push.
136+
137+
- [ ] **Step 5: Confirm no files were modified**
138+
139+
Run: `git status --short`
140+
Expected: only `A .pre-commit-config.yaml` staged; no unexpected modifications from the fixer hooks.
141+
142+
- [ ] **Step 6: Prove `forbid-rej` has teeth (negative check)**
143+
144+
Every other step exercises only the green path; confirm the one un-CI-backstopped, repo-authored hook can actually fail.
145+
146+
Run: `printf 'x\n' > scratch.rej && uv run pre-commit run forbid-rej --files scratch.rej; rm -f scratch.rej`
147+
Expected: `forbid-rej` reports `Failed` (non-zero) with the "unresolved copier .rej conflict files present" message, proving the gate has teeth. Use `--files scratch.rej`, NOT `git add -N` + `--all-files`: `*.rej` is gitignored, so an intent-to-add path would never feed the file to the hook and would give a false pass.
148+
149+
- [ ] **Step 7: Commit**
150+
151+
```bash
152+
git add .pre-commit-config.yaml
153+
git commit -m "chore(pre-commit): add local pre-commit config"
154+
```
155+
156+
---
157+
158+
### Task 3: Add `just setup` and `just precommit` recipes
159+
160+
**Files:**
161+
- Modify: `justfile`
162+
163+
**Interfaces:**
164+
- Consumes: `.pre-commit-config.yaml` (Task 2).
165+
- Produces: `just setup` (installs the git hooks) and `just precommit` (runs both hook stages).
166+
167+
- [ ] **Step 1: Add the recipes to `justfile`**
168+
169+
Add, after the existing `fmt-check` recipe:
170+
171+
```just
172+
173+
# one-time: sync the venv and install the git hooks (maintainer is not copier-generated)
174+
setup:
175+
uv sync
176+
uv run pre-commit install
177+
178+
# run every hook over the whole tree: commit-stage hooks, then pre-push basedpyright
179+
precommit:
180+
uv run pre-commit run --all-files
181+
uv run pre-commit run --all-files --hook-stage pre-push
182+
```
183+
184+
- [ ] **Step 2: Verify the recipes are listed**
185+
186+
Run: `just --list`
187+
Expected: `setup` and `precommit` appear among the recipes with their doc comments.
188+
189+
- [ ] **Step 3: Install the git hooks**
190+
191+
Run: `just setup`
192+
Expected: `uv sync` reports up-to-date (or a no-op resolve); `pre-commit install` prints `pre-commit installed at .git/hooks/pre-commit` and `pre-commit installed at .git/hooks/pre-push`.
193+
194+
- [ ] **Step 4: Verify both git hooks were written**
195+
196+
Run: `test -f "$(git rev-parse --git-path hooks/pre-commit)" && test -f "$(git rev-parse --git-path hooks/pre-push)" && echo OK`
197+
Expected: prints `OK` (both hook files exist).
198+
199+
Note: run this plan from a normal clone, not a linked git worktree — `just setup` inside a worktree installs the hooks into the shared common `.git/hooks/`, mutating the main checkout.
200+
201+
- [ ] **Step 5: Run the full local gate via the recipe**
202+
203+
Run: `just precommit`
204+
Expected: line 1 (commit stage) shows `check-merge-conflict`, `end-of-file-fixer`, `trailing-whitespace`, `ruff-check`, `ruff-format` `Passed` and `forbid-rej` `(no files to check) Skipped`; line 2 (pre-push) shows `end-of-file-fixer`, `trailing-whitespace`, and `basedpyright` `Passed`; recipe exits 0.
205+
206+
- [ ] **Step 6: Commit**
207+
208+
Note: the git hooks are now installed, so this commit triggers the commit-stage hooks — they should pass on a clean tree (this is the dogfooding working).
209+
210+
```bash
211+
git add justfile
212+
git commit -m "chore: add just setup and precommit recipes"
213+
```
214+
215+
---
216+
217+
### Task 4: Document pre-commit in AGENTS.md
218+
219+
**Files:**
220+
- Modify: `AGENTS.md` (insert a new `## Pre-commit` section immediately after the "Lint & format this repo" section, before "Add a guardrail layer")
221+
222+
**Interfaces:**
223+
- Consumes: the recipes (Task 3) and config (Task 2) that the section describes.
224+
- Produces: nothing downstream.
225+
226+
- [ ] **Step 1: Insert the `## Pre-commit` section**
227+
228+
Insert immediately after the "Lint & format this repo" section's final paragraph and before `## Add a guardrail layer`:
229+
230+
````markdown
231+
## Pre-commit
232+
233+
```bash
234+
just setup # one-time: sync the venv and install the git hooks
235+
just precommit # run every hook (commit-stage + pre-push basedpyright) over the whole tree
236+
```
237+
238+
`pre-commit install` registers both git hooks (`default_install_hook_types: [pre-commit, pre-push]`). On commit: ruff-check `--fix`, ruff-format, end-of-file-fixer / trailing-whitespace (both `exclude: '^template/'` — template Jinja whitespace is deliberate), check-merge-conflict, forbid-rej. On push: end-of-file-fixer and trailing-whitespace also run (they inherit a `pre-push` stage from the pinned `pre-commit-hooks` manifest, on any changed non-`template/` text file), plus basedpyright when the push includes a `*.py` file.
239+
240+
This is a **local-only** gate: there is no pre-commit CI job, matching the template (whose downstream CI also never runs pre-commit). The ruff and basedpyright *substance* is enforced by the existing `lint` and `typecheck` CI jobs; the hygiene hooks (eof / trailing-whitespace / check-merge-conflict / forbid-rej) have **no** CI backstop and are a local convenience here. A bare `uv run pre-commit run --all-files` runs commit-stage hooks only — basedpyright fires on push or via `just typecheck`; `just precommit` runs both.
241+
242+
Deliberate divergences from `template/.pre-commit-config.yaml.jinja`: ruff runs via local `uv run ruff` hooks (locked 0.15.19) instead of the `astral-sh/ruff-pre-commit` repo (pinned 0.15.18) — the venv is always synced here, so there is no bootstrap reason to keep the isolated-env repo hook; the pytest hook is dropped (the maintainer's only suite is the heavy generation matrix — CI-only). The two configs share the SHA-pinned `pre-commit-hooks` block: **bump both `rev:` pins together** (v6.0.0 = `3e8a8703…`).
243+
````
244+
245+
- [ ] **Step 2: Verify the section is present and placed correctly**
246+
247+
Run: `grep -nE '^## ' AGENTS.md`
248+
Expected: `## Pre-commit` appears between `## Lint & format this repo` and `## Add a guardrail layer`.
249+
250+
- [ ] **Step 3: Verify AGENTS.md passes the hygiene hooks (it is not under template/, so it is checked)**
251+
252+
Run: `git add AGENTS.md && uv run pre-commit run --files AGENTS.md`
253+
Expected: the three hygiene hooks (`trailing-whitespace`, `end-of-file-fixer`, `check-merge-conflict`) print `Passed`; `ruff-check`, `ruff-format`, and `forbid-rej` report `(no files to check) Skipped`; `basedpyright` does not appear (it is pre-push-only, while `--files` runs the commit stage).
254+
255+
- [ ] **Step 4: Commit**
256+
257+
```bash
258+
git add AGENTS.md
259+
git commit -m "docs(agents): document pre-commit workflow"
260+
```
261+
262+
---
263+
264+
### Task 5: Final acceptance
265+
266+
**Files:** none (verification only).
267+
268+
- [ ] **Step 1: Full gate green from a clean tree**
269+
270+
Run: `just precommit`
271+
Expected: commit-stage line — five hooks `Passed` and `forbid-rej` `Skipped`; pre-push line — `end-of-file-fixer`, `trailing-whitespace`, `basedpyright` `Passed`; exit 0.
272+
273+
- [ ] **Step 2: Confirm working tree is clean (no hook left an unstaged fix)**
274+
275+
Run: `git status --short`
276+
Expected: empty output.
277+
278+
- [ ] **Step 3: Confirm all commits are present on the branch**
279+
280+
Run: `git log --oneline main..HEAD`
281+
Expected: six commits — the spec commit, the plan commit, and the four implementation commits (`build(deps)…`, `chore(pre-commit)…`, `chore: add just setup…`, `docs(agents)…`).
282+
283+
- [ ] **Step 4: Push and open the PR** (only when instructed by the operator)
284+
285+
Push the branch and open a PR with the pr-descriptions skill. The PR contains the spec, this plan, and the pre-commit wiring; note in the body that it is a local-only gate with no CI change, and reference gap #1 of the dogfooding audit. Also add a body line flagging the tracked follow-up: reconcile the template's shipped `ruff-pre-commit` v0.15.18 pin with the locked ruff 0.15.19 at the source (see the spec's Out-of-scope section).

0 commit comments

Comments
 (0)