diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 3ad8012a..af1a44b3 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -31,7 +31,7 @@ A state-changing GitHub call is the highest-blast-radius thing an agent does her - **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound: it covers the commits needed for that specific task, not a blanket commit license for the rest of the session. - **Check the working tree for the maintainer's own uncommitted edits before committing.** The maintainer hand-edits files live (often `README.md`/`HISTORY.md`, sometimes with the editor's LF->CRLF flip on top). Review `git status` first. If there are changes you did not make, ask whether to include them rather than bundling half-finished work or stranding it in an unrelated commit. - **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches, and unsigned commits are rejected on push. Signing depends on environment configuration: `git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent (loaded `ssh-agent` for SSH, or `gpg-agent` for GPG). If signing is not configured in the environment, **do not commit**. Surface the missing config to the developer and stop at `git add`. Verify before any agent-authored commit (`git config --get commit.gpgsign && ssh-add -L` or the GPG equivalent). **Signing must be live before the *first* commit, not retrofitted.** Turning on `Require signed commits` against a branch that already has unsigned commits forces a rewrite of that entire history to re-sign it, changing every commit SHA and making whoever does the rewrite the committer and signer of every commit (a rebase preserves the `author` field but not the original signatures, and you cannot sign another contributor's commits for them). During new-repo setup, never create commits until signing is verified. -- **Commit under the committing account's own GitHub `noreply` identity, never a private, personal, or invented address.** The `author` and `committer` on every agent-authored commit are the GitHub `noreply` address of the account whose key signs the commit (above). GitHub issues these in a `username@users.noreply.github.com` or `ID+username@users.noreply.github.com` form, and for this single-maintainer fleet it is the owner's `ptr727@users.noreply.github.com`. Do not set `user.name`/`user.email` to a fabricated persona, bot name, or product name, and do not commit under whatever identity the environment happens to carry: verify `git config --get user.email` is that GitHub `noreply` address before committing, and fix it if not. A wrong identity is not cosmetic: a private email trips GitHub's email-privacy push protection (GH007), and an unrecognized or invented author pollutes history. Identity is separate from signing: a wrong author does not by itself fail the signature rule, but the ad-hoc identities that produce it are typically also unsigned, which the signing rule above then rejects on push. +- **Commit under the committing account's own GitHub `noreply` identity, never a private, personal, or invented address.** The `author` and `committer` on every agent-authored commit are the GitHub `noreply` address of the account whose key signs the commit (above). GitHub issues these in a `username@users.noreply.github.com` or `ID+username@users.noreply.github.com` form, and for this single-maintainer fleet it is the owner's `ptr727@users.noreply.github.com`. Do not set `user.name`/`user.email` to a fabricated persona, bot name, or product name, and do not commit under whatever identity the environment happens to carry: verify `git config --get user.email` is that GitHub `noreply` address before committing. **Verify it, do not set it.** The identity is host configuration, set globally once, so a repo-local `user.email` is redundant where the global is right and a wrong identity where it is not, and it silently shadows the global it overrides. A mismatch is a host fault to surface to the maintainer rather than to patch per repo, because a local override hides a broken host that then commits under the wrong identity in every other repo on that machine. A wrong identity is not cosmetic: a private email trips GitHub's email-privacy push protection (GH007), and an unrecognized or invented author pollutes history. Identity is separate from signing: a wrong author does not by itself fail the signature rule, but the ad-hoc identities that produce it are typically also unsigned, which the signing rule above then rejects on push. - **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. - **A history rewrite includes only the commits that must change, and re-identifies any commit it rewrites that is not yours.** Filtering history (`git filter-repo` / `filter-branch`, e.g. to strip PII) rewrites the touched commits and you re-sign them with your key, while the tooling preserves each commit's original `author` and `committer` unless told otherwise. GitHub verifies a signature against the commit's `committer` identity, so a signature from your key over a commit still committed by a bot (`dependabot[bot]`, `github-actions[bot]`) or by GitHub's web-flow does not match its committer and is marked `unknown_key`/unverified, which a require-signed-commits ruleset then rejects. Two gates keep committer and signature aligned. **First, scope the rewrite to only the commits that must be modified.** By default those are your own, whose committer is already your identity, and a commit that does not need changing is kept out of the rewrite so its identity and signature are never touched. **Second, if a commit that must change is not yours, set its `committer` to the signing identity before re-signing** (and its `author` too, since a rewrite that alters the content should not keep attributing it to the bot), so the committer GitHub verifies matches your key. The original bot attribution is deliberately given up as the cost of having to rewrite it. Never leave your signature over a commit committed by another identity. Verify after the rewrite that every rewritten commit is signed and committed under your identity (`git log --show-signature`). - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. diff --git a/STANDUP.md b/STANDUP.md index a0c7e9e6..5602d1fa 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -1,46 +1,101 @@ # STANDUP.md -How an agent takes a repository from nothing (or a partial state) to **operational** against the fleet ground truth. This is the create-to-conformance procedure; [`AUDIT.md`][audit] is its read-only verifier and owns the definition of done. Both read the same ground truth - [`registry/repos.json`][repos], the [`spec/`][spec] manifests, [`repo-config/`][repo-config], and the prose authorities ([`GOVERNANCE.md`][governance], [`CODESTYLE.md`][codestyle], [`WORKFLOW.md`][workflow]) - so a repo stood up by this file passes the audit by construction. +How an agent takes a repository from nothing (or a partial state) to **operational** against the fleet ground truth. This is the create-to-conformance procedure, and [`AUDIT.md`][audit] is its read-only verifier and owns the definition of done. Both read the same ground truth ([`registry/repos.json`][repos], the [`spec/`][spec] manifests, [`repo-config/`][repo-config], and the prose authorities [`GOVERNANCE.md`][governance], [`CODESTYLE.md`][codestyle] and [`WORKFLOW.md`][workflow]), so a repo stood up by this file passes the audit by construction. -Standing up a repo is **applying the manifests until the audit passes**, nothing more invented. If a repo needs a construct no manifest covers, that is a spec gap: raise it ([`AUDIT.md`][audit] section 9), never improvise a per-repo answer. This is the downward-audit model - standard-style repos the hub audits against their declared type - which the fleet uses because managing downstream divergence is too costly. +Standing up a repo is **applying the manifests until the audit passes**, nothing more invented. If a repo needs a construct no manifest covers, that is a spec gap: raise it ([`AUDIT.md`][audit] section 9), never improvise a per-repo answer. This is the downward-audit model (standard-style repos the hub audits against their declared type), which the fleet uses because managing downstream divergence is too costly. + +## 0. Verify Commit Identity and Signing, Before the First Commit + +Do this before `git init` or any commit, because the window closes at the first one. A repo whose initial history is unsigned or committed under the wrong identity cannot be cleanly repaired: `Require signed commits` blocks the first `develop -> main` release, re-signing that history is a non-fast-forward the `Block force pushes` rule rejects, and completing it needs the ruleset temporarily disabled plus a maintainer force-push that [`docs/repo-config-carry.md`][repo-config-carry] forbids an agent to perform. Greenfield repos where signing is live before the first commit never hit this. + +**Verify the inherited configuration. Never set it.** The host already carries the correct identity, so a repo-local `user.email` is redundant at best and a wrong identity at worst, and it silently shadows the global it overrides. Read the **`--global`** scope explicitly, and run these before there is a repo: + +```shell +git config --global --get user.email # the GitHub noreply address, per GOVERNANCE.md "Git and Commit Rules" +git config --global --get commit.gpgsign # true +git config --global --get user.signingkey # set +git config --global --get gpg.format # ssh for an SSH key; unset or openpgp for GPG + +# the agent holding the key, selected by the format above +if [ "$(git config --global --get gpg.format)" = ssh ]; then ssh-add -L; else gpg --list-secret-keys; fi +``` + +`--global` rather than the effective config, because the effective value depends on where the command runs: inside any existing repository a repo-local override wins, so a bare `git config --get user.email` there reports that repository's identity and hides the host setting this step exists to check. The two scopes together are what make the result sound, since this block proves the host is right and the block below proves nothing shadows it. + +The agent check branches rather than listing both forms, because they are alternatives and running the wrong one fails on a correctly configured host: an SSH host need not have `gpg` installed at all. Signing is **SSH or GPG**, so judge the format and its agent together rather than requiring `ssh`: what matters is that the configured format has a matching agent holding the key, which is the check [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules] prescribes. Any of these wrong or absent is a **host** misconfiguration to surface to the maintainer ([`docs/host-setup.md`][host-setup] is the setup procedure), not something to patch per repo. Patching it locally hides a broken host that then produces wrong identities in every other repo on that machine. + +After `git init` and before the first commit, confirm the repo added no override of its own. This one needs a repository, since `--local` fails outside one: + +```shell +git config --local --get user.email || true # expect no output +``` + +**The finding is a printed value, never the exit code.** An unset key prints nothing and exits `1`, so the passing case is a non-zero exit with empty output, and reading the exit status as failure inverts the check. The tolerant tail is in the snippet above so a copy into a `set -e` script does not abort on the expected case. + +After the first commit, confirm it took with `git log -1 --format='%G? author=%an <%ae> committer=%cn <%ce>'`, so the passing result is `G` plus the expected `noreply` address in **both** identities. Read both rather than the author alone: the rule governs the `author` and the `committer` together, GitHub verifies the signature against the **committer**, and a rebase, amend, or cherry-pick rewrites the committer while leaving the author untouched, which is exactly the case an author-only check passes and should not. `git verify-commit HEAD` is the pass/fail form, exiting non-zero on a bad signature and writing its "Good signature" line to stderr rather than emitting a status letter. ## 1. Classify and Catalog -Resolve the repo's type(s) with the [`AUDIT.md`][audit] section 2 detection rules, then write or repair its [`registry/repos.json`][repos] entry: `status`, `types[]`, `groundTruthBranch`, `hasDevelop`, `publish[]`, `requiredSecrets[]`, `consumerModel`, `releaseTrigger`, `workflowModel` (omit to take the `release` default), `configLayout`, and `driftNotes` that describe what the repo **actually is**. Run [`spec/validate.py`][validate] to confirm it classifies cleanly. The registry is ground truth about reality, not intent - a `validate.py`-clean entry is still false if it disagrees with the live repo. +Resolve the repo's type(s) with the [`AUDIT.md`][audit] section 2 detection rules, then write or repair its [`registry/repos.json`][repos] entry: `status`, `types[]`, `groundTruthBranch`, `hasDevelop`, `publish[]`, `requiredSecrets[]`, `consumerModel`, `releaseTrigger`, `workflowModel` (omit to take the `release` default), `configLayout`, and `driftNotes` that describe what the repo **actually is**. Run [`spec/validate.py`][validate] to confirm it classifies cleanly. The registry is ground truth about reality, not intent, and a `validate.py`-clean entry is still false if it disagrees with the live repo. + +## 1A. Carry the Instruction Set, Before Authoring Anything + +**Stop here until the instruction set is present and read.** The baseline in step 2 is one list, but it holds two kinds of file, and this kind is not a deliverable. `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md` and `WORKFLOW.md` are **the rules for producing every other file in the repo**, so carrying them late means everything authored beforehand was authored against unknown rules. The cost of that is rework rather than a warning, and it scales with how much got written first. + +This is the same shape as step 0. Signing has to be live before the first commit rather than retrofitted, and governance has to be loaded before the first authored file for the same reason: the window closes quietly, and the repair is expensive out of proportion to the prevention. + +Carry these before writing any repo content of your own: + +- [`AGENTS.md`][agents], [`GOVERNANCE.md`][governance], [`CODESTYLE.md`][codestyle], [`WORKFLOW.md`][workflow] and [`AUDIT.md`][audit], adapted rather than cloned for the ones that describe a repo. +- **`.markdownlint-cli2.jsonc` and `cspell.json`**, which are the mechanical half. A rule nothing checks drifts silently, so a repo that carries the prose authorities without the linter configs has guidance and no gate. Scope a linter's **file set in the workflow** rather than relaxing either config, since `.markdownlint-cli2.jsonc` is carried `verbatim`. + +Then **read** `CODESTYLE.md` and the `GOVERNANCE.md` documentation-style rules, rather than only placing the files. Comment shape, one sentence per line, US spelling and the character rules all govern the code and config you are about to write, and none of them are recoverable cheaply afterwards. + +**A caution about learning house style from the carried files.** Some carried configuration still holds comment blocks that predate the current rules, so read the rule text as the authority and do not infer style from a file's existing formatting. Where a carried file and the rules disagree, the rules win and the file is a backlog item for the hub. ## 2. Carry the Baseline Files -Copy every [`spec/files.json`][files] entry that applies to the repo's types - the `appliesTo: "*"` baseline plus the per-type additions - **adapted, not cloned**. The prose files (`CODESTYLE.md`, `README.md`, and the like) describe the repo's own toolchain, so adapt them to reality rather than propagating template specifics verbatim (see the "Adapt before propagating" callout in [`CODESTYLE.md`][codestyle]; a verbatim copy that misdescribes the repo is rejected in review). The baseline covers `WORKFLOW.md`, `version.json`, `repo-config/develop.json` + `main.json`, `.github/dependabot.yml`, `.editorconfig`, `.gitattributes`, the linter configs, and the per-type files (`.vscode/tasks.json` from the language's snippet, `codecov.yml`, `.dockerignore`, `Docker/README.md`). +Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's **selector set**, **adapted, not cloned**. The selector set is the repo's `types` plus its `workflowModel`, `releaseTrigger`, and `consumerModel`, so filtering on type alone silently drops the entries a non-type selector carries ([`spec/scope-model.md`][scope-model] defines the four namespaces and how they resolve). The prose files (`CODESTYLE.md`, `README.md`, and the like) describe the repo's own toolchain, so adapt them to reality rather than propagating template specifics verbatim (see the "Adapt before propagating" callout in [`CODESTYLE.md`][codestyle], since a verbatim copy that misdescribes the repo is rejected in review). The baseline covers `WORKFLOW.md`, `version.json`, the two rulesets, `.github/dependabot.yml`, `.editorconfig`, `.gitattributes`, the linter configs, and the per-type files (`.vscode/tasks.json` from the language's snippet, `codecov.yml`, `.dockerignore`, `Docker/README.md`). **Every repo carries `repo-config/main.json`**, and only the `develop` payload varies by workflow model: `repo-config/develop.json` for a release repo, `repo-config/operational/develop.json` for an operational one. + +**Repo-specific content has a declared destination, not a judgment call.** The baseline is what a repo *carries*. Anything the repo knows that the fleet does not needs somewhere to live, and improvising a location per repo is what the destinations in [`spec/section-model.md`][section-model] exist to prevent. Three topical docs take it, chosen by what the content **is**: + +- [`CODESTYLE.md`][codestyle]: the repo's language and formatting conventions beyond the carried rules. +- `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. +- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. + +**`OPERATIONS.md` is required on an `operational` repo**, not optional, so it appears in the baseline above with `appliesTo: ["operational"]`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file. It is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` stays advisory and is not required by any selector. + +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. ## 3. Stand Up the Workflows -Implement the Actions that satisfy [`WORKFLOW.md`][workflow] for the repo's type (its section 6 per-type walkthrough): the source-only subset for a source-only repo, the file-target leaf(s) for a publishing repo, the two-workflow shape for an operational config repo. Reuse [`catalog/snippets/workflows/`][workflows] as the reference implementation - satisfy the contract by outcome, not byte for byte. +Implement the Actions that satisfy [`WORKFLOW.md`][workflow] for the repo's type (its section 6 per-type walkthrough): the source-only subset for a source-only repo, the file-target leaf(s) for a publishing repo, the two-workflow shape for an operational config repo. Reuse [`catalog/snippets/workflows/`][workflows] as the reference implementation, satisfying the contract by outcome rather than byte for byte. ## 4. Apply Settings, Rulesets, and Secrets -Run `repo-config/configure.sh apply [owner/repo] [release|operational]` (the repo defaults to the current one, the model to the registry lookup or, absent a registry, to the carried payload) to apply the fleet settings, the Dependabot security features, and the two rulesets idempotently (import the JSON, never hand-build - see [`docs/repo-config-carry.md`][repo-config-carry]), then `repo-config/configure.sh check [owner/repo] [release|operational]` to validate the repo and exit non-zero on any drift. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s) - Actions, and Dependabot where the mechanism needs it - and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once. +Run `repo-config/configure.sh apply [owner/repo] [release|operational]` (the repo defaults to the current one, the model to the registry lookup or, absent a registry, to the carried payload) to apply the fleet settings, the Dependabot security features, and the two rulesets idempotently (import the JSON, never hand-build it, per [`docs/repo-config-carry.md`][repo-config-carry]), then `repo-config/configure.sh check [owner/repo] [release|operational]` to validate the repo and exit non-zero on any drift. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s), meaning Actions plus Dependabot where the mechanism needs it, and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once. -## 5. Verify - Run the Audit +## 5. Verify: Run the Audit -Run [`AUDIT.md`][audit] end to end. The repo is stood up only when it is **operational** (every applicable check passes) or its residual deltas are tracked in `reports//audit.md` plus an issue. Converge any drift through a Copilot-reviewed target PR ([`AUDIT.md`][audit] section 10); the maintainer merges. A repo left partially set up and unrecorded is the exact failure this procedure exists to prevent. +Run [`AUDIT.md`][audit] end to end. The repo is stood up only when it is **operational** (every applicable check passes) or its residual deltas are tracked in `reports//audit.md` plus an issue. Converge any drift through a Copilot-reviewed target PR ([`AUDIT.md`][audit] section 10), and the maintainer merges. A repo left partially set up and unrecorded is the exact failure this procedure exists to prevent. ## Onboarding a New Repo Type When a repo matches no existing type, the work is onboarding a **type**, not just a repo: -1. Add the type to [`spec/project-types.json`][project-types] (`detect[]`, plus `checks` with verdict tiers and intent refs) and any per-type files to [`spec/files.json`][files]; add its publish mechanism to [`spec/secrets.json`][secrets] if new. +1. Add the type to [`spec/project-types.json`][project-types] (`detect[]`, plus `checks` with verdict tiers and intent refs) and any per-type files to [`spec/files.json`][files], then add its publish mechanism to [`spec/secrets.json`][secrets] if new. 2. Add the reference workflow leaf to [`catalog/snippets/workflows/`][workflows] and document the type's [`WORKFLOW.md`][workflow] walkthrough. 3. Add the type to the [conformance matrix][matrix] and run the cold-start self-test until a context-free agent stands it up to operational. -## Self-Test - Cold-Start Conformance +## Self-Test: Cold-Start Conformance -The onboarding docs are sufficient only if a **context-free agent stands up each supported repo shape from them alone** - a shape being the project type(s) plus the workflow model (`operational` is a `workflowModel` overlay, not a `spec/project-types.json` type). Run this whenever the onboarding docs or manifests change, and periodically as a fleet health check: +The onboarding docs are sufficient only if a **context-free agent stands up each supported repo shape from them alone**, a shape being the project type(s) plus the workflow model (`operational` is a `workflowModel` overlay, not a `spec/project-types.json` type). Run this whenever the onboarding docs or manifests change, and periodically as a fleet health check: - For each shape in the [conformance matrix][matrix], task a fresh agent (no prior context) with "Using only this repo's docs, stand up a `` repo," pointing it at this file. - Run [`AUDIT.md`][audit] against the result. Record pass or fail, and the first doc gap that tripped the agent, in the [conformance matrix][matrix]. - Iterate the **docs and tooling** (not the agent's memory) until every supported shape stands up cold to operational. A shape that cannot be stood up cold is a documentation defect, tracked like any other. -The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo; its report lists deviations and repo-specific deltas. The self-test and the fleet audit are one procedure, pointed at a new repo or an existing one. +The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo, and its report lists deviations and repo-specific deltas. The self-test and the fleet audit are one procedure, pointed at a new repo or an existing one. @@ -48,16 +103,21 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo; its +[agents]: ./AGENTS.md [audit]: ./AUDIT.md [codestyle]: ./CODESTYLE.md [files]: ./spec/files.json [governance]: ./GOVERNANCE.md +[governance-git-and-commit-rules]: ./GOVERNANCE.md#git-and-commit-rules +[host-setup]: ./docs/host-setup.md [matrix]: ./reports/conformance-matrix.md [project-types]: ./spec/project-types.json [repo-config]: ./repo-config/ [repo-config-carry]: ./docs/repo-config-carry.md [repos]: ./registry/repos.json +[scope-model]: ./spec/scope-model.md [secrets]: ./spec/secrets.json +[section-model]: ./spec/section-model.md [spec]: ./spec/ [validate]: ./spec/validate.py [workflow]: ./WORKFLOW.md diff --git a/docs/host-setup.md b/docs/host-setup.md index a103678c..ae4cb16e 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -1,24 +1,44 @@ # Host Setup -Prerequisites for working with this repo locally - apply once per machine before opening the devcontainer or building outside one. +Prerequisites for working with this repo locally, applied once per machine before opening the devcontainer or building outside one. Supported hosts: - **Linux** - both the devcontainer flow and the host-install flow. - **macOS** - both the devcontainer flow and the host-install flow. -- **Windows** - the devcontainer flow requires **WSL2**; native Windows (PowerShell + winget) is supported only for the host-install flow described in `README.md`. The bind-mounts in `.devcontainer/dotnet/devcontainer.json` and `.devcontainer/python/devcontainer.json` rely on POSIX paths and only work from Linux/macOS/WSL2. +- **Windows** - the devcontainer flow requires **WSL2**, and native Windows (PowerShell + winget) is supported only for the host-install flow described in `README.md`. The bind-mounts in `.devcontainer/dotnet/devcontainer.json` and `.devcontainer/python/devcontainer.json` rely on POSIX paths and only work from Linux/macOS/WSL2. -> **Shell assumptions in this doc**: every command snippet below assumes a **POSIX shell** (bash/zsh) and POSIX path conventions (`~/.ssh/...`, `mkdir -p`, `$(...)` command substitution). On Windows, run them from **WSL2** or **Git Bash** - they will not work as-is in PowerShell or `cmd.exe`. The git config and `gh` commands are portable; only the file/path manipulation differs by shell. +> **Shell assumptions in this doc**: every command snippet below assumes a **POSIX shell** (bash/zsh) and POSIX path conventions (`~/.ssh/...`, `mkdir -p`, `$(...)` command substitution), with one exception. A block marked `powershell` is the **Windows-native** form of the step it sits in, meant to run in PowerShell rather than translated. On Windows, run the POSIX snippets from **WSL2** or **Git Bash**, since they will not work as-is in PowerShell or `cmd.exe`. The git config and `gh` commands are portable, and only the file and path manipulation differs by shell. + +## What a Host Must Provide + +This section is the **contract**: which tools a host needs and which repo procedure stops working without each one. It deliberately names no installer, because `winget`, `brew` and `apt` differ per platform while the requirement does not. Per-platform install commands are tracked separately, so this table stays true on every host. + +| Tool | Needed by | Present when | +| --- | --- | --- | +| `git` | everything, and the identity and signing contract in [`STANDUP.md`][standup] step 0 | `git --version` | +| `gh` | the PR and review loop, `gh api` queries, `repo-config/configure.sh` | `gh --version` | +| Python 3 | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version`, or `py -3 --version` on native Windows | +| `docker` | the four linters, which run as pinned images rather than local installs | `docker --version` | +| `uv` / `uvx` | coverage runs, and the Python toolchain (`ruff`, `pyright` or `mypy`) in a Python repo | `uv --version` | + +Two consequences worth reading off the table rather than discovering later. **Python 3 needs no packages**, because every script here is standard library only, so a bare interpreter is enough. And **the linters need only `docker`**, not `node`, `dotnet` or a local `markdownlint`, since each runs as a pinned image, which is what keeps a local run and CI the same check. + +**The interpreter is not called `python3` everywhere.** On native Windows the installer registers `python`, `py` and `python3.13` but **not** `python3`, where that name instead resolves to a Microsoft Store alias stub that reports the interpreter as missing, so a correctly set-up host fails a `python3` check. Stock Debian is the mirror image, carrying `python3` and no bare `python`. Use `py -3` on native Windows and `python3` elsewhere, and prefer `python3` in any script that must run on both, since WSL2 shadows the Windows stub. + +A missing tool is a host gap, not a repo problem. Install it and re-run, rather than working around it in a repo. ## Git Identity -Configure your name and email - used for commit authorship. +Configure your name and email, used for commit authorship. **The email is the committing account's GitHub `noreply` address, never a private, personal, or invented one**, per [GOVERNANCE.md "Git and Commit Rules"][governance-git-and-commit-rules], which owns the rule and states the fleet's value. A private address trips GitHub's email-privacy push protection (GH007), and an invented one pollutes history. ```shell git config --global user.name "Your Name" -git config --global user.email "you@example.com" +git config --global user.email "+@users.noreply.github.com" ``` +Set this **globally**, once per machine. Repositories inherit it, so a repo-local `user.email` is redundant where the global is right and a wrong identity where it is not. An agent standing up a repo verifies this configuration rather than setting it ([`STANDUP.md`][standup] step 0). + ## SSH Key Generate an Ed25519 SSH key for both authentication and commit signing. One key serves both roles. @@ -60,7 +80,7 @@ systemctl --user enable --now ssh-agent.socket ssh-add ~/.ssh/id_ed25519 ``` -For non-systemd shells, add to `~/.bashrc` or `~/.zshrc`. The check probes the agent for at least one loaded key - `[ -z "$SSH_AUTH_SOCK" ]` alone would miss the case where `SSH_AUTH_SOCK` is set but points at a stale socket or a keyless agent: +For non-systemd shells, add to `~/.bashrc` or `~/.zshrc`. The check probes the agent for at least one loaded key, because `[ -z "$SSH_AUTH_SOCK" ]` alone would miss the case where `SSH_AUTH_SOCK` is set but points at a stale socket or a keyless agent: ```shell if [ -z "$SSH_AUTH_SOCK" ] || ! ssh-add -l >/dev/null 2>&1; then @@ -120,9 +140,26 @@ gh auth login --hostname github.com --git-protocol ssh Choose the SSH key generated above when prompted. +## Agent Write-Safety Kit + +Required on any host where an agent runs with the `gh` credentials logged in, and its own README calls it the first thing to deploy on a new system. Install it from this repo, since the installer is idempotent and safe to re-run to update: + +```shell +host-setup/agent-safety/install.sh # Linux, WSL, macOS, Proxmox +``` + +```powershell +.\host-setup\agent-safety\install.ps1 # Windows, and the .\ prefix is required +``` + +Both wrap one `install.py`, so every platform runs the same tested path. Restart Claude Code sessions on the machine afterward so the hook and the `CLAUDE.md` block load. Details, verification, and scope limits are in [`host-setup/agent-safety/README.md`][agent-safety]. + +This is a **host** control, not a repo one. The carried `GOVERNANCE.md` rules reach fleet repos only, while the hook and the `CLAUDE.md` block cover every session on the machine, including ad-hoc work in no project at all, which is where the incident behind the kit happened. + ## Verify Host Setup ```shell +git --version && gh --version && python3 --version && docker --version && uv --version git config --global --list | grep -E "user\.|signing|gpg\." ssh-add -L # should list your public key git -c gpg.format=ssh commit -S --allow-empty -m "verify-signing" @@ -130,17 +167,35 @@ git log --show-signature -1 gh auth status ``` -If signing fails locally, the devcontainer will fail too - fix here first. +If signing fails locally, the devcontainer will fail too, so fix here first. + +**This block is POSIX, and on native Windows the interpreter line needs translating**, since `python3` is the one name a correctly set-up Windows host does not have. Read it as `py -3 --version` there, matching the contract table above, and run the rest from WSL2 or Git Bash per the shell note. Git Bash inherits the Windows `PATH`, so `python3` reaches the same Store alias stub it does in PowerShell and reports a working interpreter as missing. A PowerShell equivalent of this block is deliberately **not** given here, because it has not been run on a Windows host, and an unverified verification command is worse than none. [#483][issue-483] is where one belongs once someone has executed it. + +**What the host can do once this passes**, which is the point of the contract above: + +| Now possible | Because | +| --- | --- | +| Stand up a new repo through [`STANDUP.md`][standup] | step 0 verifies identity and signing, and its window closes at the first commit | +| Run the four linters locally, matching CI | `docker` runs each as the same pinned image CI uses | +| Run the repo's own gates and tests | Python 3 covers `scripts/` and `spec/` with no packages to install | +| Drive the PR and Copilot review loop | `gh` and an authenticated session | +| Let an agent work with the `gh` credentials live | the write-safety kit is installed | + +A host that fails any row is not ready for the procedure that row names, and the fix belongs on the host rather than in a repo. ## Next Steps -- [Devcontainer setup][devcontainer] - open the repo in the per-language .NET or Python devcontainer. -- [SSH commit signing][ssh-signing] - per-OS setup details, verification, and troubleshooting. +- [Devcontainer setup][devcontainer]: open the repo in the per-language .NET or Python devcontainer. +- [SSH commit signing][ssh-signing]: per-OS setup details, verification, and troubleshooting. +[agent-safety]: ../host-setup/agent-safety/README.md [devcontainer]: ./devcontainer.md +[governance-git-and-commit-rules]: ../GOVERNANCE.md#git-and-commit-rules +[issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 [ssh-signing]: ./ssh-signing.md +[standup]: ../STANDUP.md diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 630610f8..b9c25318 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -1,17 +1,17 @@ # Agent Write-Safety Kit -Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a branch rule or overrides a required check** - a push, force-push, or delete that an active branch rule forbids, or an override flag (`--admin` past the server-side merge gate, `--no-verify` past the local git hooks). Deploy it as the **first thing on any new system** where Claude Code runs with the `gh` credentials logged in (WSL, Linux, macOS, Proxmox, Windows). +Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a branch rule or overrides a required check**, meaning a push, force-push, or delete that an active branch rule forbids, or an override flag (`--admin` past the server-side merge gate, `--no-verify` past the local git hooks). Deploy it as the **first thing on any new system** where Claude Code runs with the `gh` credentials logged in (WSL, Linux, macOS, Proxmox, Windows). ## What It Installs Into `~/.claude/` (or `%USERPROFILE%\.claude\` on Windows): -- **`hooks/gh-write-guard.py`** - a PreToolUse hook that denies two classes of dangerous action. First, the GitHub **write** footguns behind the cross-repo comment incident: a state-changing `gh` call whose output is discarded, a GraphQL mutation passing a **literal** node id instead of a `$variable`, and a `gh` write whose explicit target is outside the checkout's `origin`. Second, a **git operation that bypasses a repository protection**. The branch-rule cases - a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked - are judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no per-repo configuration. A push to a protected-default branch fails closed when its rules cannot be determined - the API is unreachable, or the checkout's origin cannot be resolved to query them. The explicit-bypass flags are denied **unconditionally**, since the flag is itself the bypass and needs no branch query: `gh pr merge --admin` overrides the server-side merge gate, and `git commit`/`git push --no-verify` skips the local git hooks. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. -- **A `## GitHub Write Safety (Any Project, Every Session)` section in `CLAUDE.md`** - the same three rules as behavioral guidance, loaded into every session on the machine (including ad-hoc work outside any project). It mirrors the committed `GOVERNANCE.md` "Repository Boundaries and Write Safety" rules, which only reach fleet repos. +- **`hooks/gh-write-guard.py`**: a PreToolUse hook that denies two classes of dangerous action. First, the GitHub **write** footguns behind the cross-repo comment incident: a state-changing `gh` call whose output is discarded, a GraphQL mutation passing a **literal** node id instead of a `$variable`, and a `gh` write whose explicit target is outside the checkout's `origin`. Second, a **git operation that bypasses a repository protection**. The branch-rule cases (a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked) are judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no per-repo configuration. A push to a protected-default branch fails closed when its rules cannot be determined, either because the API is unreachable or because the checkout's origin cannot be resolved to query them. The explicit-bypass flags are denied **unconditionally**, since the flag is itself the bypass and needs no branch query: `gh pr merge --admin` overrides the server-side merge gate, and `git commit`/`git push --no-verify` skips the local git hooks. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. +- **A `## GitHub Write Safety (Any Project, Every Session)` section in `CLAUDE.md`**: the same three rules as behavioral guidance, loaded into every session on the machine (including ad-hoc work outside any project). It mirrors the committed `GOVERNANCE.md` "Repository Boundaries and Write Safety" rules, which only reach fleet repos. -The hook is the mechanical backstop. The CLAUDE.md rules and the carried GOVERNANCE.md rules are the behavioral layer. Prose alone is not enough - the incident happened under prose rules - so both ship. +The hook is the mechanical backstop. The CLAUDE.md rules and the carried GOVERNANCE.md rules are the behavioral layer. Prose alone is not enough, since the incident happened under prose rules, so both ship. -## Install (Idempotent - Safe to Re-Run to Update) +## Install (Idempotent, Safe to Re-Run to Update) ```sh # Linux / WSL / macOS / Proxmox @@ -19,8 +19,8 @@ host-setup/agent-safety/install.sh ``` ```powershell -# Windows -host-setup\agent-safety\install.ps1 +# Windows - the .\ prefix is required, PowerShell does not run a script from a relative path without it +.\host-setup\agent-safety\install.ps1 ``` Both are thin wrappers around `install.py`, so every OS runs one tested code path. The installer self-tests the hook before registering it, merges the settings.json entry without clobbering other keys, and updates the CLAUDE.md block in place (marker-delimited) rather than duplicating it. @@ -70,7 +70,7 @@ The installer writes this. It is here so you can inspect or hand-place it: - **Per-machine.** `~/.claude/` does not travel, so run the installer on each box. This is the rollout that [#365][issue-365] tracks. - **Precision over recall for the write footguns.** The hook denies the specific dangerous write shapes with high confidence rather than gating every write, so it never blocks legitimate work. A shape it does not catch still falls under the behavioral rules. - **The branch-bypass rule fails closed.** Unlike the write-footgun rules, a push to `main`/`master`/`develop` is denied even when its rules cannot be determined (the API is unreachable, or the checkout's origin cannot be resolved to query them), because the harm there is a silent success under the maintainer's admin bypass. The rule reads each branch's live rules, so it adapts to every repo (a code-style `develop` denies, a config-style `develop` allows) with no per-repo configuration, and hands the exact command to the maintainer to run when a bypass is genuinely intended. -- **Opaque targets are unseen.** The hook cannot see the repository behind a GraphQL node id, which is exactly why rule 2 blocks a *literal* id at all - a captured `$variable` is trusted. Likewise, the cross-origin check only runs when an `origin` can be resolved and the write names an explicit `-R`/`repos//` target. A write from a non-git directory, or one whose target is only a node id, is evaluated by rules 1 and 2 alone. +- **Opaque targets are unseen.** The hook cannot see the repository behind a GraphQL node id, which is exactly why rule 2 blocks a *literal* id at all, since a captured `$variable` is trusted. Likewise, the cross-origin check only runs when an `origin` can be resolved and the write names an explicit `-R`/`repos//` target. A write from a non-git directory, or one whose target is only a node id, is evaluated by rules 1 and 2 alone. - **Not a credential control.** A fine-grained PAT limited to owned repositories is a separate, stronger structural guard (a hard `403` on any non-owned repo) and is left to per-machine credential setup, out of this kit. diff --git a/reports/conformance-matrix.md b/reports/conformance-matrix.md index b037d5ec..30d92422 100644 --- a/reports/conformance-matrix.md +++ b/reports/conformance-matrix.md @@ -19,7 +19,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug | `upstream-wrapper` | - | not-tested | - | Tag from a committed state file, not SemVer2. | | `codegen` | - | not-tested | - | Deterministic matrix over both branches. | | `docs` | ProjectTemplate | not-tested | - | Governance hub; CI is lint-only. | -| `operational` config | - | not-tested | - | `workflowModel: operational`; direct signed commits to `develop`, promotion-PR gate. | +| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Carries a required `OPERATIONS.md` (`appliesTo: ["operational"]`, presence-checked) for its runbooks. Blog (#456) is the next standup and the first cold test of `STANDUP.md` step 0. | ## Composable Targets diff --git a/spec/files.json b/spec/files.json index 051e7cad..120276c7 100644 --- a/spec/files.json +++ b/spec/files.json @@ -8,6 +8,7 @@ { "path": "WORKFLOW.md", "fidelity": "intent", "whole": true, "intentRef": "WORKFLOW.md", "appliesTo": "*" }, { "path": "README.md", "appliesTo": "*" }, { "path": "HISTORY.md", "appliesTo": "*" }, + { "path": "OPERATIONS.md", "appliesTo": ["operational"] }, { "path": ".github/copilot-instructions.md", "fidelity": "intent", "whole": true, "sections": ["Commit Messages and Pull Request Titles", "Reviewing Carried Fleet Content", "GitHub Copilot Review Runbook"], "placeholders": ["", "", ""], "appliesTo": "*" }, { "path": ".editorconfig", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" }, { "path": ".editorconfig-checker.json", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" }, diff --git a/spec/section-model.md b/spec/section-model.md index d49e6857..3efaad79 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -12,7 +12,7 @@ The set spans two files. [`AGENTS.md`][agents] is the entry point every agent re A section is one of the following. Fidelity is declared in [files.json][files], never inferred. -- **verbatim** - universal fleet-law rule *text*, byte-identical in every repo after EOL and action-pin normalization. The audit content-hashes each downstream copy against the hub's `## ` block. A verbatim section may carry clauses only some repos exercise (for example "a source-only repo carries no build task"). The *text* is still identical everywhere - applicability is per-repo, the wording is not. +- **verbatim** - universal fleet-law rule *text*, byte-identical in every repo after EOL and action-pin normalization. The audit content-hashes each downstream copy against the hub's `## ` block. A verbatim section may carry clauses only some repos exercise (for example "a source-only repo carries no build task"). The *text* is still identical everywhere, since applicability varies by repo while the wording does not. - **intent** - the section *describes this particular repo* (its own directory tree, its own devcontainer and toolchain), so its content legitimately varies. The heading must be present, the body is judged by meaning rather than hashed. `appliesTo` scope is orthogonal to fidelity. A section may apply to a subset of repos (for example `operational`) and is checked only for repos in that subset. @@ -31,7 +31,7 @@ A section is one of the following. Fidelity is declared in [files.json][files], | Git and Commit Rules | `GOVERNANCE.md` | verbatim | universal git law | | Branching Model | `GOVERNANCE.md` | verbatim | universal (repo-specific history SHAs removed so it can carry) | | Release Model | `GOVERNANCE.md` | verbatim | universal release contract, all target shapes described inline | -| Operational Repositories | `GOVERNANCE.md` | verbatim | fleet context (the two workflow models); carried by all so the cross-references to it resolve | +| Operational Repositories | `GOVERNANCE.md` | verbatim | fleet context (the two workflow models), carried by all so the cross-references to it resolve | | Pull Request Title and Commit Message Conventions | `GOVERNANCE.md` | verbatim | universal, with generic examples | | Documentation Style Conventions | `GOVERNANCE.md` | verbatim | all rule text, including the Line Endings *rule* (a repo's `.editorconfig` value is not here) | | Verification Discipline | `GOVERNANCE.md` | verbatim | universal verification law | @@ -44,38 +44,42 @@ A section is one of the following. Fidelity is declared in [files.json][files], | Devcontainer | `GOVERNANCE.md` | intent | describes this repo's toolchain and devcontainer, genuinely per-repo | | Repository Layout | `GOVERNANCE.md` | intent | describes this repo's directory tree, genuinely per-repo | -**Devcontainer content.** A devcontainer is optional infrastructure, not required by any repo type. An operational (live config) repo is edited and deployed live and typically has none, so its Devcontainer section states that plainly. A repo that keeps one - a code repo's toolchain, or an offline-debugging aid for a config repo - describes it. The section is present in every carried `GOVERNANCE.md` so the development model is always answered, even when the answer is none. +**Devcontainer content.** A devcontainer is optional infrastructure, not required by any repo type. An operational (live config) repo is edited and deployed live and typically has none, so its Devcontainer section states that plainly. A repo that keeps one (a code repo's toolchain, or an offline-debugging aid for a config repo) describes it. The section is present in every carried `GOVERNANCE.md` so the development model is always answered, even when the answer is none. -**Not carried (hub-only).** `Repository Onboarding and Conformance` lives in the hub's `GOVERNANCE.md` as hub-audit context (reconciling the registry, the STANDUP cold-start, the conformance matrix) but is not a carried section - a downstream agent never runs those. Its one universal rule, that a repo is done when it passes `AUDIT.md` for its type, is carried in `AUDIT.md` itself. Like the model docs and `STANDUP.md`, it is hub machinery, not fleet law. +**Not carried (hub-only).** `Repository Onboarding and Conformance` lives in the hub's `GOVERNANCE.md` as hub-audit context (reconciling the registry, the STANDUP cold-start, the conformance matrix) but is not a carried section, since a downstream agent never runs those. Its one universal rule, that a repo is done when it passes `AUDIT.md` for its type, is carried in `AUDIT.md` itself. Like the model docs and `STANDUP.md`, it is hub machinery, not fleet law. ## Changing the structure carries review weight The set of sections, and each section's fidelity, is itself governed. -- **Adding a section** is a fleet-wide act - it declares a new rule every repo must carry. Add it to the file it belongs in, to `files.json` under that file, and to the table above in the same change, with its fidelity chosen deliberately. A rule that applies to every task belongs in `AGENTS.md`, and a topical rule belongs in `GOVERNANCE.md`. +- **Adding a section** is a fleet-wide act, declaring a new rule every repo must carry. Add it to the file it belongs in, to `files.json` under that file, and to the table above in the same change, with its fidelity chosen deliberately. A rule that applies to every task belongs in `AGENTS.md`, and a topical rule belongs in `GOVERNANCE.md`. - **Changing a verbatim section** re-vendors it across the whole fleet. The audit drift wave that follows is the mechanism working, not a regression. - **Changing a section's fidelity** (intent to verbatim or back) is a governance decision, recorded here with its reason. -- **A downstream repo's extra section** the hub does not declare is drift to reconcile, not a local liberty - including a section whose *heading* differs but whose *content* duplicates a verbatim section (compare by content, not heading name). Either the rule belongs fleet-wide, so promote it here, or its unique part is repo-specific content that moves to one of the topical docs below and the duplicate is deleted. The audit lists a repo's undeclared sections as an advisory so the reconciliation is not missed. +- **A downstream repo's extra section** the hub does not declare is drift to reconcile, not a local liberty, including a section whose *heading* differs but whose *content* duplicates a verbatim section (compare by content, not heading name). Either the rule belongs fleet-wide, so promote it here, or its unique part is repo-specific content that moves to one of the topical docs below and the duplicate is deleted. The audit lists a repo's undeclared sections as an advisory so the reconciliation is not missed. ## Where repo-specific content goes A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Three topical docs take it, chosen by what the content *is*: -- [`CODESTYLE.md`][codestyle] - a repo's language and formatting conventions beyond the carried rules. -- `ARCHITECTURE.md` - how a code repo is built: its module layout, data flow, and design decisions. -- `OPERATIONS.md` - how an operational repo is run: runbooks, backup, log and debug procedures, tool-usage notes, and config layout. This is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. +- [`CODESTYLE.md`][codestyle]: a repo's language and formatting conventions beyond the carried rules. +- `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. +- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. This is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. -`OPERATIONS.md` is agent-instruction content, so it takes the inline-link exception the markdown rules name, not the reference-style default. +**`OPERATIONS.md` is required for an `operational` repo**, declared in [`files.json`][files] as `appliesTo: ["operational"]` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. A repo with nothing to say in it still carries it, since an empty file that names the destination is cheaper than rediscovering the destination. + +`CODESTYLE.md` is carried by every repo already. `ARCHITECTURE.md` stays **advisory**, deliberately: "a code repo" is not one selector token, and mandating it would produce empty files in repos whose design needs no separate document. Promote it if the same unprompted-convention evidence appears for it. + +`OPERATIONS.md` is agent-instruction content, so it takes the inline-link exception the markdown rules name, not the reference-style default. `ARCHITECTURE.md` is not on that closed list and follows the reference-style rule. ## Migrating a repo onto the split A repo that carried its governance inside `AGENTS.md` before the router split holds two things in one file: sections that are stale copies of fleet law, and local additions written after a fault the fleet has never seen. Re-vendoring the canonical over the whole file silently deletes the second kind. -**Probe the canonical for each local rule's distinctive phrase.** That is the check that works. A word-overlap or similarity heuristic does not: a repo-specific rule written in ordinary governance vocabulary scores as a reworded duplicate of a rule it has nothing to do with, so the cheap check is confidently wrong in exactly the direction that loses content. Take each candidate rule, pick the phrasing that is peculiar to it, and grep the hub's canonical for that. Absent means it is a local addition, and it is then either promoted here or moved to the repo's topical doc - never dropped because a heuristic called it redundant. +**Probe the canonical for each local rule's distinctive phrase.** That is the check that works. A word-overlap or similarity heuristic does not: a repo-specific rule written in ordinary governance vocabulary scores as a reworded duplicate of a rule it has nothing to do with, so the cheap check is confidently wrong in exactly the direction that loses content. Take each candidate rule, pick the phrasing that is peculiar to it, and grep the hub's canonical for that. Absent means it is a local addition, and it is then either promoted here or moved to the repo's topical doc, never dropped because a heuristic called it redundant. ## Enforcement -`files.json` declares each section's fidelity. [validate.py][validate] proves every declared section resolves to a real level-two heading in the hub's own copy of the file that declares it, so a renamed or mistyped section cannot silently stop being checked. [audit.py][audit] checks each repo's copy - presence for `intent`, byte-match for `verbatim` - and classifies a mismatch as stale (re-vendor) or modified (review). +`files.json` declares each section's fidelity. [validate.py][validate] proves every declared section resolves to a real level-two heading in the hub's own copy of the file that declares it, so a renamed or mistyped section cannot silently stop being checked. [audit.py][audit] checks each repo's copy (presence for `intent`, byte-match for `verbatim`) and classifies a mismatch as stale (re-vendor) or modified (review).