From 07ceaefa966d6fe57bffa8e3f9ea7dbcebe1e8df Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 17:47:36 -0700 Subject: [PATCH 1/4] Fix Promotion-PR Review Findings on Pre-Commit Hooks Rollout A fresh review round on the develop-to-main promotion PR caught real gaps the earlier feature-PR rounds missed. The pre-commit-framework snippet called uv run pre-commit install and uv run ruff/pyright unconditionally, which fails on the lint-only profile (no project environment, no uv.lock). Switches to uv tool install pre-commit once, then plain pre-commit install, and uvx for ruff/pyright, matching CI's own lint-only invocation. Verified end to end: a fresh repo, pre-commit install, and a real commit running all five hooks clean. parity.hooks universally required hub-fetch-run.py, contradicting the hub's own documented exception (it calls its local script copies directly). Adds that exception to both the machine-readable assertion and AUDIT.md's summary. Also fixes a leftover one-fetch count in the pre-commit README (should be two, same class of mistake already fixed in the Husky README on the prior PR) and notes the Husky-vs-pre-commit path mismatch in the Python VS Code tasks for a repo using Husky instead of the pre-commit framework. --- AUDIT.md | 2 +- .../snippets/configs/vscode-tasks-python.json | 2 ++ .../pre-commit/.pre-commit-config.yaml | 14 +++++++------ catalog/snippets/pre-commit/README.md | 21 ++++++++++++------- spec/project-types.json | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 03d57b71..7be358cc 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -87,7 +87,7 @@ A check with `intentRef`/`workflowRef` points at the prose section that owns the - **carried-scope** - the repo carries no file the hub hosts rather than carries. The set is derived, not listed: the hub's git-tracked paths minus the [`spec/files.json`][files] baseline, so a file dropped from the manifest starts being reported on the next run with no retirement list to remember to edit. The remedy is the opposite of every other file finding, a **deletion**, since the repo reaches the hub's copy per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. The match is on path alone, so a hit is a candidate and not a verdict: a repo's own content at a path the hub also uses matches while carrying nothing of the hub's, which the first fleet run showed twice, a KiCad tooling doc at `scripts/README.md` and per-repo formatting hooks at `.husky/pre-commit`. A [`spec/divergences.json`][divergences] `gaps` disposition decides which case a hit is, so only `retire` asserts a deletion, `accepted` closes a collision or a repo-owned file, and an untriaged hit is read before it is acted on. - **verbatim-tree** - every applicable `trees[]` declaration in [`spec/files.json`][files] owns its target tree. The audit reports missing files as letter findings, stale or modified bytes as drift, and extra files under a pruned target as drift. An unreadable or truncated repository tree is undecided and produces drift rather than a clean result. - **repo-setup** - every required secret for the repo's publish mechanisms is configured, and no forbidden secret is present (per [`spec/secrets.json`][secrets]). -- **linter-parity** - one config per linter (`.markdownlint-cli2.jsonc`, `cspell.json`, ruff/pyright, editorconfig/csharpier, actionlint) drives the editor extension, the CLI, and CI, and CI runs each. A local hook exists and runs at minimum the diff-scoped prose gate and the eol check (`parity.hooks`, intent). A repo with none wired is a defect, and one mid-convergence on the language-formatting half stays operational. +- **linter-parity** - one config per linter (`.markdownlint-cli2.jsonc`, `cspell.json`, ruff/pyright, editorconfig/csharpier, actionlint) drives the editor extension, the CLI, and CI, and CI runs each. A local hook exists and runs at minimum the diff-scoped prose gate and the eol check via `hub-fetch-run.py`, or the hub's own local script copies for the hub repo itself (`parity.hooks`, intent). A repo with none wired is a defect, and one mid-convergence on the language-formatting half stays operational. - **recurring-violations** - comments concise and non-narrative, ASCII only (no em-dash, no smart quotes), US spelling, line endings per `.editorconfig`. These are frequent regressions, so this dimension is high priority and always runs, and each check is grep-able (see below). - **readme-structure** - the README follows [`spec/readme-structure.md`][readme-structure] (applicable sections, in order). Mechanically checked against the declared model in [`spec/readme-sections.json`][readme-sections]: required sections present, declared sections in their relative order, `License` last, the shields each deliverable implies, the license shield in the closing License section, and the tagline and its mirrors. A heading the model does not name is dropped before the order comparison, so a repo-specific section is never a finding. diff --git a/catalog/snippets/configs/vscode-tasks-python.json b/catalog/snippets/configs/vscode-tasks-python.json index 2bd6a4fb..e24ce450 100644 --- a/catalog/snippets/configs/vscode-tasks-python.json +++ b/catalog/snippets/configs/vscode-tasks-python.json @@ -127,6 +127,8 @@ // Lint: Prose and Lint: EOL below add the same two checks in whole-repo mode, for on-demand full-tree validation. // Lint: EditorConfig/Workflows/Markdown/Spelling are language-agnostic, identical to the // .NET snippet's Lint group. + // Lint: Prose and Lint: EOL assume the pre-commit-framework layout (hub-fetch-run.py at the workspace root). + // A repo using the Husky.Net snippet instead points these two tasks at .husky/hub-fetch-run.py. { "label": "Lint: EditorConfig", "type": "process", diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml index b85bf7c6..567623a0 100644 --- a/catalog/snippets/pre-commit/.pre-commit-config.yaml +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -1,8 +1,10 @@ # Reference pre-commit (pre-commit.com) config for a fleet Python repo with no .NET/Husky.Net tree. # Copy into the repo root alongside ../hub-fetch-run.py. # Adapt the type-check hook to match this repo's own CI checker (pyright or mypy, per python-codestyle). -# Enable with `uv run pre-commit install`. -# The type checker and ruff hooks are native/uv-run, no Docker. +# Install and enable with `uv tool install pre-commit` once, then `pre-commit install`. +# The type checker and ruff hooks run via uvx, native tooling, no Docker. +# That needs no project dependency, matching CI's own invocation for the lint-only profile. +# A repo on the build profile with a uv.lock may swap an entry for `uv run ` to pin its own project version instead. # The prose/EOL hooks fetch their scripts fresh from ptr727/ProjectTemplate's `main` branch via hub-fetch-run.py, rather than vendoring or pinning a copy. # See that file's docstring for why. # A fetch failure fails the commit, and it never silently skips the gate. @@ -11,17 +13,17 @@ repos: hooks: - id: ruff-format name: ruff format --check - entry: uv run ruff format --check + entry: uvx ruff@latest format --check language: system pass_filenames: false - id: ruff-check name: ruff check - entry: uv run ruff check + entry: uvx ruff@latest check language: system pass_filenames: false - id: type-check - name: pyright (swap for "uv run mypy" if this repo's CI runs mypy instead) - entry: uv run pyright + name: pyright (swap for "uvx mypy@latest" if this repo's CI runs mypy instead) + entry: uvx pyright@latest language: system pass_filenames: false - id: prose-gate diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index 66985759..20d7fb85 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -3,19 +3,26 @@ `.pre-commit-config.yaml` is the reference config for the Python `pre-commit` framework (pre-commit.com), for a Python repo with no `.husky/` tree of its own. It runs `ruff format --check`, `ruff check`, and this repo's declared type checker (`pyright` or `mypy`, match -whichever `python-codestyle` says this repo's CI runs) via `uv run`, native tooling, no -Docker, plus the same two shared doc gates the Husky.Net snippet carries: the diff-scoped -prose/comment-style gate and the whole-tree line-ending check. +whichever `python-codestyle` says this repo's CI runs) via `uvx`, native tooling, no Docker, +plus the same two shared doc gates the Husky.Net snippet carries: the diff-scoped +prose/comment-style gate and the whole-tree line-ending check. `uvx` runs each tool from its +own latest release with no project dependency required, matching CI's own invocation for the +lint-only profile (`CODESTYLE.md` "Two profiles"). A repo on the build profile with a +`uv.lock` may swap in `uv run ` per entry to pin the project's own version instead. Copy `../hub-fetch-run.py` alongside `.pre-commit-config.yaml` (repo root) for the doc gates to run: it fetches those two checks fresh from `ptr727/ProjectTemplate`'s `main` branch and runs them, rather than vendoring or pinning a copy. A pin nothing keeps current goes stale by construction, and CI (this repo's own, and the hub's) is the backstop for a change that lands -broken on `main` before it does real damage locally. This is one more network fetch alongside -the Docker pulls the VS Code Lint tasks already do. A fetch failure fails the commit rather -than silently skipping the gate. +broken on `main` before it does real damage locally. These are two more network fetches +alongside the Docker pulls the VS Code Lint tasks already do. A fetch failure fails the +commit rather than silently skipping the gate. -Enable the hook with `uv run pre-commit install`. Full linting (workflow YAML, Markdown, +Install and enable with `uv tool install pre-commit` once, then `pre-commit install`. +`pre-commit` itself is never added as a project dependency: the lint-only profile has no +project environment to add it to, and `uv tool install` gives a persistent, PATH-available +command independent of any project, the same footing `uvx` gives the tools the hooks run. +Full linting (workflow YAML, Markdown, spelling, EditorConfig) stays out of the hook: it runs in CI as pinned action wrappers, and on demand via the VS Code **Lint** tasks in `catalog/snippets/configs/vscode-tasks-python.json` (Docker at `:latest`), which also carries the same prose/EOL gates in whole-repo mode for diff --git a/spec/project-types.json b/spec/project-types.json index 06ebb9cc..7db32de5 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -168,7 +168,7 @@ { "id": "parity.markdownlint", "verdict": "letter", "assert": "One .markdownlint-cli2.jsonc drives the VS Code extension, the CLI, and CI, and CI runs it.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" }, { "id": "parity.cspell", "verdict": "letter", "assert": "A cspell.json (not only a workspace word list) drives the extension, the CLI, and CI.", "intentRef": "CODESTYLE.md" }, { "id": "parity.lang", "verdict": "intent", "assert": "ruff/pyright (Python) and editorconfig/csharpier (C#) configs are shared by editor, CLI, and CI where those languages are present.", "intentRef": "CODESTYLE.md" }, - { "id": "parity.hooks", "verdict": "intent", "assert": "A local hook config file exists in the repo's tracked tree (.husky/pre-commit or .pre-commit-config.yaml), and its content, read directly rather than inferred from per-clone core.hooksPath or pre-commit install state neither of which the audit can observe, invokes at minimum the diff-scoped prose gate and the eol check via hub-fetch-run.py. The language-formatting half (ruff/pyright or mypy, CSharpier/dotnet format) is added once the repo's own corpus passes it clean, a repo mid-convergence rather than a repo out of conformance.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" } + { "id": "parity.hooks", "verdict": "intent", "assert": "A local hook config file exists in the repo's tracked tree (.husky/pre-commit or .pre-commit-config.yaml), and its content, read directly rather than inferred from per-clone core.hooksPath or pre-commit install state neither of which the audit can observe, invokes at minimum the diff-scoped prose gate and the eol check via hub-fetch-run.py. The hub repo itself is the one exception, calling its own scripts/prose_lint.py and scripts/repo_gate.py directly, since it has no separate hub to fetch from. The language-formatting half (ruff/pyright or mypy, CSharpier/dotnet format) is added once the repo's own corpus passes it clean, a repo mid-convergence rather than a repo out of conformance.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" } ] }, "recurring-violations": { From 52a274e3afd96b837a589586934f76a77a8cab90 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 17:56:07 -0700 Subject: [PATCH 2/4] Fix Prose and PATH Findings in Pre-Commit README CodeRabbit's review on the fix PR itself caught three real issues: a run-on sentence over the 25-word cap, an ungrammatical pinning sentence (a leftover of an earlier edit), and no guidance for the case where uv tool install's bin directory is not yet on PATH, which makes pre-commit install fail right after the install step that was supposed to fix that. Splits the long sentence, repairs the grammar, and adds the uv tool update-shell / uv tool dir --bin guidance. Verified end to end again: a fresh repo, pre-commit install, and a real commit running all five hooks clean. --- catalog/snippets/pre-commit/README.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index 20d7fb85..10453ad2 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -2,26 +2,29 @@ `.pre-commit-config.yaml` is the reference config for the Python `pre-commit` framework (pre-commit.com), for a Python repo with no `.husky/` tree of its own. It runs `ruff format ---check`, `ruff check`, and this repo's declared type checker (`pyright` or `mypy`, match -whichever `python-codestyle` says this repo's CI runs) via `uvx`, native tooling, no Docker, -plus the same two shared doc gates the Husky.Net snippet carries: the diff-scoped -prose/comment-style gate and the whole-tree line-ending check. `uvx` runs each tool from its -own latest release with no project dependency required, matching CI's own invocation for the -lint-only profile (`CODESTYLE.md` "Two profiles"). A repo on the build profile with a -`uv.lock` may swap in `uv run ` per entry to pin the project's own version instead. +--check` and `ruff check`. It also runs this repo's declared type checker, `pyright` or +`mypy`, matching whichever `python-codestyle` says this repo's CI runs. Each tool runs via +`uvx`, native tooling, never Docker. `uvx` needs no project dependency, matching CI's own +invocation for the lint-only profile (`CODESTYLE.md` "Two profiles"). A repo on the build +profile with a `uv.lock` may swap in `uv run ` per entry to pin the project's own +version instead. The config also runs the same two shared doc gates the Husky.Net snippet +carries: the diff-scoped prose/comment-style gate and the whole-tree line-ending check. Copy `../hub-fetch-run.py` alongside `.pre-commit-config.yaml` (repo root) for the doc gates to run: it fetches those two checks fresh from `ptr727/ProjectTemplate`'s `main` branch and -runs them, rather than vendoring or pinning a copy. A pin nothing keeps current goes stale by -construction, and CI (this repo's own, and the hub's) is the backstop for a change that lands -broken on `main` before it does real damage locally. These are two more network fetches -alongside the Docker pulls the VS Code Lint tasks already do. A fetch failure fails the -commit rather than silently skipping the gate. +runs them, rather than vendoring or pinning a copy. A pin that no tool keeps current goes +stale by construction, and CI (this repo's own, and the hub's) is the backstop for a change +that lands broken on `main` before it does real damage locally. These are two more network +fetches alongside the Docker pulls the VS Code Lint tasks already do. A fetch failure fails +the commit rather than silently skipping the gate. Install and enable with `uv tool install pre-commit` once, then `pre-commit install`. `pre-commit` itself is never added as a project dependency: the lint-only profile has no project environment to add it to, and `uv tool install` gives a persistent, PATH-available command independent of any project, the same footing `uvx` gives the tools the hooks run. +If `pre-commit install` reports the command not found right after installing it, `uv tool +install`'s own bin directory is not yet on `PATH`: run `uv tool update-shell` and restart or +re-source the shell, or add the directory `uv tool dir --bin` prints directly. Full linting (workflow YAML, Markdown, spelling, EditorConfig) stays out of the hook: it runs in CI as pinned action wrappers, and on demand via the VS Code **Lint** tasks in `catalog/snippets/configs/vscode-tasks-python.json` From d240dd1049eaf038130d524eab463f9d2e6a8ae2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 18:01:57 -0700 Subject: [PATCH 3/4] Unify Pre-Commit Enable Instructions and Fix the Mypy Swap Footgun A fresh qodo review round caught two real gaps this PR's own earlier fixes left behind. GOVERNANCE.md, STANDUP.md, docs/pre-commit-hooks-rollout.md, and the repo-worktree skill still told a reader to enable the Python hook with uv run pre-commit install, the exact instruction the prior commit replaced in the canonical README because it fails on the lint-only profile. Updates all four to the same uv tool install pre-commit once, then pre-commit install flow, so there is one instruction set rather than two that quietly diverged. The suggested mypy swap (uvx mypy@latest, no target) fails on a repo whose [tool.mypy] declares no files/packages, since pass_filenames: false means pre-commit never passes file arguments and mypy requires an explicit target without one. Verified: adding a bare . target fixes it even with no mypy config declared, tested against a real mypy run. --- .agents/skills/repo-worktree/SKILL.md | 6 +++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md | 6 +++--- .github/skills/repo-worktree/SKILL.md | 6 +++--- GOVERNANCE.md | 2 +- STANDUP.md | 2 +- catalog/snippets/pre-commit/.pre-commit-config.yaml | 2 +- docs/pre-commit-hooks-rollout.md | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 1d60f3a1..d4f9e0e4 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -185,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index c378a05e..93518134 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -309580363f245493 +332c16d2c066383b diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 1d60f3a1..d4f9e0e4 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -185,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 1d60f3a1..d4f9e0e4 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -185,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 4de8069e..59ed616f 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -221,7 +221,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i - **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** and **shfmt** the same way for a repo that carries a shell script, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (none of the three has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. - **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Two catalog snippets carry the canonical shape, `catalog/snippets/husky/` (Husky.Net, for .NET or any project including Python) and `catalog/snippets/pre-commit/` (the Python `pre-commit` framework, for a repo with no `.husky/` tree), each carrying a copy of `catalog/snippets/hub-fetch-run.py` alongside it. - **The hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff and the repo's type checker for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to the working tree diff against `HEAD` rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. That scope is the working tree rather than the staged index, so a partially staged file is judged on all of its edits, not only the staged ones, and CI re-checks the whole tree regardless, which is what makes that scope affordable in a hook. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out of the hook regardless, since it resolves a same-owner pin against the GitHub API. -- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the hook makes, one per fetched script, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or `uv run pre-commit install`, and CI remains the authoritative run either way. +- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the hook makes, one per fetched script, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`, and CI remains the authoritative run either way. - **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks, plus `Lint: Prose` and `Lint: EOL`, the same two hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. The Docker invocations below run the same tools and configs as the VS Code tasks. Their headless form separates the image pull and minimizes repository exposure for an agent executor. diff --git a/STANDUP.md b/STANDUP.md index f90e46bb..d7288b12 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -179,7 +179,7 @@ Carry `AGENTS.md`'s skill-dependency pointer paragraph, the one naming `scripts/ Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place. -**Wire a local commit hook here, not after the fact.** `.husky/pre-commit` and `.pre-commit-config.yaml` are deliberately excluded from the baseline above (each repo's own formatters make the content repo-owned, per [`spec/divergences.json`][divergences]), so nothing in the carry step above wires one. Copy and adapt the applicable catalog snippet, `catalog/snippets/husky/` for Husky.Net or `catalog/snippets/pre-commit/` for the Python `pre-commit` framework, each with `catalog/snippets/hub-fetch-run.py` alongside it, and enable it (`git config core.hooksPath .husky` or `uv run pre-commit install`) before the section 5 audit run. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. +**Wire a local commit hook here, not after the fact.** `.husky/pre-commit` and `.pre-commit-config.yaml` are deliberately excluded from the baseline above (each repo's own formatters make the content repo-owned, per [`spec/divergences.json`][divergences]), so nothing in the carry step above wires one. Copy and adapt the applicable catalog snippet, `catalog/snippets/husky/` for Husky.Net or `catalog/snippets/pre-commit/` for the Python `pre-commit` framework, each with `catalog/snippets/hub-fetch-run.py` alongside it, and enable it (`git config core.hooksPath .husky`, or `uv tool install pre-commit` once then `pre-commit install`) before the section 5 audit run. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. ## 3. Stand Up the Workflows diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml index 567623a0..2e267ea4 100644 --- a/catalog/snippets/pre-commit/.pre-commit-config.yaml +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: language: system pass_filenames: false - id: type-check - name: pyright (swap for "uvx mypy@latest" if this repo's CI runs mypy instead) + name: pyright (swap for "uvx mypy@latest ." if this repo's CI runs mypy instead) entry: uvx pyright@latest language: system pass_filenames: false diff --git a/docs/pre-commit-hooks-rollout.md b/docs/pre-commit-hooks-rollout.md index d2a3c012..5b171d11 100644 --- a/docs/pre-commit-hooks-rollout.md +++ b/docs/pre-commit-hooks-rollout.md @@ -31,7 +31,7 @@ vendored or pinned. ## Per-Repo Conversion Copy and adapt the applicable catalog snippet plus `catalog/snippets/hub-fetch-run.py`, enable it -(`git config core.hooksPath .husky` or `uv run pre-commit install`), confirm the doc gates run +(`git config core.hooksPath .husky`, or `uv tool install pre-commit` once then `pre-commit install`), confirm the doc gates run clean against the repo's current tree, and open the PR through the repo's normal branching model. A repo whose corpus does not yet pass its language formatter clean ships the doc-gate half first and adds the language half once it does, per the mid-convergence carve-out. That partial state is From 803c6eb4557d395e4ea53fb8931614d2bd0cd4b2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 18:10:10 -0700 Subject: [PATCH 4/4] Scope the Hook Network-Call Claim to the Doc Gates uvx-run ruff and the type checker can also reach the network to resolve @latest on a cache miss or refresh, so "the only network calls the hook makes" was inaccurate once the Python snippet started using uvx. Scopes the claim to what it actually describes, the doc gates' two fetches, and names the tool-resolution network use as the same accepted category as the Docker pulls elsewhere in this section. --- GOVERNANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 59ed616f..491837b6 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -221,7 +221,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i - **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** and **shfmt** the same way for a repo that carries a shell script, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (none of the three has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. - **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Two catalog snippets carry the canonical shape, `catalog/snippets/husky/` (Husky.Net, for .NET or any project including Python) and `catalog/snippets/pre-commit/` (the Python `pre-commit` framework, for a repo with no `.husky/` tree), each carrying a copy of `catalog/snippets/hub-fetch-run.py` alongside it. - **The hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff and the repo's type checker for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to the working tree diff against `HEAD` rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. That scope is the working tree rather than the staged index, so a partially staged file is judged on all of its edits, not only the staged ones, and CI re-checks the whole tree regardless, which is what makes that scope affordable in a hook. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out of the hook regardless, since it resolves a same-owner pin against the GitHub API. -- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the hook makes, one per fetched script, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`, and CI remains the authoritative run either way. +- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the doc gates make, one per fetched script. A Python repo's `uvx`-run ruff and type checker can also reach the network, to resolve `@latest` on a cache miss or refresh, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`, and CI remains the authoritative run either way. - **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks, plus `Lint: Prose` and `Lint: EOL`, the same two hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. The Docker invocations below run the same tools and configs as the VS Code tasks. Their headless form separates the image pull and minimizes repository exposure for an agent executor.