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 6a30e1b0..9735c3c0 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -4,6 +4,36 @@ How an agent takes a repository from nothing (or a partial state) to **operation 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, and a `validate.py`-clean entry is still false if it disagrees with the live repo. @@ -18,7 +48,7 @@ Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's - `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 the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so an `operational` repo with real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) writes them 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. +**`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. @@ -62,6 +92,8 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo, and [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/ diff --git a/docs/host-setup.md b/docs/host-setup.md index a103678c..04698774 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -1,24 +1,26 @@ # 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). On Windows, run them 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; only the file/path manipulation differs by shell. ## 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 +62,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 @@ -130,17 +132,19 @@ 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. ## 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. [devcontainer]: ./devcontainer.md +[governance-git-and-commit-rules]: ../GOVERNANCE.md#git-and-commit-rules [ssh-signing]: ./ssh-signing.md +[standup]: ../STANDUP.md 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).