Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,10 @@ 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, which Dependabot bumps. It runs **editorconfig-checker** via Docker `:latest`, since its action only installs the CLI and the Docker one-liner is what actually runs the check. **shellcheck** and **shfmt** run the same way for a repo that carries a shell script, and **PSScriptAnalyzer** does for a repo that carries `.ps1` files. None of shellcheck, shfmt, and PSScriptAnalyzer has an action. For a repo that carries an action under `.github/actions/`, the repo gate's `composite-actions` check runs **shellcheck** in Docker over each bash or sh `run:` body. It also runs **check-jsonschema** through `uvx`, because actionlint reads workflows only. The `composite-actions` check runs shellcheck at warning severity without SC2154, weaker than for a `.sh` file. 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. Catalog snippets carry the canonical shape, each with a copy of `catalog/snippets/hub_fetch_run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo that already keeps a .NET tool manifest declaring it, since `dotnet tool restore` then `dotnet husky install` is what generates the `.husky/_/husky.sh` its hook sources, and the snippet ships no manifest of its own. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. No snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the shared doc gates. A repo drops the block for a language it does not have. A Docker, config, or docs repo therefore wires the doc gates alone, which is a mechanism rather than the absence of one. Which snippet a repo starts from is bounded by the toolchain it already keeps, never by which languages its checks cover. A snippet is a starting point rather than the only shape: a repo may wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, which is what this repository does. That path matters, since `parity.hooks` reads the tracked file at `.husky/pre-commit` or `.pre-commit-config.yaml` rather than a per-clone hooks path it cannot observe.
- **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 under a plain git hook, so a partially staged file is judged on all of its edits. Under the `pre-commit` framework it is the staged state instead, since that runner sets unstaged changes aside before running a hook, which is measured rather than assumed. That set-aside leaves untracked files in place, so the hook there passes the gate the paths `pre-commit` names instead of the tree. The pull request gate re-checks every line the branch changes against its base, which is the scope a review is judged on. A whole-repo check belongs in the hook 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 hook** runs **language formatting** and the **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 under a plain git hook, so a partially staged file is judged on all of its edits. Under the `pre-commit` framework it is the staged state instead, since that runner sets unstaged changes aside before running a hook, which is measured rather than assumed. That set-aside leaves untracked files in place, so the hook there passes the gate the paths `pre-commit` names instead of the tree. The pull request gate re-checks every line the branch changes against its base, which is the scope a review is judged on. A whole-repo check belongs in the hook 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` with the `prose-gate.json` beside it 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. The prose gate adds one `api.github.com` commit lookup to its fetch, with a `git ls-remote` against `github.com` where that fails. `uv` resolves the packages its script block declares on a cache miss. Beyond fetching the scripts, those lookups and that resolution are the only network calls the doc gates make. An allow-list permitting only `raw.githubusercontent.com` still runs the gate, with the commit reported as unresolved. 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 carries both gates 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`. The Husky.Net snippet needs one more step per clone, `dotnet tool restore` then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. CI remains the authoritative run either way.
- **The hub carries a second hook, `.husky/pre-push`, gating the local-review rule rather than lint.** It runs `scripts/local_review.py check`, refusing a branch push whose diff no recorded review pass covers, per "Verification Discipline" above. It fires only for a branch update, so a tag push and a branch delete pass through, and a branch holding no net content against its target has nothing for a review to cover. It refuses rather than guesses in every state it cannot speak for, since the engine reads the checkout it runs in while a push delivers a commit, and those describe the same content only sometimes. The order that keeps them describing the same content is to commit the change, then review the diff, record its receipt, and push, the receipt being untracked and so recorded after the last commit. A change that also carries the canonical ledger records those passes ahead of that commit instead, the ledger being tracked content the commit has to hold. It reads `develop` and accepts no override from the environment, since an environment variable is set inline on the very command being gated, by whoever is being gated, which is the same reason an authorization is never read from a channel the agent itself can set. The cost is that it measures a branch based on anything but `develop` against `develop` anyway, so its refusal there is not a verdict about that branch. Each refusal names its own case, and the `local-strict-review` Skill carries the fleet's one enumeration of them with what clears each, deliberately in one place rather than restated here. It folds the engine's three-valued exit honestly: a check that could not run blocks too, and says so in different words than a check that ran and found no pass, because a gate that waves a push through when it could not run is a gate that stopped gating. It is a backstop rather than a seal, and it is bypassable by construction: `--no-verify` is the documented route out of a pickle and is not the only one, since a git hook cannot police its own invocation. A Claude Code session meets a narrower surface, that flag being denied unconditionally by the agent-safety hook's explicit-bypass rule, and that denial is Claude Code's alone, since Codex and opencode carry no such hook yet. So the committed hook raises the cost of skipping the rule for one agent and lowers it for none, and the prose layer above stays the agent-agnostic one that actually binds. It is hub-only for now. `local_review.py` is hub-hosted per "Hub-Hosted Tooling", and a downstream repo reaches it as a hub checkout's copy run with its own worktree as the working directory, so a catalog snippet carrying this hook fleet-wide is a later step rather than part of this one. `canonical_review.py` is not run here at all, and nothing else gates a push or a pull request over a canonical unit either, that read being swept on a schedule instead, per "Verification Discipline" above. The hub's own `.github/actions/validate` composite action renders its coverage burn-down into every run's job summary and fails no pull request over what that shows, while still failing where the engine could not read the manifest or the ledger at all, that being a boundary rather than a verdict.
- **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 hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation.
- **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. `Lint: Prose` adds the hook's prose gate in whole-repo mode rather than diff-scoped, and `Lint: EOL` runs the hook's own whole-tree EOL check on demand.

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.

Expand Down
4 changes: 2 additions & 2 deletions catalog/snippets/configs/vscode-tasks-python.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@
// Lint group, run on demand via Docker, where --pull=always forces each to re-pull :latest.
// It mirrors the CI lint gate.
// Broad live spell-checking is the cspell extension's job.
// Pre-commit already runs the prose/EOL gates diff-scoped.
// Lint: Prose and Lint: EOL below add the same two checks in whole-repo mode, for on-demand full-tree validation.
// Pre-commit already runs the prose gate diff-scoped and the EOL gate over the whole tree.
// Lint: Prose below adds the prose gate in whole-repo mode, and Lint: EOL runs the hook's own EOL check on demand.
// 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).
Expand Down
4 changes: 2 additions & 2 deletions catalog/snippets/configs/vscode-tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
// Lint group, run on demand via Docker, where --pull=always pins each to the current :latest.
// It mirrors the CI lint gate.
// Broad live spell-checking is the cspell extension's job.
// Pre-commit already runs the prose/EOL gates diff-scoped.
// Lint: Prose and Lint: EOL below add the same two checks in whole-repo mode, for on-demand full-tree validation.
// Pre-commit already runs the prose gate diff-scoped and the EOL gate over the whole tree.
// Lint: Prose below adds the prose gate in whole-repo mode, and Lint: EOL runs the hook's own EOL check on demand.
{
"label": "Lint: EditorConfig",
"type": "process",
Expand Down
13 changes: 7 additions & 6 deletions catalog/snippets/husky/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ what it runs, and the two shared doc gates are what every repo owes either way.

Full linting (workflow YAML, Markdown, spelling, EditorConfig) is **not** run in 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.json` (Docker at `:latest`), which also carries the
same prose/EOL gates in whole-repo mode for on-demand full-tree validation, not just the
diff-scoped commit-time run. Keeping the Docker-dependent doc linters out of the hook is what
keeps it fast, and Docker is the dependency it stays free of: the hook still needs network
for the two `hub_fetch_run.py` calls above, so a fully offline clone fails the commit loudly
on the fetch rather than silently skipping the doc gates.
`catalog/snippets/configs/vscode-tasks.json` (Docker at `:latest`). That file also carries the
prose gate in whole-repo mode, for on-demand full-tree validation beyond the diff-scoped
commit-time run. Its `Lint: EOL` task runs the same whole-tree EOL check the hook runs.
Keeping the Docker-dependent doc linters out of the hook is what keeps it fast. Docker is the
dependency it stays free of, though the hook still needs network for the two
`hub_fetch_run.py` calls above. A fully offline clone therefore fails the commit loudly on the
fetch rather than silently skipping the doc gates.

A copied `.husky/pre-commit` is an extensionless shebang script, so pin it to **LF** in
`.gitattributes` (`.husky/pre-commit text eol=lf`), git-level enforcement independent of the
Expand Down
4 changes: 2 additions & 2 deletions catalog/snippets/pre-commit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ spelling, EditorConfig) stays out of the hook: it runs in CI as pinned action wr
demand via the VS Code **Lint** tasks. A Python repo takes those from
`catalog/snippets/configs/vscode-tasks-python.json` (Docker at `:latest`), and a repo with no
Python takes the Lint group alone rather than that file's Python tasks. Either way they carry
the same prose/EOL gates in whole-repo mode for on-demand full-tree validation, not just the
diff-scoped commit-time run.
the prose gate in whole-repo mode, for on-demand full-tree validation beyond the diff-scoped
commit-time run. Their `Lint: EOL` task runs the same whole-tree EOL check the hook runs.

No LF pin is needed for `.pre-commit-config.yaml` itself: it is plain YAML, not a shebang
script, so the fleet's `[*]` `.editorconfig`/`.gitattributes` default already covers it.
Loading