From 4d5accf5395d3cdd315eabb7beab8df203bdecf1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 18:12:22 -0700 Subject: [PATCH 01/31] Settle Three Carried Sentences Whose Hub Sources Disagreed Each of the three rested on two hub sources contradicting each other, so the fix settles the source rather than rewording the carrier. secrets: inherit. The ban was real but scoped wrongly. GitHub documents the keyword for a caller in the same organization or enterprise, and the fleet is a personal account, so it is unavailable on a cross-repository call to a hub task and available on a same-repository call by local path. The fleet does demonstrate the exception: PlexCleaner's test-pull-request.yml carries both shapes, and NxWitness's publish-release.yml calls its own build-docker-task.yml with secrets: inherit and push: true, which authenticates to Docker Hub from the two inherited names. Husky.Net versus pre-commit. Both sources used the wrong discriminator. The runner follows the host toolchain, never the languages the checks cover, and this repository is the counterexample that settles it: source-only and docs, no .NET tool manifest, running Python checks and the doc gates from a .husky/ directory enabled as a plain core.hooksPath. Stated one-directionally, since the Husky snippet needs a host Python interpreter too. This closes the gap where a Docker, config, or docs repo had no mechanism both sources agreed applied to it. Carried files edited locally. The old wording licensed a local fix alongside the upstream proposal, and it now sends the change upstream instead. AGENTS.md routed a project's own API conventions into a carried file, which spec/section-model.md calls drift to reconcile, so it routes to the repo's topical docs. What a conformance audit detects at each fidelity tier is new text in spec/fidelity-model.md, which is hub-only and therefore verifiable by its reader: notably the intent staleness advisory cannot fire on a local edit at all, since it compares last-change dates and a local edit makes the copy newer. Also fixes catalog/snippets/husky/pre-commit, which probed python3 before py -3 while its own comment claimed to mirror spec/host-tools.json's declared order. The hub's own hook was corrected already and this snippet was missed, and the reversed order defeats the Windows case the adjacent comment describes. --- .agents/skills/agent-conduct/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/agent-conduct/SKILL.md | 2 +- .github/skills/agent-conduct/SKILL.md | 2 +- AGENTS.md | 2 +- CODESTYLE.md | 2 +- GOVERNANCE.md | 4 ++-- catalog/snippets/husky/README.md | 9 +++++-- catalog/snippets/husky/pre-commit | 10 ++++---- .../pre-commit/.pre-commit-config.yaml | 4 +++- catalog/snippets/pre-commit/README.md | 15 ++++++++---- docs/reusable-workflows.md | 2 +- reports/canonical-review.json | 24 ++++++++++++------- reports/canonical-review.md | 5 ++-- spec/fidelity-model.md | 13 ++++++++++ 15 files changed, 65 insertions(+), 33 deletions(-) diff --git a/.agents/skills/agent-conduct/SKILL.md b/.agents/skills/agent-conduct/SKILL.md index 9f20255f..d02e4e04 100644 --- a/.agents/skills/agent-conduct/SKILL.md +++ b/.agents/skills/agent-conduct/SKILL.md @@ -38,7 +38,7 @@ Claims about a pull request being reviewed, clean, or mergeable are owned by the ## When a Failure Surfaces a Lesson - **Durable knowledge lands in the committed docs, not in agent memory**, as part of the change that surfaced it, per `GOVERNANCE.md` "Durable Knowledge and Self-Improvement". Memory does not survive a new session or machine, so it holds only environment nuance and in-flight state. -- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`** rather than only patching it locally. A local fix leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. +- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`.** Patching the local copy leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. - **A review flags an instance, so fix the class**: sweep for the siblings before replying, because reviewers sample rather than enumerate. - **A rule that keeps needing restating** is usually a stale or missing skills install, so run `python3 scripts/skills_install.py --report` from a hub checkout (the `fleet-conformance-check` skill) before concluding the rule does not exist. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 008f19b6..63fe4482 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -fe834763b58313f7 +23da52348160499c diff --git a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md index 9f20255f..d02e4e04 100644 --- a/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md @@ -38,7 +38,7 @@ Claims about a pull request being reviewed, clean, or mergeable are owned by the ## When a Failure Surfaces a Lesson - **Durable knowledge lands in the committed docs, not in agent memory**, as part of the change that surfaced it, per `GOVERNANCE.md` "Durable Knowledge and Self-Improvement". Memory does not survive a new session or machine, so it holds only environment nuance and in-flight state. -- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`** rather than only patching it locally. A local fix leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. +- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`.** Patching the local copy leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. - **A review flags an instance, so fix the class**: sweep for the siblings before replying, because reviewers sample rather than enumerate. - **A rule that keeps needing restating** is usually a stale or missing skills install, so run `python3 scripts/skills_install.py --report` from a hub checkout (the `fleet-conformance-check` skill) before concluding the rule does not exist. diff --git a/.github/skills/agent-conduct/SKILL.md b/.github/skills/agent-conduct/SKILL.md index 9f20255f..d02e4e04 100644 --- a/.github/skills/agent-conduct/SKILL.md +++ b/.github/skills/agent-conduct/SKILL.md @@ -38,7 +38,7 @@ Claims about a pull request being reviewed, clean, or mergeable are owned by the ## When a Failure Surfaces a Lesson - **Durable knowledge lands in the committed docs, not in agent memory**, as part of the change that surfaced it, per `GOVERNANCE.md` "Durable Knowledge and Self-Improvement". Memory does not survive a new session or machine, so it holds only environment nuance and in-flight state. -- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`** rather than only patching it locally. A local fix leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. +- **Where the governing doc is carried from the hub, file the finding against `ptr727/ProjectTemplate`.** Patching the local copy leaves every sibling repo with the same trap. Search open and closed issues first, then update the matching issue or file a new one. - **A review flags an instance, so fix the class**: sweep for the siblings before replying, because reviewers sample rather than enumerate. - **A rule that keeps needing restating** is usually a stale or missing skills install, so run `python3 scripts/skills_install.py --report` from a hub checkout (the `fleet-conformance-check` skill) before concluding the rule does not exist. diff --git a/AGENTS.md b/AGENTS.md index 2359cd0f..9b51c821 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file is the entry point every coding agent reads first, and it holds only three things: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) (a General section plus per-language sections for .NET, Python, and Shell, the language sections packaged as the `dotnet-codestyle`, `python-codestyle`, and `shell-codestyle` Skills), and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). -Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** (e.g. a "Library API Conventions" section) live in that project's own `AGENTS.md`, **not** in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), because that file targets GitHub Copilot / VS Code specifically, while this file and `GOVERNANCE.md` are the agent-agnostic ones every coding agent is directed to read, so any rule a reviewer must honor has to live in one of those two files to be provider-independent. +Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own topical docs. A library's own API conventions go to its `CODESTYLE.md` or its `ARCHITECTURE.md`. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. The topical docs are agent-agnostic, and every coding agent is directed to read them. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. ## Fleet Bootstrap diff --git a/CODESTYLE.md b/CODESTYLE.md index b9b73cd8..6e41a9ec 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so the *mechanism* stays the repo's choice: Husky.Net for .NET, `pre-commit` for Python, canonical configs for both in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* stays the repo's choice between two canonical shapes, and neither is scoped to a language by what it runs. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e6ce1b3a..930d3474 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -16,7 +16,7 @@ The specific rules in this file implement a few governing principles. Read these ## Durable Knowledge and Self-Improvement - **Durable knowledge lives in the committed docs, not in agent memory.** Anything a future agent must honor (a rule, a contract, a hard-won gotcha, a pattern worth repeating or one to avoid) belongs in a committed governance file (`GOVERNANCE.md` for a cross-cutting rule, `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, or a committed backlog the repository already keeps). Agent memory does not survive a new session, a new machine, or a new environment, so it holds only environment-specific nuance and in-flight session state, never anything whose loss on reset would matter. A durable lesson left only in memory is lost to the next agent. -- **Keep the governance current as you work.** When work surfaces something durable (a rule worth enforcing, a recurring gotcha, a positive pattern to repeat, a negative one to design out), record it in the governance docs as part of that change, rather than leaving it in a local note or routing around it with a one-off workaround. Where the governing doc is carried from a template this repo cannot edit directly, propose the change upstream instead of only fixing it locally. Governance is not static: it improves by agents folding good patterns in and designing bad ones out. +- **Keep the governance current as you work.** When work surfaces something durable (a rule worth enforcing, a recurring gotcha, a positive pattern to repeat, a negative one to design out), record it in the governance docs as part of that change, rather than leaving it in a local note or routing around it with a one-off workaround. Where the governing doc is carried from a template this repo cannot edit directly, propose the change upstream rather than patching the local copy. A local patch leaves every sibling repo with the same trap. Governance is not static: it improves by agents folding good patterns in and designing bad ones out. - **A durable rule earns a mechanical hook only where a hook can actually decide it, otherwise it stays prose.** Three conditions together, not any one alone. The failure recurs even after the governing prose was demonstrably read and understood, so it is not a discovery or loading problem a structural fix (getting the rule into context at all) would already solve. The triggering shape is decidable from the tool call's own text, arguments, and working directory alone, with no semantic or contextual judgment required. And the failure is destructive or hard to reverse rather than a quality miss. A worktree-isolation lapse met all three (it recurred under prose the agent had already read, "is this command's target a primary checkout" is a plain directory comparison, and the harm is another task's swept or reverted work), so it was promoted to a `gh-write-guard` hook rule. A skill's own trigger going unread by the session at all, by contrast, is a loading problem, fixed by getting the rule into context (the `CLAUDE.md` importing `AGENTS.md`), not by a hook. And "was this review finding actually evidence-backed" fails the second condition outright: a hook sees only the command text, never the judgment call itself, so it can only ever nag, not decide, and that class of rule stays prose and a chained Skill trigger. Those three conditions gate promotion to a **host** hook, the involuntary layer that fires in every session under the maintainer's own credentials and that only the maintainer can grant an exemption from, which is why the bar there is destructive harm. A **committed** hook in the repository's own tree is a third layer between prose and that one, and it is earned on weaker grounds: it is opt-in per clone, visible in the tree, bypassable by design, and it therefore fits a rule whose harm is a quality miss rather than a destruction. The second condition still binds it, since a hook that cannot decide its own trigger is a hook that nags, so what earns the layer is finding the decidable half of a rule whose other half is judgment. The local-review rule under "Verification Discipline" is the worked example: whether a review's findings were rightly disposed of is judgment no hook can decide and stays prose, while whether a review pass ran over exactly the content being pushed is a receipt comparison, which the hub's own `.husky/pre-push` decides. This section keeps the full rules and is surfaced at its decision moment by the `agent-conduct` Skill at `.agents/skills/agent-conduct/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. @@ -244,7 +244,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **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. +- **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 follows from whether it has a host .NET toolchain, never from which languages its checks cover. - **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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index 6f347f3d..c01c514b 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -13,6 +13,11 @@ change that lands broken on `main` before it does real damage locally. These are network fetches alongside the Docker pulls the Lint tasks below already do. A fetch failure fails the commit rather than silently skipping the gate. +This snippet needs a host .NET toolchain, since the hook sources `.husky/_/husky.sh`, a file +`dotnet tool restore` and `dotnet husky install` generate. A repo without one uses +`../pre-commit/` instead. Neither snippet is scoped to a language by 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 @@ -29,5 +34,5 @@ override needed. A CRLF shebang breaks execution. Both language blocks run unconditionally, with no tool-presence guard: a repo that keeps a block declares that tool required, so a missing one fails the commit loudly rather than -skipping the check silently. Drop the `dotnet husky run` block in a non-.NET repo, and drop -the Python block in a non-Python repo, rather than leaving it in place to no-op. +skipping the check silently. Drop the Python block in a repo with no Python, rather than +leaving it in place to no-op. diff --git a/catalog/snippets/husky/pre-commit b/catalog/snippets/husky/pre-commit index ba07fc01..c525d7ea 100644 --- a/catalog/snippets/husky/pre-commit +++ b/catalog/snippets/husky/pre-commit @@ -27,14 +27,14 @@ uv run ruff check || exit 1 # Swap for `uv run mypy` if this repo's CI runs mypy instead. uv run pyright || exit 1 -# The interpreter is chosen by probing, not assumed, mirroring spec/host-tools.json's own probe order. +# The interpreter is chosen by running the probes spec/host-tools.json declares, in its order. # On native Windows the python.org install registers `py`, not `python3`, and a `python3` found on PATH there is often the Microsoft Store alias stub, present but broken. -if python3 --version >/dev/null 2>&1; then - run_py() { python3 "$@"; } -elif py -3 --version >/dev/null 2>&1; then +if py -3 --version >/dev/null 2>&1; then run_py() { py -3 "$@"; } +elif python3 --version >/dev/null 2>&1; then + run_py() { python3 "$@"; } else - echo "pre-commit: neither 'python3 --version' nor 'py -3 --version' ran, so the doc gates did not run." >&2 + echo "pre-commit: neither 'py -3 --version' nor 'python3 --version' ran, so the doc gates did not run." >&2 exit 1 fi diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml index 2e267ea4..04924710 100644 --- a/catalog/snippets/pre-commit/.pre-commit-config.yaml +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -1,4 +1,6 @@ -# Reference pre-commit (pre-commit.com) config for a fleet Python repo with no .NET/Husky.Net tree. +# Reference pre-commit (pre-commit.com) config for a fleet repo with no host .NET toolchain. +# Husky.Net is a dotnet tool, so the discriminator is the toolchain rather than the repo's languages. +# Drop the ruff and type-check hooks in a repo with no Python, keeping the two doc gates every repo owes. # 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). # Install and enable with `uv tool install pre-commit` once, then `pre-commit install`. diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index 771d7c70..ecec3cf7 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -1,14 +1,19 @@ # Pre-commit snippet -`.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` 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 +`.pre-commit-config.yaml` is the reference config for the `pre-commit` framework +(pre-commit.com), for a repo with no host .NET toolchain. That is the discriminator, not the +repo's own languages: Husky.Net is a `dotnet` tool, and this framework installs independently +of one. In a repo with Python it runs `ruff format --check` and `ruff check`, plus that +repo's declared type checker, `pyright` or `mypy`, matching whichever `python-codestyle` says +its 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. +carries: the diff-scoped prose/comment-style gate and the whole-tree line-ending check. Those two +are what every repo owes regardless of language. A repo with no Python drops the `ruff-format`, +`ruff-check` and `type-check` hooks and keeps the doc gates, which is what a Docker, config, or +docs repo wires. 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 diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index cfce8983..2d3bfd30 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. `secrets: inherit` is not used, since it is documented for a caller in the same organization or enterprise as the called workflow and the fleet is a personal account. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is available is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task maps every secret by name, and `inherit` is never used on one. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is used. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. diff --git a/reports/canonical-review.json b/reports/canonical-review.json index cb008295..3c104117 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -9,6 +9,14 @@ "hubCommit": "0e6aacf45334149f6158aa23b8132d6aee1d1eb0", "stamp": "2026-09-01T16:37:57Z" }, + { + "unit": ".agents/skills/agent-conduct/SKILL.md > When a Failure Surfaces a Lesson", + "digest": "sha256:7bb4825e38c44c74366d2f0051d521968d87df765144bb080ca059a417e2cd75", + "reviewer": "agent-skill", + "findings": 6, + "hubCommit": "52b0c550ab07ab58b940a353a83247d7451e76e5", + "stamp": "2026-09-02T01:11:56Z" + }, { "unit": ".agents/skills/comment-and-doc-style/SKILL.md > Docker lint authorization", "digest": "sha256:b61bea3f7010cafc475ce582e234e79f1e55e4bd0e5332dbe2b05f4fb5bb03ff", @@ -299,11 +307,11 @@ }, { "unit": "CODESTYLE.md > General", - "digest": "sha256:ee34e84dab97a638e5011f7b8b5fc30893f82461cf6907ba35c96030eb2d4bb0", + "digest": "sha256:c936aef3d1e83265e9180951190749bbd389e81b9faaaccb84254597ff74bd98", "reviewer": "agent-skill", - "findings": 10, - "hubCommit": "df5e2493b48aa45d80f586db68af0c21be97ea21", - "stamp": "2026-09-01T05:58:03Z" + "findings": 11, + "hubCommit": "52b0c550ab07ab58b940a353a83247d7451e76e5", + "stamp": "2026-09-02T01:11:57Z" }, { "unit": "CODESTYLE.md > Shell", @@ -315,11 +323,11 @@ }, { "unit": "GOVERNANCE.md > Durable Knowledge and Self-Improvement", - "digest": "sha256:d2caaf6052fdf9838034bcd3dbf8a250d44b303b3bb73754ecb4c6fa0ead8bd0", + "digest": "sha256:cd5ef7be0c674a9ba29c3196ae81757ae18f7bb8ca82f573ffd77f96d363cc8e", "reviewer": "agent-skill", - "findings": 2, - "hubCommit": "d0541523890324cbbdbb63631c8dd83e6003703a", - "stamp": "2026-09-01T19:25:11Z" + "findings": 11, + "hubCommit": "52b0c550ab07ab58b940a353a83247d7451e76e5", + "stamp": "2026-09-02T01:11:58Z" }, { "unit": "GOVERNANCE.md > Repository Layout", diff --git a/reports/canonical-review.md b/reports/canonical-review.md index eee8949f..9b54c319 100644 --- a/reports/canonical-review.md +++ b/reports/canonical-review.md @@ -7,9 +7,9 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than ## Coverage - units: 288 -- covered: 48 +- covered: 49 - stale: 0 -- never read here: 240 +- never read here: 239 ## Burn-down @@ -26,7 +26,6 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than - **(preamble)** - never - **Before Assuming** - never - **Delegation, in One Paragraph** - never -- **When a Failure Surfaces a Lesson** - never - **Why This Exists** - never ### .agents/skills/audit-a-repo/SKILL.md diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 8d514b5f..9c9a4a86 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -32,6 +32,19 @@ The fixed interface of a workflow is stated in [`WORKFLOW.md`][workflow] ("Two L Once a workflow's job graph moves into the hub, the carried unit is a caller stub rather than the job graph itself, and the contract moves with it: `publish-release.yml`'s `interface` check asserts the `plan`, `validate`, and `publish` job keys and that `plan` and `publish` each name their hub task by token (`publish-plan-task.yml`, `build-release-task.yml`), never the build job list a carried `build-release-task.yml` used to expose. +## What a Local Edit Looks Like + +A downstream repo editing carried content is a different question from a copy trailing the hub, and the answer differs by tier. [`GOVERNANCE.md`][governance] "Durable Knowledge and Self-Improvement" sends a change to carried rule text upstream rather than into the local copy. What follows is why a local patch there is so easy to miss. + +The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 of its 20 sections are `verbatim`. An edit inside one of those is caught, and an edit to the prose around them is not. + +- **verbatim and verbatim-tree** - detected. The content hash matches neither the current canonical nor any past revision. "Stale Versus Modified" below reports that as modified fixed content rather than as stale. +- **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for. +- **intent** - not detected. Content is never judged, and the staleness advisory cannot fire on a local edit at all. It compares last-change dates, and editing the copy makes it newer than the hub canonical. +- **presence** - not detected. The unit is asserted to exist and is never read. + +So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. Headings are the one exception. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, for `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. + ## Normalization A verbatim check compares content by hash after **line-ending, action-pin, and job-needs normalization**. EOL variance is governed by the line-ending rules, a `uses: @` pin (with its trailing `# vN` comment) is Dependabot-owned and bumped per repo, and a job's `needs:` list is pruned per repo to its vendored targets (an unvendored name fails the workflow to load), so all three are governed drift rather than a fidelity deviation. This keeps a verbatim workflow job region (the `github-release` job) from flagging on a routine action bump or a legitimate needs-prune while still catching a real structural fork. It does **not** mask placeholders: a verbatim unit carries none. The files that declare a `placeholders` list (for example `.github/copilot-instructions.md` with ``, ``, ``) are fidelity `intent`, judged by hand and never hashed. Masking could not serve a hash anyway. A downstream copy holds the substituted value (`ptr727`), not the token (``), so masking the token in the canonical alone would guarantee a mismatch. A verbatim unit that ever needed a per-repo substitution would require template-matching (the canonical as a pattern, the copy as an instance), not this content hash. None does today. From 19e9f5b3cbd057624cfbe8856731ec82d3cfdcc5 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 18:23:55 -0700 Subject: [PATCH 02/31] Answer Seven Review Findings Against the Settled Wording The husky snippet README lost the guidance to drop its .NET block, which the new host-toolchain criterion makes reachable again: a Docker or docs repo that does have a host .NET toolchain may now pick that snippet, and keeping dotnet husky run with no project fails every commit. It also credited dotnet tool restore with generating .husky/_/husky.sh, which dotnet husky install does. A host .NET toolchain is a requirement for Husky.Net rather than a selection rule, so the snippet a repo starts from turns on its host toolchain without being determined by it, and a repo holding one may still choose either. The deploy-site paragraph explained the explicit secret mapping by inherit not carrying an environment-scoped secret across a cross-repository call, which implies inherit otherwise works across one. That is the question this change settles the other way, so it now names both reasons. The fidelity model called headings the one exception to a local edit going unreported. The undeclared-section advisory does not run against CODESTYLE.md or WORKFLOW.md, the two files the preceding sentence names, and template_ref_outside_verbatim reads non-verbatim prose as well. The section count is the declared one rather than the hub file's own. The undeclared-section advisory's rationale cited an allowance the AGENTS.md preamble no longer carries, so it cites the reconciliation choice instead. --- GOVERNANCE.md | 2 +- catalog/snippets/husky/README.md | 8 ++++---- catalog/snippets/pre-commit/.pre-commit-config.yaml | 2 +- catalog/snippets/pre-commit/README.md | 3 ++- docs/reusable-workflows.md | 5 +++-- spec/audit.py | 2 +- spec/fidelity-model.md | 4 ++-- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 930d3474..79ad18da 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -244,7 +244,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 follows from whether it has a host .NET toolchain, never from which languages its checks cover. +- **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 turns on its host toolchain, never on which languages its checks cover. - **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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index c01c514b..b5da2264 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -14,8 +14,8 @@ network fetches alongside the Docker pulls the Lint tasks below already do. A fe fails the commit rather than silently skipping the gate. This snippet needs a host .NET toolchain, since the hook sources `.husky/_/husky.sh`, a file -`dotnet tool restore` and `dotnet husky install` generate. A repo without one uses -`../pre-commit/` instead. Neither snippet is scoped to a language by what it runs, and the +`dotnet husky install` generates after `dotnet tool restore` restores the tool manifest. A repo +without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by 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 @@ -34,5 +34,5 @@ override needed. A CRLF shebang breaks execution. Both language blocks run unconditionally, with no tool-presence guard: a repo that keeps a block declares that tool required, so a missing one fails the commit loudly rather than -skipping the check silently. Drop the Python block in a repo with no Python, rather than -leaving it in place to no-op. +skipping the check silently. Drop the `dotnet husky run` block in a repo with no .NET project, +and the Python block in a repo with no Python, rather than leaving either in place to no-op. diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml index 04924710..8f16c153 100644 --- a/catalog/snippets/pre-commit/.pre-commit-config.yaml +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Reference pre-commit (pre-commit.com) config for a fleet repo with no host .NET toolchain. +# Reference pre-commit (pre-commit.com) config for a fleet repo with no host .NET toolchain, or one that prefers this runner. # Husky.Net is a dotnet tool, so the discriminator is the toolchain rather than the repo's languages. # Drop the ruff and type-check hooks in a repo with no Python, keeping the two doc gates every repo owes. # Copy into the repo root alongside ../hub-fetch-run.py. diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index ecec3cf7..560e6104 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -1,7 +1,8 @@ # Pre-commit snippet `.pre-commit-config.yaml` is the reference config for the `pre-commit` framework -(pre-commit.com), for a repo with no host .NET toolchain. That is the discriminator, not the +(pre-commit.com), for a repo with no host .NET toolchain, and for any repo that prefers it to +Husky.Net. The host toolchain is the discriminator, not the repo's own languages: Husky.Net is a `dotnet` tool, and this framework installs independently of one. In a repo with Python it runs `ruff format --check` and `ruff check`, plus that repo's declared type checker, `pyright` or `mypy`, matching whichever `python-codestyle` says diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index 2d3bfd30..a43ae6b4 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is available is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task maps every secret by name, and `inherit` is never used on one. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is used. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task maps every secret by name, and `inherit` is never used on one. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is used. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. @@ -611,7 +611,7 @@ The caller grants `contents: read` explicitly, since the task's own jobs declare ESPHome-NonRoot carries two trackers today. `check-upstream-version.yml` adopts the stub above as-is. `check-upstream-dependency.yml`, whose bump waits for a human because its head deliberately does not match a merge-bot rule, adopts a second instance of the same stub with `with: { branches: '["develop"]', bump-branch-prefix: upstream-dependency, auto-merge: false }` and a `resolve-upstream` hook shaped around its apt-package snapshot, setting `versions` to `{"docker_base_packages": ""}` rather than a name-to-version map. The generic title and body this produces read less specifically than today's bespoke "packages added/removed" wording, which is the cost of folding a bespoke tracker into the shared task. -**Deploy-site.** A site repo keeps `deploy-site.yml` as a per-repo caller (it has no manifest-wide catalog snippet either, since its `uses:` now names the hub, and it still carries the dispatch, the ref gate, and the shared validation call), but its `deploy` job reaches the hub-hosted `deploy-site-task.yml`. A job calling a reusable workflow cannot itself carry an `environment:` key ([GitHub's supported-keywords list][gh-reusing-workflows] omits it, and GitHub rejects the combination outright), so the caller's `deploy` job carries none. `DEPLOY_SSH_PRIVATE_KEY` still crosses correctly: the task's own `deploy` job binds `environment: ${{ inputs.environment }}` on itself, and per GitHub's own cross-repository behavior (the `github` context, and an OIDC token's `sub` claim, always attribute the environment to the *caller*), that resolves against the caller's own GitHub Environment store, not the hub's. The caller's `secrets: DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}` line is still required (the task declares this a required `workflow_call.secrets` input, and GitHub rejects a call omitting a required one), but what value the task's own job actually sees for it is governed by the task's own `environment:` binding, not by anything the caller's own (environment-less) job context could resolve. Confirmed both against GitHub's documented reusable-workflow secrets mechanics and with a live cross-repository run against Blog's own `staging` environment ([issue #942][issue-942]). `secrets: inherit` was never the alternative here regardless, since it does not carry environment-scoped secrets across a cross-repository call. A required `deploy` hook, `.github/actions/deploy/action.yml`, is invoked three times (`build`, `prune`, `verify`) so the site keeps its own generator, precompression, and URL contract while the upload-then-flip sequence stays hub-owned. The hook declares all four inputs the three invocations use between them, `mode`, `bundle-path`, `release-id`, and `environment`, since a composite action rejects an invocation that supplies an input it does not declare, even one a different mode leaves unset. Each invocation also passes the GitHub Environment variables that mode needs (`SITE_BASE_URL` to `build` and `verify`, `DEPLOY_SSH_USER` and `DEPLOY_SSH_HOST` to `prune`) as plain `env:` vars, since a composite action's own steps are not guaranteed to read the caller's `vars` context directly. `verify` additionally receives an optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` secret pair the same way, forwarded whenever the caller maps it. A site whose live check sits behind its own token-gated auth is the reason it exists. +**Deploy-site.** A site repo keeps `deploy-site.yml` as a per-repo caller (it has no manifest-wide catalog snippet either, since its `uses:` now names the hub, and it still carries the dispatch, the ref gate, and the shared validation call), but its `deploy` job reaches the hub-hosted `deploy-site-task.yml`. A job calling a reusable workflow cannot itself carry an `environment:` key ([GitHub's supported-keywords list][gh-reusing-workflows] omits it, and GitHub rejects the combination outright), so the caller's `deploy` job carries none. `DEPLOY_SSH_PRIVATE_KEY` still crosses correctly: the task's own `deploy` job binds `environment: ${{ inputs.environment }}` on itself, and per GitHub's own cross-repository behavior (the `github` context, and an OIDC token's `sub` claim, always attribute the environment to the *caller*), that resolves against the caller's own GitHub Environment store, not the hub's. The caller's `secrets: DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}` line is still required (the task declares this a required `workflow_call.secrets` input, and GitHub rejects a call omitting a required one), but what value the task's own job actually sees for it is governed by the task's own `environment:` binding, not by anything the caller's own (environment-less) job context could resolve. Confirmed both against GitHub's documented reusable-workflow secrets mechanics and with a live cross-repository run against Blog's own `staging` environment ([issue #942][issue-942]). `secrets: inherit` was never the alternative here regardless. It is not used on a cross-repository call at all, per [Secrets and Permissions][secrets-and-permissions], and it would not carry an environment-scoped secret across one either. A required `deploy` hook, `.github/actions/deploy/action.yml`, is invoked three times (`build`, `prune`, `verify`) so the site keeps its own generator, precompression, and URL contract while the upload-then-flip sequence stays hub-owned. The hook declares all four inputs the three invocations use between them, `mode`, `bundle-path`, `release-id`, and `environment`, since a composite action rejects an invocation that supplies an input it does not declare, even one a different mode leaves unset. Each invocation also passes the GitHub Environment variables that mode needs (`SITE_BASE_URL` to `build` and `verify`, `DEPLOY_SSH_USER` and `DEPLOY_SSH_HOST` to `prune`) as plain `env:` vars, since a composite action's own steps are not guaranteed to read the caller's `vars` context directly. `verify` additionally receives an optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` secret pair the same way, forwarded whenever the caller maps it. A site whose live check sits behind its own token-gated auth is the reason it exists. Blog is the reference adoption, and its real inventory is two scripts, not three: `deploy/make-release.sh` assembles, hard-links, stamps, and installs a release into whatever root it is pointed at, and `checks/check-live-urls.sh` verifies one against a running server. There is no `deploy/prune-releases.sh`. `build` mode wraps `make-release.sh` pointed at the hub-passed `bundle-path` rather than a live root, alongside whatever generator setup the hook itself needs, Hugo and brotli in Blog's case, that `make-release.sh` assumes are already on `PATH`. The script's own tail, a swap of a local `current` symlink to the release it just wrote and a check that the swap hard-linked something against whatever `current` pointed at before, runs entirely against that ephemeral `bundle-path`, so it is local bookkeeping rather than a second real deploy. It is also what leaves `bundle-path/current` in place for the hub task's own build-mode assertion to find. Because `bundle-path` is empty at the start of every run, that local `current` never resolves to anything and the hard-link check never has a previous release to compare against, so it is inert in CI. The `build` hook's own `current` is never the live one either way: only the hub-owned Upload release and Flip current steps that follow touch the real `//` root, so the boundary the upload-then-flip sequence draws is between `bundle-path` and the environment, not a seam inside `make-release.sh` itself. @@ -667,6 +667,7 @@ Four things the hub cannot prove fall to the first downstream adopter. They are [open-decisions]: #open-decisions [pinning]: #pinning [rollout]: #rollout +[secrets-and-permissions]: #secrets-and-permissions [stage-4]: #stage-4-the-release-chain-and-the-docker-core [the-docker-family]: #the-docker-family diff --git a/spec/audit.py b/spec/audit.py index 34f1d064..ba1c0528 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -2453,7 +2453,7 @@ def audit_repo(entry, spec, branch=None): ) ) # The undeclared-section advisory, per spec/section-model.md, treats an H2 the manifest does not declare as a candidate duplicate of a verbatim section, or as repo-specific content to relocate. - # It is advisory only, since a repo may legitimately carry its own project-specific sections, which the AGENTS.md preamble allows, so it points at the reconciliation and never fails. + # It is advisory only, since the reconciliation can end in promoting the rule here or relocating it, so it points at that choice and never fails. # It covers UNDECLARED_HEADING_SCANNED, not only AGENTS.md and GOVERNANCE.md, and never names which destination file an undeclared heading belongs in. # Skip the hub itself, since its copies are the source and legitimately hold hub-only sections, Repository Onboarding and Conformance being one, that are deliberately not carried. # A downstream repo carrying such a section is still flagged, which is the point. diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 9c9a4a86..1067f570 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -36,14 +36,14 @@ Once a workflow's job graph moves into the hub, the carried unit is a caller stu A downstream repo editing carried content is a different question from a copy trailing the hub, and the answer differs by tier. [`GOVERNANCE.md`][governance] "Durable Knowledge and Self-Improvement" sends a change to carried rule text upstream rather than into the local copy. What follows is why a local patch there is so easy to miss. -The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 of its 20 sections are `verbatim`. An edit inside one of those is caught, and an edit to the prose around them is not. +The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 of its 20 declared sections are `verbatim`. An edit inside one of those is caught, and an edit to the prose around them is not. - **verbatim and verbatim-tree** - detected. The content hash matches neither the current canonical nor any past revision. "Stale Versus Modified" below reports that as modified fixed content rather than as stale. - **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for. - **intent** - not detected. Content is never judged, and the staleness advisory cannot fire on a local edit at all. It compares last-change dates, and editing the copy makes it newer than the hub canonical. - **presence** - not detected. The unit is asserted to exist and is never read. -So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. Headings are the one exception. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, for `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. +So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. Two checks reach some non-verbatim prose, and neither reaches those two files. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, and it runs only against `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. [`audit.py`][audit]'s `template_ref_outside_verbatim` reads the prose itself, and it catches only a coordination reference rather than a changed rule. ## Normalization From 1d5060225266142d87db4fad928224002a7b42fc Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 18:39:14 -0700 Subject: [PATCH 03/31] Sweep the Prose That Still Asserted the Language-Scoped Hook Rule Settling the hook runner on the host toolchain left the replaced sentence alive in four other places, each now contradicting the rule it came from. The rollout doc and the catalog README carried the old pairing verbatim, including the circular "for a repo with no .husky/ tree" selection rule. The two language codestyle skills each named their own language's mechanism, which is the claim the settled rule removes, so each now states the toolchain requirement and names the other runner. The .NET skill named only Husky.Net, so a .NET repo preferring pre-commit read the unit as forbidding it, where the Python skill already stated the reciprocal. In that Python skill "that directory" pointed at the Husky.Net snippet named beside it rather than at the pre-commit one it means. On a local-path call inherit is available rather than used: this repository's own four local-path calls map by name or pass nothing, so the categorical was false against the tree that carries it. AGENTS.md gave two destinations for a project's own conventions with nothing to choose between them, which is the judgment call the destination model removes. A convention goes to CODESTYLE.md and a public-API or behavioral contract to ARCHITECTURE.md. --- .agents/skills/dotnet-codestyle/SKILL.md | 6 ++++-- .agents/skills/python-codestyle/SKILL.md | 8 +++++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/dotnet-codestyle/SKILL.md | 6 ++++-- .../skills/python-codestyle/SKILL.md | 8 +++++--- .github/skills/dotnet-codestyle/SKILL.md | 6 ++++-- .github/skills/python-codestyle/SKILL.md | 8 +++++--- AGENTS.md | 2 +- GOVERNANCE.md | 2 +- catalog/README.md | 2 +- catalog/snippets/husky/README.md | 4 ++-- docs/pre-commit-hooks-rollout.md | 4 ++-- docs/reusable-workflows.md | 2 +- reports/canonical-review.json | 16 ++++++++-------- 14 files changed, 44 insertions(+), 32 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 2cc81eef..f4a75d09 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,10 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - The .NET mechanism for a local hook is Husky.Net, wired from the canonical - `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. + Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local + and not carried into every fleet repo. Its hook needs a host .NET toolchain, which is what + selects it rather than the languages the hook checks. A repo without one, or one that prefers + that runner, wires `catalog/snippets/pre-commit/` instead. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 210a67b7..2b3ce9cf 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -96,9 +96,11 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. The Python mechanism for a local hook is the `pre-commit` framework, wired from the -canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet -repo. That directory's own README names the second file to copy alongside the config. +backstop. A repo with no host .NET toolchain wires its local hook from the canonical +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. The +runner follows the host toolchain rather than the languages the hook checks, so a repo already +building .NET may run these same Python checks from the Husky.Net snippet instead. The +`pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 63fe4482..79eebb8e 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -23da52348160499c +031dcbf624000bbd diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 2cc81eef..f4a75d09 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,10 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - The .NET mechanism for a local hook is Husky.Net, wired from the canonical - `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. + Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local + and not carried into every fleet repo. Its hook needs a host .NET toolchain, which is what + selects it rather than the languages the hook checks. A repo without one, or one that prefers + that runner, wires `catalog/snippets/pre-commit/` instead. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 210a67b7..2b3ce9cf 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -96,9 +96,11 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. The Python mechanism for a local hook is the `pre-commit` framework, wired from the -canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet -repo. That directory's own README names the second file to copy alongside the config. +backstop. A repo with no host .NET toolchain wires its local hook from the canonical +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. The +runner follows the host toolchain rather than the languages the hook checks, so a repo already +building .NET may run these same Python checks from the Husky.Net snippet instead. The +`pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 2cc81eef..f4a75d09 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,10 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - The .NET mechanism for a local hook is Husky.Net, wired from the canonical - `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. + Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local + and not carried into every fleet repo. Its hook needs a host .NET toolchain, which is what + selects it rather than the languages the hook checks. A repo without one, or one that prefers + that runner, wires `catalog/snippets/pre-commit/` instead. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 210a67b7..2b3ce9cf 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -96,9 +96,11 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. The Python mechanism for a local hook is the `pre-commit` framework, wired from the -canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet -repo. That directory's own README names the second file to copy alongside the config. +backstop. A repo with no host .NET toolchain wires its local hook from the canonical +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. The +runner follows the host toolchain rather than the languages the hook checks, so a repo already +building .NET may run these same Python checks from the Husky.Net snippet instead. The +`pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/AGENTS.md b/AGENTS.md index 9b51c821..252e11f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file is the entry point every coding agent reads first, and it holds only three things: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) (a General section plus per-language sections for .NET, Python, and Shell, the language sections packaged as the `dotnet-codestyle`, `python-codestyle`, and `shell-codestyle` Skills), and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). -Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own topical docs. A library's own API conventions go to its `CODESTYLE.md` or its `ARCHITECTURE.md`. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. The topical docs are agent-agnostic, and every coding agent is directed to read them. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. +Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own topical docs. A convention goes to that project's own `CODESTYLE.md`, and a public-API or behavioral contract to its own `ARCHITECTURE.md`. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. The topical docs are agent-agnostic, and every coding agent is directed to read them. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. ## Fleet Bootstrap diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 79ad18da..987776d4 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -244,7 +244,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 turns on its host toolchain, never on which languages its checks cover. +- **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 its host toolchain, never by which languages its checks cover. - **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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. diff --git a/catalog/README.md b/catalog/README.md index 8269cc2e..d9c5ad79 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -7,4 +7,4 @@ Reusable reference snippets: concrete config artifacts a repo can copy or compar - `snippets/configs/`: the config exemplars `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent, running `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), plus `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). - `snippets/devcontainer/`: `.devcontainer` definitions for the .NET and Python toolchains. - `snippets/vscode/`: the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. -- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for .NET (or any project, ruff included) and the Python `pre-commit` framework, both carrying the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. See each directory's own README.md. +- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo with a host .NET toolchain and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. See each directory's own README.md. diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index b5da2264..cb837b5f 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -14,8 +14,8 @@ network fetches alongside the Docker pulls the Lint tasks below already do. A fe fails the commit rather than silently skipping the gate. This snippet needs a host .NET toolchain, since the hook sources `.husky/_/husky.sh`, a file -`dotnet husky install` generates after `dotnet tool restore` restores the tool manifest. A repo -without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by what it runs, and the +`dotnet husky install` generates after `dotnet tool restore` restores the tool manifest. A +repo without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by 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 diff --git a/docs/pre-commit-hooks-rollout.md b/docs/pre-commit-hooks-rollout.md index d770d958..4a9d4b80 100644 --- a/docs/pre-commit-hooks-rollout.md +++ b/docs/pre-commit-hooks-rollout.md @@ -25,8 +25,8 @@ defect, the same severity a missing markdownlint config already gets. A repo mid doc-gate half wired and the language-format half not yet because its own corpus does not pass it clean, stays operational, per the existing carve-out in GOVERNANCE.md. Two catalog snippets carry the canonical shape: Husky.Net -(`catalog/snippets/husky/`, for .NET or any project including Python) and the Python `pre-commit` -framework (`catalog/snippets/pre-commit/`, for a repo with no `.husky/` tree). Both now carry the +(`catalog/snippets/husky/`, for a repo with a host .NET toolchain, which its hook needs) and the +`pre-commit` framework (`catalog/snippets/pre-commit/`, for a repo without one). Both now carry the same shared doc gates, the diff-scoped prose/comment-style gate and the whole-tree line-ending check, fetched fresh from the hub's `main` branch at run time via `hub-fetch-run.py` rather than vendored or pinned. diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index a43ae6b4..55c3971e 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task maps every secret by name, and `inherit` is never used on one. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is used. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task maps every secret by name, and `inherit` is never used on one. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. A caller still maps by name, or passes nothing, where that is what the job needs, which is what this repository's own local-path calls do. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. diff --git a/reports/canonical-review.json b/reports/canonical-review.json index 3c104117..2ff6b21f 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -35,11 +35,11 @@ }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Build requirements", - "digest": "sha256:c9a8e764ae6c8423420ba2cb7e8b65d5a0328a405be16a798b8aec521f01c459", + "digest": "sha256:f1545c3fa60c3c5338b072e85aaf709d35c2113a33c5e3c285a0c600e2304919", "reviewer": "agent-skill", - "findings": 0, - "hubCommit": "ed329f4029c9b035662a96f00834152c27bf2d8b", - "stamp": "2026-09-01T06:18:30Z" + "findings": 5, + "hubCommit": "19e9f5b3cbd057624cfbe8856731ec82d3cfdcc5", + "stamp": "2026-09-02T01:38:59Z" }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Tooling and editor", @@ -171,11 +171,11 @@ }, { "unit": ".agents/skills/python-codestyle/SKILL.md > Local development loop", - "digest": "sha256:6f1081d1c1cf0f719f9c154c0b5652a26c7c1f6d2478adc3717becb248a61458", + "digest": "sha256:2b16524e8edffe8d04c0e1d4dc2abab4e5f7ef44fe9c8908721dae88d264b485", "reviewer": "agent-skill", - "findings": 1, - "hubCommit": "5664a954bf0c11acb6bab9014b0463697d18da4b", - "stamp": "2026-09-01T23:19:53Z" + "findings": 4, + "hubCommit": "19e9f5b3cbd057624cfbe8856731ec82d3cfdcc5", + "stamp": "2026-09-02T01:38:59Z" }, { "unit": ".agents/skills/repo-worktree/SKILL.md > Creating a Worktree", From 01475e0201567a6fdd9fb9759a390875a4bc634b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:00:26 -0700 Subject: [PATCH 04/31] Sweep the Carried Copies and Correct the Deploy-Site Justification The earlier sweep fixed the hub-only copy of two claims and left the carried one, which is the copy a downstream repository actually reads. WORKFLOW.md carried the deploy-site justification that implied inherit otherwise works across a cross-repository call, and STANDUP.md carried the language-scoped snippet pairing as the only selection rule a standing-up agent is given, so a new eda, docker or docs repository read it as covering neither snippet. spec/audit.py asserted a cross-repository caller cannot use inherit, where the rule is documented scope and fleet policy rather than impossibility, and spec/third-party-tools.json described pre-commit as a hook manager for Python in the text carried into every README. The deploy-site justification is now the real mechanism. The caller maps the secrets because the task declares them, and the value that task's own job reads comes from its environment binding, since a caller job carrying uses: can hold no environment of its own. The previous reason did not survive its own premise, as that environment-less caller job cannot read an environment-scoped secret either. It also named one crossing secret where the task declares three, so a site whose live check is token-gated built a caller missing the optional pair. AGENTS.md sent a project's own public-API contracts to ARCHITECTURE.md, which no instruction file routes an agent to, so the contracts left every directed read. They go to that project's own CODESTYLE.md, which this file already routes to. The dotnet skill offered a .NET repository the pre-commit snippet, which carries no .NET checks, and named a second selector against the sentence before it. --- .agents/skills/dotnet-codestyle/SKILL.md | 6 +++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/dotnet-codestyle/SKILL.md | 6 +++--- .github/skills/dotnet-codestyle/SKILL.md | 6 +++--- AGENTS.md | 2 +- CODESTYLE.md | 2 +- STANDUP.md | 2 +- WORKFLOW.md | 2 +- catalog/snippets/husky/README.md | 4 ++-- catalog/snippets/pre-commit/README.md | 8 +++++--- docs/reusable-workflows.md | 2 +- spec/audit.py | 2 +- spec/fidelity-model.md | 2 +- spec/third-party-tools.json | 2 +- 14 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index f4a75d09..709d7c26 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -50,9 +50,9 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a host .NET toolchain, which is what - selects it rather than the languages the hook checks. A repo without one, or one that prefers - that runner, wires `catalog/snippets/pre-commit/` instead. + and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET + repo already has. Either canonical shape carries whichever language checks the repo keeps, so + the shape is bounded by the host toolchain rather than by the languages it checks. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 79eebb8e..be8fd0e1 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -031dcbf624000bbd +7ee21ad82832cb94 diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index f4a75d09..709d7c26 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -50,9 +50,9 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a host .NET toolchain, which is what - selects it rather than the languages the hook checks. A repo without one, or one that prefers - that runner, wires `catalog/snippets/pre-commit/` instead. + and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET + repo already has. Either canonical shape carries whichever language checks the repo keeps, so + the shape is bounded by the host toolchain rather than by the languages it checks. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index f4a75d09..709d7c26 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -50,9 +50,9 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a host .NET toolchain, which is what - selects it rather than the languages the hook checks. A repo without one, or one that prefers - that runner, wires `catalog/snippets/pre-commit/` instead. + and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET + repo already has. Either canonical shape carries whichever language checks the repo keeps, so + the shape is bounded by the host toolchain rather than by the languages it checks. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/AGENTS.md b/AGENTS.md index 252e11f0..0baa9055 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file is the entry point every coding agent reads first, and it holds only three things: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) (a General section plus per-language sections for .NET, Python, and Shell, the language sections packaged as the `dotnet-codestyle`, `python-codestyle`, and `shell-codestyle` Skills), and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). -Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own topical docs. A convention goes to that project's own `CODESTYLE.md`, and a public-API or behavioral contract to its own `ARCHITECTURE.md`. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. The topical docs are agent-agnostic, and every coding agent is directed to read them. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. +Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own `CODESTYLE.md`, which this file already routes every agent to. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. ## Fleet Bootstrap diff --git a/CODESTYLE.md b/CODESTYLE.md index 6e41a9ec..820eb022 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* stays the repo's choice between two canonical shapes, and neither is scoped to a language by what it runs. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/STANDUP.md b/STANDUP.md index 4617bd78..e668f4c4 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -182,7 +182,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 tool install pre-commit` once then `pre-commit install`) before the section 5 audit run. 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. 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 a repo with a host .NET toolchain, which Husky.Net needs, or `catalog/snippets/pre-commit/` for a repo without one, 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. 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. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. ## 3. Stand Up the Workflows diff --git a/WORKFLOW.md b/WORKFLOW.md index 3494f9ec..c6772e26 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -292,7 +292,7 @@ Each type maps the *applicable* S-scenarios onto its targets. The differences ar - **Docker image.** The leaf pushes the default branch multi-arch (amd64+arm64) and any other branch `amd64`-only, with a per-branch registry buildcache (`buildcache-`; a multi-image repo adds a per-image tag) (`cache-to` only the built branch and only on push, `cache-from` both branches); no `release-asset-*`, so a Docker-only repo's caller passes `expect_release_assets: false`; the readme job (`peter-evans/dockerhub-description`, `DOCKER_HUB_ACCESS_TOKEN`) runs **only** when the default branch publishes, whether called directly or reached through the hub-hosted `publish-docker-readme-task.yml`; the docker-readme task validates `repositories` XOR `manifest`+`manifest-jq` and a multi-image repo derives its publish matrix from the manifest. Docker **always re-pushes** the image, independently of a skipped release-create (S9). A **wrapper** repo tracks an upstream release: the upstream tracker writes a `name -> version` state file and the merge-bot auto-merges the bump PR (S11), and the leaf MUST read that file for the immutable tag instead of `SemVer2` (the tracker ships without this consumer wiring). Test: S7 default leg pushes `latest` + the version tag and updates the readme. Non-default pushes the develop tag (amd64 only). S9 still re-pushes. S11 ships the bumped upstream version next publish. 5C Docker probe needs `DOCKER_HUB_*` secrets and same-repo (not fork) runs. - **Data / asset library.** A single new leaf: validate -> zip -> upload `release-asset--library` (`retention-days: 1` per D5.4, upload gated `!smoke` per D1.3). Because no such leaf ships, you **add a target** (D6.4): a new `enable_library` input + `build-library` job + its `github-release` and `build-docker` `needs:` entries in the release task, and a `library` paths-filter entry + `changes` output + `smoke-build` enable-forward in the PR workflow (without it, D1.1 never smoke-builds the library). Keep `expect_release_assets: true` (it has a file target, unlike Docker). The .NET `unit-test` job is replaced by a type-appropriate validator, with the aggregator re-pointed to it (D1.2). `smoke-build` keeps `needs: [changes]`, as 5A's D1 line and the canonical stub both have it. `version.json` + the NBGV `get-version` step are retained (they own the tag). Test: S1 smoke runs validate+zip and uploads nothing; S7 attaches the zip, prerelease on the non-default leg; S9 on a *scheduled* re-run release-create + asset-delete skip (the existing zip is untouched, no registry push), while a `workflow_dispatch` re-run **refreshes** the release and re-runs the asset-delete (the asset is re-uploaded then re-deleted). N/A: the NuGet, PyPI, Docker, and .NET publish 5A addenda and their scenario clauses. - **Source-only / no build.** There is no package/image build leaf. A repo may own the reusable release task or call its hub-hosted copy. The dispatch-only `publish-release.yml` reaches the reusable plan, validation, and release tasks. Its publish job passes `github: true`, every `enable_*` input as false, and `expect_release_assets: false`. This produces tag + source zip + README + LICENSE with no asset download. With no target, the paths-filter matches nothing. A retained `smoke-build` job is therefore **structurally always skipped**. The repo may instead drop that never-running job. Validation remains the replaced, non-.NET validation job. The aggregator and any retained `smoke-build` job must depend on it (D1.2). NBGV and `version.json` own the tag. The publish job depends on the same reusable validation task that the PR workflow runs. This prevents a dispatch from releasing a ref that fails validation. Applicable scenarios are S1 (validation only), S7, S8, S9, and S10. S7 covers the tag-only release, S8 the dispatch guard, S9 no-op republish, and S10 the classification gate. S2-S6, D5/D6 artifact items, and all per-type 5A addenda are N/A. The artifact-lifecycle and registry clauses of S7/S9 are also N/A, not failed. -- **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, with the `environment:` declared inside that task rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The one crossing secret, `DEPLOY_SSH_PRIVATE_KEY`, is therefore mapped explicitly under the call's `secrets:`, because `secrets: inherit` does not carry an environment-scoped secret across a cross-repository call. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. +- **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, with the `environment:` declared inside that task rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The crossing secrets, `DEPLOY_SSH_PRIVATE_KEY` and the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair the live check needs, are therefore mapped explicitly under the call's `secrets:`, since the task declares them and `secrets: inherit` is not used on a cross-repository call. What that task's own job reads for one comes from its `environment:` binding rather than from the caller's job context. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. - **Operational (workflow model, not a build target).** A `workflowModel: operational` repo layers direct commits to `develop` onto the **source-only** release shape above. It has two workflows. The first is a **lint/validation** PR workflow that feeds the required `Check pull request workflow status job`. It uses the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator, with **no unit tests**. Examples include Home Assistant `hass --script check_config`, `esphome config`, or a firmware build. Its triggers differ from the `release` model. It runs on pushes to `develop`, pull requests to `[ main, develop ]`, and `workflow_dispatch`. Push validation is advisory. Pull request validation is enforced on `main` and reported but not required on `develop`. The second workflow is the standard **source-only publisher** with `releaseTrigger: dispatch-only`. NBGV and `version.json` own the tag. The reusable release task creates tag + source zip + README + LICENSE. **The PR trigger names both branches, and naming `main` alone is a defect.** Omitting `develop` starts no validation when a PR opens against `develop`. The aggregator then never reports, and the PR appears clean with an empty check list. D1.2 forbids that output. Naming both causes a duplicate run after a PR merge. The change validates on the PR and again on the resulting push, regardless of merge method. The operational `develop` ruleset prescribes no merge method. The concurrency group uses the workflow name plus `${{ github.ref }}` (Section 2). A pull request uses `refs/pull//merge`, while its push uses `refs/heads/develop`. The runs occupy different groups and neither cancels the other. Pay that cost. The lint-only gate costs only a few runner-minutes. Suppressing the push requires distinguishing a merge commit from a direct commit, which restores the ambiguity the trigger set removes. S1 applies to every PR, including promotion and `develop` PRs. The source-only S7, S8, S9, and S10 scenarios also apply. Bot-push and schedule paths in S5/S6 are N/A, as are every build and registry scenario. See the branch-model note in Section 3 and [GOVERNANCE.md "Branching Model"][governance-branching-model]. diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index cb837b5f..10406dcc 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -15,8 +15,8 @@ fails the commit rather than silently skipping the gate. This snippet needs a host .NET toolchain, since the hook sources `.husky/_/husky.sh`, a file `dotnet husky install` generates after `dotnet tool restore` restores the tool manifest. A -repo without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by what it runs, and the -two shared doc gates are what every repo owes either way. +repo without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by +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 diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index 560e6104..90105df1 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -33,9 +33,11 @@ install`'s own bin directory is not yet on `PATH`: run `uv tool update-shell` an 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` -(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. +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. 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. diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index 55c3971e..846cf7f5 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task maps every secret by name, and `inherit` is never used on one. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. A caller still maps by name, or passes nothing, where that is what the job needs, which is what this repository's own local-path calls do. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the stubs below do. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. diff --git a/spec/audit.py b/spec/audit.py index ba1c0528..2cc63565 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -2982,7 +2982,7 @@ def _selftest(): ), ] # The deploy-site.yml caller stub once deploy-site-task.yml is hub-hosted: no secrets: inherit - # (a cross-repository reusable workflow cannot use it), the one crossing secret named instead. + # (inherit is documented for same-org or enterprise callers, so the fleet does not use it across repositories), the one crossing secret named instead. # No job-level environment: on the caller, unsupported on a job with uses: (ptr727/ProjectTemplate#942). deploy_stub = ( "jobs:\n" diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 1067f570..09be686d 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -43,7 +43,7 @@ The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 - **intent** - not detected. Content is never judged, and the staleness advisory cannot fire on a local edit at all. It compares last-change dates, and editing the copy makes it newer than the hub canonical. - **presence** - not detected. The unit is asserted to exist and is never read. -So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. Two checks reach some non-verbatim prose, and neither reaches those two files. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, and it runs only against `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. [`audit.py`][audit]'s `template_ref_outside_verbatim` reads the prose itself, and it catches only a coordination reference rather than a changed rule. +So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. That silence is intended for a repo's own additions, which `CODESTYLE.md` is a declared destination for, and it is the hazard for a patch of carried rule text, since the two read alike to every check. Two checks reach some non-verbatim prose, and neither reaches those two files. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, and it runs only against `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. [`audit.py`][audit]'s `template_ref_outside_verbatim` reads the prose itself, and it catches only a coordination reference rather than a changed rule. ## Normalization diff --git a/spec/third-party-tools.json b/spec/third-party-tools.json index f3bcc3a1..4eb5d42b 100644 --- a/spec/third-party-tools.json +++ b/spec/third-party-tools.json @@ -32,7 +32,7 @@ { "name": "NEbml", "link": "https://github.com/OlegZee/NEbml", "description": "EBML reader and writer for .NET." }, { "name": "Nerdbank.GitVersioning", "link": "https://github.com/dotnet/Nerdbank.GitVersioning", "description": "Version computation from git height." }, { "name": "PaperMod", "link": "https://github.com/adityatelange/hugo-PaperMod", "description": "Hugo theme." }, - { "name": "pre-commit", "link": "https://pre-commit.com/", "description": "Git hook manager for Python." }, + { "name": "pre-commit", "link": "https://pre-commit.com/", "description": "Git hook manager." }, { "name": "Proxmox VE", "link": "https://www.proxmox.com/en/proxmox-virtual-environment/overview", "description": "Virtualization platform." }, { "name": "regex101.com", "link": "https://regex101.com/", "description": "Regular expression tester." }, { "name": "RFC 5646 language tags", "link": "https://www.rfc-editor.org/rfc/rfc5646.html", "description": "Language tag standard." }, From 6b31b27cce9b2177999390569687ef6dffdbf0da Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:04:14 -0700 Subject: [PATCH 05/31] Name Both Hook Shapes and Both Crossing Secrets Without a Dangling Referent The dotnet skill kept a two-option framing after one option was removed, so "either canonical shape" pointed at a choice the paragraph never presented. It names the other shape instead. The deploy-site sentence read "what that task's own job reads for one", a singular left over from the miscount it replaced. The claim holds for all three declared secrets, so it says each of them. --- .agents/skills/dotnet-codestyle/SKILL.md | 4 ++-- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/dotnet-codestyle/SKILL.md | 4 ++-- .github/skills/dotnet-codestyle/SKILL.md | 4 ++-- WORKFLOW.md | 2 +- reports/canonical-review.json | 8 ++++---- reports/canonical-review.md | 6 ++++-- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 709d7c26..86ec30b5 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -51,8 +51,8 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET - repo already has. Either canonical shape carries whichever language checks the repo keeps, so - the shape is bounded by the host toolchain rather than by the languages it checks. + repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo + without one, and each shape carries whichever language checks its own repo keeps. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index be8fd0e1..897b56c6 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -7ee21ad82832cb94 +ed0652e358dc660c diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 709d7c26..86ec30b5 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -51,8 +51,8 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET - repo already has. Either canonical shape carries whichever language checks the repo keeps, so - the shape is bounded by the host toolchain rather than by the languages it checks. + repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo + without one, and each shape carries whichever language checks its own repo keeps. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 709d7c26..86ec30b5 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -51,8 +51,8 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET - repo already has. Either canonical shape carries whichever language checks the repo keeps, so - the shape is bounded by the host toolchain rather than by the languages it checks. + repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo + without one, and each shape carries whichever language checks its own repo keeps. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/WORKFLOW.md b/WORKFLOW.md index c6772e26..f1958e6a 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -292,7 +292,7 @@ Each type maps the *applicable* S-scenarios onto its targets. The differences ar - **Docker image.** The leaf pushes the default branch multi-arch (amd64+arm64) and any other branch `amd64`-only, with a per-branch registry buildcache (`buildcache-`; a multi-image repo adds a per-image tag) (`cache-to` only the built branch and only on push, `cache-from` both branches); no `release-asset-*`, so a Docker-only repo's caller passes `expect_release_assets: false`; the readme job (`peter-evans/dockerhub-description`, `DOCKER_HUB_ACCESS_TOKEN`) runs **only** when the default branch publishes, whether called directly or reached through the hub-hosted `publish-docker-readme-task.yml`; the docker-readme task validates `repositories` XOR `manifest`+`manifest-jq` and a multi-image repo derives its publish matrix from the manifest. Docker **always re-pushes** the image, independently of a skipped release-create (S9). A **wrapper** repo tracks an upstream release: the upstream tracker writes a `name -> version` state file and the merge-bot auto-merges the bump PR (S11), and the leaf MUST read that file for the immutable tag instead of `SemVer2` (the tracker ships without this consumer wiring). Test: S7 default leg pushes `latest` + the version tag and updates the readme. Non-default pushes the develop tag (amd64 only). S9 still re-pushes. S11 ships the bumped upstream version next publish. 5C Docker probe needs `DOCKER_HUB_*` secrets and same-repo (not fork) runs. - **Data / asset library.** A single new leaf: validate -> zip -> upload `release-asset--library` (`retention-days: 1` per D5.4, upload gated `!smoke` per D1.3). Because no such leaf ships, you **add a target** (D6.4): a new `enable_library` input + `build-library` job + its `github-release` and `build-docker` `needs:` entries in the release task, and a `library` paths-filter entry + `changes` output + `smoke-build` enable-forward in the PR workflow (without it, D1.1 never smoke-builds the library). Keep `expect_release_assets: true` (it has a file target, unlike Docker). The .NET `unit-test` job is replaced by a type-appropriate validator, with the aggregator re-pointed to it (D1.2). `smoke-build` keeps `needs: [changes]`, as 5A's D1 line and the canonical stub both have it. `version.json` + the NBGV `get-version` step are retained (they own the tag). Test: S1 smoke runs validate+zip and uploads nothing; S7 attaches the zip, prerelease on the non-default leg; S9 on a *scheduled* re-run release-create + asset-delete skip (the existing zip is untouched, no registry push), while a `workflow_dispatch` re-run **refreshes** the release and re-runs the asset-delete (the asset is re-uploaded then re-deleted). N/A: the NuGet, PyPI, Docker, and .NET publish 5A addenda and their scenario clauses. - **Source-only / no build.** There is no package/image build leaf. A repo may own the reusable release task or call its hub-hosted copy. The dispatch-only `publish-release.yml` reaches the reusable plan, validation, and release tasks. Its publish job passes `github: true`, every `enable_*` input as false, and `expect_release_assets: false`. This produces tag + source zip + README + LICENSE with no asset download. With no target, the paths-filter matches nothing. A retained `smoke-build` job is therefore **structurally always skipped**. The repo may instead drop that never-running job. Validation remains the replaced, non-.NET validation job. The aggregator and any retained `smoke-build` job must depend on it (D1.2). NBGV and `version.json` own the tag. The publish job depends on the same reusable validation task that the PR workflow runs. This prevents a dispatch from releasing a ref that fails validation. Applicable scenarios are S1 (validation only), S7, S8, S9, and S10. S7 covers the tag-only release, S8 the dispatch guard, S9 no-op republish, and S10 the classification gate. S2-S6, D5/D6 artifact items, and all per-type 5A addenda are N/A. The artifact-lifecycle and registry clauses of S7/S9 are also N/A, not failed. -- **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, with the `environment:` declared inside that task rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The crossing secrets, `DEPLOY_SSH_PRIVATE_KEY` and the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair the live check needs, are therefore mapped explicitly under the call's `secrets:`, since the task declares them and `secrets: inherit` is not used on a cross-repository call. What that task's own job reads for one comes from its `environment:` binding rather than from the caller's job context. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. +- **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, with the `environment:` declared inside that task rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The crossing secrets, `DEPLOY_SSH_PRIVATE_KEY` and the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair the live check needs, are therefore mapped explicitly under the call's `secrets:`, since the task declares them and `secrets: inherit` is not used on a cross-repository call. What that task's own job reads for each of them comes from its `environment:` binding rather than from the caller's job context. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. - **Operational (workflow model, not a build target).** A `workflowModel: operational` repo layers direct commits to `develop` onto the **source-only** release shape above. It has two workflows. The first is a **lint/validation** PR workflow that feeds the required `Check pull request workflow status job`. It uses the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator, with **no unit tests**. Examples include Home Assistant `hass --script check_config`, `esphome config`, or a firmware build. Its triggers differ from the `release` model. It runs on pushes to `develop`, pull requests to `[ main, develop ]`, and `workflow_dispatch`. Push validation is advisory. Pull request validation is enforced on `main` and reported but not required on `develop`. The second workflow is the standard **source-only publisher** with `releaseTrigger: dispatch-only`. NBGV and `version.json` own the tag. The reusable release task creates tag + source zip + README + LICENSE. **The PR trigger names both branches, and naming `main` alone is a defect.** Omitting `develop` starts no validation when a PR opens against `develop`. The aggregator then never reports, and the PR appears clean with an empty check list. D1.2 forbids that output. Naming both causes a duplicate run after a PR merge. The change validates on the PR and again on the resulting push, regardless of merge method. The operational `develop` ruleset prescribes no merge method. The concurrency group uses the workflow name plus `${{ github.ref }}` (Section 2). A pull request uses `refs/pull//merge`, while its push uses `refs/heads/develop`. The runs occupy different groups and neither cancels the other. Pay that cost. The lint-only gate costs only a few runner-minutes. Suppressing the push requires distinguishing a merge commit from a direct commit, which restores the ambiguity the trigger set removes. S1 applies to every PR, including promotion and `develop` PRs. The source-only S7, S8, S9, and S10 scenarios also apply. Bot-push and schedule paths in S5/S6 are N/A, as are every build and registry scenario. See the branch-model note in Section 3 and [GOVERNANCE.md "Branching Model"][governance-branching-model]. diff --git a/reports/canonical-review.json b/reports/canonical-review.json index 2ff6b21f..010ce209 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -307,11 +307,11 @@ }, { "unit": "CODESTYLE.md > General", - "digest": "sha256:c936aef3d1e83265e9180951190749bbd389e81b9faaaccb84254597ff74bd98", + "digest": "sha256:7c8339e809b36168a3d2707e4b723005bda81debe0ba8f6018b4b3b95697e87a", "reviewer": "agent-skill", - "findings": 11, - "hubCommit": "52b0c550ab07ab58b940a353a83247d7451e76e5", - "stamp": "2026-09-02T01:11:57Z" + "findings": 0, + "hubCommit": "01475e0201567a6fdd9fb9759a390875a4bc634b", + "stamp": "2026-09-02T02:00:40Z" }, { "unit": "CODESTYLE.md > Shell", diff --git a/reports/canonical-review.md b/reports/canonical-review.md index 9b54c319..90bdadb1 100644 --- a/reports/canonical-review.md +++ b/reports/canonical-review.md @@ -7,8 +7,8 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than ## Coverage - units: 288 -- covered: 49 -- stale: 0 +- covered: 47 +- stale: 2 - never read here: 239 ## Burn-down @@ -106,6 +106,7 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than - **(preamble)** - never - **Analyzer suppressions (.NET)** - never - **Best practices** - never +- **Build requirements** - stale - **Code patterns** - never - **Coding standards and conventions** - never - **Error handling and logging** - never @@ -393,6 +394,7 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than - **(preamble)** - never - **2. Workflow Style Conventions** - never +- **6. Per-Project-Type Test Walkthroughs** - stale ### cspell.json From b2601b05930c55239c54ab9869d282de56d71ac6 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:08:13 -0700 Subject: [PATCH 06/31] Record the Canonical Review Ledger for This Branch's Changed Units --- reports/canonical-review.json | 14 +++++++------- reports/canonical-review.md | 6 ++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index 010ce209..e3ad4574 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -35,11 +35,11 @@ }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Build requirements", - "digest": "sha256:f1545c3fa60c3c5338b072e85aaf709d35c2113a33c5e3c285a0c600e2304919", + "digest": "sha256:a8ee161d2903746f7ee333ba9444211a99013a18f02ca9ddd2f9710b6cfc80d9", "reviewer": "agent-skill", - "findings": 5, - "hubCommit": "19e9f5b3cbd057624cfbe8856731ec82d3cfdcc5", - "stamp": "2026-09-02T01:38:59Z" + "findings": 0, + "hubCommit": "6b31b27cce9b2177999390569687ef6dffdbf0da", + "stamp": "2026-09-02T02:08:08Z" }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Tooling and editor", @@ -387,11 +387,11 @@ }, { "unit": "WORKFLOW.md > 6. Per-Project-Type Test Walkthroughs", - "digest": "sha256:f6b2044c5b5de3aafd760cf001797e807749c391af93ca2ec0cb3d436b884a33", + "digest": "sha256:b22d155cf3ec026b6e206124189684208979b79e3468fcb4f40e4519fab61eae", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "c3b2898feef97459d01ce7c0b63de25b6c5524bf", - "stamp": "2026-09-01T13:57:21Z" + "hubCommit": "6b31b27cce9b2177999390569687ef6dffdbf0da", + "stamp": "2026-09-02T02:08:08Z" } ] } diff --git a/reports/canonical-review.md b/reports/canonical-review.md index 90bdadb1..9b54c319 100644 --- a/reports/canonical-review.md +++ b/reports/canonical-review.md @@ -7,8 +7,8 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than ## Coverage - units: 288 -- covered: 47 -- stale: 2 +- covered: 49 +- stale: 0 - never read here: 239 ## Burn-down @@ -106,7 +106,6 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than - **(preamble)** - never - **Analyzer suppressions (.NET)** - never - **Best practices** - never -- **Build requirements** - stale - **Code patterns** - never - **Coding standards and conventions** - never - **Error handling and logging** - never @@ -394,7 +393,6 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than - **(preamble)** - never - **2. Workflow Style Conventions** - never -- **6. Per-Project-Type Test Walkthroughs** - stale ### cspell.json From d4cb996ed67bed9e327c5a3b3c4b441466bc37bb Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:22:17 -0700 Subject: [PATCH 07/31] Keep the Third Hook Shape the Two Snippets Do Not Cover Settling the runner on the host toolchain narrowed the rule to two snippets and excluded the shape this repository itself runs. It has no host .NET toolchain and no tool manifest, and its .husky/pre-commit deliberately does not source _/husky.sh, running instead as a plain hook on core.hooksPath, which GOVERNANCE.md already documents as an enablement route and the rollout tracker already records as a conforming parity.hooks state. The evidence the settled rule was built from was therefore the case the rule went on to exclude, so the surfaces that name the two snippets now say a snippet is a starting point rather than the only shape. The husky snippet's probe comment gave a rationale spec/host-tools.json displaces, about the Windows launcher and the Store alias stub, where that file's own reason is that a probe is run rather than tested for, since Git Bash inherits a Windows PATH carrying names that fail when run. The hub's own hook already carried the right line for the byte-identical block. AGENTS.md named one carried file as the destination for a project's own contracts, which contradicts the four-destination model, where naming ARCHITECTURE.md instead sends the reader to a file nothing routes to. Neither enumeration survives, so it names the topical docs without choosing between them. The reusable-workflows paragraph said passing no secrets key is what the stubs below do, where six of them map secrets, so it names the one stub it means. The deploy-site walkthrough asserted all three declared secrets are mapped, where the contract requires one and forwards the optional pair only when a caller maps it. --- .agents/skills/dotnet-codestyle/SKILL.md | 3 ++- .agents/skills/python-codestyle/SKILL.md | 3 ++- .claude-plugin/fleet-skills/.source-digest | 2 +- .claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md | 3 ++- .claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md | 3 ++- .github/skills/dotnet-codestyle/SKILL.md | 3 ++- .github/skills/python-codestyle/SKILL.md | 3 ++- AGENTS.md | 2 +- CODESTYLE.md | 2 +- GOVERNANCE.md | 2 +- STANDUP.md | 2 +- WORKFLOW.md | 2 +- catalog/README.md | 2 +- catalog/snippets/husky/pre-commit | 2 +- docs/reusable-workflows.md | 2 +- 15 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 86ec30b5..35238f93 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -52,7 +52,8 @@ All builds must complete without warnings, enforced three ways: Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo - without one, and each shape carries whichever language checks its own repo keeps. + without one, and each shape carries whichever language checks its own repo keeps. A repo may + also wire an equivalent hook of its own on `core.hooksPath`. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 2b3ce9cf..e4b568ad 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -97,7 +97,8 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. The +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or +wires an equivalent hook of its own on `core.hooksPath`. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 897b56c6..d68c2575 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -ed0652e358dc660c +f18e6780f7dd52c0 diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 86ec30b5..35238f93 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -52,7 +52,8 @@ All builds must complete without warnings, enforced three ways: Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo - without one, and each shape carries whichever language checks its own repo keeps. + without one, and each shape carries whichever language checks its own repo keeps. A repo may + also wire an equivalent hook of its own on `core.hooksPath`. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 2b3ce9cf..e4b568ad 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -97,7 +97,8 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. The +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or +wires an equivalent hook of its own on `core.hooksPath`. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 86ec30b5..35238f93 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -52,7 +52,8 @@ All builds must complete without warnings, enforced three ways: Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo - without one, and each shape carries whichever language checks its own repo keeps. + without one, and each shape carries whichever language checks its own repo keeps. A repo may + also wire an equivalent hook of its own on `core.hooksPath`. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 2b3ce9cf..e4b568ad 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -97,7 +97,8 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. The +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or +wires an equivalent hook of its own on `core.hooksPath`. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/AGENTS.md b/AGENTS.md index 0baa9055..18191e54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file is the entry point every coding agent reads first, and it holds only three things: the bootstrap that says where the canonical rules live and which procedure to follow for the state this repository is actually in, the rules for managing context and delegation, which apply to every task, and a map of where every other rule lives. The rule text itself is in [`GOVERNANCE.md`](./GOVERNANCE.md), one section per topic. Code style lives in [`CODESTYLE.md`](./CODESTYLE.md) (a General section plus per-language sections for .NET, Python, and Shell, the language sections packaged as the `dotnet-codestyle`, `python-codestyle`, and `shell-codestyle` Skills), and the CI/CD workflow contract in [`WORKFLOW.md`](./WORKFLOW.md). -Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own `CODESTYLE.md`, which this file already routes every agent to. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. +Treat this file and `GOVERNANCE.md` as authoritative for cross-cutting rules, and do not restate their rules elsewhere. A project's **project-specific conventions and public-API/behavioral contracts** live in that project's own topical docs. They do **not** go in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md), which targets GitHub Copilot and VS Code specifically. A rule a reviewer must honor therefore has to sit in an agent-agnostic file to be provider-independent. A section of its own in this file is not the alternative. An undeclared section here is drift to reconcile rather than a local liberty. ## Fleet Bootstrap diff --git a/CODESTYLE.md b/CODESTYLE.md index 820eb022..abb8aabf 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit` or wires an equivalent hook of its own. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 987776d4..fc6d669f 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -244,7 +244,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 its host toolchain, never by which languages its checks cover. +- **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 its host toolchain, 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 directly on `core.hooksPath`, which is what this repository does, since what this check reads is a working gate rather than a particular runner. - **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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. diff --git a/STANDUP.md b/STANDUP.md index e668f4c4..15cf41ec 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -182,7 +182,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 a repo with a host .NET toolchain, which Husky.Net needs, or `catalog/snippets/pre-commit/` for a repo without one, 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. 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. 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 a repo with a host .NET toolchain, which Husky.Net needs, or `catalog/snippets/pre-commit/` for a repo without one, or wire an equivalent hook of your own on `core.hooksPath` as this repository does, 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. 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. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. ## 3. Stand Up the Workflows diff --git a/WORKFLOW.md b/WORKFLOW.md index f1958e6a..76ebc514 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -292,7 +292,7 @@ Each type maps the *applicable* S-scenarios onto its targets. The differences ar - **Docker image.** The leaf pushes the default branch multi-arch (amd64+arm64) and any other branch `amd64`-only, with a per-branch registry buildcache (`buildcache-`; a multi-image repo adds a per-image tag) (`cache-to` only the built branch and only on push, `cache-from` both branches); no `release-asset-*`, so a Docker-only repo's caller passes `expect_release_assets: false`; the readme job (`peter-evans/dockerhub-description`, `DOCKER_HUB_ACCESS_TOKEN`) runs **only** when the default branch publishes, whether called directly or reached through the hub-hosted `publish-docker-readme-task.yml`; the docker-readme task validates `repositories` XOR `manifest`+`manifest-jq` and a multi-image repo derives its publish matrix from the manifest. Docker **always re-pushes** the image, independently of a skipped release-create (S9). A **wrapper** repo tracks an upstream release: the upstream tracker writes a `name -> version` state file and the merge-bot auto-merges the bump PR (S11), and the leaf MUST read that file for the immutable tag instead of `SemVer2` (the tracker ships without this consumer wiring). Test: S7 default leg pushes `latest` + the version tag and updates the readme. Non-default pushes the develop tag (amd64 only). S9 still re-pushes. S11 ships the bumped upstream version next publish. 5C Docker probe needs `DOCKER_HUB_*` secrets and same-repo (not fork) runs. - **Data / asset library.** A single new leaf: validate -> zip -> upload `release-asset--library` (`retention-days: 1` per D5.4, upload gated `!smoke` per D1.3). Because no such leaf ships, you **add a target** (D6.4): a new `enable_library` input + `build-library` job + its `github-release` and `build-docker` `needs:` entries in the release task, and a `library` paths-filter entry + `changes` output + `smoke-build` enable-forward in the PR workflow (without it, D1.1 never smoke-builds the library). Keep `expect_release_assets: true` (it has a file target, unlike Docker). The .NET `unit-test` job is replaced by a type-appropriate validator, with the aggregator re-pointed to it (D1.2). `smoke-build` keeps `needs: [changes]`, as 5A's D1 line and the canonical stub both have it. `version.json` + the NBGV `get-version` step are retained (they own the tag). Test: S1 smoke runs validate+zip and uploads nothing; S7 attaches the zip, prerelease on the non-default leg; S9 on a *scheduled* re-run release-create + asset-delete skip (the existing zip is untouched, no registry push), while a `workflow_dispatch` re-run **refreshes** the release and re-runs the asset-delete (the asset is re-uploaded then re-deleted). N/A: the NuGet, PyPI, Docker, and .NET publish 5A addenda and their scenario clauses. - **Source-only / no build.** There is no package/image build leaf. A repo may own the reusable release task or call its hub-hosted copy. The dispatch-only `publish-release.yml` reaches the reusable plan, validation, and release tasks. Its publish job passes `github: true`, every `enable_*` input as false, and `expect_release_assets: false`. This produces tag + source zip + README + LICENSE with no asset download. With no target, the paths-filter matches nothing. A retained `smoke-build` job is therefore **structurally always skipped**. The repo may instead drop that never-running job. Validation remains the replaced, non-.NET validation job. The aggregator and any retained `smoke-build` job must depend on it (D1.2). NBGV and `version.json` own the tag. The publish job depends on the same reusable validation task that the PR workflow runs. This prevents a dispatch from releasing a ref that fails validation. Applicable scenarios are S1 (validation only), S7, S8, S9, and S10. S7 covers the tag-only release, S8 the dispatch guard, S9 no-op republish, and S10 the classification gate. S2-S6, D5/D6 artifact items, and all per-type 5A addenda are N/A. The artifact-lifecycle and registry clauses of S7/S9 are also N/A, not failed. -- **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, with the `environment:` declared inside that task rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The crossing secrets, `DEPLOY_SSH_PRIVATE_KEY` and the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair the live check needs, are therefore mapped explicitly under the call's `secrets:`, since the task declares them and `secrets: inherit` is not used on a cross-repository call. What that task's own job reads for each of them comes from its `environment:` binding rather than from the caller's job context. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. +- **Static site deployed to a host the project owns.** Two independent surfaces, and keeping them apart is the point. The **release** is the source-only shape above, unchanged: a dispatch-only `publish-release.yml` where NBGV and `version.json` own the tag, producing tag + source zip + README + LICENSE. The **deploy** is its own `workflow_dispatch` carrying an `environment` choice input, so redeploying an unchanged commit mints no tag, which matters because redeploying is routine. It runs a ref gate **first**, before anything is installed or written (production from the default branch only, while any ref may reach a non-production environment, since proving a branch before it merges is what that environment is for), then the **same** reusable validation task the PR gate runs, so a dispatch cannot deploy a ref that fails validation, then calls the hub-hosted `deploy-site-task.yml`, with the `environment:` declared inside that task rather than on the calling job, since GitHub rejects a job carrying both `uses:` and `environment:`. The crossing secrets, `DEPLOY_SSH_PRIVATE_KEY` and the optional `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair the live check needs, are therefore mapped explicitly under the call's `secrets:`, the pair only where a token-gated live check needs it, since the task declares them and `secrets: inherit` is not used on a cross-repository call. What that task's own job reads for each of them comes from its `environment:` binding rather than from the caller's job context. Concurrency is keyed on the environment with `cancel-in-progress: false`, because a cancelled deploy leaves a release uploaded and unflipped. The task re-asserts the environment name in a job of its own, because the `environment:` binding resolves before any step runs and a `workflow_call` caller is not bound by the dispatch choice list a human sees. Its environment-bound job then: checks out full history (a shallow clone silently changes page metadata), derives the release id **once** and exports it (deriving it twice yields ids seconds apart, and the live check then asserts a version nothing installed), runs a required deploy hook that builds the tree with whatever generator and precompression the site owns, installs the deploy credential from the environment, uploads into a per-release directory hard-linked against the current release and carrying **no** delete flag (at an environment root a delete removes the rollback targets), flips the pointer as a separate atomic step so a failed transfer cannot half-publish, then runs the same hook again to prune old releases and to check the running host (D4.6). Retention (D5.6) is bounded by a declared count with one side recorded as owning it: a deploy whose credential can observe the destination prunes and asserts the count here, while a credential confined **write-only** can neither delete nor read back, so there the prune is a host-side timer and the repo's runbook records that ownership. Widening the credential to bring the prune in-pipeline would trade a real confinement boundary for a check, and is the wrong trade. What the guarantee rejects is neither side owning it. One thing the pipeline cannot assert and the server config must: a non-public environment serving a byte-identical copy must not be indexed, and that default belongs on the side that is harmless in production, since a non-public container missing the value is still behind its gate while a production container inheriting it deindexes the site silently. Applicable scenarios: S1 (validation), the source-only release set S7/S8/S9/S10, and S12/S13 (the deploy dispatch). N/A: S2-S4, every registry scenario, and D5.1-D5.4 (the pipeline uploads no workflow artifact at all, so D5.6 is what applies in their place), all recorded N/A, not failed. - **Operational (workflow model, not a build target).** A `workflowModel: operational` repo layers direct commits to `develop` onto the **source-only** release shape above. It has two workflows. The first is a **lint/validation** PR workflow that feeds the required `Check pull request workflow status job`. It uses the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator, with **no unit tests**. Examples include Home Assistant `hass --script check_config`, `esphome config`, or a firmware build. Its triggers differ from the `release` model. It runs on pushes to `develop`, pull requests to `[ main, develop ]`, and `workflow_dispatch`. Push validation is advisory. Pull request validation is enforced on `main` and reported but not required on `develop`. The second workflow is the standard **source-only publisher** with `releaseTrigger: dispatch-only`. NBGV and `version.json` own the tag. The reusable release task creates tag + source zip + README + LICENSE. **The PR trigger names both branches, and naming `main` alone is a defect.** Omitting `develop` starts no validation when a PR opens against `develop`. The aggregator then never reports, and the PR appears clean with an empty check list. D1.2 forbids that output. Naming both causes a duplicate run after a PR merge. The change validates on the PR and again on the resulting push, regardless of merge method. The operational `develop` ruleset prescribes no merge method. The concurrency group uses the workflow name plus `${{ github.ref }}` (Section 2). A pull request uses `refs/pull//merge`, while its push uses `refs/heads/develop`. The runs occupy different groups and neither cancels the other. Pay that cost. The lint-only gate costs only a few runner-minutes. Suppressing the push requires distinguishing a merge commit from a direct commit, which restores the ambiguity the trigger set removes. S1 applies to every PR, including promotion and `develop` PRs. The source-only S7, S8, S9, and S10 scenarios also apply. Bot-push and schedule paths in S5/S6 are N/A, as are every build and registry scenario. See the branch-model note in Section 3 and [GOVERNANCE.md "Branching Model"][governance-branching-model]. diff --git a/catalog/README.md b/catalog/README.md index d9c5ad79..5510ff9f 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -7,4 +7,4 @@ Reusable reference snippets: concrete config artifacts a repo can copy or compar - `snippets/configs/`: the config exemplars `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent, running `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), plus `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). - `snippets/devcontainer/`: `.devcontainer` definitions for the .NET and Python toolchains. - `snippets/vscode/`: the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. -- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo with a host .NET toolchain and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. See each directory's own README.md. +- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo with a host .NET toolchain and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. A repo may instead wire an equivalent hook on `core.hooksPath`, which is what this repository's own `.husky/` does. See each directory's own README.md. diff --git a/catalog/snippets/husky/pre-commit b/catalog/snippets/husky/pre-commit index c525d7ea..26a7984d 100644 --- a/catalog/snippets/husky/pre-commit +++ b/catalog/snippets/husky/pre-commit @@ -28,7 +28,7 @@ uv run ruff check || exit 1 uv run pyright || exit 1 # The interpreter is chosen by running the probes spec/host-tools.json declares, in its order. -# On native Windows the python.org install registers `py`, not `python3`, and a `python3` found on PATH there is often the Microsoft Store alias stub, present but broken. +# Running one rather than testing for a name is the point, since Git Bash inherits a Windows PATH that can carry a name which fails when run. if py -3 --version >/dev/null 2>&1; then run_py() { py -3 "$@"; } elif python3 --version >/dev/null 2>&1; then diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index 846cf7f5..9f423dd1 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -68,7 +68,7 @@ The sequencing consequence is that a hub task lands on `develop`, promotes to `m ### Secrets and Permissions -Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the stubs below do. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. +Every hub task declares the secrets it needs by name under `on.workflow_call.secrets`, and a caller maps each one explicitly. Most are `required: true`. A mechanism's secret is `required: false` where the task treats it as one of several opt-in targets, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-release-task.yml`. A package-registry credential is not among them, since `NUGET_USERNAME` and the PyPI OIDC exchange are read by the caller stub's own `publish-nuget` / `publish-pypi` job rather than passed into the task, per [Adopting the Release Chain][adopting-the-release-chain]. The same names are `required: true` in a task built around that one mechanism instead, such as `DOCKER_HUB_USERNAME`/`DOCKER_HUB_ACCESS_TOKEN` in `build-docker-task.yml`. Whether `secrets: inherit` is used is decided by the call's own boundary, not by the fleet's preference. [GitHub documents the keyword][gh-reusing-workflows] for a caller in the same organization or enterprise as the called workflow, and the fleet is a personal account. So a cross-repository call to a hub task names each secret it passes, and `inherit` is never used on one. A call whose job needs none passes no `secrets:` key, which is what the [Adopting the Gates][adopting-the-gates] `validate` stub does. A call by local path stays inside one repository. There the caller's own secret store is the one the called workflow reads, so `inherit` is available. Availability is not a reason to use it, and this repository's own local-path calls name their secrets or pass none. Both shapes sit side by side in PlexCleaner's `test-pull-request.yml`. Its `validate` job calls `./.github/workflows/validate-task.yml` with `secrets: inherit`, to thread `CODECOV_TOKEN` through. Its `smoke-build` job names `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` explicitly for the hub's `build-release-task.yml`. The [Adopting the Gates][adopting-the-gates] smoke-build stub carries the local-path shape for the same reason. NxWitness proves an inherited value actually arrives. Its `publish-release.yml` calls its own `build-docker-task.yml` with `secrets: inherit` and `push: true`. That task authenticates to Docker Hub from the two inherited names. The declared names are the ones [`spec/secrets.json`][secrets] already declares for the mechanism the task implements, so the secret audit and the workflow agree by construction. An environment-scoped secret is the exception. `DEPLOY_SSH_PRIVATE_KEY` and the `SITE_AUTH_TOKEN_ID`/`SITE_AUTH_TOKEN` pair beside it cross a GitHub Environment boundary `spec/secrets.json` has no vocabulary for, per its `deploy-ssh` mechanism note. A hub task declares no job-level `permissions:` where every write goes through the App token, and the caller sets `permissions: {}`. A called workflow can only keep or reduce the caller's grant. A callee job naming a scope the caller did not grant fails at startup even when its `if:` is false. Declaring nothing in the callee is therefore the shape that cannot fail against any caller, and it gives `GITHUB_TOKEN` no scope. A task whose job genuinely writes with `GITHUB_TOKEN`, such as a release upload, declares that scope in the callee job and documents it in the stub's comment so the caller grants it. From 6877a869cd95bb32e7a4d3741225833d01f604f8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:28:25 -0700 Subject: [PATCH 08/31] Record the Canonical Review Ledger for the Third-Hook-Shape Round --- reports/canonical-review.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index e3ad4574..afe710f3 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -35,11 +35,11 @@ }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Build requirements", - "digest": "sha256:a8ee161d2903746f7ee333ba9444211a99013a18f02ca9ddd2f9710b6cfc80d9", + "digest": "sha256:c8736b941276f2470f35150ead197697ef7fbc521ae7ad9db8d94021eb536ace", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "6b31b27cce9b2177999390569687ef6dffdbf0da", - "stamp": "2026-09-02T02:08:08Z" + "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", + "stamp": "2026-09-02T02:28:22Z" }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Tooling and editor", @@ -171,11 +171,11 @@ }, { "unit": ".agents/skills/python-codestyle/SKILL.md > Local development loop", - "digest": "sha256:2b16524e8edffe8d04c0e1d4dc2abab4e5f7ef44fe9c8908721dae88d264b485", + "digest": "sha256:c3bacf3c4a90133d669a48523dbb37ff074e455c6d0c7e6e5c20eeb3a7cc66e8", "reviewer": "agent-skill", - "findings": 4, - "hubCommit": "19e9f5b3cbd057624cfbe8856731ec82d3cfdcc5", - "stamp": "2026-09-02T01:38:59Z" + "findings": 1, + "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", + "stamp": "2026-09-02T02:28:23Z" }, { "unit": ".agents/skills/repo-worktree/SKILL.md > Creating a Worktree", @@ -307,11 +307,11 @@ }, { "unit": "CODESTYLE.md > General", - "digest": "sha256:7c8339e809b36168a3d2707e4b723005bda81debe0ba8f6018b4b3b95697e87a", + "digest": "sha256:fee3f2edbc2d775c9464ac7fddf134e8b45119f4efe718c4f61215f2de292f8c", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "01475e0201567a6fdd9fb9759a390875a4bc634b", - "stamp": "2026-09-02T02:00:40Z" + "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", + "stamp": "2026-09-02T02:28:23Z" }, { "unit": "CODESTYLE.md > Shell", @@ -387,11 +387,11 @@ }, { "unit": "WORKFLOW.md > 6. Per-Project-Type Test Walkthroughs", - "digest": "sha256:b22d155cf3ec026b6e206124189684208979b79e3468fcb4f40e4519fab61eae", + "digest": "sha256:90cd1e08e32954143484bbd75d2e90fd6b59e70141eac0bcd4e20a5d662335ac", "reviewer": "agent-skill", - "findings": 0, - "hubCommit": "6b31b27cce9b2177999390569687ef6dffdbf0da", - "stamp": "2026-09-02T02:08:08Z" + "findings": 1, + "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", + "stamp": "2026-09-02T02:28:24Z" } ] } From 52eafe040c3fa6cb9b169eb98ea6349231a69f28 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:40:23 -0700 Subject: [PATCH 09/31] Put the Third Hook Shape at the Path the Audit Actually Reads Granting an equivalent hook on any core.hooksPath granted a shape parity.hooks then fails, since that check reads the tracked file at .husky/pre-commit or .pre-commit-config.yaml rather than a per-clone hooks path it cannot observe. A repo following the grant literally to .githooks/pre-commit has a working gate and a linter-parity defect at once. The exemplar hid it, because this repository's own third-shape hook sits at .husky/pre-commit. The grant now names that path. STANDUP.md cited this repository as the exemplar to copy, where the hub is parity.hooks' one exception to fetching through hub-fetch-run.py and calls its own scripts directly. A new repo copying it gets a hook that fails on its first commit for a script that is not in its tree, then fails the check for not fetching. The copilot instructions told a reviewer to locate a fix at its canonical source rather than propose a local edit that its fidelity rejects. No fidelity tier rejects one, which is what this change's own fidelity-model section establishes, so the qualifier invited the patch it meant to prevent. The staleness advisory bullet said it cannot fire on a local edit at all. It stays silent only until the hub next changes that file, and then reports the patched copy as trailing, which routes to the re-vendor that overwrites the edit. That is the carried-instruction-file-guard incident, so the bullet says it now. The deploy task's own comment still called DEPLOY_SSH_PRIVATE_KEY the one crossing credential, with three named secrets declared under it. --- .agents/skills/dotnet-codestyle/SKILL.md | 3 ++- .agents/skills/python-codestyle/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digest | 2 +- .claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md | 3 ++- .claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md | 2 +- .github/copilot-instructions.md | 4 ++-- .github/skills/dotnet-codestyle/SKILL.md | 3 ++- .github/skills/python-codestyle/SKILL.md | 2 +- .github/workflows/deploy-site-task.yml | 2 +- CODESTYLE.md | 2 +- GOVERNANCE.md | 2 +- STANDUP.md | 2 +- catalog/README.md | 2 +- catalog/snippets/husky/README.md | 2 +- spec/fidelity-model.md | 2 +- 15 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 35238f93..f71121a6 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -53,7 +53,8 @@ All builds must complete without warnings, enforced three ways: and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own on `core.hooksPath`. + also wire an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit + reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index e4b568ad..131363bd 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -98,7 +98,7 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own on `core.hooksPath`. The +wires an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index d68c2575..b1d7dcb1 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -f18e6780f7dd52c0 +46072149cd7d4c32 diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 35238f93..f71121a6 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -53,7 +53,8 @@ All builds must complete without warnings, enforced three ways: and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own on `core.hooksPath`. + also wire an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit + reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index e4b568ad..131363bd 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -98,7 +98,7 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own on `core.hooksPath`. The +wires an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b39ce5c4..79a23956 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -22,8 +22,8 @@ put a release-bump magnitude in the title. The full contract is in Follow the fidelity declared for the file. A byte-locked reference to shared infrastructure that this repository does not carry is intentional, not a broken link. Raise substantive defects in -canonical content, but locate the fix at its canonical source instead of proposing a local edit -that its fidelity rejects. +canonical content, but locate the fix at its canonical source instead of proposing a local edit. +No fidelity tier rejects one on its own, so the audit is not what stops it. ## GitHub Copilot Review Runbook diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 35238f93..f71121a6 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -53,7 +53,8 @@ All builds must complete without warnings, enforced three ways: and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own on `core.hooksPath`. + also wire an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit + reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index e4b568ad..131363bd 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -98,7 +98,7 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own on `core.hooksPath`. The +wires an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.github/workflows/deploy-site-task.yml b/.github/workflows/deploy-site-task.yml index 51bcede7..9b743724 100644 --- a/.github/workflows/deploy-site-task.yml +++ b/.github/workflows/deploy-site-task.yml @@ -27,7 +27,7 @@ on: required: true type: string secrets: - # The one credential that crosses this reusable workflow's boundary as a named secret. + # The credentials that cross this reusable workflow's boundary as named secrets. # The host address, the user, and the known-hosts value are GitHub Environment variables instead, since they are integrity-critical but not confidential. # The deploy job below reads them directly through its own environment binding. DEPLOY_SSH_PRIVATE_KEY: diff --git a/CODESTYLE.md b/CODESTYLE.md index abb8aabf..f5e11e98 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit` or wires an equivalent hook of its own. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit` or wires an equivalent hook of its own at `.husky/pre-commit`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/GOVERNANCE.md b/GOVERNANCE.md index fc6d669f..f9bd759c 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -244,7 +244,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 its host toolchain, 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 directly on `core.hooksPath`, which is what this repository does, since what this check reads is a working gate rather than a particular runner. +- **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 its host toolchain, 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, 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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. diff --git a/STANDUP.md b/STANDUP.md index 15cf41ec..6046800b 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -182,7 +182,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 a repo with a host .NET toolchain, which Husky.Net needs, or `catalog/snippets/pre-commit/` for a repo without one, or wire an equivalent hook of your own on `core.hooksPath` as this repository does, 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. 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. 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 a repo with a host .NET toolchain, which Husky.Net needs, or `catalog/snippets/pre-commit/` for a repo without one, or wire an equivalent hook of your own at `.husky/pre-commit`, 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. 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. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. ## 3. Stand Up the Workflows diff --git a/catalog/README.md b/catalog/README.md index 5510ff9f..3ea74e95 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -7,4 +7,4 @@ Reusable reference snippets: concrete config artifacts a repo can copy or compar - `snippets/configs/`: the config exemplars `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent, running `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), plus `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). - `snippets/devcontainer/`: `.devcontainer` definitions for the .NET and Python toolchains. - `snippets/vscode/`: the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. -- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo with a host .NET toolchain and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. A repo may instead wire an equivalent hook on `core.hooksPath`, which is what this repository's own `.husky/` does. See each directory's own README.md. +- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo with a host .NET toolchain and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. A repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, sourcing nothing and enabled with `core.hooksPath`, which is what this repository's own `.husky/` does. See each directory's own README.md. diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index 10406dcc..882d3005 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -2,7 +2,7 @@ `pre-commit` is the reference git pre-commit hook (installed under `.husky/` by Husky). It runs **language formatting/lint and the fleet's shared doc gates**: CSharpier and -`dotnet format` style via `dotnet husky run` for .NET, or `ruff format --check` / `ruff check` / +`dotnet format` style via `dotnet husky run` for .NET, plus `ruff format --check` / `ruff check` / the repo's type checker for a Python repo (native tooling, no Docker, the same checks the `../pre-commit/.pre-commit-config.yaml` snippet runs), plus the diff-scoped prose/comment-style gate and the whole-tree line-ending check. Copy `../hub-fetch-run.py` alongside `pre-commit` for diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 09be686d..e18f41a6 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -40,7 +40,7 @@ The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 - **verbatim and verbatim-tree** - detected. The content hash matches neither the current canonical nor any past revision. "Stale Versus Modified" below reports that as modified fixed content rather than as stale. - **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for. -- **intent** - not detected. Content is never judged, and the staleness advisory cannot fire on a local edit at all. It compares last-change dates, and editing the copy makes it newer than the hub canonical. +- **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. Once the hub next changes that file the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself. - **presence** - not detected. The unit is asserted to exist and is never read. So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. That silence is intended for a repo's own additions, which `CODESTYLE.md` is a declared destination for, and it is the hazard for a patch of carried rule text, since the two read alike to every check. Two checks reach some non-verbatim prose, and neither reaches those two files. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, and it runs only against `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. [`audit.py`][audit]'s `template_ref_outside_verbatim` reads the prose itself, and it catches only a coordination reference rather than a changed rule. From 09d33a35e442b7f986365f645fc3e3cc9663ce36 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:46:15 -0700 Subject: [PATCH 10/31] Say Which Fidelity Tiers the Audit Detects, and Name Both Hook Paths The copilot instructions said no fidelity tier rejects a local edit on its own, which contradicts the fidelity-model section this same change adds: a verbatim unit's edit hashes against neither the canonical nor any past revision and is reported as modified content, and interface catches an edit that moves the contract. Only intent and presence go unreported, so the sentence stated the exception as the rule and told a reviewer a patch to a verbatim section is invisible. Three surfaces called .husky/pre-commit the path the audit reads, where parity.hooks reads that file or .pre-commit-config.yaml. The python skill was the sharpest, recommending the pre-commit shape and then naming a different path as the audited one, so its own recommendation read as the failing choice. CODESTYLE.md attached the third hook shape to repos with no .NET toolchain, where the other carried file grants it unconditionally, so a .NET repo reading only its own copy concluded Husky.Net was mandatory for it. A tracked script at that path is also not yet a hook, so the enablement step is named with it. The .NET skill called the three clean-compile tasks carried verbatim, where spec/files.json declares .vscode/tasks.json with no fidelity key, so presence, which the audit asserts exists and never reads. --- .agents/skills/dotnet-codestyle/SKILL.md | 6 +++--- .agents/skills/python-codestyle/SKILL.md | 3 ++- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/dotnet-codestyle/SKILL.md | 6 +++--- .../fleet-skills/skills/python-codestyle/SKILL.md | 3 ++- .github/copilot-instructions.md | 3 ++- .github/skills/dotnet-codestyle/SKILL.md | 6 +++--- .github/skills/python-codestyle/SKILL.md | 3 ++- CODESTYLE.md | 2 +- 9 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index f71121a6..a1d2af23 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -53,8 +53,8 @@ All builds must complete without warnings, enforced three ways: and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit - reads. + also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. + That path and `.pre-commit-config.yaml` are the two the audit reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. @@ -85,7 +85,7 @@ in place. ### Build tasks Run these from VS Code's task runner (Terminal -> Run Task) or an agent's task-running tool. The -three clean-compile tasks are carried verbatim, and a repo adds its own convenience tasks (tool +three clean-compile tasks are carried unchanged, and a repo adds its own convenience tasks (tool updates, dependency upgrades, benchmarks) on top: - `.NET Build`: build with diagnostic verbosity *(clean-compile)* diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 131363bd..0a54017e 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -98,7 +98,8 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit reads. The +wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That +path and the `.pre-commit-config.yaml` this snippet produces are the two the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index b1d7dcb1..f37aeeaf 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -46072149cd7d4c32 +3963a636e2a4c360 diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index f71121a6..a1d2af23 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -53,8 +53,8 @@ All builds must complete without warnings, enforced three ways: and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit - reads. + also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. + That path and `.pre-commit-config.yaml` are the two the audit reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. @@ -85,7 +85,7 @@ in place. ### Build tasks Run these from VS Code's task runner (Terminal -> Run Task) or an agent's task-running tool. The -three clean-compile tasks are carried verbatim, and a repo adds its own convenience tasks (tool +three clean-compile tasks are carried unchanged, and a repo adds its own convenience tasks (tool updates, dependency upgrades, benchmarks) on top: - `.NET Build`: build with diagnostic verbosity *(clean-compile)* diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 131363bd..0a54017e 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -98,7 +98,8 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit reads. The +wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That +path and the `.pre-commit-config.yaml` this snippet produces are the two the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 79a23956..ee5c4837 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,7 +23,8 @@ put a release-bump magnitude in the title. The full contract is in Follow the fidelity declared for the file. A byte-locked reference to shared infrastructure that this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. -No fidelity tier rejects one on its own, so the audit is not what stops it. +An edit to a verbatim unit is reported as modified content, and one to an intent or presence +unit is reported nowhere, so the audit is not what decides this. ## GitHub Copilot Review Runbook diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index f71121a6..a1d2af23 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -53,8 +53,8 @@ All builds must complete without warnings, enforced three ways: and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit - reads. + also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. + That path and `.pre-commit-config.yaml` are the two the audit reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. @@ -85,7 +85,7 @@ in place. ### Build tasks Run these from VS Code's task runner (Terminal -> Run Task) or an agent's task-running tool. The -three clean-compile tasks are carried verbatim, and a repo adds its own convenience tasks (tool +three clean-compile tasks are carried unchanged, and a repo adds its own convenience tasks (tool updates, dependency upgrades, benchmarks) on top: - `.NET Build`: build with diagnostic verbosity *(clean-compile)* diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 131363bd..0a54017e 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -98,7 +98,8 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, which is the path the audit reads. The +wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That +path and the `.pre-commit-config.yaml` this snippet produces are the two the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/CODESTYLE.md b/CODESTYLE.md index f5e11e98..a7e6bcd4 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit` or wires an equivalent hook of its own at `.husky/pre-commit`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit`. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions From c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:50:22 -0700 Subject: [PATCH 11/31] Drop a Provenance Qualifier and Name the Stale Verdict Too The audit reads the tracked hook path with no condition on where the file came from, so a hand-written config at that path is read the same as a copied snippet. classify_verbatim reports an edit matching an earlier canonical revision as stale rather than as modified content, so a reviewer told only the second reading sees one of the two verdicts a verbatim edit actually produces. --- .agents/skills/python-codestyle/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digest | 2 +- .claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md | 2 +- .github/copilot-instructions.md | 5 +++-- .github/skills/python-codestyle/SKILL.md | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 0a54017e..b320cf85 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -99,7 +99,7 @@ runs the same on any task shell. CI runs the same clean-compile commands as the backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That -path and the `.pre-commit-config.yaml` this snippet produces are the two the audit reads. The +path and `.pre-commit-config.yaml` are the two the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index f37aeeaf..4d940cd2 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -3963a636e2a4c360 +5278eece0d27f54b diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 0a54017e..b320cf85 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -99,7 +99,7 @@ runs the same on any task shell. CI runs the same clean-compile commands as the backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That -path and the `.pre-commit-config.yaml` this snippet produces are the two the audit reads. The +path and `.pre-commit-config.yaml` are the two the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ee5c4837..077492d8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,8 +23,9 @@ put a release-bump magnitude in the title. The full contract is in Follow the fidelity declared for the file. A byte-locked reference to shared infrastructure that this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. -An edit to a verbatim unit is reported as modified content, and one to an intent or presence -unit is reported nowhere, so the audit is not what decides this. +An edit to a verbatim unit is reported, as modified content or as stale where it matches an +earlier canonical revision, and one to an intent or presence unit is reported nowhere, so the +audit is not what decides this. ## GitHub Copilot Review Runbook diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 0a54017e..b320cf85 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -99,7 +99,7 @@ runs the same on any task shell. CI runs the same clean-compile commands as the backstop. A repo with no host .NET toolchain wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That -path and the `.pre-commit-config.yaml` this snippet produces are the two the audit reads. The +path and `.pre-commit-config.yaml` are the two the audit reads. The runner follows the host toolchain rather than the languages the hook checks, so a repo already building .NET may run these same Python checks from the Husky.Net snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the config. From 4049af1d94b8d5df266745fe37b8e8a0331a01ec Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:50:40 -0700 Subject: [PATCH 12/31] Record the Canonical Review Ledger for the Final Round --- reports/canonical-review.json | 26 +++++++++++++++++--------- reports/canonical-review.md | 5 ++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index afe710f3..7ba268b7 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -35,11 +35,11 @@ }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Build requirements", - "digest": "sha256:c8736b941276f2470f35150ead197697ef7fbc521ae7ad9db8d94021eb536ace", + "digest": "sha256:c4aceca7dd8122930e174f57d2ae24e341d4ad79f3e1f9b66962343025a620f0", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", - "stamp": "2026-09-02T02:28:22Z" + "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", + "stamp": "2026-09-02T02:50:39Z" }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Tooling and editor", @@ -171,11 +171,11 @@ }, { "unit": ".agents/skills/python-codestyle/SKILL.md > Local development loop", - "digest": "sha256:c3bacf3c4a90133d669a48523dbb37ff074e455c6d0c7e6e5c20eeb3a7cc66e8", + "digest": "sha256:e51a6803398bea187434b1d44f815d35cbe1428f04c0938d3c3217a0a202029c", "reviewer": "agent-skill", "findings": 1, - "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", - "stamp": "2026-09-02T02:28:23Z" + "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", + "stamp": "2026-09-02T02:50:40Z" }, { "unit": ".agents/skills/repo-worktree/SKILL.md > Creating a Worktree", @@ -281,6 +281,14 @@ "hubCommit": "37d116a2fa0cecf85a220c9005375091db240a8d", "stamp": "2026-09-01T14:23:11Z" }, + { + "unit": ".github/copilot-instructions.md > Reviewing Carried Fleet Content", + "digest": "sha256:f2be90313d77f3b213e9e773c8d7b11625b96471e6c6dd03801406b0359bdfd7", + "reviewer": "agent-skill", + "findings": 1, + "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", + "stamp": "2026-09-02T02:50:40Z" + }, { "unit": "AGENTS.md > Where the Rules Live", "digest": "sha256:2526a341afe0c6f1e2d2e1fe954855398742474a816e2bcba640672d1363766f", @@ -307,11 +315,11 @@ }, { "unit": "CODESTYLE.md > General", - "digest": "sha256:fee3f2edbc2d775c9464ac7fddf134e8b45119f4efe718c4f61215f2de292f8c", + "digest": "sha256:989ca6bf7882dd1efdeeda0122241eed68cadfc62c4daa5a1ab8696dbd62667d", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "d4cb996ed67bed9e327c5a3b3c4b441466bc37bb", - "stamp": "2026-09-02T02:28:23Z" + "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", + "stamp": "2026-09-02T02:50:40Z" }, { "unit": "CODESTYLE.md > Shell", diff --git a/reports/canonical-review.md b/reports/canonical-review.md index 9b54c319..f8fe568a 100644 --- a/reports/canonical-review.md +++ b/reports/canonical-review.md @@ -7,9 +7,9 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than ## Coverage - units: 288 -- covered: 49 +- covered: 50 - stale: 0 -- never read here: 239 +- never read here: 238 ## Burn-down @@ -335,7 +335,6 @@ A unit is what a reviewer reads whole, decided by the carry manifest rather than - **(preamble)** - never - **Commit Messages and Pull Request Titles** - never - **GitHub Copilot Review Runbook** - never -- **Reviewing Carried Fleet Content** - never - **When in Doubt** - never ### .markdownlint-cli2.jsonc From 926154f362568795de48008fe3ad45b282df53cb Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 19:59:04 -0700 Subject: [PATCH 13/31] Select the Husky Snippet by the Tool Manifest It Actually Needs Selecting it on a host .NET toolchain sent a repo there that cannot install it. The snippet ships no .config/dotnet-tools.json, so dotnet tool restore restores nothing, dotnet husky install never runs, and the hook sources a file that is never generated. A Docker, config, or docs repo with .NET on the host and no manifest of its own was newly directed into exactly that state. The discriminator is the manifest the repo already keeps, which is what restore reads. The fidelity model said a verbatim local edit matches no past revision and reports as modified. Classification turns on which revision the bytes match rather than on how they got there, so an edit restoring an earlier canonical revision reports as stale, and both readings are named now. The copilot instructions said an intent edit is reported nowhere. The undeclared-heading advisory and the template-reference check both read non-verbatim prose in AGENTS.md, GOVERNANCE.md and that file itself, so the sentence named the general case as the whole of it. STANDUP.md restated the hook selection, enablement and per-clone steps that GOVERNANCE.md keeps, which is how it came to carry the wrong discriminator in the first place. It keeps the onboarding sequencing and routes the rest. A comment in the deploy stub wrapped one sentence across two lines. --- .agents/skills/dotnet-codestyle/SKILL.md | 4 ++-- .agents/skills/python-codestyle/SKILL.md | 2 +- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/dotnet-codestyle/SKILL.md | 4 ++-- .../fleet-skills/skills/python-codestyle/SKILL.md | 2 +- .github/copilot-instructions.md | 5 +++-- .github/skills/dotnet-codestyle/SKILL.md | 4 ++-- .github/skills/python-codestyle/SKILL.md | 2 +- CODESTYLE.md | 2 +- GOVERNANCE.md | 2 +- STANDUP.md | 2 +- catalog/README.md | 2 +- catalog/snippets/husky/README.md | 7 ++++--- catalog/snippets/pre-commit/.pre-commit-config.yaml | 2 +- catalog/snippets/pre-commit/README.md | 2 +- spec/audit.py | 4 ++-- spec/fidelity-model.md | 2 +- 17 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index a1d2af23..29d0b23d 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -50,8 +50,8 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET - repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo + and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring Husky.Net, + which a .NET repo already keeps and the snippet does not ship. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That path and `.pre-commit-config.yaml` are the two the audit reads. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index b320cf85..028c64a4 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -96,7 +96,7 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo with no host .NET toolchain wires its local hook from the canonical +backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That path and `.pre-commit-config.yaml` are the two the audit reads. The diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 4d940cd2..8b0df309 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -5278eece0d27f54b +bd7b63976ac7168f diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index a1d2af23..29d0b23d 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -50,8 +50,8 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET - repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo + and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring Husky.Net, + which a .NET repo already keeps and the snippet does not ship. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That path and `.pre-commit-config.yaml` are the two the audit reads. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index b320cf85..028c64a4 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -96,7 +96,7 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo with no host .NET toolchain wires its local hook from the canonical +backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That path and `.pre-commit-config.yaml` are the two the audit reads. The diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 077492d8..217ec142 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,8 +24,9 @@ Follow the fidelity declared for the file. A byte-locked reference to shared inf this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. An edit to a verbatim unit is reported, as modified content or as stale where it matches an -earlier canonical revision, and one to an intent or presence unit is reported nowhere, so the -audit is not what decides this. +earlier canonical revision. An edit to an intent or presence unit is reported nowhere, beyond +two advisories that read undeclared headings and template references in this file, `AGENTS.md` +and `GOVERNANCE.md`. So the audit is not what decides this. ## GitHub Copilot Review Runbook diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index a1d2af23..29d0b23d 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -50,8 +50,8 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a host .NET toolchain, which a .NET - repo already has. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo + and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring Husky.Net, + which a .NET repo already keeps and the snippet does not ship. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo without one, and each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That path and `.pre-commit-config.yaml` are the two the audit reads. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index b320cf85..028c64a4 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -96,7 +96,7 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo with no host .NET toolchain wires its local hook from the canonical +backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That path and `.pre-commit-config.yaml` are the two the audit reads. The diff --git a/CODESTYLE.md b/CODESTYLE.md index a7e6bcd4..4ae6a36e 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the host toolchain rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net needs a host .NET toolchain, so a repo without one uses `pre-commit`. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses `pre-commit`. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/GOVERNANCE.md b/GOVERNANCE.md index f9bd759c..0c0add8c 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -244,7 +244,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **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, each with a copy of `catalog/snippets/hub-fetch-run.py` alongside it. `catalog/snippets/husky/` is Husky.Net, for a repo with a host .NET toolchain, since its hook sources a file `dotnet husky install` generates. `catalog/snippets/pre-commit/` is the `pre-commit` framework, for a repo without one and for any repo that prefers it. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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 its host toolchain, 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. +- **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, 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. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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, 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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. diff --git a/STANDUP.md b/STANDUP.md index 6046800b..27e33f11 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -182,7 +182,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 a repo with a host .NET toolchain, which Husky.Net needs, or `catalog/snippets/pre-commit/` for a repo without one, or wire an equivalent hook of your own at `.husky/pre-commit`, 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. 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. 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 with `catalog/snippets/hub-fetch-run.py` alongside it, then enable it, before the section 5 audit run. Which shape applies, what the gate must cover, and the per-clone enablement steps are all in [`GOVERNANCE.md`][governance] "Running the Linters Locally (Known-Working Invocations)", not restated here. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. ## 3. Stand Up the Workflows diff --git a/catalog/README.md b/catalog/README.md index 3ea74e95..534b58fd 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -7,4 +7,4 @@ Reusable reference snippets: concrete config artifacts a repo can copy or compar - `snippets/configs/`: the config exemplars `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent, running `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), plus `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). - `snippets/devcontainer/`: `.devcontainer` definitions for the .NET and Python toolchains. - `snippets/vscode/`: the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. -- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo with a host .NET toolchain and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. A repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, sourcing nothing and enabled with `core.hooksPath`, which is what this repository's own `.husky/` does. See each directory's own README.md. +- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for a repo that already keeps a .NET tool manifest declaring it and the `pre-commit` framework for a repo without one, both carrying any language's checks plus the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. A repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, sourcing nothing and enabled with `core.hooksPath`, which is what this repository's own `.husky/` does. See each directory's own README.md. diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index 882d3005..854e3805 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -13,9 +13,10 @@ change that lands broken on `main` before it does real damage locally. These are network fetches alongside the Docker pulls the Lint tasks below already do. A fetch failure fails the commit rather than silently skipping the gate. -This snippet needs a host .NET toolchain, since the hook sources `.husky/_/husky.sh`, a file -`dotnet husky install` generates after `dotnet tool restore` restores the tool manifest. A -repo without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by +This snippet is for a repo that already keeps a .NET tool manifest declaring Husky.Net, since +the hook sources `.husky/_/husky.sh`, a file `dotnet husky install` generates after +`dotnet tool restore` restores that manifest. This snippet ships no manifest, so a host .NET +toolchain alone is not enough and a repo without one uses `../pre-commit/` instead. Neither snippet is scoped to a language by 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 diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml index 8f16c153..40524e9a 100644 --- a/catalog/snippets/pre-commit/.pre-commit-config.yaml +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Reference pre-commit (pre-commit.com) config for a fleet repo with no host .NET toolchain, or one that prefers this runner. +# Reference pre-commit (pre-commit.com) config for a fleet repo that keeps no .NET tool manifest, or one that prefers this runner. # Husky.Net is a dotnet tool, so the discriminator is the toolchain rather than the repo's languages. # Drop the ruff and type-check hooks in a repo with no Python, keeping the two doc gates every repo owes. # Copy into the repo root alongside ../hub-fetch-run.py. diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index 90105df1..ea36a055 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -1,7 +1,7 @@ # Pre-commit snippet `.pre-commit-config.yaml` is the reference config for the `pre-commit` framework -(pre-commit.com), for a repo with no host .NET toolchain, and for any repo that prefers it to +(pre-commit.com), for a repo that keeps no .NET tool manifest, and for any repo that prefers it to Husky.Net. The host toolchain is the discriminator, not the repo's own languages: Husky.Net is a `dotnet` tool, and this framework installs independently of one. In a repo with Python it runs `ruff format --check` and `ruff check`, plus that diff --git a/spec/audit.py b/spec/audit.py index 2cc63565..0bbe589c 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -2981,8 +2981,8 @@ def _selftest(): 1, ), ] - # The deploy-site.yml caller stub once deploy-site-task.yml is hub-hosted: no secrets: inherit - # (inherit is documented for same-org or enterprise callers, so the fleet does not use it across repositories), the one crossing secret named instead. + # The deploy-site.yml caller stub once deploy-site-task.yml is hub-hosted: no secrets: inherit, the one crossing secret named instead. + # inherit is documented for same-org or enterprise callers, so the fleet does not use it across repositories. # No job-level environment: on the caller, unsupported on a job with uses: (ptr727/ProjectTemplate#942). deploy_stub = ( "jobs:\n" diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index e18f41a6..4a9e73e6 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -38,7 +38,7 @@ A downstream repo editing carried content is a different question from a copy tr The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 of its 20 declared sections are `verbatim`. An edit inside one of those is caught, and an edit to the prose around them is not. -- **verbatim and verbatim-tree** - detected. The content hash matches neither the current canonical nor any past revision. "Stale Versus Modified" below reports that as modified fixed content rather than as stale. +- **verbatim and verbatim-tree** - detected. The content hash is compared against the canonical and every past revision, and "Stale Versus Modified" below reads the result. An edit matching no revision reports as modified fixed content, and one restoring an earlier revision reports as stale, since the hash cannot say how the bytes got there. - **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for. - **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. Once the hub next changes that file the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself. - **presence** - not detected. The unit is asserted to exist and is never read. From bb29973eb27c2de8d55b9e57a96d7d80475dac64 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:05:08 -0700 Subject: [PATCH 14/31] Propagate the Manifest Discriminator to Every Surface That Still Named the Host The manifest fix landed on some surfaces and left the pre-fix framing on others, which is how the two came to disagree in the first place. The .NET skill asserted every .NET repo already keeps a Husky.Net manifest, a universal that reinstates the language discriminator and sends a .NET repo without one into the hook failure the fix exists to prevent. The Python skill still read that the runner follows the host toolchain, contradicting the sentence two before it. The pre-commit README and the rollout doc carried the same. Every sentence offering an equivalent hook of a repo's own now carries sourcing nothing, which is load-bearing rather than descriptive: the one canonical .husky/pre-commit the fleet ships does source _/husky.sh, so a reader starting from it reproduces exactly the breakage. CODESTYLE.md called the destination pre-commit one sentence before using .husky/pre-commit as a path, so it names the framework. --- .agents/skills/dotnet-codestyle/SKILL.md | 12 +++++++----- .agents/skills/python-codestyle/SKILL.md | 11 ++++++----- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/dotnet-codestyle/SKILL.md | 12 +++++++----- .../fleet-skills/skills/python-codestyle/SKILL.md | 11 ++++++----- .github/skills/dotnet-codestyle/SKILL.md | 12 +++++++----- .github/skills/python-codestyle/SKILL.md | 11 ++++++----- CODESTYLE.md | 2 +- catalog/snippets/pre-commit/README.md | 6 +++--- docs/pre-commit-hooks-rollout.md | 2 +- 10 files changed, 45 insertions(+), 36 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 29d0b23d..b092ae5c 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -50,11 +50,13 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring Husky.Net, - which a .NET repo already keeps and the snippet does not ship. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo - without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. - That path and `.pre-commit-config.yaml` are the two the audit reads. + and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring + Husky.Net, which the snippet does not ship, so a repo keeping no such manifest takes the other + canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks + its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, + enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources + a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the + two the audit reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 028c64a4..85441e55 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -98,11 +98,12 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That -path and `.pre-commit-config.yaml` are the two the audit reads. The -runner follows the host toolchain rather than the languages the hook checks, so a repo already -building .NET may run these same Python checks from the Husky.Net snippet instead. The -`pre-commit` directory's own README names the second file to copy alongside the config. +wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and +sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The +runner follows the toolchain the repo already keeps rather than the languages the hook checks, +so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet +instead. The `pre-commit` directory's own README names the second file to copy alongside the +config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 8b0df309..39ec46fb 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -bd7b63976ac7168f +7d1e58bffde7828a diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 29d0b23d..b092ae5c 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -50,11 +50,13 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring Husky.Net, - which a .NET repo already keeps and the snippet does not ship. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo - without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. - That path and `.pre-commit-config.yaml` are the two the audit reads. + and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring + Husky.Net, which the snippet does not ship, so a repo keeping no such manifest takes the other + canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks + its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, + enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources + a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the + two the audit reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 028c64a4..85441e55 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -98,11 +98,12 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That -path and `.pre-commit-config.yaml` are the two the audit reads. The -runner follows the host toolchain rather than the languages the hook checks, so a repo already -building .NET may run these same Python checks from the Husky.Net snippet instead. The -`pre-commit` directory's own README names the second file to copy alongside the config. +wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and +sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The +runner follows the toolchain the repo already keeps rather than the languages the hook checks, +so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet +instead. The `pre-commit` directory's own README names the second file to copy alongside the +config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 29d0b23d..b092ae5c 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -50,11 +50,13 @@ All builds must complete without warnings, enforced three ways: narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local - and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring Husky.Net, - which a .NET repo already keeps and the snippet does not ship. The other canonical shape, `catalog/snippets/pre-commit/`, is for a repo - without one, and each shape carries whichever language checks its own repo keeps. A repo may - also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. - That path and `.pre-commit-config.yaml` are the two the audit reads. + and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring + Husky.Net, which the snippet does not ship, so a repo keeping no such manifest takes the other + canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks + its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, + enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources + a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the + two the audit reads. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 028c64a4..85441e55 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -98,11 +98,12 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. That -path and `.pre-commit-config.yaml` are the two the audit reads. The -runner follows the host toolchain rather than the languages the hook checks, so a repo already -building .NET may run these same Python checks from the Husky.Net snippet instead. The -`pre-commit` directory's own README names the second file to copy alongside the config. +wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and +sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The +runner follows the toolchain the repo already keeps rather than the languages the hook checks, +so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet +instead. The `pre-commit` directory's own README names the second file to copy alongside the +config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/CODESTYLE.md b/CODESTYLE.md index 4ae6a36e..c88f5e87 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses `pre-commit`. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath`. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses the `pre-commit` framework instead. Any repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index ea36a055..566d808e 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -2,9 +2,9 @@ `.pre-commit-config.yaml` is the reference config for the `pre-commit` framework (pre-commit.com), for a repo that keeps no .NET tool manifest, and for any repo that prefers it to -Husky.Net. The host toolchain is the discriminator, not the -repo's own languages: Husky.Net is a `dotnet` tool, and this framework installs independently -of one. In a repo with Python it runs `ruff format --check` and `ruff check`, plus that +Husky.Net. The toolchain the repo already keeps is the +discriminator, not the repo's own languages: Husky.Net runs from a .NET tool manifest declaring +it, and this framework needs none. In a repo with Python it runs `ruff format --check` and `ruff check`, plus that repo's declared type checker, `pyright` or `mypy`, matching whichever `python-codestyle` says its CI runs. Each tool runs via `uvx`, native tooling, never Docker. `uvx` needs no project dependency, matching CI's own diff --git a/docs/pre-commit-hooks-rollout.md b/docs/pre-commit-hooks-rollout.md index 4a9d4b80..748d3fae 100644 --- a/docs/pre-commit-hooks-rollout.md +++ b/docs/pre-commit-hooks-rollout.md @@ -25,7 +25,7 @@ defect, the same severity a missing markdownlint config already gets. A repo mid doc-gate half wired and the language-format half not yet because its own corpus does not pass it clean, stays operational, per the existing carve-out in GOVERNANCE.md. Two catalog snippets carry the canonical shape: Husky.Net -(`catalog/snippets/husky/`, for a repo with a host .NET toolchain, which its hook needs) and the +(`catalog/snippets/husky/`, for a repo already keeping a .NET tool manifest declaring it) and the `pre-commit` framework (`catalog/snippets/pre-commit/`, for a repo without one). Both now carry the same shared doc gates, the diff-scoped prose/comment-style gate and the whole-tree line-ending check, fetched fresh from the hub's `main` branch at run time via `hub-fetch-run.py` rather than From 112ffd874eaec3784a747b341fbdd0b19d75502c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:09:04 -0700 Subject: [PATCH 15/31] Point Every Single-Unit Reader at the Per-Clone Enablement Steps CODESTYLE.md routed to the governance section for what the gate must cover and its audit treatment but not for the per-clone steps, which both language skills name. A reader with only that unit who took the Husky.Net shape was never pointed at dotnet tool restore and dotnet husky install, and the copied hook sources a file neither has generated yet. That was the one path from a single unit still reaching the hook failure this branch exists to close. The Python skill attached the own-hook route to a manifest-less repo, where both carried files open it to any repo. --- .agents/skills/python-codestyle/SKILL.md | 6 +++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/python-codestyle/SKILL.md | 6 +++--- .github/skills/python-codestyle/SKILL.md | 6 +++--- CODESTYLE.md | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 85441e55..c2f7ba93 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -97,9 +97,9 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and -sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any +repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with +`core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner follows the toolchain the repo already keeps rather than the languages the hook checks, so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 39ec46fb..8f9fc23b 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -7d1e58bffde7828a +3affbc030e65da7a diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 85441e55..c2f7ba93 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -97,9 +97,9 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and -sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any +repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with +`core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner follows the toolchain the repo already keeps rather than the languages the hook checks, so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 85441e55..c2f7ba93 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -97,9 +97,9 @@ VS Code tasks mirror that the fleet baseline expects. Every command-executing ta `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo, or -wires an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and -sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The +`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any +repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with +`core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner follows the toolchain the repo already keeps rather than the languages the hook checks, so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet instead. The `pre-commit` directory's own README names the second file to copy alongside the diff --git a/CODESTYLE.md b/CODESTYLE.md index c88f5e87..9e478e32 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", a hub-only section read in a hub checkout rather than carried into every fleet repo. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses the `pre-commit` framework instead. Any repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** The *mechanism* is bounded by the toolchain the repo already keeps rather than by which languages its checks cover, and two canonical shapes carry it. Husky.Net runs from a .NET tool manifest declaring it, so a repo that keeps no such manifest uses the `pre-commit` framework instead. Any repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. Canonical shapes for both live in `catalog/snippets/` in the hub, not a repo-relative path since it is hub-local and not carried into every fleet repo. What that gate must cover, its per-clone enablement steps, and what its absence means for the audit, is `GOVERNANCE.md` "Running the Linters Locally (Known-Working Invocations)", the same hub-only section, not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions From 9bdecbb82ead597270217c06a8038b27adf68666 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:09:18 -0700 Subject: [PATCH 16/31] Record the Canonical Review Ledger for the Review-Response Rounds --- reports/canonical-review.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index 7ba268b7..fb0a0ffc 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -35,11 +35,11 @@ }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Build requirements", - "digest": "sha256:c4aceca7dd8122930e174f57d2ae24e341d4ad79f3e1f9b66962343025a620f0", + "digest": "sha256:b20de4ba05724bfa3bb598deec592789320e9119b44cca3452793a2526e8c23f", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", - "stamp": "2026-09-02T02:50:39Z" + "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", + "stamp": "2026-09-02T03:09:16Z" }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Tooling and editor", @@ -171,11 +171,11 @@ }, { "unit": ".agents/skills/python-codestyle/SKILL.md > Local development loop", - "digest": "sha256:e51a6803398bea187434b1d44f815d35cbe1428f04c0938d3c3217a0a202029c", + "digest": "sha256:1721ef040c7be6f3c0651cd06fe6ba2b03336f284b30886f53f6a0e9f4a9d103", "reviewer": "agent-skill", "findings": 1, - "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", - "stamp": "2026-09-02T02:50:40Z" + "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", + "stamp": "2026-09-02T03:09:17Z" }, { "unit": ".agents/skills/repo-worktree/SKILL.md > Creating a Worktree", @@ -283,11 +283,11 @@ }, { "unit": ".github/copilot-instructions.md > Reviewing Carried Fleet Content", - "digest": "sha256:f2be90313d77f3b213e9e773c8d7b11625b96471e6c6dd03801406b0359bdfd7", + "digest": "sha256:7ae77c0dfd2a948c68ee9bde900c4c14153407534510f697facf9ce3ffd1805f", "reviewer": "agent-skill", - "findings": 1, - "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", - "stamp": "2026-09-02T02:50:40Z" + "findings": 0, + "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", + "stamp": "2026-09-02T03:09:17Z" }, { "unit": "AGENTS.md > Where the Rules Live", @@ -315,11 +315,11 @@ }, { "unit": "CODESTYLE.md > General", - "digest": "sha256:989ca6bf7882dd1efdeeda0122241eed68cadfc62c4daa5a1ab8696dbd62667d", + "digest": "sha256:54fcc07fc743507089e95ab8f1e7aeb64f0d83e31716514dadb666871a495684", "reviewer": "agent-skill", - "findings": 0, - "hubCommit": "c6fd0b6ff0b4e5213c11acc2cedf9ba54af14356", - "stamp": "2026-09-02T02:50:40Z" + "findings": 1, + "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", + "stamp": "2026-09-02T03:09:17Z" }, { "unit": "CODESTYLE.md > Shell", From 87aed6488bff0c09f2131861a3084e52a527c565 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:11:44 -0700 Subject: [PATCH 17/31] Separate a Structural Verbatim-Tree Failure and a Deleted Presence Unit A verbatim-tree declaration fails on a missing included path or an extra one under a pruned root before any hash is read, so the tier bullet described only half of what detects an edit there. The presence tier asserts existence, so a deleted unit is still a finding and only a content edit to a surviving one goes unreported. The bullet and the copilot guidance both said the tier detects nothing. The intent staleness advisory walks past a revision whose diff is normalization only, so the trigger is the hub's next substantive change to that file rather than its next change of any kind. --- .github/copilot-instructions.md | 2 +- spec/fidelity-model.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 217ec142..992a8c2b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,7 +24,7 @@ Follow the fidelity declared for the file. A byte-locked reference to shared inf this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. An edit to a verbatim unit is reported, as modified content or as stale where it matches an -earlier canonical revision. An edit to an intent or presence unit is reported nowhere, beyond +earlier canonical revision. A content edit to an intent or presence unit is reported nowhere, though a deleted unit is still caught by the presence check, and beyond two advisories that read undeclared headings and template references in this file, `AGENTS.md` and `GOVERNANCE.md`. So the audit is not what decides this. diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 4a9e73e6..23c2173f 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -38,10 +38,10 @@ A downstream repo editing carried content is a different question from a copy tr The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 of its 20 declared sections are `verbatim`. An edit inside one of those is caught, and an edit to the prose around them is not. -- **verbatim and verbatim-tree** - detected. The content hash is compared against the canonical and every past revision, and "Stale Versus Modified" below reads the result. An edit matching no revision reports as modified fixed content, and one restoring an earlier revision reports as stale, since the hash cannot say how the bytes got there. +- **verbatim and verbatim-tree** - detected. The content hash is compared against the canonical and every past revision, and "Stale Versus Modified" below reads the result. An edit matching no revision reports as modified fixed content, and one restoring an earlier revision reports as stale, since the hash cannot say how the bytes got there. A `verbatim-tree` declaration also fails structurally, on a missing included path or an extra one under a pruned root, before any hash is read. - **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for. -- **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. Once the hub next changes that file the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself. -- **presence** - not detected. The unit is asserted to exist and is never read. +- **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. After the hub's next substantive change to that file, meaning one the normalization above does not explain away, the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself. +- **presence** - a content edit is not detected. The unit is asserted to exist and is never read, so deleting it is still a finding and editing it is not. So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. That silence is intended for a repo's own additions, which `CODESTYLE.md` is a declared destination for, and it is the hazard for a patch of carried rule text, since the two read alike to every check. Two checks reach some non-verbatim prose, and neither reaches those two files. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, and it runs only against `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. [`audit.py`][audit]'s `template_ref_outside_verbatim` reads the prose itself, and it catches only a coordination reference rather than a changed rule. From 355606a97664644233bc9866634680f340f88352 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:14:31 -0700 Subject: [PATCH 18/31] Count the Third Advisory and Unpick the Garbled Sentence Inserting the presence-check clause split "reported nowhere" from "beyond two advisories", leaving a fragment that reads as the advisories catching the edit, in the file the review bot reads directly. The count was also wrong for the intent half. check_intent_staleness skips a byte-identical copy outright, so an edited intent copy is exposed to a DRIFT that an untouched one never sees, and .github/copilot-instructions.md is itself declared intent, so the advisory applies to the file making the claim. --- .github/copilot-instructions.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 992a8c2b..e152dfb8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,9 +24,12 @@ Follow the fidelity declared for the file. A byte-locked reference to shared inf this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. An edit to a verbatim unit is reported, as modified content or as stale where it matches an -earlier canonical revision. A content edit to an intent or presence unit is reported nowhere, though a deleted unit is still caught by the presence check, and beyond -two advisories that read undeclared headings and template references in this file, `AGENTS.md` -and `GOVERNANCE.md`. So the audit is not what decides this. +earlier canonical revision. A content edit to an intent or presence unit is never compared +against the canonical. Three advisories still reach it. Two read undeclared headings and +template references in this file, `AGENTS.md` and `GOVERNANCE.md`. The third is the intent +staleness advisory, which skips a byte-identical copy outright and so leaves an edited one +exposed once the hub next changes that file substantively. Deleting a unit is caught by the +presence check either way. So the audit is not what decides this. ## GitHub Copilot Review Runbook From 9189ab47ec12146e8b35da0249e6795dad0b6c10 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:18:48 -0700 Subject: [PATCH 19/31] Give the Reason a Local Patch Is Wrong Instead of Enumerating the Audit Six rounds of findings landed on one passage because it tried to compress a tier-dependent and file-dependent mechanism into a carried file that cannot reference the hub-only spec that defines it. Each qualifier fixed one reading and broke another: the advisory count was right for intent and wrong for presence, the presence check catches a deleted Markdown section but not a section declared on a non-Markdown file, and a later downstream commit for any unrelated reason re-suppresses the staleness advisory indefinitely. None of that is why a local patch is wrong. It is wrong because it reaches only this repository, which holds whatever the audit reports. The tier mechanics stay in spec/fidelity-model.md, where a reader can verify them. This is the same correction already applied to GOVERNANCE.md earlier in this branch, for the same reason. --- .github/copilot-instructions.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e152dfb8..c5529544 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,13 +23,8 @@ put a release-bump magnitude in the title. The full contract is in Follow the fidelity declared for the file. A byte-locked reference to shared infrastructure that this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. -An edit to a verbatim unit is reported, as modified content or as stale where it matches an -earlier canonical revision. A content edit to an intent or presence unit is never compared -against the canonical. Three advisories still reach it. Two read undeclared headings and -template references in this file, `AGENTS.md` and `GOVERNANCE.md`. The third is the intent -staleness advisory, which skips a byte-identical copy outright and so leaves an edited one -exposed once the hub next changes that file substantively. Deleting a unit is caught by the -presence check either way. So the audit is not what decides this. +What the audit does or does not detect is not the reason: a local patch reaches only this +repository, whatever any tier reports. ## GitHub Copilot Review Runbook From f0ff674ec0d59bb2c9ec3736ef22d9de205059c8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:21:45 -0700 Subject: [PATCH 20/31] Reduce the Copilot Guidance Change to Deleting the False Qualifier The added sentence carried nothing the sentence before it does not, and it broke twice over. It named an audit and its tiers, terms this file never introduces and whose defining spec is hub-only, so a first-time carrier could bind neither. And it was false in the hub, where this file is carried with no exemption and a local patch to canonical content is the fix rather than a detour around it. The net change to this file is now the deletion the issue actually needed: no fidelity tier rejects a local edit on its own, so the qualifier invited the patch it meant to prevent. --- .github/copilot-instructions.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c5529544..e69b1dea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,8 +23,6 @@ put a release-bump magnitude in the title. The full contract is in Follow the fidelity declared for the file. A byte-locked reference to shared infrastructure that this repository does not carry is intentional, not a broken link. Raise substantive defects in canonical content, but locate the fix at its canonical source instead of proposing a local edit. -What the audit does or does not detect is not the reason: a local patch reaches only this -repository, whatever any tier reports. ## GitHub Copilot Review Runbook From 81da922e4812ea2f9b479441bbdf7199fb15b9af Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:21:56 -0700 Subject: [PATCH 21/31] Record the Canonical Review Ledger for the Copilot Guidance Reduction --- reports/canonical-review.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index fb0a0ffc..cd87aa6c 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -283,11 +283,11 @@ }, { "unit": ".github/copilot-instructions.md > Reviewing Carried Fleet Content", - "digest": "sha256:7ae77c0dfd2a948c68ee9bde900c4c14153407534510f697facf9ce3ffd1805f", + "digest": "sha256:83570e2b15143c6f9b79e4d381b7a578d3ae7970d74ae676405a402f6d13630f", "reviewer": "agent-skill", - "findings": 0, - "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", - "stamp": "2026-09-02T03:09:17Z" + "findings": 2, + "hubCommit": "f0ff674ec0d59bb2c9ec3736ef22d9de205059c8", + "stamp": "2026-09-02T03:21:55Z" }, { "unit": "AGENTS.md > Where the Rules Live", From 211f8c1e6cf6451419175b9c5ef446478bdfde41 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:25:05 -0700 Subject: [PATCH 22/31] Record That the pre-commit Runner Judges the Staged State, Not the Tree The two hook shapes were documented as running the same diff-scoped prose gate at the same scope. They do not. The pre-commit framework stashes unstaged changes before running a hook, so --diff HEAD there compares the staged state against HEAD, where the plain git hook compares the working tree. Measured rather than reasoned: in a throwaway repo whose tracked file was staged as 'staged' and then edited to 'unstaged' in the tree, a local pre-commit hook reading that file saw 'staged'. GOVERNANCE.md asserted the working-tree scope for both shapes, and a repo relying on it under this runner had a partially staged file judged on its staged edits alone. The canonical selection also allows pre-commit both where no Husky.Net manifest exists and where a repo prefers that runner. The rollout doc and both language skills carried only the manifest case. --- .agents/skills/dotnet-codestyle/SKILL.md | 4 ++-- .agents/skills/python-codestyle/SKILL.md | 3 ++- .claude-plugin/fleet-skills/.source-digest | 2 +- .claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md | 4 ++-- .claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md | 3 ++- .github/skills/dotnet-codestyle/SKILL.md | 4 ++-- .github/skills/python-codestyle/SKILL.md | 3 ++- GOVERNANCE.md | 2 +- catalog/snippets/pre-commit/README.md | 5 ++++- docs/pre-commit-hooks-rollout.md | 3 ++- 10 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index b092ae5c..06c8b9a6 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -51,8 +51,8 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring - Husky.Net, which the snippet does not ship, so a repo keeping no such manifest takes the other - canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks + Husky.Net, which the snippet does not ship, so a repo keeping no such manifest, or one that + prefers the other runner, takes the other canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index c2f7ba93..822ada69 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -96,7 +96,8 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical +backstop. A repo that keeps no .NET tool manifest, or one that prefers this runner, wires its +local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 8f9fc23b..8b0f3721 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -3affbc030e65da7a +c58d340e453d934e diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index b092ae5c..06c8b9a6 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -51,8 +51,8 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring - Husky.Net, which the snippet does not ship, so a repo keeping no such manifest takes the other - canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks + Husky.Net, which the snippet does not ship, so a repo keeping no such manifest, or one that + prefers the other runner, takes the other canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index c2f7ba93..822ada69 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -96,7 +96,8 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical +backstop. A repo that keeps no .NET tool manifest, or one that prefers this runner, wires its +local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index b092ae5c..06c8b9a6 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -51,8 +51,8 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring - Husky.Net, which the snippet does not ship, so a repo keeping no such manifest takes the other - canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks + Husky.Net, which the snippet does not ship, so a repo keeping no such manifest, or one that + prefers the other runner, takes the other canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index c2f7ba93..822ada69 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -96,7 +96,8 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest wires its local hook from the canonical +backstop. A repo that keeps no .NET tool manifest, or one that prefers this runner, wires its +local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 0c0add8c..de286e7f 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -245,7 +245,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, 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. Neither snippet is scoped to a language by what it runs. Each carries the repo's own language checks plus the two 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, 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 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 stashes unstaged changes before running a hook, which is measured rather than assumed. CI re-checks the whole tree regardless, which is what makes the scope affordable in a hook either way. 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 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`. 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` and `scripts/canonical_review.py check`, refusing a branch push whose diff no recorded review pass covers and one whose changed canonical units none covers, per "Verification Discipline" above. Both run before either verdict is read, so a blocked push names every reason it was refused rather than only the first, which would otherwise cost a second push to discover the next. 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 run the carried-content pass and record it, commit that together with the change, then review the diff, record its receipt, and push. What the two gates actually require is narrower than that sequence, since the canonical ledger and burn-down are tracked and need only be committed before the push, while the diff receipt is not tracked and must be recorded after the last commit. The sequence above is the shortest one meeting both, which is why every capture point's wording assumes it. 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 hub-hosted too and is not reached that way at all: it reads `spec/files.json` to learn what is carried, `spec/` is hub-hosted rather than carried, and run against a downstream worktree it exits 2 saying that tree describes no carried set. It runs in the repository that authors the content, which is this one. The canonical-unit half is also run by the hub's own `.github/actions/validate` composite action, as a step on every pull request, which is where it actually binds, since a hook a push can bypass raises the cost of skipping the rule without ever settling it. That step is scoped to a pull request because a unit's change is measured against the branch it is proposed into, and a push carrying no pull request names none. - **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. diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index 566d808e..e51bc6c7 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -12,7 +12,10 @@ invocation for the lint-only profile (`CODESTYLE.md` "Two profiles"). A repo on 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. Those two -are what every repo owes regardless of language. A repo with no Python drops the `ruff-format`, +are what every repo owes regardless of language. The prose gate's `--diff HEAD` sees the staged +state under this runner rather than the working tree, since `pre-commit` stashes unstaged changes +before it runs a hook, so a partially staged file is judged on its staged edits alone here where +the Husky.Net shape judges all of them. CI re-checks the whole tree either way. A repo with no Python drops the `ruff-format`, `ruff-check` and `type-check` hooks and keeps the doc gates, which is what a Docker, config, or docs repo wires. diff --git a/docs/pre-commit-hooks-rollout.md b/docs/pre-commit-hooks-rollout.md index 748d3fae..eb1e4e3f 100644 --- a/docs/pre-commit-hooks-rollout.md +++ b/docs/pre-commit-hooks-rollout.md @@ -26,7 +26,8 @@ doc-gate half wired and the language-format half not yet because its own corpus clean, stays operational, per the existing carve-out in GOVERNANCE.md. Two catalog snippets carry the canonical shape: Husky.Net (`catalog/snippets/husky/`, for a repo already keeping a .NET tool manifest declaring it) and the -`pre-commit` framework (`catalog/snippets/pre-commit/`, for a repo without one). Both now carry the +`pre-commit` framework (`catalog/snippets/pre-commit/`, for a repo without one and for any repo +that prefers it). Both now carry the same shared doc gates, the diff-scoped prose/comment-style gate and the whole-tree line-ending check, fetched fresh from the hub's `main` branch at run time via `hub-fetch-run.py` rather than vendored or pinned. From 38c408cdee70579453d43a3b3442f82bc2fcfcc2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:29:10 -0700 Subject: [PATCH 23/31] Name the Alternative Runner and Keep the Two Selection Reasons Coordinate The .NET skill welded the preference case to the missing-manifest premise with a 'so', where the canonical keeps them as two reasons rather than one consequence, and it called the alternative 'the other runner' in a unit that names no runner but Husky.Net. Its Tooling bullet still called Husky.Net the .NET mechanism, an exclusive claim the same file's selection text no longer makes. The Python skill said 'this runner' where nothing before it names a hook runner, so the proximal binding was the uv toolchain. It also said the runner follows the toolchain, which determines the choice and contradicts the preference case the same paragraph had just added, where the canonical says bounded by. That skill also lacked the clause its .NET counterpart carries, so a repo with both languages acting on the new preference case wired a config shipping no .NET hooks and lost those checks locally with nothing saying to add them back. --- .agents/skills/dotnet-codestyle/SKILL.md | 8 ++++---- .agents/skills/python-codestyle/SKILL.md | 17 +++++++++-------- .claude-plugin/fleet-skills/.source-digest | 2 +- .../skills/dotnet-codestyle/SKILL.md | 8 ++++---- .../skills/python-codestyle/SKILL.md | 17 +++++++++-------- .github/skills/dotnet-codestyle/SKILL.md | 8 ++++---- .github/skills/python-codestyle/SKILL.md | 17 +++++++++-------- 7 files changed, 40 insertions(+), 37 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 06c8b9a6..01a6e28e 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -51,9 +51,9 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring - Husky.Net, which the snippet does not ship, so a repo keeping no such manifest, or one that - prefers the other runner, takes the other canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks - its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, + Husky.Net, which the snippet does not ship. A repo keeping no such manifest takes the other + canonical shape, `catalog/snippets/pre-commit/`, and so does a repo that simply prefers the + `pre-commit` framework. Each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the two the audit reads. @@ -103,7 +103,7 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. The .NET mechanism for a local pre-commit hook is +- CI is the authoritative lint backstop. A local pre-commit hook running .NET checks is usually Husky.Net, wired from `catalog/snippets/husky/` in the hub, hub-local and not carried into every fleet repo, and covering the shared doc gates alongside the language checks. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 822ada69..26b8e4cf 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -96,14 +96,15 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest, or one that prefers this runner, wires its -local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any -repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with -`core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The -runner follows the toolchain the repo already keeps rather than the languages the hook checks, -so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet -instead. The `pre-commit` directory's own README names the second file to copy alongside the +backstop. A repo that keeps no .NET tool manifest, or one that prefers the `pre-commit` +framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, +hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of +its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and +`.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the +repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net +manifest may run these same Python checks from that snippet instead. Each shape carries whichever +language checks its own repo keeps, so a repo with .NET as well adds that block rather than +losing it. The `pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 8b0f3721..7ee3023e 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -c58d340e453d934e +bd6a9bf4d7e4e707 diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 06c8b9a6..01a6e28e 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -51,9 +51,9 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring - Husky.Net, which the snippet does not ship, so a repo keeping no such manifest, or one that - prefers the other runner, takes the other canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks - its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, + Husky.Net, which the snippet does not ship. A repo keeping no such manifest takes the other + canonical shape, `catalog/snippets/pre-commit/`, and so does a repo that simply prefers the + `pre-commit` framework. Each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the two the audit reads. @@ -103,7 +103,7 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. The .NET mechanism for a local pre-commit hook is +- CI is the authoritative lint backstop. A local pre-commit hook running .NET checks is usually Husky.Net, wired from `catalog/snippets/husky/` in the hub, hub-local and not carried into every fleet repo, and covering the shared doc gates alongside the language checks. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 822ada69..26b8e4cf 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -96,14 +96,15 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest, or one that prefers this runner, wires its -local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any -repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with -`core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The -runner follows the toolchain the repo already keeps rather than the languages the hook checks, -so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet -instead. The `pre-commit` directory's own README names the second file to copy alongside the +backstop. A repo that keeps no .NET tool manifest, or one that prefers the `pre-commit` +framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, +hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of +its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and +`.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the +repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net +manifest may run these same Python checks from that snippet instead. Each shape carries whichever +language checks its own repo keeps, so a repo with .NET as well adds that block rather than +losing it. The `pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 06c8b9a6..01a6e28e 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -51,9 +51,9 @@ All builds must complete without warnings, enforced three ways: - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. Husky.Net is wired from the canonical `catalog/snippets/husky/` config in the hub, hub-local and not carried into every fleet repo. Its hook needs a .NET tool manifest declaring - Husky.Net, which the snippet does not ship, so a repo keeping no such manifest, or one that - prefers the other runner, takes the other canonical shape, `catalog/snippets/pre-commit/`. Each shape carries whichever language checks - its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, + Husky.Net, which the snippet does not ship. A repo keeping no such manifest takes the other + canonical shape, `catalog/snippets/pre-commit/`, and so does a repo that simply prefers the + `pre-commit` framework. Each shape carries whichever language checks its own repo keeps. A repo may also wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing, since the husky snippet's own hook sources a file only `dotnet husky install` generates. That path and `.pre-commit-config.yaml` are the two the audit reads. @@ -103,7 +103,7 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. The .NET mechanism for a local pre-commit hook is +- CI is the authoritative lint backstop. A local pre-commit hook running .NET checks is usually Husky.Net, wired from `catalog/snippets/husky/` in the hub, hub-local and not carried into every fleet repo, and covering the shared doc gates alongside the language checks. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 822ada69..26b8e4cf 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -96,14 +96,15 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest, or one that prefers this runner, wires its -local hook from the canonical -`catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any -repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with -`core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The -runner follows the toolchain the repo already keeps rather than the languages the hook checks, -so a repo keeping a Husky.Net manifest may run these same Python checks from that snippet -instead. The `pre-commit` directory's own README names the second file to copy alongside the +backstop. A repo that keeps no .NET tool manifest, or one that prefers the `pre-commit` +framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, +hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of +its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and +`.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the +repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net +manifest may run these same Python checks from that snippet instead. Each shape carries whichever +language checks its own repo keeps, so a repo with .NET as well adds that block rather than +losing it. The `pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement From a2c9dab3bfde574d179b0765f9bb50f413ce7a80 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:32:53 -0700 Subject: [PATCH 24/31] State the Selection Condition Instead of Hedging It The .NET skill's Tooling bullet replaced an exclusive claim with 'usually', which states no rule and reached Husky.Net by the languages the hook checks, the one discriminator the canonical selection forbids. It now gives the condition, the manifest the repo already keeps, and names the shape for a repo keeping none. The Python skill referred to 'that snippet' and 'that block' where the unit names neither the Husky.Net snippet nor its dotnet husky run block, so a carrier could not resolve what to copy. Both are named. --- .agents/skills/dotnet-codestyle/SKILL.md | 8 +++++--- .agents/skills/python-codestyle/SKILL.md | 6 +++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/dotnet-codestyle/SKILL.md | 8 +++++--- .../fleet-skills/skills/python-codestyle/SKILL.md | 6 +++--- .github/skills/dotnet-codestyle/SKILL.md | 8 +++++--- .github/skills/python-codestyle/SKILL.md | 6 +++--- 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index 01a6e28e..db41583e 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -103,9 +103,11 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. A local pre-commit hook running .NET checks is usually - Husky.Net, wired from `catalog/snippets/husky/` in the hub, hub-local and not carried into every - fleet repo, and covering the shared doc gates alongside the language checks. +- CI is the authoritative lint backstop. A repo already keeping a .NET tool manifest declaring + Husky.Net wires its local pre-commit hook from `catalog/snippets/husky/` in the hub, hub-local + and not carried into every fleet repo, covering the shared doc gates alongside the language + checks. A repo keeping none, or one preferring that runner, takes `catalog/snippets/pre-commit/` + instead. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 26b8e4cf..2e5d40de 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -102,9 +102,9 @@ hub-local and not carried into every fleet repo. Any repo may instead wire an eq its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net -manifest may run these same Python checks from that snippet instead. Each shape carries whichever -language checks its own repo keeps, so a repo with .NET as well adds that block rather than -losing it. The `pre-commit` directory's own README names the second file to copy alongside the +manifest may run these same Python checks from the `catalog/snippets/husky/` shape instead. Each +shape carries whichever language checks its own repo keeps, so a repo with .NET as well adds its +`dotnet husky run` block there rather than losing those checks. The `pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 7ee3023e..c9a3ca6f 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -bd6a9bf4d7e4e707 +467d4dd5fafb380f diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index 01a6e28e..db41583e 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -103,9 +103,11 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. A local pre-commit hook running .NET checks is usually - Husky.Net, wired from `catalog/snippets/husky/` in the hub, hub-local and not carried into every - fleet repo, and covering the shared doc gates alongside the language checks. +- CI is the authoritative lint backstop. A repo already keeping a .NET tool manifest declaring + Husky.Net wires its local pre-commit hook from `catalog/snippets/husky/` in the hub, hub-local + and not carried into every fleet repo, covering the shared doc gates alongside the language + checks. A repo keeping none, or one preferring that runner, takes `catalog/snippets/pre-commit/` + instead. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 26b8e4cf..2e5d40de 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -102,9 +102,9 @@ hub-local and not carried into every fleet repo. Any repo may instead wire an eq its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net -manifest may run these same Python checks from that snippet instead. Each shape carries whichever -language checks its own repo keeps, so a repo with .NET as well adds that block rather than -losing it. The `pre-commit` directory's own README names the second file to copy alongside the +manifest may run these same Python checks from the `catalog/snippets/husky/` shape instead. Each +shape carries whichever language checks its own repo keeps, so a repo with .NET as well adds its +`dotnet husky run` block there rather than losing those checks. The `pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index 01a6e28e..db41583e 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -103,9 +103,11 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. A local pre-commit hook running .NET checks is usually - Husky.Net, wired from `catalog/snippets/husky/` in the hub, hub-local and not carried into every - fleet repo, and covering the shared doc gates alongside the language checks. +- CI is the authoritative lint backstop. A repo already keeping a .NET tool manifest declaring + Husky.Net wires its local pre-commit hook from `catalog/snippets/husky/` in the hub, hub-local + and not carried into every fleet repo, covering the shared doc gates alongside the language + checks. A repo keeping none, or one preferring that runner, takes `catalog/snippets/pre-commit/` + instead. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 26b8e4cf..2e5d40de 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -102,9 +102,9 @@ hub-local and not carried into every fleet repo. Any repo may instead wire an eq its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net -manifest may run these same Python checks from that snippet instead. Each shape carries whichever -language checks its own repo keeps, so a repo with .NET as well adds that block rather than -losing it. The `pre-commit` directory's own README names the second file to copy alongside the +manifest may run these same Python checks from the `catalog/snippets/husky/` shape instead. Each +shape carries whichever language checks its own repo keeps, so a repo with .NET as well adds its +`dotnet husky run` block there rather than losing those checks. The `pre-commit` directory's own README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement From 863320e324180b34f2f0bdac5eb4a9f65747c9ac Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:33:05 -0700 Subject: [PATCH 25/31] Record the Canonical Review Ledger for the Skill Selection Rounds --- reports/canonical-review.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index cd87aa6c..4365d987 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -35,19 +35,19 @@ }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Build requirements", - "digest": "sha256:b20de4ba05724bfa3bb598deec592789320e9119b44cca3452793a2526e8c23f", + "digest": "sha256:96e6651c2e024fae3bca713f4a8cab34b37a9bb9eb693a3e25de04465b03113c", "reviewer": "agent-skill", "findings": 0, - "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", - "stamp": "2026-09-02T03:09:16Z" + "hubCommit": "a2c9dab3bfde574d179b0765f9bb50f413ce7a80", + "stamp": "2026-09-02T03:33:05Z" }, { "unit": ".agents/skills/dotnet-codestyle/SKILL.md > Tooling and editor", - "digest": "sha256:d00e3da0fde14b915ee959bcab0edb68d21cd0b22d954054058bfea21e7473f5", + "digest": "sha256:bee41f58a11d3ee23789d5839a26798dba19c89d1e500ec468c918f55a41cf40", "reviewer": "agent-skill", - "findings": 0, - "hubCommit": "a76dda7d67033f43c260d21e3ae6a291e1869c0a", - "stamp": "2026-09-01T06:46:39Z" + "findings": 1, + "hubCommit": "a2c9dab3bfde574d179b0765f9bb50f413ce7a80", + "stamp": "2026-09-02T03:33:05Z" }, { "unit": ".agents/skills/drive-pr/SKILL.md > Disposing of Every Finding", @@ -171,11 +171,11 @@ }, { "unit": ".agents/skills/python-codestyle/SKILL.md > Local development loop", - "digest": "sha256:1721ef040c7be6f3c0651cd06fe6ba2b03336f284b30886f53f6a0e9f4a9d103", + "digest": "sha256:48e3ad53d1dc296d0480241d0bc39de3a98577268b47506518f0100d3b6efc64", "reviewer": "agent-skill", "findings": 1, - "hubCommit": "112ffd874eaec3784a747b341fbdd0b19d75502c", - "stamp": "2026-09-02T03:09:17Z" + "hubCommit": "a2c9dab3bfde574d179b0765f9bb50f413ce7a80", + "stamp": "2026-09-02T03:33:05Z" }, { "unit": ".agents/skills/repo-worktree/SKILL.md > Creating a Worktree", From d0e2b62e03371955fcb6c8714bdad5b0a11474d5 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:34:16 -0700 Subject: [PATCH 26/31] Reflow the pre-commit Snippet's Opening Paragraph The applicability sentence ran on across a missing comma and an unwrapped edit seam left by an earlier round. --- catalog/snippets/pre-commit/README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index e51bc6c7..ec134781 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -1,13 +1,12 @@ # Pre-commit snippet `.pre-commit-config.yaml` is the reference config for the `pre-commit` framework -(pre-commit.com), for a repo that keeps no .NET tool manifest, and for any repo that prefers it to -Husky.Net. The toolchain the repo already keeps is the -discriminator, not the repo's own languages: Husky.Net runs from a .NET tool manifest declaring -it, and this framework needs none. In a repo with Python it runs `ruff format --check` and `ruff check`, plus that -repo's declared type checker, `pyright` or `mypy`, matching whichever `python-codestyle` says -its CI runs. Each tool runs via -`uvx`, native tooling, never Docker. `uvx` needs no project dependency, matching CI's own +(pre-commit.com), for a repo that keeps no .NET tool manifest, and for any repo that prefers it +to Husky.Net. The toolchain the repo already keeps is the discriminator, not the repo's own +languages: Husky.Net runs from a .NET tool manifest declaring it, and this framework needs none. +In a repo with Python, it runs `ruff format --check` and `ruff check`. It also runs that repo's +declared type checker, `pyright` or `mypy`, matching whichever `python-codestyle` says its 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 From 082fcbb761c91e4900cb0582a041dec1d66991d0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:40:41 -0700 Subject: [PATCH 27/31] Say the Required Crossing Secret Rather Than the One Crossing Secret The stub maps DEPLOY_SSH_PRIVATE_KEY alone, which is correct, but the comment describing it read as though the task declares only that secret. It declares an optional SITE_AUTH_TOKEN_ID/SITE_AUTH_TOKEN pair as well, which a caller maps where its live check is token-gated. --- spec/audit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/audit.py b/spec/audit.py index 0bbe589c..448bf748 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -2981,7 +2981,8 @@ def _selftest(): 1, ), ] - # The deploy-site.yml caller stub once deploy-site-task.yml is hub-hosted: no secrets: inherit, the one crossing secret named instead. + # The deploy-site.yml caller stub once deploy-site-task.yml is hub-hosted: no secrets: inherit, the required crossing secret named instead. + # The task also declares an optional SITE_AUTH_TOKEN_ID/SITE_AUTH_TOKEN pair, which a caller maps only where its live check needs one. # inherit is documented for same-org or enterprise callers, so the fleet does not use it across repositories. # No job-level environment: on the caller, unsupported on a job with uses: (ptr727/ProjectTemplate#942). deploy_stub = ( From b3c0c0ce4d0a4f142ba01f265cc1c5966c089fe6 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:47:38 -0700 Subject: [PATCH 28/31] Scope the Manifest Condition to One Declaring Husky.Net A repo can keep a .NET tool manifest for another dotnet tool and still need the pre-commit shape, so excluding every repo with any manifest was wrong. The condition is a manifest declaring Husky.Net, which is what dotnet tool restore reads before dotnet husky install can run. GOVERNANCE.md, CODESTYLE.md, catalog/README.md, the rollout doc, the husky README and the .NET skill already said declaring it. The pre-commit README, its config header and the Python skill did not. --- .agents/skills/python-codestyle/SKILL.md | 4 ++-- .claude-plugin/fleet-skills/.source-digest | 2 +- .../fleet-skills/skills/python-codestyle/SKILL.md | 4 ++-- .github/skills/python-codestyle/SKILL.md | 4 ++-- catalog/snippets/pre-commit/.pre-commit-config.yaml | 2 +- catalog/snippets/pre-commit/README.md | 7 ++++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 2e5d40de..51c91f02 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -96,8 +96,8 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest, or one that prefers the `pre-commit` -framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, +backstop. A repo that keeps no .NET tool manifest declaring Husky.Net, or one that prefers the +`pre-commit` framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index c9a3ca6f..c32d6831 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -467d4dd5fafb380f +a285505ce432ee6c diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 2e5d40de..51c91f02 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -96,8 +96,8 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest, or one that prefers the `pre-commit` -framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, +backstop. A repo that keeps no .NET tool manifest declaring Husky.Net, or one that prefers the +`pre-commit` framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 2e5d40de..51c91f02 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -96,8 +96,8 @@ those. These are documented commands, and the hub's `vscode-tasks-python.json` s VS Code tasks mirror that the fleet baseline expects. Every command-executing task in it is `type: process`, and every aggregator is `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs the same clean-compile commands as the authoritative -backstop. A repo that keeps no .NET tool manifest, or one that prefers the `pre-commit` -framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, +backstop. A repo that keeps no .NET tool manifest declaring Husky.Net, or one that prefers the +`pre-commit` framework, wires its local hook from the canonical `catalog/snippets/pre-commit/` directory, hub-local and not carried into every fleet repo. Any repo may instead wire an equivalent hook of its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothing. That path and `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml index 40524e9a..9fbda874 100644 --- a/catalog/snippets/pre-commit/.pre-commit-config.yaml +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Reference pre-commit (pre-commit.com) config for a fleet repo that keeps no .NET tool manifest, or one that prefers this runner. +# Reference pre-commit (pre-commit.com) config for a fleet repo that keeps no .NET tool manifest declaring Husky.Net, or one that prefers this runner. # Husky.Net is a dotnet tool, so the discriminator is the toolchain rather than the repo's languages. # Drop the ruff and type-check hooks in a repo with no Python, keeping the two doc gates every repo owes. # Copy into the repo root alongside ../hub-fetch-run.py. diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md index ec134781..6241c0bf 100644 --- a/catalog/snippets/pre-commit/README.md +++ b/catalog/snippets/pre-commit/README.md @@ -1,9 +1,10 @@ # Pre-commit snippet `.pre-commit-config.yaml` is the reference config for the `pre-commit` framework -(pre-commit.com), for a repo that keeps no .NET tool manifest, and for any repo that prefers it -to Husky.Net. The toolchain the repo already keeps is the discriminator, not the repo's own -languages: Husky.Net runs from a .NET tool manifest declaring it, and this framework needs none. +(pre-commit.com), for a repo that keeps no .NET tool manifest declaring Husky.Net, and for any +repo that prefers it to Husky.Net. The toolchain the repo already keeps is the discriminator, +not the repo's own languages: Husky.Net runs from a .NET tool manifest declaring it, and this +framework needs none. In a repo with Python, it runs `ruff format --check` and `ruff check`. It also runs that repo's declared type checker, `pyright` or `mypy`, matching whichever `python-codestyle` says its CI runs. Each tool runs via `uvx`, native tooling, never Docker. `uvx` needs no project dependency, matching CI's own From 5eb14540421d630293807288845284a78f8ecc7d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:51:02 -0700 Subject: [PATCH 29/31] Drop the .NET Example the Sharpened Manifest Condition Falsifies Once having .NET and keeping a Husky.Net manifest are separable, a .NET repo without one is routed to the pre-commit shape, where it cannot add a dotnet husky run block, since dotnet husky is resolved from the manifest it lacks. Read the other way the example is wrong too, because the husky snippet ships that block already and its README says to drop it rather than add it. GOVERNANCE.md and the .NET skill both state the general rule without the example, which is the wording that survives the sharpened condition. --- .agents/skills/python-codestyle/SKILL.md | 5 ++--- .claude-plugin/fleet-skills/.source-digest | 2 +- .claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md | 5 ++--- .github/skills/python-codestyle/SKILL.md | 5 ++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 51c91f02..160bf0bf 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -103,9 +103,8 @@ its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothi `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net manifest may run these same Python checks from the `catalog/snippets/husky/` shape instead. Each -shape carries whichever language checks its own repo keeps, so a repo with .NET as well adds its -`dotnet husky run` block there rather than losing those checks. The `pre-commit` directory's own README names the second file to copy alongside the -config. +shape carries whichever language checks its own repo keeps. The `pre-commit` directory's own +README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index c32d6831..c2108a0c 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -a285505ce432ee6c +c7efdbdc80111e8f diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 51c91f02..160bf0bf 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -103,9 +103,8 @@ its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothi `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net manifest may run these same Python checks from the `catalog/snippets/husky/` shape instead. Each -shape carries whichever language checks its own repo keeps, so a repo with .NET as well adds its -`dotnet husky run` block there rather than losing those checks. The `pre-commit` directory's own README names the second file to copy alongside the -config. +shape carries whichever language checks its own repo keeps. The `pre-commit` directory's own +README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 51c91f02..160bf0bf 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -103,9 +103,8 @@ its own at `.husky/pre-commit`, enabled with `core.hooksPath` and sourcing nothi `.pre-commit-config.yaml` are the two the audit reads. The runner is bounded by the toolchain the repo already keeps rather than by the languages the hook checks, so a repo keeping a Husky.Net manifest may run these same Python checks from the `catalog/snippets/husky/` shape instead. Each -shape carries whichever language checks its own repo keeps, so a repo with .NET as well adds its -`dotnet husky run` block there rather than losing those checks. The `pre-commit` directory's own README names the second file to copy alongside the -config. +shape carries whichever language checks its own repo keeps. The `pre-commit` directory's own +README names the second file to copy alongside the config. GOVERNANCE.md's hub-only "Running the Linters Locally (Known-Working Invocations)" section carries the obligation itself, what the hook must cover, its audit treatment, and the per-clone enablement steps. From 39fb83f8acc2e08ad580b55ce12d81acd40f225a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:51:13 -0700 Subject: [PATCH 30/31] Record the Canonical Review Ledger for the Manifest Scoping Round --- reports/canonical-review.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reports/canonical-review.json b/reports/canonical-review.json index 4365d987..68d04040 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -171,11 +171,11 @@ }, { "unit": ".agents/skills/python-codestyle/SKILL.md > Local development loop", - "digest": "sha256:48e3ad53d1dc296d0480241d0bc39de3a98577268b47506518f0100d3b6efc64", + "digest": "sha256:64911d87dea3c5c46bbe2abccab0bed590ee65a3e93e9083b403ceee77bde450", "reviewer": "agent-skill", "findings": 1, - "hubCommit": "a2c9dab3bfde574d179b0765f9bb50f413ce7a80", - "stamp": "2026-09-02T03:33:05Z" + "hubCommit": "5eb14540421d630293807288845284a78f8ecc7d", + "stamp": "2026-09-02T03:51:13Z" }, { "unit": ".agents/skills/repo-worktree/SKILL.md > Creating a Worktree", From aeaccc7c1c420c809bd9f69a3e12b940fa824b7b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 1 Sep 2026 20:55:46 -0700 Subject: [PATCH 31/31] Name the Normalization Rather Than Point Above at It The Normalization section sits below What a Local Edit Looks Like, so the directional reference pointed the wrong way. Naming the line-ending, action-pin and job-needs normalization directly makes the bullet self-contained and stops it depending on section order. --- spec/fidelity-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 23c2173f..7407c733 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -40,7 +40,7 @@ The unit decides, never the file. `GOVERNANCE.md` is `intent` as a file while 18 - **verbatim and verbatim-tree** - detected. The content hash is compared against the canonical and every past revision, and "Stale Versus Modified" below reads the result. An edit matching no revision reports as modified fixed content, and one restoring an earlier revision reports as stale, since the hash cannot say how the bytes got there. A `verbatim-tree` declaration also fails structurally, on a missing included path or an extra one under a pruned root, before any hash is read. - **interface** - detected only where the edit moves the contract. A changed job key, check name, or hub task token is a finding. An edit to the owned body is the override this tier exists for. -- **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. After the hub's next substantive change to that file, meaning one the normalization above does not explain away, the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself. +- **intent** - not detected. Content is never judged, and the staleness advisory never identifies a local edit. It compares last-change dates, so the edit first suppresses the advisory by making the copy newer. After the hub's next substantive change to that file, meaning one the line-ending, action-pin, and job-needs normalization does not explain away, the advisory fires and reports the copy as trailing, which routes to a re-vendor that overwrites the edit rather than to the edit itself. - **presence** - a content edit is not detected. The unit is asserted to exist and is never read, so deleting it is still a finding and editing it is not. So a local rule edit to `CODESTYLE.md` or `WORKFLOW.md`, both `intent` and whole, is reported nowhere. That silence is intended for a repo's own additions, which `CODESTYLE.md` is a declared destination for, and it is the hazard for a patch of carried rule text, since the two read alike to every check. Two checks reach some non-verbatim prose, and neither reaches those two files. [`section-model.md`][section-model]'s undeclared-section advisory names a heading the hub does not declare, and it runs only against `AGENTS.md`, `GOVERNANCE.md` and `.github/copilot-instructions.md`. It reads headings rather than rule text, so a rule rewritten under a declared heading stays invisible. [`audit.py`][audit]'s `template_ref_outside_verbatim` reads the prose itself, and it catches only a coordination reference rather than a changed rule.