From eb3c83cbf06f3dc46f82d4fba05069736abdae81 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 13:30:47 -0700 Subject: [PATCH 1/7] Verify commit identity before the first commit, and require OPERATIONS.md Two onboarding gaps, both surfaced by the Blog standup. The Blog agent set a repo-local user.email during scaffolding, then caught it while verifying the first commit was signed. The rule was never missing: GOVERNANCE.md "Git and Commit Rules" states it, and AGENTS.md routes to it. STANDUP.md, the procedure an agent standing up a repo actually follows, never mentioned identity, signing, or the first commit at all, and docs/host-setup.md modeled the failure by telling the reader to set user.email to "you@example.com" with no pointer to the rule. The window matters more than most: a repo whose initial history carries the wrong identity or no signature blocks its own first develop -> main release, and repairing it needs a maintainer force-push that repo-config-carry.md forbids an agent to run. Cheap to prevent, expensive to fix, and the agent is barred from fixing it. STANDUP.md gains a step 0 that verifies identity and signing before git init. The framing is verify, never set: the host carries the identity globally, so a repo-local user.email is redundant where the global is right and wrong where it is not, and it shadows the global silently. A mismatch is a host fault to surface, not a per-repo patch, since patching locally hides a broken host that then commits wrongly in every other repo on that machine. GOVERNANCE.md's "fix it if not" is narrowed to match, which re-vendors. Second, OPERATIONS.md becomes required for an operational repo, declared appliesTo ["operational"] and presence-checked like README.md and HISTORY.md, so its content stays entirely the repo's own. The evidence for mandating rather than advising is that the convention was already emerging unevenly: of four operational repos two wrote one unprompted, while another scattered the same material across Hardware.md and ZFS.md, which is the improvisation the destinations exist to prevent. ARCHITECTURE.md stays advisory, because "a code repo" is not one selector token and mandating it would produce genuinely empty files. The dash and semicolon backlog in spec/section-model.md and docs/host-setup.md is cleared, per the corrected-as-next-edited rule. Both are hub-only. reports/conformance-matrix.md keeps its backlog: only one table cell is touched here and sweeping 38 findings across that table would bury the change. Co-Authored-By: Claude Opus 5 (1M context) --- GOVERNANCE.md | 2 +- STANDUP.md | 20 +++++++++++++++++++- docs/host-setup.md | 22 +++++++++++++--------- reports/conformance-matrix.md | 2 +- spec/files.json | 1 + spec/section-model.md | 28 ++++++++++++++++------------ 6 files changed, 51 insertions(+), 24 deletions(-) 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..8bbb76ec 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -4,6 +4,23 @@ 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: + +```shell +git config --get user.email # the GitHub noreply address, per GOVERNANCE.md "Git and Commit Rules" +git config --get commit.gpgsign # true +git config --get gpg.format # ssh +git config --local --get user.email # expect empty - a repo-local override is itself a finding +``` + +If any of the first three is wrong or absent, that 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 the first commit, confirm it took: `git verify-commit HEAD` reports `G`, and `git log -1 --format='%an <%ae>'` shows the expected identity. + ## 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 +35,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 +79,7 @@ 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 +[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..dc41709f 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 is per-repo and the wording is 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). From 26950af185f7ceb221a2fdc3f3d707410e33a260 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 13:36:47 -0700 Subject: [PATCH 2/7] Read the signature status from %G?, not from verify-commit Step 0 told the agent that `git verify-commit HEAD` reports `G`. It does not. Verified locally: verify-commit signals through its exit code and writes "Good "git" signature for ..." to stderr, while the `G` status letter comes from `%G?` in git log's pretty format. An agent following the old text would have looked for output that never appears, on the one check standing between it and an unrepairable initial history. One command now prints both facts the step needs: `git log -1 --format='%G? %an <%ae>'` gives the status letter and the identity together. Co-Authored-By: Claude Opus 5 (1M context) --- STANDUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STANDUP.md b/STANDUP.md index 8bbb76ec..d2e775b6 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -19,7 +19,7 @@ git config --local --get user.email # expect empty - a repo-local override is If any of the first three is wrong or absent, that 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 the first commit, confirm it took: `git verify-commit HEAD` reports `G`, and `git log -1 --format='%an <%ae>'` shows the expected identity. +After the first commit, confirm it took with `git log -1 --format='%G? %an <%ae>'`, which prints the signature status letter followed by the identity, so `G` plus the expected `noreply` address is the passing result. `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 From 9f3b9eed2fd0f6c52c621f63cbe3b707a589cde7 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 13:42:03 -0700 Subject: [PATCH 3/7] Split step 0 by what needs a repository, and allow GPG signing Three suppressed findings, all correct, all verified rather than accepted. The step said to run its checks before `git init` but included `git config --local`, which exits 128 with "fatal: --local can only be used inside a git repository". The procedure was unrunnable exactly where it is aimed, a greenfield directory. Confirmed in an empty scratch directory, where every other read returns the global value and only --local fails. The effective reads now run first, before there is a repo, and the local-override check moves to its own block after git init and before the first commit. The step also required gpg.format to be ssh. GOVERNANCE.md "Git and Commit Rules" says commits are signed with SSH **or** GPG, so a valid GPG host (gpg.format unset or openpgp) would have been reported as misconfigured and sent to a maintainer as a fault. The check now names both forms and asks for the configured format to have a matching loaded agent, which is what that rule actually prescribes. Co-Authored-By: Claude Opus 5 (1M context) --- STANDUP.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/STANDUP.md b/STANDUP.md index d2e775b6..e1f74a59 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -8,16 +8,23 @@ Standing up a repo is **applying the manifests until the audit passes**, nothing 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: +**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. These read the effective configuration and work in any directory, so run them first, before there is a repo: ```shell git config --get user.email # the GitHub noreply address, per GOVERNANCE.md "Git and Commit Rules" git config --get commit.gpgsign # true -git config --get gpg.format # ssh -git config --local --get user.email # expect empty - a repo-local override is itself a finding +git config --get user.signingkey # set +git config --get gpg.format # ssh for an SSH key; unset or openpgp for GPG +ssh-add -L # SSH: at least one loaded key. GPG: gpg --list-secret-keys ``` -If any of the first three is wrong or absent, that 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. +Signing is **SSH or GPG**, so judge the last two 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 # expect empty - a repo-local override is itself a finding +``` After the first commit, confirm it took with `git log -1 --format='%G? %an <%ae>'`, which prints the signature status letter followed by the identity, so `G` plus the expected `noreply` address is the passing result. `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. @@ -79,6 +86,7 @@ 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 From eb15c5861b9a1b0ad5dea8a31d45ca3ca3609d78 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 13:45:41 -0700 Subject: [PATCH 4/7] Check the committer identity too, not the author alone The post-commit check read %an <%ae>. The rule it verifies covers both: "The author and committer on every agent-authored commit are the GitHub noreply address of the account whose key signs the commit". GitHub verifies the signature against the committer, which this repo's own docs/repo-config-carry.md states while explaining a history rewrite. An author-only read passes on the case most likely to produce the fault. A rebase, amend, or cherry-pick rewrites the committer and leaves the author intact, so the wrong identity lands in the field the check never looked at and the check reports success. Co-Authored-By: Claude Opus 5 (1M context) --- STANDUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STANDUP.md b/STANDUP.md index e1f74a59..e700e7af 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -26,7 +26,7 @@ After `git init` and before the first commit, confirm the repo added no override git config --local --get user.email # expect empty - a repo-local override is itself a finding ``` -After the first commit, confirm it took with `git log -1 --format='%G? %an <%ae>'`, which prints the signature status letter followed by the identity, so `G` plus the expected `noreply` address is the passing result. `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. +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 From e412c45015c5bb0efc4eea04d3d3c9a002ef3ae4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 13:51:01 -0700 Subject: [PATCH 5/7] Read the global scope explicitly, and judge the local check by output Two suppressed findings, both reproduced before fixing. The pre-init block read the effective config and claimed it works in any directory. It does, but it answers a different question depending on where it runs: inside a repository carrying a local override, `git config --get user.email` returns that repository's identity, not the host's. Reproduced by setting user.email to wrong@example.com in a scratch repo, where the effective read returned the override while the global stayed correct. The block now reads --global explicitly, which is the scope the step is actually verifying, and the post-init --local check is what proves nothing shadows it. Two narrow scopes, rather than one broad read that silently changes meaning with the working directory. The local check also treated an unset key as an empty success. It exits 1 with no output, so a set -e script aborts on the passing case and a reader who tests the exit status inverts the check. The finding is a printed value, never the exit code, and a tolerant form is given for set -e. Every command in the step was executed as written before committing: from a non-repo directory, from inside a repo carrying a bogus override, and under set -e. Co-Authored-By: Claude Opus 5 (1M context) --- STANDUP.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/STANDUP.md b/STANDUP.md index e700e7af..ae914400 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -8,24 +8,28 @@ Standing up a repo is **applying the manifests until the audit passes**, nothing 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. These read the effective configuration and work in any directory, so run them first, before there is a repo: +**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 --get user.email # the GitHub noreply address, per GOVERNANCE.md "Git and Commit Rules" -git config --get commit.gpgsign # true -git config --get user.signingkey # set -git config --get gpg.format # ssh for an SSH key; unset or openpgp for GPG -ssh-add -L # SSH: at least one loaded key. GPG: gpg --list-secret-keys +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 +ssh-add -L # SSH: at least one loaded key. GPG: gpg --list-secret-keys ``` +`--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. + Signing is **SSH or GPG**, so judge the last two 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 # expect empty - a repo-local override is itself a finding +git config --local --get user.email # 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. Under `set -e`, write it `git config --local --get user.email || true` so the expected case does not abort the script. + 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 From 6a0952ecbf77b0eb96491b8e267f80e9ed605f90 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 13:56:32 -0700 Subject: [PATCH 6/7] Make the step 0 snippets runnable on their own Three suppressed findings, all accepted. Two share one principle: a snippet presented for copy-paste has to be correct by itself, because the paragraph explaining it does not travel with it. The agent check ran ssh-add -L with the GPG alternative mentioned only in a trailing comment, so a GPG host copying the block ran an SSH-specific command and saw it fail. Both forms are now their own line, with the prose saying to run the one matching the configured format rather than both. The local-override check carried its set -e caveat in the following paragraph instead of the snippet. The tolerant tail is now in the command, so a copy into a strict script does not abort on the passing case. The paragraph still states that the finding is a printed value and never the exit code, which is the part a reader has to understand rather than paste. The verbatim section definition in spec/section-model.md ended on an elliptical "and the wording is not", which is grammatical but asks the reader to supply the missing predicate in a definition that should not need parsing twice. It now reads "applicability varies by repo while the wording does not". Every command was run as written before committing, from a non-repo directory and under set -e. gpg --list-secret-keys executes here but this host signs over SSH and holds no secret key, so its passing output is documented rather than observed. Co-Authored-By: Claude Opus 5 (1M context) --- STANDUP.md | 9 +++++---- spec/section-model.md | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/STANDUP.md b/STANDUP.md index ae914400..dcea8f22 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -15,20 +15,21 @@ git config --global --get user.email # the GitHub noreply address, per GO 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 -ssh-add -L # SSH: at least one loaded key. GPG: gpg --list-secret-keys +ssh-add -L # SSH host: at least one loaded key +gpg --list-secret-keys # GPG host instead: the signing key is present ``` `--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. -Signing is **SSH or GPG**, so judge the last two 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. +Run the agent check that matches the configured format, not both. 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 # expect no output +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. Under `set -e`, write it `git config --local --get user.email || true` so the expected case does not abort the script. +**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. diff --git a/spec/section-model.md b/spec/section-model.md index dc41709f..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, since applicability is per-repo and 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. From e28935fd4a18a48e4a281d47b1312111ed252597 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 14:01:01 -0700 Subject: [PATCH 7/7] Branch the agent check instead of listing both forms Listing ssh-add -L and gpg --list-secret-keys as consecutive lines reads as a sequence to run, while the paragraph below said to run only the one matching the configured format. An SSH host, which need not have gpg installed at all, would fail a verification step it actually passes, and step 0 routes a failure to the maintainer, so the false positive becomes a false bug report. The two forms now sit in one conditional keyed on gpg.format, so the block selects the right check instead of asking the reader to. Both forms stay visible, which was the round 5 finding, and only one executes, which is the round 6 finding. Those two pulled in opposite directions and a branch is what satisfies both. Verified on both paths: run verbatim here, where gpg.format is ssh and the ssh branch returns the loaded key, and again with gpg absent from PATH, where the ssh branch is taken and gpg is never invoked. Co-Authored-By: Claude Opus 5 (1M context) --- STANDUP.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/STANDUP.md b/STANDUP.md index dcea8f22..9735c3c0 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -15,13 +15,14 @@ git config --global --get user.email # the GitHub noreply address, per GO 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 -ssh-add -L # SSH host: at least one loaded key -gpg --list-secret-keys # GPG host instead: the signing key is present + +# 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. -Run the agent check that matches the configured format, not both. 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. +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: