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 @@ -152,7 +152,7 @@ The fleet's tooling lives in the hub once and a repository runs it from there ra

**A report or finding a hub tool produces names the hub commit it ran from.** The tool moves independently of the repository it measures. So a verdict carrying no hub commit cannot be re-run, and two runs that disagree cannot be attributed to the tree or to the tool. The obligation is the runner's rather than the tool's, since a tool reports on the repository it measures rather than on itself. So the commit is read from the hub checkout and written into the report beside the verdict. This is the same requirement "Verification Discipline" places on any claim that gets acted on.

**CI reaches the same tooling as a pinned action or reusable workflow.** A runner holds no hub checkout. So a workflow consumes the hub's composite action or reusable workflow and pins it to a commit SHA, per the action-pinning rule under "Workflow YAML Conventions". A standard workflow whose job graph is identical across repos of a type is reached the same way. It is a `workflow_call` task the hub hosts once. The repository carries only the caller stub and a composite-action hook for what is genuinely its own. The pin is what makes a released repository's gate reproducible. That is because an unpinned consume lets a later hub commit fail a re-run of a change that already passed. Branch-dependent behavior belongs inside the consumed action, because `uses:` takes no expressions and a per-branch ref therefore cannot be selected in the workflow file.
**CI reaches the same tooling as a pinned action or reusable workflow.** A runner holds no hub checkout. So a workflow consumes the hub's composite action or reusable workflow and pins it to a commit SHA, per the action-pinning rule under "Workflow YAML Conventions". A standard workflow whose job graph is identical across repos of a type is reached the same way. It is a `workflow_call` task the hub hosts once. The repository carries only the caller stub and a composite-action hook for what is genuinely its own. The pin is what makes a released repository's gate reproducible. That is because an unpinned consume lets a later hub commit fail a re-run of a change that already passed. A doc gate fetched from `main` stays unpinned because a pin nothing keeps current goes stale. Dependabot keeps this `uses:` pin current, so that reason does not reach it. Branch-dependent behavior belongs inside the consumed action, because `uses:` takes no expressions and a per-branch ref therefore cannot be selected in the workflow file.

**An unreachable hub means the tool did not run, and that is the result reported.** A carried copy still works offline and a reached one does not. That is the cost this model trades away, and the reason to state the failure rather than route around it. A check that cannot run reports itself as not run, never as clean, which is the silent-narrowing failure "Verification Discipline" names. A hand-rolled substitute is not the tool either. A reconstructed gate encodes its author's reading of the rule rather than the rule, and agrees with no other repository. It is the duplicated effort this model exists to end, so an agent that cannot reach the hub says so and stops.

Expand Down Expand Up @@ -297,7 +297,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, 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. That second shape is the one 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. It is 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. 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. That is because `dotnet tool restore` then `dotnet husky install` is what generates the `.husky/_/husky.sh` its hook sources. It is also because 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. That 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 **doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, via native tooling. That is CSharpier and `dotnet format` for .NET, or ruff and the repo's type checker for Python. A repo adds each half once its tree passes that half. That is because 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. That is the difference between about 2.2 seconds and about 0.13, and is what makes it affordable in a hook at all. Under a plain git hook that scope is the working tree rather than the staged index. So a partially staged file is judged on all of its edits. Under the `pre-commit` framework it is the staged state instead. That is because 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. The line-ending consistency check is one such check. 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`. Each file comes 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. CI (this repo's own, and the hub's) is the backstop. It 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. That is 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 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`. Each file comes 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. CI (this repo's own, and the hub's) is the backstop. It 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. The unresolved-commit fallback reads the prose gate and its data from `main` twice. When the two reads differ, it refuses to run a mixed pair, and a re-run is the remedy. A Python repo's `uvx`-run ruff and type checker can also reach the network, to resolve `@latest` on a cache miss or refresh. That is 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. 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. That is because 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 is untracked and so is recorded after the last commit. A change that also carries the canonical ledger records those passes ahead of that commit instead. The ledger is tracked content the commit has to hold. It reads `develop` and accepts no override from the environment. That is because an environment variable is set inline on the very command being gated, by whoever is being gated. It 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. 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. That is 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. 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. The prose layer above therefore stays the agent-agnostic one that actually binds. It is hub-only for now. `local_review.py` is hub-hosted per "Hub-Hosted Tooling". 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. 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. It fails no pull request over what that shows. It still fails 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. `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.

Expand Down
2 changes: 2 additions & 0 deletions catalog/snippets/husky/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ where that fails. An allow-list permitting only
`raw.githubusercontent.com` gets an unresolved-commit fallback. These are network calls
alongside the Docker pulls the Lint tasks below already do. A fetch failure fails the commit
rather than silently skipping the gate.
The unresolved-commit fallback reads the prose gate and its data from `main` twice.
When the two reads differ, it refuses to run a mixed pair, and a re-run is the remedy.

This snippet is for a repo that already keeps a .NET tool manifest declaring Husky.Net.
That is because the hook sources `.husky/_/husky.sh`, a file `dotnet husky install` generates after
Expand Down
2 changes: 2 additions & 0 deletions catalog/snippets/pre-commit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ where that fails. An allow-list permitting only
`raw.githubusercontent.com` gets an unresolved-commit fallback. These are network calls
alongside the Docker pulls the VS Code Lint tasks already do. A fetch failure fails the commit
rather than silently skipping the gate.
The unresolved-commit fallback reads the prose gate and its data from `main` twice.
When the two reads differ, it refuses to run a mixed pair, and a re-run is the remedy.

Install and enable with `uv tool install pre-commit` once, then `pre-commit install`.
`pre-commit` itself is never added as a project dependency.
Expand Down
2 changes: 1 addition & 1 deletion catalog/snippets/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

merge-bot:
name: Merge bot pull request job
uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@20616e0a70613ad8727d567990f5d0e082f5275c # 2.0.338
uses: ptr727/ProjectTemplate/.github/workflows/merge-bot-task.yml@320713d9d491f578b5604a8d67b82478717d7b05 # 2.0.1319
secrets:
CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

run-codegen:
name: Run codegen and pull request job
uses: ptr727/ProjectTemplate/.github/workflows/run-codegen-pull-request-task.yml@0b07a59d7c65d07d8df275a96deaf2e06cbefd51 # 2.0.352
uses: ptr727/ProjectTemplate/.github/workflows/run-codegen-pull-request-task.yml@320713d9d491f578b5604a8d67b82478717d7b05 # 2.0.1319
secrets:
CODEGEN_APP_CLIENT_ID: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
CODEGEN_APP_PRIVATE_KEY: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}
Loading