diff --git a/.agents/skills/audit-a-repo/SKILL.md b/.agents/skills/audit-a-repo/SKILL.md index 01ee0b2b..b13106ab 100644 --- a/.agents/skills/audit-a-repo/SKILL.md +++ b/.agents/skills/audit-a-repo/SKILL.md @@ -22,7 +22,7 @@ The audit is the fleet's measurement procedure, and the two failure shapes it gu - **Know what the runner does and does not prove.** `spec/audit.py` mechanizes the deterministic subset only: settings, rulesets, secret names, file and section presence, verbatim hashing, interface wiring, Dependabot coverage, branch facts. It evaluates no check under a type in `spec/project-types.json`, so every per-type check is judged by hand, and a clean run is no evidence for them (`AUDIT.md` section 4). Silence from a tool that was never looking reads exactly like a pass. - **Judge letter and intent per check** and keep the vocabulary: letter miss with intent satisfied is a drift finding, both missing is a defect, and operational is binary over the applicable set (`AUDIT.md` sections 4 and 7). Do not invent a parallel scheme. - **Assert the Actions implement `WORKFLOW.md`** by outcome, not by matching catalog snippets byte for byte: the 5A static audit with a `file:line` citation per applicable guarantee, then the 5B trace scenarios (`AUDIT.md` section 5). The `workflow-ci-contract` skill summarizes that contract. -- **Diff live settings, rulesets, and secrets** against `repo-config/` with the normalized comparisons `AUDIT.md` section 6 gives, using its commands rather than hand-rolled ones, since the normalization details (rule sorting, `bypass_actors` deliberately excluded, the operational develop payload) are where hand-rolled diffs go wrong. +- **Check live settings, rulesets, and secrets from a hub checkout at `main`** with `AUDIT.md` section 6. Run `repo-config/configure.sh check` with the target repository and model rather than constructing a local comparison. The hub payloads are the only repository-configuration source. ## Reporting diff --git a/.agents/skills/comment-and-doc-style/SKILL.md b/.agents/skills/comment-and-doc-style/SKILL.md index dc834f25..e753cd03 100644 --- a/.agents/skills/comment-and-doc-style/SKILL.md +++ b/.agents/skills/comment-and-doc-style/SKILL.md @@ -194,7 +194,7 @@ file type, operational-repo overrides, extensionless-script pins, and auditing, ## Carried files reference no coordination machinery `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md` never reference the template repo +the `spec/` files and the carried `AUDIT.md` never reference the template repo (in prose or a link), and never name a sibling fleet repo as an illustrative example. State the behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies the destination out of band. A contextually relevant link to a related project (the image this diff --git a/.agents/skills/comment-and-doc-style/references/carried-doc-references.md b/.agents/skills/comment-and-doc-style/references/carried-doc-references.md index 972c63f5..125bf0f8 100644 --- a/.agents/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.agents/skills/comment-and-doc-style/references/carried-doc-references.md @@ -7,7 +7,7 @@ doc. ## Which files this governs `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md`, the files the fleet carries +the `spec/` files and the carried `AUDIT.md`, the files the fleet carries verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template content only. A repo's own `README.md` and topical docs are its own content, never carried verbatim, and this rule does not reach them. diff --git a/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index f30abd0a..b0a1c1a7 100644 --- a/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.agents/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -8,18 +8,13 @@ covers that case). ## Configuring branch protection: don't hand-build the rules -Reconstructing rulesets by hand is error-prone and has gone wrong on past ports. First delete -**all** legacy classic branch-protection rules and any stray rulesets (rulesets are the *only* -mechanism used), then create **exactly two rulesets named `develop` and `main`** by importing the -committed `repo-config/*.json` ruleset payloads via `gh api -X POST "repos///rulesets"` -(`gh ruleset` is read-only). The names are load-bearing, other governance content and the -workflows reference them. Operational repos import `repo-config/operational/develop.json` as their -`develop` ruleset (the `main` ruleset is shared), and the hub's `repo-config/configure.sh`, run -from a hub checkout against the repository named on its command line, selects the right `develop` -payload from the registry `workflowModel` automatically. **Brownfield repos** (pre-existing -history) need an extra step: `Require signed commits` rejects legacy unsigned commits and the -admin bypass does not cover `git push --force`, so re-signing requires temporarily disabling the -ruleset. See `repo-config/README.md` "Rulesets" for the configured state. +Delete **all** classic branch-protection rules and stray rulesets because rulesets are the only +protection mechanism. Create **exactly two rulesets named `develop` and `main`** from the hub's +`repo-config/*.json` payloads. Run `repo-config/configure.sh apply / +release|operational` from a hub checkout at `main`. The names are load-bearing because governance +content and workflows reference them. The registry `workflowModel` selects the `develop` payload +for a registered repository. Pass the model explicitly for a repository outside the registry. +See `repo-config/README.md` "Rulesets" for the configured state. ## Executing a `develop -> main` promotion safely diff --git a/.agents/skills/resync-a-repo/SKILL.md b/.agents/skills/resync-a-repo/SKILL.md index 308dcbc9..cf5bd6ae 100644 --- a/.agents/skills/resync-a-repo/SKILL.md +++ b/.agents/skills/resync-a-repo/SKILL.md @@ -63,7 +63,7 @@ earlier in the session. 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` against the repo by name, + `repo-config/configure.sh check / release|operational` from the hub at `main`, then `apply` for what it reports, never from a carried copy. 6. **Intent files last, and by hand,** since nothing mechanical judges these. diff --git a/.agents/skills/standup-a-repo/SKILL.md b/.agents/skills/standup-a-repo/SKILL.md index 56c66a1e..54c277a5 100644 --- a/.agents/skills/standup-a-repo/SKILL.md +++ b/.agents/skills/standup-a-repo/SKILL.md @@ -75,7 +75,7 @@ maintainer can supply what section 0A lists. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` and check with the same + `repo-config/configure.sh apply owner/repo release|operational` from the hub at `main` and check with the same command's `check` subcommand, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood diff --git a/.agents/skills/workflow-ci-contract/SKILL.md b/.agents/skills/workflow-ci-contract/SKILL.md index b36ec4a1..e02fdfb4 100644 --- a/.agents/skills/workflow-ci-contract/SKILL.md +++ b/.agents/skills/workflow-ci-contract/SKILL.md @@ -21,7 +21,7 @@ description: >- ## Style Rules That Break in One-Line Diffs - **Pin every action to a commit SHA** with a trailing `# vX.Y.Z` comment, first-party included. The one documented no-pin exception is `dotnet/nbgv@master`. Invent no others. -- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and `repo-config/{develop,main}.json` in lockstep, or required-check enforcement silently breaks. +- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and the hub's `repo-config/` payloads in lockstep, or required-check enforcement silently breaks. - **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'` with `cancel-in-progress: true`. The publisher is the documented exception: a global ref-independent group with `cancel-in-progress: false`, so publishes serialize and never cancel mid-push. - **Shells**: every multi-line bash `run:` starts `set -Eeuo pipefail`. Multi-line `if:` uses `>-`, never `|`. - **Boolean inputs** are declared in both trigger blocks and compared against both forms, `${{ inputs.foo == true || inputs.foo == 'true' }}`, since `workflow_dispatch` delivers strings. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index 386480a3..77c3c3aa 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -2a00d50463f7d04c +d8de3601d734f326 diff --git a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md index 01ee0b2b..b13106ab 100644 --- a/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md @@ -22,7 +22,7 @@ The audit is the fleet's measurement procedure, and the two failure shapes it gu - **Know what the runner does and does not prove.** `spec/audit.py` mechanizes the deterministic subset only: settings, rulesets, secret names, file and section presence, verbatim hashing, interface wiring, Dependabot coverage, branch facts. It evaluates no check under a type in `spec/project-types.json`, so every per-type check is judged by hand, and a clean run is no evidence for them (`AUDIT.md` section 4). Silence from a tool that was never looking reads exactly like a pass. - **Judge letter and intent per check** and keep the vocabulary: letter miss with intent satisfied is a drift finding, both missing is a defect, and operational is binary over the applicable set (`AUDIT.md` sections 4 and 7). Do not invent a parallel scheme. - **Assert the Actions implement `WORKFLOW.md`** by outcome, not by matching catalog snippets byte for byte: the 5A static audit with a `file:line` citation per applicable guarantee, then the 5B trace scenarios (`AUDIT.md` section 5). The `workflow-ci-contract` skill summarizes that contract. -- **Diff live settings, rulesets, and secrets** against `repo-config/` with the normalized comparisons `AUDIT.md` section 6 gives, using its commands rather than hand-rolled ones, since the normalization details (rule sorting, `bypass_actors` deliberately excluded, the operational develop payload) are where hand-rolled diffs go wrong. +- **Check live settings, rulesets, and secrets from a hub checkout at `main`** with `AUDIT.md` section 6. Run `repo-config/configure.sh check` with the target repository and model rather than constructing a local comparison. The hub payloads are the only repository-configuration source. ## Reporting diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md index dc834f25..e753cd03 100644 --- a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md @@ -194,7 +194,7 @@ file type, operational-repo overrides, extensionless-script pins, and auditing, ## Carried files reference no coordination machinery `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md` never reference the template repo +the `spec/` files and the carried `AUDIT.md` never reference the template repo (in prose or a link), and never name a sibling fleet repo as an illustrative example. State the behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies the destination out of band. A contextually relevant link to a related project (the image this diff --git a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md index 972c63f5..125bf0f8 100644 --- a/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.claude-plugin/fleet-skills/skills/comment-and-doc-style/references/carried-doc-references.md @@ -7,7 +7,7 @@ doc. ## Which files this governs `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md`, the files the fleet carries +the `spec/` files and the carried `AUDIT.md`, the files the fleet carries verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template content only. A repo's own `README.md` and topical docs are its own content, never carried verbatim, and this rule does not reach them. diff --git a/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index f30abd0a..b0a1c1a7 100644 --- a/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -8,18 +8,13 @@ covers that case). ## Configuring branch protection: don't hand-build the rules -Reconstructing rulesets by hand is error-prone and has gone wrong on past ports. First delete -**all** legacy classic branch-protection rules and any stray rulesets (rulesets are the *only* -mechanism used), then create **exactly two rulesets named `develop` and `main`** by importing the -committed `repo-config/*.json` ruleset payloads via `gh api -X POST "repos///rulesets"` -(`gh ruleset` is read-only). The names are load-bearing, other governance content and the -workflows reference them. Operational repos import `repo-config/operational/develop.json` as their -`develop` ruleset (the `main` ruleset is shared), and the hub's `repo-config/configure.sh`, run -from a hub checkout against the repository named on its command line, selects the right `develop` -payload from the registry `workflowModel` automatically. **Brownfield repos** (pre-existing -history) need an extra step: `Require signed commits` rejects legacy unsigned commits and the -admin bypass does not cover `git push --force`, so re-signing requires temporarily disabling the -ruleset. See `repo-config/README.md` "Rulesets" for the configured state. +Delete **all** classic branch-protection rules and stray rulesets because rulesets are the only +protection mechanism. Create **exactly two rulesets named `develop` and `main`** from the hub's +`repo-config/*.json` payloads. Run `repo-config/configure.sh apply / +release|operational` from a hub checkout at `main`. The names are load-bearing because governance +content and workflows reference them. The registry `workflowModel` selects the `develop` payload +for a registered repository. Pass the model explicitly for a repository outside the registry. +See `repo-config/README.md` "Rulesets" for the configured state. ## Executing a `develop -> main` promotion safely diff --git a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md index 308dcbc9..cf5bd6ae 100644 --- a/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md @@ -63,7 +63,7 @@ earlier in the session. 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` against the repo by name, + `repo-config/configure.sh check / release|operational` from the hub at `main`, then `apply` for what it reports, never from a carried copy. 6. **Intent files last, and by hand,** since nothing mechanical judges these. diff --git a/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md b/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md index 56c66a1e..54c277a5 100644 --- a/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md @@ -75,7 +75,7 @@ maintainer can supply what section 0A lists. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` and check with the same + `repo-config/configure.sh apply owner/repo release|operational` from the hub at `main` and check with the same command's `check` subcommand, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood diff --git a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md index b36ec4a1..e02fdfb4 100644 --- a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md @@ -21,7 +21,7 @@ description: >- ## Style Rules That Break in One-Line Diffs - **Pin every action to a commit SHA** with a trailing `# vX.Y.Z` comment, first-party included. The one documented no-pin exception is `dotnet/nbgv@master`. Invent no others. -- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and `repo-config/{develop,main}.json` in lockstep, or required-check enforcement silently breaks. +- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and the hub's `repo-config/` payloads in lockstep, or required-check enforcement silently breaks. - **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'` with `cancel-in-progress: true`. The publisher is the documented exception: a global ref-independent group with `cancel-in-progress: false`, so publishes serialize and never cancel mid-push. - **Shells**: every multi-line bash `run:` starts `set -Eeuo pipefail`. Multi-line `if:` uses `>-`, never `|`. - **Boolean inputs** are declared in both trigger blocks and compared against both forms, `${{ inputs.foo == true || inputs.foo == 'true' }}`, since `workflow_dispatch` delivers strings. diff --git a/.github/actions/prose-gate/prose_lint.py b/.github/actions/prose-gate/prose_lint.py index b4cab30d..2d3272ad 100755 --- a/.github/actions/prose-gate/prose_lint.py +++ b/.github/actions/prose-gate/prose_lint.py @@ -333,20 +333,16 @@ def path_candidate(token: str, in_span: bool = True) -> str | None: return token.removeprefix("./") -# Paths retired per a `retire` disposition in `spec/divergences.json`, per GOVERNANCE.md "Hub-Hosted Tooling". -# Most are hub-hosted, so a mention names the hub's copy rather than a file this tree lost. -# One is retired outright with no hub replacement (`build-datebadge-task.yml`): the mention still names a real, intentional deletion rather than a stale reference, so the same exemption applies. -# A caller stub that persists with edited content, such as `run-periodic-codegen-pull-request.yml`, is neither of these: it stays a per-repo file, so it carries no entry here at all. -# Carried text naming a tool is required to name it that way, so the mention is never a dead path. -# The manifest exemption cannot reach this class, since no repository carries `spec/files.json`. -# Downstream that set is empty, and a repository that retired its copy carries the full signature. -# It surfaces at the promotion, whose diff base brings the retirement and its prose into scope. -# That is the gate with the least room to fix it, and a ruleset bypass is the only local remedy. -# Held as a literal because the prose-gate action fetches this one file with no hub tree beside it. -# The `retire` dispositions in `spec/divergences.json` are the source, and a hub test asserts this. +# Paths with a `retire` disposition remain valid references to a hub-hosted tool or a declared deletion. +# The action fetches this file without the hub tree, so a test keeps this literal set equal to the ledger. HUB_HOSTED = frozenset( { "repo-config/configure.sh", + "repo-config/develop.json", + "repo-config/operational/develop.json", + "repo-config/main.json", + "repo-config/README.md", + "repo-config/settings.json", ".github/workflows/get-version-task.yml", ".github/workflows/publish-plan-task.yml", ".github/workflows/build-release-task.yml", @@ -461,17 +457,9 @@ def shallow_checkout(root: Path) -> bool: return r.returncode == 0 and r.stdout.strip() == "true" -def operational_checkout(root: Path) -> bool: - """Whether this checkout is an operational repository, read from what it carries. - - `spec/files.json` declares `repo-config/operational/develop.json` for the operational model - and `repo-config/develop.json` for the release one, so a repository states its own model and - nothing has to reach the hub registry to ask. The hub itself carries both payloads, being the - template for each, so carrying the release payload decides it. - """ - return (root / "repo-config" / "operational" / "develop.json").is_file() and not ( - root / "repo-config" / "develop.json" - ).is_file() +def is_operations_runbook(path: Path, root: Path | None) -> bool: + """Whether this path is the repository operations runbook.""" + return root is not None and path.resolve() == (root / "OPERATIONS.md").resolve() def quoted(paths) -> str: @@ -1528,7 +1516,7 @@ def check_file(path: Path, rules: set[str], root: Path | None = None) -> list[tu line = line.rstrip("\r") # Judged before the fence and inline-code handling below, deliberately. # A path pasted inside a fenced transcript is the same exposure as one in a sentence. - if "home-path" in rules: + if "home-path" in rules and not is_operations_runbook(path, root): out.extend(home_path_findings(i, line)) if CODE_FENCE.match(line): in_fence = not in_fence @@ -1694,18 +1682,6 @@ def main(argv: list[str] | None = None) -> int: first = Path(scan_paths[0]) scan_root = first if first.is_dir() else first.parent - # An operational repository's runbook carries the literal path an operator types. - # That is the repository's own content, not an agent quoting an environment it observed. - # The skip is announced, since a rule that silently stops running reads as one that passed. - # That is the same failure the diff-scope floor below exists to prevent. - if "home-path" in rules and operational_checkout(scan_root): - rules.discard("home-path") - print( - "note: home-path is not checked in an operational repository, where an absolute " - "path is the operator instruction rather than observed data.", - file=sys.stderr, - ) - # Announced for the same reason the skip above is, a silent stand-down reads as a pass. if "dead-path" in rules and git_roots and shallow_checkout(scan_root): rules.discard("dead-path") diff --git a/.github/skills/audit-a-repo/SKILL.md b/.github/skills/audit-a-repo/SKILL.md index 01ee0b2b..b13106ab 100644 --- a/.github/skills/audit-a-repo/SKILL.md +++ b/.github/skills/audit-a-repo/SKILL.md @@ -22,7 +22,7 @@ The audit is the fleet's measurement procedure, and the two failure shapes it gu - **Know what the runner does and does not prove.** `spec/audit.py` mechanizes the deterministic subset only: settings, rulesets, secret names, file and section presence, verbatim hashing, interface wiring, Dependabot coverage, branch facts. It evaluates no check under a type in `spec/project-types.json`, so every per-type check is judged by hand, and a clean run is no evidence for them (`AUDIT.md` section 4). Silence from a tool that was never looking reads exactly like a pass. - **Judge letter and intent per check** and keep the vocabulary: letter miss with intent satisfied is a drift finding, both missing is a defect, and operational is binary over the applicable set (`AUDIT.md` sections 4 and 7). Do not invent a parallel scheme. - **Assert the Actions implement `WORKFLOW.md`** by outcome, not by matching catalog snippets byte for byte: the 5A static audit with a `file:line` citation per applicable guarantee, then the 5B trace scenarios (`AUDIT.md` section 5). The `workflow-ci-contract` skill summarizes that contract. -- **Diff live settings, rulesets, and secrets** against `repo-config/` with the normalized comparisons `AUDIT.md` section 6 gives, using its commands rather than hand-rolled ones, since the normalization details (rule sorting, `bypass_actors` deliberately excluded, the operational develop payload) are where hand-rolled diffs go wrong. +- **Check live settings, rulesets, and secrets from a hub checkout at `main`** with `AUDIT.md` section 6. Run `repo-config/configure.sh check` with the target repository and model rather than constructing a local comparison. The hub payloads are the only repository-configuration source. ## Reporting diff --git a/.github/skills/comment-and-doc-style/SKILL.md b/.github/skills/comment-and-doc-style/SKILL.md index dc834f25..e753cd03 100644 --- a/.github/skills/comment-and-doc-style/SKILL.md +++ b/.github/skills/comment-and-doc-style/SKILL.md @@ -194,7 +194,7 @@ file type, operational-repo overrides, extensionless-script pins, and auditing, ## Carried files reference no coordination machinery `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md` never reference the template repo +the `spec/` files and the carried `AUDIT.md` never reference the template repo (in prose or a link), and never name a sibling fleet repo as an illustrative example. State the behavior a carried rule needs, not the coordination flow that produced it, the maintainer supplies the destination out of band. A contextually relevant link to a related project (the image this diff --git a/.github/skills/comment-and-doc-style/references/carried-doc-references.md b/.github/skills/comment-and-doc-style/references/carried-doc-references.md index 972c63f5..125bf0f8 100644 --- a/.github/skills/comment-and-doc-style/references/carried-doc-references.md +++ b/.github/skills/comment-and-doc-style/references/carried-doc-references.md @@ -7,7 +7,7 @@ doc. ## Which files this governs `AGENTS.md`, `GOVERNANCE.md`, `CODESTYLE.md`, `WORKFLOW.md`, `.github/copilot-instructions.md`, -the `repo-config/` and `spec/` files, and the carried `AUDIT.md`, the files the fleet carries +the `spec/` files and the carried `AUDIT.md`, the files the fleet carries verbatim or at `intent` fidelity from the hub into every repo. This rule governs carried template content only. A repo's own `README.md` and topical docs are its own content, never carried verbatim, and this rule does not reach them. diff --git a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md index f30abd0a..b0a1c1a7 100644 --- a/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md +++ b/.github/skills/operational-vs-release-workflow/references/branch-protection-and-promotion.md @@ -8,18 +8,13 @@ covers that case). ## Configuring branch protection: don't hand-build the rules -Reconstructing rulesets by hand is error-prone and has gone wrong on past ports. First delete -**all** legacy classic branch-protection rules and any stray rulesets (rulesets are the *only* -mechanism used), then create **exactly two rulesets named `develop` and `main`** by importing the -committed `repo-config/*.json` ruleset payloads via `gh api -X POST "repos///rulesets"` -(`gh ruleset` is read-only). The names are load-bearing, other governance content and the -workflows reference them. Operational repos import `repo-config/operational/develop.json` as their -`develop` ruleset (the `main` ruleset is shared), and the hub's `repo-config/configure.sh`, run -from a hub checkout against the repository named on its command line, selects the right `develop` -payload from the registry `workflowModel` automatically. **Brownfield repos** (pre-existing -history) need an extra step: `Require signed commits` rejects legacy unsigned commits and the -admin bypass does not cover `git push --force`, so re-signing requires temporarily disabling the -ruleset. See `repo-config/README.md` "Rulesets" for the configured state. +Delete **all** classic branch-protection rules and stray rulesets because rulesets are the only +protection mechanism. Create **exactly two rulesets named `develop` and `main`** from the hub's +`repo-config/*.json` payloads. Run `repo-config/configure.sh apply / +release|operational` from a hub checkout at `main`. The names are load-bearing because governance +content and workflows reference them. The registry `workflowModel` selects the `develop` payload +for a registered repository. Pass the model explicitly for a repository outside the registry. +See `repo-config/README.md` "Rulesets" for the configured state. ## Executing a `develop -> main` promotion safely diff --git a/.github/skills/resync-a-repo/SKILL.md b/.github/skills/resync-a-repo/SKILL.md index 308dcbc9..cf5bd6ae 100644 --- a/.github/skills/resync-a-repo/SKILL.md +++ b/.github/skills/resync-a-repo/SKILL.md @@ -63,7 +63,7 @@ earlier in the session. 4. **Interface workflows.** Honor the named contract, required jobs, the ruleset-bound check name, the artifact-name handoff, rather than copying bytes. 5. **Settings, rulesets, and secrets.** Run - `repo-config/configure.sh check / release|operational` against the repo by name, + `repo-config/configure.sh check / release|operational` from the hub at `main`, then `apply` for what it reports, never from a carried copy. 6. **Intent files last, and by hand,** since nothing mechanical judges these. diff --git a/.github/skills/standup-a-repo/SKILL.md b/.github/skills/standup-a-repo/SKILL.md index 56c66a1e..54c277a5 100644 --- a/.github/skills/standup-a-repo/SKILL.md +++ b/.github/skills/standup-a-repo/SKILL.md @@ -75,7 +75,7 @@ maintainer can supply what section 0A lists. 8. **Settings, rulesets, and secrets.** STANDUP.md section 4: confirm the remote and the GitHub repository agree before running anything else here, then apply with - `repo-config/configure.sh apply owner/repo release|operational` and check with the same + `repo-config/configure.sh apply owner/repo release|operational` from the hub at `main` and check with the same command's `check` subcommand, never from a hand-built or carried copy. 9. **Verify with the audit.** STANDUP.md section 5: run `AUDIT.md` end to end. The repo is stood diff --git a/.github/skills/workflow-ci-contract/SKILL.md b/.github/skills/workflow-ci-contract/SKILL.md index b36ec4a1..e02fdfb4 100644 --- a/.github/skills/workflow-ci-contract/SKILL.md +++ b/.github/skills/workflow-ci-contract/SKILL.md @@ -21,7 +21,7 @@ description: >- ## Style Rules That Break in One-Line Diffs - **Pin every action to a commit SHA** with a trailing `# vX.Y.Z` comment, first-party included. The one documented no-pin exception is `dotnet/nbgv@master`. Invent no others. -- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and `repo-config/{develop,main}.json` in lockstep, or required-check enforcement silently breaks. +- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and the hub's `repo-config/` payloads in lockstep, or required-check enforcement silently breaks. - **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'` with `cancel-in-progress: true`. The publisher is the documented exception: a global ref-independent group with `cancel-in-progress: false`, so publishes serialize and never cancel mid-push. - **Shells**: every multi-line bash `run:` starts `set -Eeuo pipefail`. Multi-line `if:` uses `>-`, never `|`. - **Boolean inputs** are declared in both trigger blocks and compared against both forms, `${{ inputs.foo == true || inputs.foo == 'true' }}`, since `workflow_dispatch` delivers strings. diff --git a/AUDIT.md b/AUDIT.md index fac1abe3..6ebb4051 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -93,43 +93,7 @@ Run [`WORKFLOW.md`][workflow]'s methodology against the repo's **own** Actions: ## 6. Validate Settings, Rulesets, and Secrets -- **General settings** - diff the live repository settings against [`repo-config/settings.json`][repo-config-settings], and confirm the two state-dependent settings: `has_discussions` follows visibility (public on / private off) and `default_branch` is `main`. - - ```bash - live=$(gh api "repos//" --jq '{has_wiki,has_projects,allow_merge_commit,allow_squash_merge,allow_rebase_merge,allow_auto_merge,allow_update_branch,delete_branch_on_merge}') - diff <(jq -S . repo-config/settings.json) <(jq -S . <<<"$live") \ - && echo "settings: in sync" || echo "settings: DRIFT" - ``` - -- **Rulesets** - diff each live ruleset against the committed expected payload with a normalized comparison (sort the order-insensitive `rules[]` on each rule's whole content before diffing, so a reordered but equivalent ruleset does not read as drift). The compared subset is `name`, `target`, `enforcement`, `conditions` and `rules`, and `bypass_actors` sits deliberately outside it, which is the same subset and the same sort key [`spec/audit.py`][audit-runner] uses. Who may bypass a ruleset is a per-repository human decision taken in the UI, no payload declares one, and [`repo-config/configure.sh`][repo-config] treats it that way in both modes, writing the live list back unchanged on `apply` and reporting it without asserting on `check`. Comparing it here would contradict that and report a ruleset finding against every repository that has any bypass actor, which is the field's normal state rather than a deviation: - - ```bash - # bypass_actors stays outside the projection, since no payload declares one and jq cannot sort the null that leaves. - # Rules sort on each rule's whole content, matching the key normalize_ruleset in audit.py sorts by. - # Sorting on .type alone leaves two rules of one type in input order, so a reordered pair would read as drift. - # canon sorts keys at every depth before serializing, because the committed payload is written key-sorted and the API returns its own order, so a bare tojson gives the same rule two different sort keys. - # It recurses rather than calling walk/1, which the declared floor does make available, because the recursion costs nothing and compiles below the floor as well. - # A host on jq 1.5 would not degrade on walk, it would fail to compile the filter and report drift on every ruleset it never compared, which is what repo-config/configure.sh defines its own recursion to avoid. - canon='def canon: . as $in | if type == "object" then reduce (keys_unsorted|sort)[] as $k ({}; . + { ($k): ($in[$k]|canon) }) elif type == "array" then map(canon) else . end;' - norm="$canon"'{name,target,enforcement,conditions,rules} | .rules|=sort_by(canon|tojson)' - # Model-aware expected payload: an operational repo's develop ruleset diffs against - # operational/develop.json (registry workflowModel; the same selection audit.py makes). - model=$(jq -r --arg n "" '(.repos[] | select(.name==$n) | .workflowModel) // .defaults.workflowModel // "release"' registry/repos.json) - # Paginate so later-page rulesets count: --paginate with --jq '.[]' emits one JSON object per ruleset - # across all pages; jq -s re-assembles them into the single array the selections below expect. - rulesets=$(gh api --paginate "repos///rulesets" --jq '.[]' | jq -s '.') - for b in develop main; do - file="repo-config/$b.json" - [ "$b" = "develop" ] && [ "$model" = "operational" ] && file="repo-config/operational/develop.json" - # Exactly one ruleset per name: zero or duplicates is itself a finding - report it, never diff a guess. - count=$(jq --arg n "$b" '[.[] | select(.name==$n)] | length' <<<"$rulesets") - [ "$count" -eq 1 ] || { echo "$b: expected exactly 1 ruleset, found $count (defect/drift)"; continue; } - id=$(jq --arg n "$b" '.[] | select(.name==$n) | .id' <<<"$rulesets") - diff <(jq -S "$norm" "$file") \ - <(gh api "repos///rulesets/$id" --jq '{name,target,enforcement,conditions,rules}' | jq -S "$norm") \ - && echo "$b: in sync" || echo "$b: DRIFT" - done - ``` +- **General settings and rulesets** - fetch the hub and check out `main`. Run `repo-config/configure.sh check / release|operational` from that checkout. Pass the target repository and its registry `workflowModel` explicitly. The command checks the shared settings, state-dependent settings, Dependabot security features, and both rulesets against the hub payloads. It preserves and reports `bypass_actors` without asserting them because bypass authority is a per-repository human decision. - **Secrets** - confirm each required secret exists (name only, not the values). Check the Actions store and, where the mechanism needs it (Docker Hub, codegen App), the Dependabot store too. @@ -196,7 +160,7 @@ Sections 1-9 (the audit and its report) are **read-only** and never touch the ta - **Merge only with explicit maintainer approval.** The agent drives to green and stops. The maintainer merges. - **One focused PR per drift class**, cross-referencing the audit finding. A sprawling all-drifts PR draws many review rounds and never feels done. - **A `hub-only:` finding converges by deleting the file, not by updating it.** It is the one class where the fix removes content, so it is easy to convert into a re-vendor by reflex and end up refreshing a copy that should not exist. Delete the repo's copy and reach the hub's per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. Confirm the disposition is `retire` before deleting anything: an untriaged hit may be the repo's own content at a shared path, and deleting that destroys work the hub never owned. -- **Any deletion sweeps the inbound references to the path, and the sweep is part of the deletion rather than follow-up.** This governs every removal and not only a `hub-only:` one, because nothing about it depends on who owned the file: the removal is one edit and finishing the job usually takes several more, so grep the path tree-wide first and read every hit. Then read the files whose job is to say what the repo holds, since a grep for the path finds uses of the file and misses descriptions of it: `GOVERNANCE.md` "Repository Layout" is the one that has gone stale this way, calling a deleted script "the apply script", which names no path and survives every search for one. A link whose target has an equivalent elsewhere is re-pointed at it, the hub's copy being that equivalent for a hub-hosted file, a **runnable command** citing the path is rewritten to the invocation that still works, and a mention with no equivalent anywhere is removed along with its reference definition, which [GOVERNANCE.md "Documentation Style Conventions"][governance-documentation-style] requires because an orphaned definition fails the no-unused-defs rule. Measured rather than hypothetical, in both directions: retiring `configure.sh` makes five lines of one repo's carried `repo-config/README.md` wrong, two of them commands a reader would run, and deleting a repo-owned nested `AGENTS.md` took three edits across two files, where removing the inline link alone tripped `MD053` on the definition it orphaned and would have failed CI rather than merely breaking prose. +- **Any deletion sweeps the inbound references to the path, and the sweep is part of the deletion rather than follow-up.** This governs every removal and not only a `hub-only:` one. Grep the path tree-wide and read every hit. Then read the files whose job is to say what the repo holds because a path search cannot find a description that names no path. Point a link at the hub's copy when that is the equivalent. Rewrite a runnable command to the invocation that works. Remove a mention with no equivalent and remove its reference definition in the same edit, per [GOVERNANCE.md "Documentation Style Conventions"][governance-documentation-style]. - **Fix systemic drift in the hub, not per repo.** When many repos share a drift, fix the spec/rule (or add a machine check) here and let a re-audit re-flag it, rather than hand-patching each repo for the shared cause. The convergence model: the hub audits and the agent **applies** the fixes via target PRs, and the maintainer gates every merge. It supersedes any "the hub only reports; downstream operators apply by hand" framing. @@ -219,7 +183,6 @@ The convergence model: the hub audits and the agent **applies** the fixes via ta [readme-sections]: https://github.com/ptr727/ProjectTemplate/blob/main/spec/readme-sections.json [readme-structure]: https://github.com/ptr727/ProjectTemplate/blob/main/spec/readme-structure.md [repo-config]: https://github.com/ptr727/ProjectTemplate/tree/main/repo-config -[repo-config-settings]: ./repo-config/settings.json [reports]: https://github.com/ptr727/ProjectTemplate/tree/main/reports [repos]: https://github.com/ptr727/ProjectTemplate/blob/main/registry/repos.json [resync]: https://github.com/ptr727/ProjectTemplate/blob/main/RESYNC.md diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 88dcf1f9..0765fd52 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -199,8 +199,8 @@ This section and [`WORKFLOW.md`](./WORKFLOW.md) keep the full rules, this sectio - **Action pinning**: pin **every** action, first-party (`actions/*`) and third-party alike, to a commit SHA with a trailing `# vX.Y.Z` comment, so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. Use `# vX` (major-only) only when the upstream's floating major tag doesn't correspond to a specific patch/minor release SHA, since pinning to the floating-tag SHA still gives the SHA guarantee, the version comment just records the major line. Documented exception (no SHA pin at all): `dotnet/nbgv` is consumed via `@master` because the upstream tag stream lags `master` substantially and Dependabot's tag-tracking would propose a downgrade. **This applies to repo-owned build-layer leaves too**, since a leaf owning its build specifics is not a reason to use floating tags, and Dependabot still bumps SHA pins (updating the SHA + version comment). - **Filename**: reusable workflows (those with `on: workflow_call`) end in `-task.yml`. Entry-point workflows (`on: push` / `pull_request` / `schedule` / `workflow_dispatch`) do NOT use the `-task` suffix. They end with what they do: `-pull-request.yml`, `-release.yml`, etc. The suffix carries semantic meaning: a `-task.yml` file is meant to be `uses:`-d, never triggered directly. - **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`), and entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee. -- **Job and step `name:` suffixes**: every job's `name:` ends in **"job"** and every step's `name:` ends in **"step"**, including the PR-gate aggregator, whose `name:` is a required-status-check `context:` in a branch ruleset (`Check pull request workflow status job` in `test-pull-request.yml`). A ruleset-bound job's `name:` and its ruleset `context:` are the **same string**: rename them **together**, updating the live ruleset and `repo-config/{develop,main}.json` in lockstep with the job `name:`, never one without the other, or required-status-check enforcement silently breaks. There is no un-suffixed exception. -- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exceptions** (both record the rationale inline in their header comment): (1) [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because the merge-bot's job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order, because cancellation would leave auto-merge in an inconsistent state. (2) The publisher workflow uses both a **global, ref-independent group** (`group: ${{ github.workflow }}`, dropping the usual `-${{ github.ref }}`) and `cancel-in-progress: false`. It publishes shared ref-independent artifacts (both branches' Docker tags/caches and GitHub releases) on schedule/dispatch regardless of the triggering ref, so a ref-scoped group would let a scheduled run (ref `main`) and a manual dispatch (ref `develop`) run concurrently and double-push, and cancelling a publish mid-flight can leave a partially pushed tag set or a half-created release. The global group + queueing serializes every publish run to completion. +- **Job and step `name:` suffixes**: every job's `name:` ends in **"job"** and every step's `name:` ends in **"step"**, including the PR-gate aggregator, whose `name:` is a required-status-check `context:` in a branch ruleset (`Check pull request workflow status job` in `test-pull-request.yml`). A ruleset-bound job's `name:` and its ruleset `context:` are the **same string**: rename them **together**, updating the live ruleset and the hub's `repo-config/` payloads in lockstep with the job `name:`, never one without the other, or required-status-check enforcement silently breaks. There is no un-suffixed exception. +- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exceptions** (both record the rationale inline in their header comment): (1) [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because the merge-bot's job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order, because cancellation would leave auto-merge in an inconsistent state. (2) `.github/workflows/publish-release.yml` uses both a **global, ref-independent group** (`group: ${{ github.workflow }}`, dropping the usual `-${{ github.ref }}`) and `cancel-in-progress: false`. It publishes shared ref-independent artifacts (both branches' Docker tags/caches and GitHub releases) on schedule/dispatch regardless of the triggering ref, so a ref-scoped group would let a scheduled run (ref `main`) and a manual dispatch (ref `develop`) run concurrently and double-push, and cancelling a publish mid-flight can leave a partially pushed tag set or a half-created release. The global group + queueing serializes every publish run to completion. - **Shells**: every bash surface, a multi-line `run:` block and every committed `.sh` script alike, starts with `set -Eeuo pipefail`: fail fast, fail on undefined vars, fail on a failed pipe segment, and let an `ERR` trap inherit into functions, subshells, and command substitutions (`-E`). The `-E` is defense in depth: the fleet ships no `ERR` trap today, so a script that later adds one inherits the behavior instead of silently losing it. - **Conditionals**: multi-line `if:` uses folded scalar `if: >-` so YAML preserves whitespace correctly. Literal block (`if: |`) is wrong because it embeds newlines inside the boolean expression. - **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks, since one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans, and `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms: `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`. @@ -308,10 +308,10 @@ Every repo's GitHub repository details (the About panel) follow a fixed conventi - [`GOVERNANCE.md`](./GOVERNANCE.md), [`CODESTYLE.md`](./CODESTYLE.md), [`WORKFLOW.md`](./WORKFLOW.md), [`AUDIT.md`](./AUDIT.md): the governance and audit docs. This file is the cross-cutting-rules authority. - [`spec/`](./spec/): the machine-readable ground truth, covering project-type requirements, the file/section baseline, secrets, the host tool contract and its floors, and the preferred README structure. - [`registry/repos.json`](./registry/repos.json): the fleet registry, holding every project, its type(s), and its status. -- [`repo-config/`](./repo-config/): branch rulesets, the fleet settings, and the apply script every repo is configured by, kept out of `.github/` (which is Actions-owned). The payloads carry to the fleet and the script is reached here. +- [`repo-config/`](./repo-config/): hub-only branch rulesets, fleet settings, documentation, and the apply script every repo is configured by, kept out of `.github/` (which is Actions-owned). - [`catalog/`](./catalog/): reusable reference snippets (workflows, configs, devcontainers) the audit compares implementations against. - [`scripts/`](./scripts/): the fleet's lint and review tooling, with the deterministic checks also gating CI, for the documented rules no CI linter checks. Its unit tests sit apart under [`scripts/tests/`](./scripts/tests/). Hosted here and reached rather than carried, per "Hub-Hosted Tooling". -- [`docs/`](./docs/): hub-only procedure and measurement docs (host setup, config carry, signing, agent token cost), none of them carried downstream. +- [`docs/`](./docs/): hub-only procedure and measurement docs (host setup, repository configuration, signing, agent token cost), none of them carried downstream. - [`reports/`](./reports/): per-repo audit output. - [`.github/workflows/`](./.github/workflows/): this repo's own lint-only CI and source-only release, the workflow contract it dogfoods. diff --git a/OPERATIONS.md b/OPERATIONS.md index bef6911f..f04da91b 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -73,12 +73,14 @@ Findings are a point-in-time snapshot read live over the API. Re-run before acti ### Apply or verify repository configuration +Run these commands from a hub checkout at `main`. The payloads, registry, and script are hub-hosted. + ```sh repo-config/configure.sh check owner/repo release|operational repo-config/configure.sh apply owner/repo release|operational ``` -**Always pass the command, the repository, and the model.** A bare `repo-config/configure.sh` with no arguments defaults to `apply` against the current repo, so an invocation meant to test whether the script runs performs a live write instead. Never run it without a command. The repository argument matters for the same reason now that the fleet runs this copy rather than its own: an omitted target resolves to this repository, and applying the fleet configuration to the hub while meaning to configure a downstream repo is a well-formed write to the wrong place. The model is the third argument for the same reason. This checkout has the registry beside the script, so a repo the registry does not yet name resolves through `defaults.workflowModel` to `release` rather than aborting, and an operational repo then takes the release `develop` ruleset. +**Always pass the command, the repository, and the model.** A bare `repo-config/configure.sh` with no arguments defaults to `apply` against the current repo, so an invocation meant to test whether the script runs performs a live write instead. Never run it without a command. An omitted target resolves to the hub repository, and applying fleet configuration there while meaning to configure a downstream repo is a well-formed write to the wrong place. The hub checkout has the registry beside the script, so a repo the registry does not name resolves through `defaults.workflowModel` to `release`. An operational repo outside the registry therefore requires the explicit model argument. `check` is read-only and exits non-zero on drift. `apply` is idempotent and drives entirely from the committed payloads, so it is a no-op on a conformant repo. @@ -128,7 +130,7 @@ The `gh api --method PATCH repos/[owner/repo]/pulls/[number]` form still works a - [spec/](./spec/) is the machine-readable ground truth, holding project types, the file and section baseline, and required or forbidden secrets. - [registry/repos.json](./registry/repos.json) is the fleet registry, naming every project with its types, publish mechanism, and status. -- [repo-config/](./repo-config/) holds the branch rulesets, the fleet settings, and the apply script. The payloads carry to the fleet and the script is reached here. It sits outside `.github/`, which is Actions-owned. +- [repo-config/](./repo-config/) holds the hub-only branch rulesets, fleet settings, documentation, and apply script. It sits outside `.github/`, which is Actions-owned. - [catalog/](./catalog/) holds reference snippets the audit compares implementations against. - [reports/](./reports/) holds per-repo audit output. - [scripts/](./scripts/) holds the gates that run in CI and locally, and that every fleet repository reaches rather than carries. diff --git a/README.md b/README.md index 8655a98a..377575d1 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ This repo is the single home for those rules, a machine-readable spec they are c - **[RESYNC.md][resync]** - how an agent brings an already-stood-up repository back into line, in the order the remedies require. - **[spec/][spec]** - the machine-readable ground truth: project-type requirements, the file/section baseline, required/forbidden secrets, the host tool contract and its version floors, and the preferred README structure. - **[registry/repos.json][repos]** - the fleet registry: every project, its type(s), publish mechanism, and status (cataloged or standardization backlog). -- **[repo-config/][repo-config]** - branch rulesets and the apply script (kept out of `.github/`, which is Actions-owned), plus the GitHub setup reference. +- **[repo-config/][repo-config]** - hub-only branch rulesets, fleet settings, the apply script, and the GitHub setup reference (kept out of `.github/`, which is Actions-owned). - **[host-setup/][host-setup-dir]** - the host guardrail kit, which is per machine rather than per repository. - **[catalog/][catalog]** - reusable reference snippets (workflow tasks, config exemplars, devcontainers) the audit compares implementations against. - **[reports/][reports]** - per-repo audit output. @@ -192,7 +192,7 @@ A repository that already exists is measured with [`AUDIT.md`][audit] instead. T A repository that is stood up already and has fallen behind is resynced with [`RESYNC.md`][resync], which is the third entry point and the one a request to sync a repository with the hub resolves to. It runs the audit for the findings and then applies them in an order that matters: the rules first, because they govern what comes after them, then the deletions, because a re-vendor would otherwise refresh a file that is about to go, then the re-vendors, the workflow contracts, and the repository configuration. It also states what the measurement cannot see, since a carried file at `intent` fidelity is checked for presence alone and a hub revision inside one raises no finding at all. -The mechanical helpers that go with those procedures are documented beside them: [`docs/repo-config-carry.md`][repo-config-carry] for branch rulesets and repository settings, and [`docs/content-import.md`][content-import] for importing existing content into a new repo. +The mechanical helpers that go with those procedures are documented beside them: [`docs/repo-config.md`][repo-config-doc] for branch rulesets and repository settings, and [`docs/content-import.md`][content-import] for importing existing content into a new repo. ### Adopting Outside This Fleet @@ -364,7 +364,7 @@ Licensed under the [MIT License][license]\ [project-types]: ./spec/project-types.json [readme-structure]: ./spec/readme-structure.md [repo-config]: ./repo-config/ -[repo-config-carry]: ./docs/repo-config-carry.md +[repo-config-doc]: ./docs/repo-config.md [reports]: ./reports/ [repos]: ./registry/repos.json [resync]: ./RESYNC.md diff --git a/RESYNC.md b/RESYNC.md index ef35dff7..28e9cbad 100644 --- a/RESYNC.md +++ b/RESYNC.md @@ -74,7 +74,7 @@ The order is load-bearing. Each step below either changes the rules the later st 3. **Verbatim re-vendors.** Copy the current hub canonical down, whole file or the one named `## heading` region. A finding classified **stale** matches a past hub revision and needs no judgment. One classified **modified** matches no revision, so the repository changed fixed content and the change is read before it is overwritten, since it may be an improvement the hub should adopt instead. Run `python3 scripts/carry.py check --target /path/to/worktree` for manifest-owned trees. Read each modified-file diff and every extra path before running the same command with `apply`. The tree declaration supplies the prune authority, so the carried-instruction-file guard does not apply to this fully owned content. 4. **Interface workflows.** Honor the named contract (required jobs, the ruleset-bound check name, the artifact-name handoff) rather than copying bytes. The body is the repository's own. -5. **Settings, rulesets, and secrets.** Run the hub's script against the repository by name, never a carried copy: `repo-config/configure.sh check / release|operational`, then `apply` for what it reports. Pass the model explicitly rather than relying on the registry lookup. +5. **Settings, rulesets, and secrets.** From a hub checkout at `main`, run `repo-config/configure.sh check / release|operational`, then `apply` for what it reports. Pass the model explicitly. Delete each target-repository path under `repo-config/` that the audit classified `retire`. Inspect every untriaged path collision before deciding its disposition. 6. **Intent files last, and by hand.** See section 5, which states why these carry no mechanical signal at all. **Reconcile the registry entry in the same pass.** `status`, `types`, `releaseTrigger`, `workflowModel` and `driftNotes` record reality rather than intent, and a `driftNote` describing work that is now finished is deleted rather than left standing. A note asserting outstanding work in prose ("pending", "not yet", "behind") contradicts a clean audit outright. @@ -99,7 +99,7 @@ flowchart LR - **An untriaged hit is read before it is touched.** A repository's own content at a path the hub also uses matches this check while carrying nothing of the hub's. The first fleet-wide run found two: a KiCad tooling document at `scripts/README.md`, and per-repository formatting hooks at `.husky/pre-commit`, each of which shares the path and none of the content. Deleting either would have destroyed work the hub never owned. - **An `accepted` disposition closes the hit permanently**, whether it is a path collision or a file every repository legitimately owns, such as `LICENSE` and `TODO.md`. - **Sweep every inbound reference to the path, and count that as part of the deletion.** This governs any deletion rather than only a `hub-only:` finding. The three bullets above turn on a disposition and so belong to this detector, where the sweep turns on nothing: it applies the same way to a file the repository owns and removes for reasons of its own, which is where the fleet first hit it. Deleting the file is one edit and finishing the job usually takes several more, so grep the path tree-wide before deleting and read every hit. **Then read the files whose job is to say what the repo holds, since a grep for the path finds uses of the file and misses descriptions of it.** A layout section calling a deleted script "the apply script" names no path, so it survives every search for one and goes stale silently. The named-path half of this class is mechanized: `scripts/prose_lint.py --check dead-path` reports a Markdown mention of a path git once tracked and the tree no longer holds, continuously rather than only at deletion time, so the manual read here is what covers the description that names no path, which no pattern reaches. This is measured rather than predicted: a resync deleted a carried script, swept its path across the tree, merged, and left `GOVERNANCE.md` "Repository Layout" still describing the directory as holding it. Three shapes appear among the hits and they are fixed differently. A link whose target has an equivalent elsewhere is **re-pointed** at it, the hub's copy being that equivalent for a hub-hosted file, since a pointer that resolves nowhere teaches a reader that a pointer in carried text is decorative. A **runnable command** citing the deleted path is rewritten to the invocation that still works, and this is the one that costs a reader real time, because a documented command fails where a dead link merely disappoints. A mention with no equivalent anywhere is **removed** rather than re-pointed, and removing a reference-style link removes its definition with it, per [GOVERNANCE.md "Documentation Style Conventions"][governance-documentation-style], since an orphaned definition fails the no-unused-defs rule and is a gate failure rather than untidiness. -- **The sweep is measured, not hypothetical, and the second case is the one that proves it is not hub-only.** One repository carries an inline link to `configure.sh` in its `repo-config/README.md`, alongside four commands invoking `./repo-config/configure.sh`, so the retirement of that one file makes five lines of a carried document wrong, two of them commands a reader would run. Another repository's nested `AGENTS.md` was deleted for reasons entirely its own, with no disposition and no hub equivalent, and it took three edits across two files: removing the inline link alone tripped `MD053` on the definition it orphaned, so the delete-the-file-only path failed CI rather than merely breaking prose. +- **The sweep covers references and descriptions.** Remove or replace every link, command, and prose claim that depends on the deleted path. Remove each orphaned reference definition in the same edit so Markdown remains valid. ## 5. What a Resync Cannot Detect diff --git a/STANDUP.md b/STANDUP.md index 3cf247cc..33bbb549 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -21,7 +21,7 @@ flowchart TD ## 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. +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.md`][repo-config-doc] 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: @@ -149,7 +149,7 @@ python3 scripts/carry.py apply --target /path/to/worktree Read every extra-path report before `apply` prunes it. Finish with another `check`. It uses the same comparison as `apply` and must report clean. -Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's **selector set**, **adapted, not cloned**. The selector set is the repo's `types` plus its `workflowModel`, `releaseTrigger`, and `consumerModel`, so filtering on type alone silently drops the entries a non-type selector carries ([`spec/scope-model.md`][scope-model] defines the four namespaces and how they resolve). The prose files (`CODESTYLE.md`, `README.md`, and the like) describe the repo's own toolchain, so adapt them to reality rather than propagating template specifics verbatim (see the "Adapt before propagating" callout in [`CODESTYLE.md`][codestyle], since a verbatim copy that misdescribes the repo is rejected in review). The baseline covers `WORKFLOW.md`, `version.json`, the two rulesets, `.github/dependabot.yml`, `.editorconfig`, `.gitattributes`, `host-tools.json`, the linter configs, and the per-type files (`.vscode/tasks.json` from the language's snippet, `codecov.yml`, `.dockerignore`, `Docker/README.md`). **Every repo carries `repo-config/main.json`**, and only the `develop` payload varies by workflow model: `repo-config/develop.json` for a release repo, `repo-config/operational/develop.json` for an operational one. +Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's **selector set**, **adapted, not cloned**. The selector set is the repo's `types` plus its `workflowModel`, `releaseTrigger`, and `consumerModel`, so filtering on type alone silently drops the entries a non-type selector carries ([`spec/scope-model.md`][scope-model] defines the four namespaces and how they resolve). The prose files (`CODESTYLE.md`, `README.md`, and the like) describe the repo's own toolchain, so adapt them to reality rather than propagating template specifics verbatim (see the "Adapt before propagating" callout in [`CODESTYLE.md`][codestyle], since a verbatim copy that misdescribes the repo is rejected in review). The baseline covers `WORKFLOW.md`, `version.json`, `.github/dependabot.yml`, `.editorconfig`, `.gitattributes`, `host-tools.json`, the linter configs, and the per-type files (`.vscode/tasks.json` from the language's snippet, `codecov.yml`, `.dockerignore`, `Docker/README.md`). Repository settings and ruleset payloads stay in the hub's `repo-config/` directory. Carry `AGENTS.md`'s skill-dependency pointer paragraph, the one naming `scripts/skills_install.py` and where the fleet's Skills live, as one more verbatim unit in this same step, not a separate pass. It reads like boilerplate next to the surrounding text a new repo adapts to describe itself, and a repo that carries `AGENTS.md` without it stands up with no path to the fleet's Skills at all. `RESYNC.md` carries the identical instruction for a repo already stood up, so the two procedures agree on what belongs in every copy. @@ -200,7 +200,7 @@ Three conditions fail here, and the two commands together are what separate them Each is step 0A's escalation rather than something to work around. -Run `repo-config/configure.sh apply owner/repo release|operational` from a hub checkout, naming the repo being stood up and its model, to apply the fleet settings, the Dependabot security features, and the two rulesets idempotently (import the JSON, never hand-build it, per [`docs/repo-config-carry.md`][repo-config-carry]), then `repo-config/configure.sh check owner/repo release|operational` to validate the repo and exit non-zero on any drift. The script is hub-hosted rather than carried, so the repo being stood up holds no copy of it and never needs one, and naming the target is what keeps the write off the checkout the command runs in. Pass the model explicitly here rather than relying on the lookup. Run from a hub checkout the registry is present, so a repo not yet registered resolves through `defaults.workflowModel` to `release` and applies the wrong `develop` ruleset to an operational repo, and a repo being stood up is exactly the one the registry has not got yet. Reconcile its registry entry in step 6 either way. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s), meaning Actions plus Dependabot where the mechanism needs it, and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once, which is why this step follows step 3 rather than preceding it. A ruleset requiring a name no run has ever reported leaves the first pull request waiting on a status nothing produces, and on an operational repo the `develop -> main` promotion is a pull request too, so the same wait applies there. +Run `repo-config/configure.sh apply owner/repo release|operational` from a hub checkout at `main`, naming the repo being stood up and its model, to apply the fleet settings, Dependabot security features, and two rulesets idempotently (import the JSON, never hand-build it, per [`docs/repo-config.md`][repo-config-doc]). Then run `repo-config/configure.sh check owner/repo release|operational` from the same checkout. Pass the model explicitly because the repository is outside the registry during this step. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s), meaning Actions plus Dependabot where the mechanism needs it, and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once, which is why this step follows step 3 rather than preceding it. A ruleset requiring a name no run has ever reported leaves the first pull request waiting on a status nothing produces, and on an operational repo the `develop -> main` promotion is a pull request too, so the same wait applies there. ## 5. Verify: Run the Audit @@ -247,7 +247,7 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo, and [project-types]: ./spec/project-types.json [readme-structure]: ./spec/readme-structure.md [repo-config]: ./repo-config/ -[repo-config-carry]: ./docs/repo-config-carry.md +[repo-config-doc]: ./docs/repo-config.md [repo-config-main]: ./repo-config/main.json [repo-config-readme]: ./repo-config/README.md [repos]: ./registry/repos.json diff --git a/WORKFLOW.md b/WORKFLOW.md index 50f3baf0..313414e9 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -64,7 +64,7 @@ flowchart LR The direct commit is an **allowance, not a substitute for review**. The ruleset drops the pull-request *requirement*, which permits a direct push without withdrawing the pull request, so a change worth reviewing still takes one and both paths reach `develop` legally. Which changes those are is stated as a shape rather than a line count in [GOVERNANCE.md "Operational Repositories"][governance-operational-repositories], which owns the test and is the one place it is written, since nothing in a ruleset can apply it. What differs is when validation lands. On the direct-commit path the commit is already on the branch, so CI can only be advisory after the fact, and that is the accepted cost of the model. On the pull-request path the change has not landed, so validation is pre-merge and actionable, which is the moment it is worth the most, and the lint workflow's `pull_request` trigger therefore names `develop` alongside `main` (Section 6). That is what makes **D1.2** hold here, since its input is *any* PR and the operational model is no exception. The check is reported on a `develop` PR rather than required, because a required status check on `develop` binds the direct push too and would dissolve the allowance the model is built on. -Their CI is lint/validation only (editorconfig/EOL plus domain linters such as Home Assistant or ESPHome config validation or a firmware build, but **no unit tests**), so the D-guarantees below that assume a build/test pipeline are **N/A** exactly as for `source-only` (Section 6). What binds: the promotion gate, where the `develop -> main` PR must pass the required `Check pull request workflow status job`, and the source-only release on manual dispatch (`releaseTrigger: dispatch-only`; tag + source zip). Branch-model rulesets are specified in [GOVERNANCE.md "Branching Model"][governance-branching-model] and [repo-config/README.md][repo-config-readme], not here. +Their CI is lint/validation only (editorconfig/EOL plus domain linters such as Home Assistant or ESPHome config validation or a firmware build, but **no unit tests**), so the D-guarantees below that assume a build/test pipeline are **N/A** exactly as for `source-only` (Section 6). What binds: the promotion gate, where the `develop -> main` PR must pass the required `Check pull request workflow status job`, and the source-only release on manual dispatch (`releaseTrigger: dispatch-only`; tag + source zip). Branch-model rulesets are specified in [GOVERNANCE.md "Branching Model"][governance-branching-model], not here. ### Two Layers: Orchestration vs Build @@ -295,4 +295,3 @@ Each type maps the *applicable* S-scenarios onto its targets. The differences ar [codestyle]: ./CODESTYLE.md [governance-branching-model]: ./GOVERNANCE.md#branching-model [governance-operational-repositories]: ./GOVERNANCE.md#operational-repositories -[repo-config-readme]: ./repo-config/README.md diff --git a/docs/repo-config-carry.md b/docs/repo-config.md similarity index 55% rename from docs/repo-config-carry.md rename to docs/repo-config.md index d5065baa..f8c07ff1 100644 --- a/docs/repo-config-carry.md +++ b/docs/repo-config.md @@ -1,21 +1,30 @@ -# repo-config: Carry, Apply, and Regenerate (Hub-Only) +# Repository Configuration (Hub-Only) -The **process** for carrying the `repo-config/` baseline to a fleet repo, applying it, and regenerating the canonical payloads. This doc is **hub-only** and is not carried downstream (it describes what the hub does *to* a repo, not a fact about any one repo). The carried [`repo-config/README.md`][repo-config-readme] states only the current facts about a repo's own config. This carry/apply/regen procedure lives here so it never ships into a downstream copy. +The process for applying, checking, and regenerating the canonical repository configuration. This document and the entire `repo-config/` directory are hub-only. Every command runs from a hub checkout at `main` and names its target repository. -## Downstream Carry +## Configuration Source -Every fleet repo carries the `repo-config/` directory. The hub keeps the canonical copy. Rules for the carried copy: +The hub holds all fleet-wide repository configuration: + +- `settings.json` declares the shared repository settings. +- `main.json` declares the shared `main` ruleset. +- `develop.json` declares the release-model `develop` ruleset. +- `operational/develop.json` declares the operational-model `develop` ruleset. +- `configure.sh` applies or checks those payloads through the GitHub API. + +Downstream repositories carry no `repo-config/` directory. The registry's `workflowModel` selects the `develop` payload. Commands that operate before registry enrollment pass the model explicitly. + +The carried `AUDIT.md` reaches the hub at `main` for its configuration check. The fleet-wide `spec/audit.py` reads the same hub payloads directly. Both paths compare live state against one source. + +## Per-Repository Secrets -- **The payloads carry and the script does not.** A `release` repo carries `develop.json`, an `operational` repo carries `operational/develop.json` instead, and `main.json` and `settings.json` are shared by both models. `configure.sh` stays in the hub and is run from a hub checkout against the repo named on the command line, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling], because it holds nothing per-repo and a copy of it is only current until the next fix. A repo still holding a copy has it deleted as it is next visited. Note the split this leaves: an apply or check run from the hub reads the hub's payloads rather than the repo's own, which is the single source the model is for, and the repo's carried payloads remain what its own `AUDIT.md` diffs the live rulesets against. -- **Carried files name no fleet repo as an illustrative example.** A carried file adds no template-repo reference and names no sibling fleet repo as an example (any fleet repo may be private, so such a link 404s in a public carrier, and it couples the repos). A contextually relevant link a reader of *this* repo's content needs is fine. See [GOVERNANCE.md "Documentation Style Conventions"][governance-documentation-style]. To point at a current good example, name it in the onboarding/conformance issue or the hub-only [`reports/conformance-matrix.md`][conformance-matrix]. -- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline, the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo. Adapt this shape, don't invent. A current well-formed example is named in the onboarding/conformance issue. - **Adapted `spec/secrets.json` shape.** The repo-scoped adaptation carries `baseline` (the App pair, which every fleet repo needs for the merge-bot) plus a `mechanisms` entry for each publish mechanism the repo actually uses, and the `targetMechanisms` routing entries for those mechanisms. **A source-only repo whose publish targets all map to a null mechanism (nothing to route) carries just `baseline` (plus a `note`)**, omitting `targetMechanisms` and `mechanisms` entirely, because a lone `targetMechanisms` map with no `mechanisms` reads as a schema bug (the audit enumerates `baseline` + `mechanisms`, never `targetMechanisms`, so an all-null routing map is dead weight). A `release` repo that uses a real mechanism (e.g. `nuget-oidc`, `docker-hub`, `codecov`) carries that `mechanisms` entry **and** its `targetMechanisms`/`typeMechanisms` routing, which the audit then picks up. ## Applying the Config **Configure by importing the JSON payloads, never by hand-building the rules** (hand reconstruction has gone wrong on past setups). The result must be **exactly two rulesets named `develop` and `main`**, and the names are load-bearing (`AGENTS.md` and the workflows reference them). Only the `develop` *content* varies by model. -First remove all legacy classic branch-protection rules and any stray rulesets, then run `configure.sh apply` from a hub checkout, naming the target repo and its model (the script applies `settings.json` and the Dependabot security features alongside the rulesets). Name the model rather than leaving it to the lookup. The script reads the registry beside it and resolves a repo it does not find through `defaults.workflowModel` to `release`, so an unregistered operational repo silently takes the release `develop` ruleset. The carried-payload inference the script also carries is for a run with no registry to consult, which a run from here never is: +Remove all classic branch-protection rules and stray rulesets. Run `configure.sh apply` from a hub checkout at `main`, naming the target repository and its model. The script applies `settings.json`, the Dependabot security features, and both rulesets. A registered repository can omit the model and use the registry lookup. A repository outside the registry passes the model explicitly: ```sh repo-config/configure.sh apply owner/repo release|operational @@ -28,6 +37,7 @@ Then validate the result with `repo-config/configure.sh check owner/repo release To change the canonical rulesets, edit the live rulesets (fleet-wide changes happen at the hub), then regenerate the committed files from the current repo: ```sh +model=release # Set to release or operational for the payload set being regenerated. repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" # Paginate so a name match on a later page is never missed - the same trap configure.sh guards against. # --paginate with --jq '.[]' emits one JSON object per ruleset across all pages; jq -s re-assembles them @@ -35,8 +45,8 @@ repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" rulesets=$(gh api --paginate "repos/$repo/rulesets" --jq '.[]' | jq -s '.') for name in develop main; do out="repo-config/$name.json" - # An operational carry keeps its develop payload at operational/develop.json (develop.json is absent). - [ "$name" = "develop" ] && [ ! -f "$out" ] && out="repo-config/operational/develop.json" + # The operational model writes its develop payload under operational/; both files exist in the hub. + [ "$name" = "develop" ] && [ "$model" = "operational" ] && out="repo-config/operational/develop.json" # Exactly one ruleset per name: zero or duplicates is declared drift - fail loudly, never regen from a guess. count=$(jq --arg n "$name" '[.[] | select(.name==$n)] | length' <<<"$rulesets") [ "$count" -eq 1 ] || { echo "expected exactly 1 ruleset named $name, found $count (drift)" >&2; exit 1; } @@ -54,8 +64,4 @@ done -[conformance-matrix]: ../reports/conformance-matrix.md -[governance-documentation-style]: ../GOVERNANCE.md#documentation-style-conventions [governance-git-and-commit-rules]: ../GOVERNANCE.md#git-and-commit-rules -[governance-hub-hosted-tooling]: ../GOVERNANCE.md#hub-hosted-tooling -[repo-config-readme]: ../repo-config/README.md diff --git a/repo-config/README.md b/repo-config/README.md index 9399bf3f..d6f5e451 100644 --- a/repo-config/README.md +++ b/repo-config/README.md @@ -1,10 +1,9 @@ # repo-config -Repository and branch configuration held as committed files, kept out of `.github/` (which holds the GitHub-consumed configuration: workflows, Dependabot). +Hub-only repository and branch configuration held as committed files, kept out of `.github/` (which holds the GitHub-consumed configuration: workflows, Dependabot). Downstream repositories carry no `repo-config/` directory. Apply and check commands run from a hub checkout at `main` and name the target repository. -- `main.json` plus one `develop` variant: the branch rulesets as the managed part of the writable API subset (`name`, `target`, `enforcement`, `conditions`, `rules`). The `develop` payload is `develop.json` (`release` repos) or `operational/develop.json` (`operational` repos). These are the canonical expected payloads that the self-audit (`AUDIT.md`) diffs the live rulesets against. `bypass_actors` is writable and deliberately unmanaged, so no payload declares one and nothing diffs it: who may bypass a ruleset is a human decision taken in the UI, which `configure.sh` preserves on `apply` and reports without asserting on `check`. -- `operational/develop.json`: the `develop` ruleset for **operational** repos (registry `workflowModel: operational`), taking direct signed pushes with no PR gate. Present in operational repos only, since a `release` repo does not have it. See "Rulesets" below. -- `configure.sh`: **hosted in the hub and run from a hub checkout, not carried here**, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. The payloads above are what this repo is audited against and stay with it. The script holds nothing per-repo and is one copy for the fleet, and it resolves every payload path against its own directory rather than against the target repo, so a run from a hub checkout compares this repo against the hub's payloads and never reads the copies above. Name the target repository explicitly, since the command defaults to whichever repository the shell is sitting in. Two modes over the GitHub API. `configure.sh apply owner/repo release|operational` creates-or-updates the settings, the Dependabot security features, and the rulesets idempotently (a full-payload update). `configure.sh check owner/repo release|operational` is the read-only inverse and exits non-zero on any drift, with the ruleset and settings assertions driven by the committed payloads so they stay repo-agnostic (rule presence, merge methods, and required checks, not a byte diff, so a GitHub-normalized stored ruleset does not false-positive). The command defaults to `apply`, the repo to the current one, and the model to the registry `workflowModel` lookup, falling back to inference from the carried `develop` payload where there is no registry to consult and aborting rather than guessing on an ambiguous layout. Pass the model rather than leaving it to the lookup. A hub checkout has the registry, so a repo the registry does not yet name resolves through `defaults.workflowModel` to `release`, which applies the release `develop` ruleset to an operational repo. Both arguments are optional to the parser rather than to the caller, which is the hazard: a lone positional naming a model is read as the model, so `configure.sh check operational` resolves against whichever repository the shell is in rather than erroring on a missing target. +- `main.json`, `develop.json`, and `operational/develop.json`: the canonical branch rulesets as the managed part of the writable API subset (`name`, `target`, `enforcement`, `conditions`, `rules`). `main.json` is shared. `develop.json` serves release repos, and `operational/develop.json` serves operational repos. `configure.sh check` compares the selected payloads with the live rulesets. `bypass_actors` is writable and deliberately unmanaged, so no payload declares one and nothing diffs it: who may bypass a ruleset is a human decision taken in the UI, which `configure.sh` preserves on `apply` and reports without asserting on `check`. +- `configure.sh`: run from a hub checkout at `main`, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. It resolves every payload path against the hub's `repo-config/` directory. Name the target repository explicitly, since the command defaults to whichever repository the shell is sitting in. `configure.sh apply owner/repo release|operational` creates or updates the settings, Dependabot security features, and rulesets idempotently. `configure.sh check owner/repo release|operational` is the read-only inverse and exits non-zero on drift. The model defaults to the registry `workflowModel` lookup. Pass it explicitly for a repository outside the registry. ## Rulesets diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 0c3858a0..2972e361 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -6,7 +6,6 @@ # # Both modes need admin on the repo, because the rulesets endpoints require it. # The command defaults to apply, the repo to the current gh repo, and the model to the registry lookup. -# With no registry to consult, the model is inferred from the carried develop payload. # The model may be passed as the sole positional, as in `configure.sh check operational`. # The command may be omitted for the apply default, so `configure.sh owner/repo` still applies. # @@ -53,19 +52,8 @@ if [ -z "$model" ]; then exit 1 fi else - # With no registry to consult (a downstream carry), infer the model from which develop payload is carried. - # A carry holds exactly its own model's payload. - # An ambiguous layout (both or neither, as in a partial copy) aborts rather than guesses. - # A wrong guess would apply or check the wrong develop ruleset. - if [ -f "$script_dir/develop.json" ] && [ ! -f "$script_dir/operational/develop.json" ]; then - model="release" - elif [ -f "$script_dir/operational/develop.json" ] && [ ! -f "$script_dir/develop.json" ]; then - model="operational" - else - echo "Registry $registry not found and the carried develop payloads are ambiguous (expected exactly one of develop.json or operational/develop.json). Pass the model explicitly (release|operational)." >&2 - exit 1 - fi - echo "Registry $registry not found. Inferred workflow model '$model' from the carried develop payload." >&2 + echo "Registry $registry not found. Run this script from a hub checkout at main or pass the model explicitly (release|operational)." >&2 + exit 1 fi fi case "$model" in diff --git a/scripts/README.md b/scripts/README.md index be19af0e..02f39a6e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -20,7 +20,7 @@ uvx coverage@latest run --source=scripts,spec,host-setup -m unittest discover -s ## `prose_lint.py` -Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. It carries one rule from elsewhere in that document, `home-path`, which comes from "Representative Data in Agent-Authored Text" and catches an absolute home path naming a real account. That rule closes the pattern-detectable sliver of its section and nothing beyond it, since the exposure the section exists for was name-shaped and no pattern finds a name. It is the one rule a checkout can turn off: an operational repository's runbook carries the literal path an operator types, so a run there drops `home-path` and says so on stderr rather than going quiet. A repository states its own model by which configuration payload it carries, and the hub carries both, so it reads as a release repository and the rule gates here. +Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. It carries one rule from elsewhere in that document, `home-path`, which comes from "Representative Data in Agent-Authored Text" and catches an absolute home path naming a real account. That rule closes the pattern-detectable sliver of its section and nothing beyond it, since the exposure the section exists for was name-shaped and no pattern finds a name. `OPERATIONS.md` is exempt because its runbooks carry the literal path an operator types. The tiers decide by context rather than by a flat ban. Tier 1 carries no meaning its ASCII form loses and always flags. Tier 2 is an operator, kept next to a figure or another operator and replaced between words, so a threshold table reads as the range it is. Tier 3 is a unit or scientific symbol whose ASCII form would be a lie and never flags. Developer-typed characters such as emoji are preserved regardless of tier, and an un-tiered one is still reported as `charset-unknown` until it is classified. diff --git a/scripts/tests/test_prose_lint.py b/scripts/tests/test_prose_lint.py index 26baf35b..dc846db1 100755 --- a/scripts/tests/test_prose_lint.py +++ b/scripts/tests/test_prose_lint.py @@ -2033,12 +2033,8 @@ def test_neither_source_file_carries_a_literal_home_path(self) -> None: self.assertEqual([], found) -class TestOperationalExemption(unittest.TestCase): - """An operational repository's runbook carries the path an operator types. - - That is the repository's own content rather than an agent quoting an environment it observed, - which is the distinction the rule is about. - """ +class TestOperationsRunbookExemption(unittest.TestCase): + """The operations runbook carries the literal path an operator types.""" def setUp(self) -> None: self.tmp = Path(self.enterContext(tempfile.TemporaryDirectory())) @@ -2050,44 +2046,40 @@ def _payload(self, *parts: str) -> None: target.parent.mkdir(parents=True, exist_ok=True) target.write_text("{}\n", encoding="utf-8") - def test_an_operational_checkout_is_read_from_what_it_carries(self) -> None: - """`spec/files.json` declares the payload per model, so the repository states its own.""" - self._payload("repo-config", "operational", "develop.json") - self.assertTrue(prose_lint.operational_checkout(self.tmp)) - - def test_a_release_checkout_is_not_operational(self) -> None: - self._payload("repo-config", "develop.json") - self.assertFalse(prose_lint.operational_checkout(self.tmp)) + def test_a_checkout_with_the_runbook_is_detected(self) -> None: + """Every repository carries the runbook that owns literal operator paths.""" + self._payload("OPERATIONS.md") + self.assertTrue(prose_lint.is_operations_runbook(self.tmp / "OPERATIONS.md", self.tmp)) - def test_the_hub_carrying_both_payloads_is_not_operational(self) -> None: - """The hub is the template for each model, so carrying the release payload decides it. + def test_a_checkout_without_the_runbook_is_not_detected(self) -> None: + self.assertFalse(prose_lint.is_operations_runbook(self.tmp / "README.md", self.tmp)) - Read as operational, the hub would exempt itself from a rule it authors, which is the - one repository where that matters most. - """ + def test_configuration_payloads_do_not_select_the_exemption(self) -> None: + """Configuration payload names do not identify the operations runbook.""" self._payload("repo-config", "develop.json") self._payload("repo-config", "operational", "develop.json") - self.assertFalse(prose_lint.operational_checkout(self.tmp)) + self.assertFalse( + prose_lint.is_operations_runbook(self.tmp / "repo-config/develop.json", self.tmp) + ) - def test_a_checkout_carrying_neither_payload_is_not_operational(self) -> None: - """An unknown model is gated rather than exempted, since exempting on doubt is the risk.""" - self.assertFalse(prose_lint.operational_checkout(self.tmp)) + def test_an_empty_checkout_is_not_detected(self) -> None: + self.assertFalse(prose_lint.is_operations_runbook(self.tmp, self.tmp)) - def test_the_skip_is_announced_rather_than_silent(self) -> None: - """A rule that stops running without saying so reads as a rule that passed.""" - self._payload("repo-config", "operational", "develop.json") - bait = self.tmp / "runbook.md" + def test_the_runbook_allows_a_literal_operator_path(self) -> None: + """The operations runbook owns the literal path an operator types.""" + bait = self.tmp / "OPERATIONS.md" bait.write_text(f"Deploy into {NIX_HOME}/stack here.\n", encoding="utf-8") - with ( - mock.patch.object(prose_lint, "repo_root", return_value=str(self.tmp)), - mock.patch.object(prose_lint, "discover", return_value=[bait]), - ): - self.assertEqual(0, prose_lint.main(["--check", "home-path"])) - self.assertIn("operational repository", self.err.getvalue()) + self.assertEqual([], prose_lint.check_file(bait, {"home-path"}, self.tmp)) - def test_a_release_repository_still_reports_the_finding(self) -> None: + def test_a_nested_runbook_does_not_receive_the_exemption(self) -> None: + """Only the repository-root runbook owns literal operator paths.""" + bait = self.tmp / "docs" / "OPERATIONS.md" + bait.parent.mkdir() + bait.write_text(f"Deploy into {NIX_HOME}/stack here.\n", encoding="utf-8") + self.assertEqual(1, len(prose_lint.check_file(bait, {"home-path"}, self.tmp))) + + def test_another_file_still_reports_the_finding(self) -> None: """The exemption must not be the whole rule.""" - self._payload("repo-config", "develop.json") bait = self.tmp / "runbook.md" bait.write_text(f"Deploy into {NIX_HOME}/stack here.\n", encoding="utf-8") with ( @@ -2141,7 +2133,7 @@ def test_standing_in_an_operational_repo_does_not_exempt_a_release_repo(self) -> self.assertNotIn("operational repository", self.err.getvalue()) def test_standing_in_a_release_repo_does_not_un_exempt_an_operational_repo(self) -> None: - """The inverse error, which reports a finding the exemption exists to suppress.""" + """A non-runbook file remains checked in every repository.""" runbook = self.operational / "runbook.md" runbook.write_text(f"Deploy into {NIX_HOME}/stack here.\n", encoding="utf-8") with ( @@ -2150,8 +2142,8 @@ def test_standing_in_a_release_repo_does_not_un_exempt_an_operational_repo(self) ), mock.patch.object(prose_lint, "discover", return_value=[runbook]), ): - self.assertEqual(0, prose_lint.main([str(self.operational), "--check", "home-path"])) - self.assertIn("operational repository", self.err.getvalue()) + self.assertEqual(1, prose_lint.main([str(self.operational), "--check", "home-path"])) + self.assertNotIn("operational repository", self.err.getvalue()) def test_paths_spanning_two_repositories_refuse_rather_than_pick_one(self) -> None: """Two repositories declare two models, and one rule set cannot be correct for both.""" @@ -2267,9 +2259,6 @@ def test_a_loose_file_anchors_on_its_own_parent_rather_than_on_the_caller(self) # The caller stands somewhere operational; the scanned file's own directory does not. with ( mock.patch.object(prose_lint, "repo_root", return_value=""), - mock.patch.object( - prose_lint, "operational_checkout", side_effect=lambda root: Path(root) == Path(".") - ), mock.patch.object(prose_lint, "discover", return_value=[bait]), ): self.assertEqual(1, prose_lint.main([str(bait), "--check", "home-path"])) @@ -2282,9 +2271,6 @@ def test_a_loose_directory_anchors_on_itself(self) -> None: bait.write_text(f"Deploy into {NIX_HOME}/stack here.\n", encoding="utf-8") with ( mock.patch.object(prose_lint, "repo_root", return_value=""), - mock.patch.object( - prose_lint, "operational_checkout", side_effect=lambda root: Path(root) == Path(".") - ), mock.patch.object(prose_lint, "discover", return_value=[bait]), ): self.assertEqual(1, prose_lint.main([str(loose), "--check", "home-path"])) diff --git a/spec/divergences.json b/spec/divergences.json index e74ff5e2..f7156905 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -6,7 +6,12 @@ { "path": ".markdownlint-cli2.jsonc", "repos": ["aiopurpleair", "PhotoCleaner", "AudioCleaner"], "disposition": "re-vendor", "reason": "Verbatim config held as a hand-modified copy rather than a past hub revision. Restore the current canonical.", "tracking": null } ], "gaps": [ - { "path": "repo-config/configure.sh", "disposition": "retire", "reason": "The script is hub-hosted rather than carried, per GOVERNANCE.md \"Hub-Hosted Tooling\", so it is no longer a manifest entry and a downstream copy is retired rather than re-vendored. Every copy the fleet holds is the hub's content with nothing per-repo in it, and the copies predate the payload-driven check mode, which is the drift this removes rather than converges. Delete the copy as each repo is next visited and run the hub's script against the repo instead. The carriers are not enumerated in this ledger entry, because the fleet-wide gap pass names them from live state in the generated report and a hand-kept list understated them: it read six while fifteen repos carry one. The ruleset payloads and settings.json stay carried, since those are what the repo is audited against and what its own AUDIT.md diffs the live configuration against.", "tracking": null }, + { "path": "repo-config/configure.sh", "disposition": "retire", "reason": "The hub hosts the repository-configuration script. A downstream repository runs the hub's main copy against its repository name and workflow model.", "tracking": null }, + { "path": "repo-config/develop.json", "disposition": "retire", "reason": "The hub hosts the release-model develop ruleset payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, + { "path": "repo-config/operational/develop.json", "disposition": "retire", "reason": "The hub hosts the operational-model develop ruleset payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, + { "path": "repo-config/main.json", "disposition": "retire", "reason": "The hub hosts the shared main ruleset payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, + { "path": "repo-config/settings.json", "disposition": "retire", "reason": "The hub hosts the shared repository settings payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, + { "path": "repo-config/README.md", "disposition": "retire", "reason": "The hub hosts the repository-configuration reference beside the payloads and script it documents.", "tracking": null }, { "path": ".github/workflows/build-release-task.yml", "disposition": "retire", "reason": "The release chain is hub-hosted as a workflow_call task, per docs/reusable-workflows.md \"Stage 4: The Release Chain and the Docker Core\", so a downstream copy of this filename is retired rather than re-vendored: the caller stub a repo carries after adoption is publish-release.yml and test-pull-request.yml calling the hub task by pin, and no adopting repo carries a same-named local file. The ten carriers measured on develop at hub 7c67328 are PhotoCleaner, PlexCleaner, LanguageTags, MediaTools, Utilities, aiopurpleair, ESPHome-NonRoot, VSCode-Server-DotNetCore, KiCadLibrary, and homeassistant-purpleair. Delete the copy as each repo adopts the hub task; adoption is a separate, later change per repo (docs/reusable-workflows.md \"Rollout\" Stage 4).", "tracking": null }, { "path": ".github/workflows/build-docker-task.yml", "disposition": "retire", "reason": "The Docker core is hub-hosted as its own workflow_call task alongside build-release-task.yml, per docs/reusable-workflows.md \"Stage 4: The Release Chain and the Docker Core\", so a downstream copy of this filename is retired rather than re-vendored the same way. The five carriers measured on develop at hub 7c67328 are ESPHome-NonRoot, NxWitness, PhotoCleaner, PlexCleaner, and VSCode-Server-DotNetCore. Delete the copy as each repo adopts the hub task; adoption is a separate, later change per repo (docs/reusable-workflows.md \"Rollout\" Stage 4).", "tracking": null }, { "path": "LICENSE", "disposition": "accepted", "reason": "Each repo owns its license file. The hub does not standardize license text, so it is intentionally outside the manifest.", "tracking": null }, diff --git a/spec/files.json b/spec/files.json index 5b009c72..4be0854c 100644 --- a/spec/files.json +++ b/spec/files.json @@ -22,13 +22,8 @@ { "path": ".gitignore", "appliesTo": "*" }, { "path": "host-tools.json", "appliesTo": "*" }, { "path": "version.json", "fidelity": "intent", "intentRef": "WORKFLOW.md#d3---versioning-and-classification", "appliesTo": "*" }, - { "path": "repo-config/develop.json", "fidelity": "verbatim", "whole": true, "appliesTo": ["release"] }, - { "path": "repo-config/operational/develop.json", "fidelity": "verbatim", "whole": true, "appliesTo": ["operational"] }, - { "path": "repo-config/main.json", "fidelity": "verbatim", "whole": true, "appliesTo": "*" }, - { "path": "repo-config/README.md", "fidelity": "intent", "whole": true, "intentRef": "repo-config/README.md", "appliesTo": "*" }, - { "path": "repo-config/settings.json", "fidelity": "intent", "whole": true, "intentRef": "repo-config/README.md", "appliesTo": "*" }, - { "path": "AUDIT.md", "fidelity": "intent", "intentRef": "docs/repo-config-carry.md", "appliesTo": "*" }, - { "path": "spec/secrets.json", "fidelity": "intent", "intentRef": "docs/repo-config-carry.md", "appliesTo": "*" }, + { "path": "AUDIT.md", "fidelity": "intent", "intentRef": "docs/repo-config.md", "appliesTo": "*" }, + { "path": "spec/secrets.json", "fidelity": "intent", "intentRef": "docs/repo-config.md", "appliesTo": "*" }, { "path": ".github/dependabot.yml", "appliesTo": "*" }, { "path": ".github/workflows/test-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["check-workflow-status", "validate"], "requiredCheckName": "Check pull request workflow status job", "requireTokensInJob": { "validate": ["validate-task.yml"] } }, "intentRef": "GOVERNANCE.md#workflow-yaml-conventions", "appliesTo": "*" }, { "path": ".github/workflows/publish-release.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["plan", "validate", "publish"], "requireTokensInJob": { "plan": ["publish-plan-task.yml"], "validate": ["validate-task.yml"], "publish": ["build-release-task.yml", "needs.validate.result == 'success'"] } }, "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] }, diff --git a/spec/host-tools.json b/spec/host-tools.json index 61d40303..6c8e310a 100644 --- a/spec/host-tools.json +++ b/spec/host-tools.json @@ -69,7 +69,7 @@ "probes": [["jq", "--version"]], "pattern": "jq-(\\d+(?:\\.\\d+)*)", "minimum": "1.7", - "why": "Needed by the ruleset normalizer in repo-config/configure.sh, the ruleset diff in AUDIT.md section 6, and the payload regeneration in docs/repo-config-carry.md, so a host without it cannot apply, check, or audit a repository's branch configuration. The floor is a target rather than a measured breakage one version below it, and it says so rather than implying a defect nobody found. It is anchored to what the fleet's own platform provides: current Debian stable packages 1.7.1, so a distribution install satisfies the floor and no host needs a manual build to meet it, while an older release sits below and is the case the floor is for. That anchor is what keeps it actionable, since a floor set to the newest release instead would fail a host running every documented filter correctly. Two lower versions matter to a reader and neither is the floor. 1.6 is where walk/1 arrived, so the floor sits above it and walk is available, and the filters nonetheless define their own recursion, which costs nothing and keeps them compiling below the floor. On jq 1.5 walk/1 does not degrade, it fails to compile the filter, so a diff built on it reports drift on every ruleset it never compared, which is the inverse of the false clean the comparison exists to close. The neighbouring claim that keys_unsorted needs 1.6 is disproved rather than assumed, measured on jq-1.5-1-a5b5cbe where keys_unsorted evaluates and walk(.) answers a compile error, so the two builtins are not in the same position and the analogy between them is the whole of what carried that claim. A host below the floor is therefore unverified rather than known broken, since every filter here is written to a feature set older than the floor requires. A separate implementation is worth knowing about and is not this entry's subject: gh --jq is gojq built into the CLI rather than this binary, and it emits object keys sorted, which is why the section 6 filter canonicalizes key order itself rather than depending on whichever implementation produced its input.", + "why": "Needed by the ruleset normalizer in repo-config/configure.sh and the payload regeneration in docs/repo-config.md, so a host without it cannot apply, check, or audit a repository's branch configuration. The floor is a target rather than a measured breakage one version below it, and it says so rather than implying a defect nobody found. It is anchored to what the fleet's own platform provides: current Debian stable packages 1.7.1, so a distribution install satisfies the floor and no host needs a manual build to meet it, while an older release sits below and is the case the floor is for. That anchor is what keeps it actionable, since a floor set to the newest release instead would fail a host running every documented filter correctly. Two lower versions matter to a reader and neither is the floor. 1.6 is where walk/1 arrived, so the floor sits above it and walk is available, and the filters nonetheless define their own recursion, which costs nothing and keeps them compiling below the floor. On jq 1.5 walk/1 does not degrade, it fails to compile the filter, so a diff built on it reports drift on every ruleset it never compared, which is the inverse of the false clean the comparison exists to close. The neighbouring claim that keys_unsorted needs 1.6 is disproved rather than assumed, measured on jq-1.5-1-a5b5cbe where keys_unsorted evaluates and walk(.) answers a compile error, so the two builtins are not in the same position and the analogy between them is the whole of what carried that claim. A host below the floor is therefore unverified rather than known broken, since every filter here is written to a feature set older than the floor requires. A separate implementation is worth knowing about and is not this entry's subject: gh --jq is gojq built into the CLI rather than this binary, and it emits object keys sorted, which is why the ruleset filter canonicalizes key order itself rather than depending on whichever implementation produced its input.", "source": { "linux": "The distribution package where it meets the floor, which on current Debian stable it does at 1.7.1, so the floor is chosen to be satisfiable without a manual build. An older release sits below it, and there the upstream release at https://github.com/jqlang/jq is the source. Preferring the current upstream release everywhere keeps the fleet on one version in practice, which is what the other two platforms already do.", "macos": "Homebrew, which tracks upstream releases.", diff --git a/spec/project-types.json b/spec/project-types.json index 1531aec7..0dca70ae 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -141,8 +141,8 @@ "appliesTo": "*", "checks": [ { "id": "branch.both-protected", "verdict": "letter", "assert": "main and develop both exist and are protected.", "intentRef": "GOVERNANCE.md#branching-model" }, - { "id": "branch.ruleset.develop", "verdict": "letter", "assert": "The live develop ruleset matches the expected payload for the repo's workflowModel (normalized diff): release -> repo-config/develop.json (PR-gated), operational -> repo-config/operational/develop.json (direct signed pushes; deletion + non_fast_forward + required_signatures only).", "intentRef": "repo-config/README.md" }, - { "id": "branch.ruleset.main", "verdict": "letter", "assert": "The live main ruleset matches repo-config/main.json (normalized diff); this ruleset is shared by both workflow models.", "intentRef": "repo-config/README.md" }, + { "id": "branch.ruleset.develop", "verdict": "letter", "assert": "The live develop ruleset matches the hub's main payload for the repo's workflowModel (normalized diff): release -> repo-config/develop.json (PR-gated), operational -> repo-config/operational/develop.json (direct signed pushes, deletion + non_fast_forward + required_signatures only).", "intentRef": "repo-config/README.md" }, + { "id": "branch.ruleset.main", "verdict": "letter", "assert": "The live main ruleset matches the hub's main repo-config/main.json payload (normalized diff). This ruleset is shared by both workflow models.", "intentRef": "repo-config/README.md" }, { "id": "branch.operational.lintci", "verdict": "intent", "assert": "An operational (workflowModel) repo runs a lint/validation CI (editorconfig/EOL plus domain linters, e.g. Home Assistant or ESPHome config validation or a firmware build; no unit testing) feeding the required Check pull request workflow status job, so the develop -> main promotion PR is gated even though develop takes direct commits. N/A for release repos.", "intentRef": "GOVERNANCE.md#branching-model" }, { "id": "branch.operational.prtriggers", "verdict": "letter", "assert": "The operational repo's lint/validation workflow triggers on pull_request with branches: [ main, develop ], plus push to develop and workflow_dispatch. A pull_request set naming main alone leaves a PR into develop matching no trigger, so it merges with no validation at all (WORKFLOW.md D1.2). The develop result is reported, not required: the required status check stays on main only, since requiring it on develop would gate the direct push the model allows. N/A for release repos.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" } ] @@ -178,7 +178,7 @@ { "id": "recurring.charset", "verdict": "letter", "assert": "ASCII only in agent-authored text: no em-dash (use a spaced hyphen), no smart quotes, no stray non-ASCII.", "intentRef": "GOVERNANCE.md#character-set" }, { "id": "recurring.spelling", "verdict": "letter", "assert": "US English spelling; the shared cspell.json sets language en-US (a bare en accepts British spellings too).", "intentRef": "CODESTYLE.md#markdown-and-spelling" }, { "id": "recurring.eol", "verdict": "letter", "assert": "Line endings follow paired repository-wide defaults: .editorconfig carries [*] end_of_line, and .gitattributes carries * text=auto eol=. Both set *.bat/*.cmd to CRLF. A missing or mismatched global default is a drift finding. The global default is LF for release repos and Linux-native operational repos. A Windows-native app that uses CRLF requires CRLF in both defaults. Do not add per-language or per-file LF pins where the global default already applies. Edits preserve the file's endings.", "intentRef": "GOVERNANCE.md#line-endings" }, - { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md, repo-config/README.md, repo-config/develop.json, repo-config/main.json, spec/secrets.json, the carried AUDIT.md) carries no coordination reference: no reference to the template repo in prose or link (the coordination flow is machinery a consumer should not see - state the behavior, not the destination), and no sibling fleet repo named as an illustrative example of a rule or adoption. A contextually relevant link to a related project is NOT a coordination reference and is expected (the image that consumes this config, a library this depends on) - the test is whether the link serves a reader of this repo's content. The rule governs carried template content. A repo's own README.md and topical docs are its own content. Two bounded exceptions serve carried behavior: AGENTS.md 'Fleet Bootstrap' must name the hub and its bytes are fixed fleet-wide, and the carried AUDIT.md links hub-hosted audit inputs and procedures to their absolute hub URLs because those files are deliberately not carried. The AGENTS.md exception stops at the verbatim region boundary, and the same file's own prose is judged normally. spec/audit.py mechanically checks AGENTS.md, GOVERNANCE.md and .github/copilot-instructions.md for the template name outside their verbatim sections, and skips the hub itself, whose copies are the source.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" } + { "id": "recurring.norepoxref", "verdict": "intent", "assert": "A carried file (AGENTS.md, GOVERNANCE.md, CODESTYLE.md, WORKFLOW.md, .github/copilot-instructions.md, spec/secrets.json, the carried AUDIT.md) carries no coordination reference: no reference to the template repo in prose or link (the coordination flow is machinery a consumer should not see - state the behavior, not the destination), and no sibling fleet repo named as an illustrative example of a rule or adoption. A contextually relevant link to a related project is NOT a coordination reference and is expected (the image that consumes this config, a library this depends on) - the test is whether the link serves a reader of this repo's content. The rule governs carried template content. A repo's own README.md and topical docs are its own content. Two bounded exceptions serve carried behavior: AGENTS.md 'Fleet Bootstrap' must name the hub and its bytes are fixed fleet-wide, and the carried AUDIT.md links hub-hosted audit inputs and procedures to their absolute hub URLs because those files are deliberately not carried. The AGENTS.md exception stops at the verbatim region boundary, and the same file's own prose is judged normally. spec/audit.py mechanically checks AGENTS.md, GOVERNANCE.md and .github/copilot-instructions.md for the template name outside their verbatim sections, and skips the hub itself, whose copies are the source.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" } ] }, "readme-structure": { diff --git a/spec/readme-structure.md b/spec/readme-structure.md index 0a747129..babfc7c8 100644 --- a/spec/readme-structure.md +++ b/spec/readme-structure.md @@ -19,7 +19,7 @@ The order, the requirement, and the shield sets are declared data in [`readme-se 8. **Usage (`##`)** - commands, API, or integration. For a CLI, sub-sections such as `### Command Quick Reference`, `### Global Options`, and a test/example command. N/A for source-only. 9. **Questions or Issues (`##`)** - where to file issues and ask questions. **Required in a public repo**, and optional in a private one, since a private repo has no audience to route. 10. **Development Environment Setup (`##`)** - *optional.* Build, test, and lint locally; commit-signing; devcontainer. Point at shared docs rather than restating them. -11. **Contributing (`##`)** - *optional.* The branching workflow, the code style, and the repository setup, each pointing at [`WORKFLOW.md`][workflow], [`CODESTYLE.md`][codestyle], and `repo-config/README.md` rather than restating them. +11. **Contributing (`##`)** - *optional.* The branching workflow, code style, and repository setup, pointing at [`WORKFLOW.md`][workflow], [`CODESTYLE.md`][codestyle], and the applicable governance sections rather than restating them. 12. **3rd Party Tools (`##`)** - the notable dependencies, actions, and tools the project uses, alphabetized, each linked and paired with a **short description of what the tool is**. Required, since every project stands on something and a reader has no other place to find what. The section opens with one line, **"The third-party tools, libraries, and actions this project depends on."**, and that wording is the same in every repo, since a lead-in written per repo is a sentence about this repo's circumstances sitting where a reader expects a list. Each description is written as a sentence, opening with a capital and closing with a full stop. Describe the tool rather than this repo's use of it: "Spell checker." rather than "spell-checks `README.md` and `HISTORY.md` in CI", and "Version computation from git height." rather than a named config file. A role written against local wiring is a second copy of a fact that lives in the workflow, so it goes stale on its own schedule and reads as fleet divergence when the next repo wires the same tool differently. Carry no license column: a license belongs to the dependency and is authoritative at its source, so restating it here adds a maintenance obligation and no information. **A tool the fleet shares is named, linked, and described identically everywhere it appears**, from the catalog in [`third-party-tools.json`][third-party-tools], which is a standard set rather than a complete one: most of a repo's tools are its own and the audit says nothing about those, but a repo using a cataloged tool matches its link and its description. A tool becomes shared by being added to the catalog, which is a hub edit rather than a finding on a repo. 13. **Credits (`##`)** - *optional, and present when the project is directly based on someone else's work.* What it is based on and why it diverged, then which original copyright is retained, in `LICENSE` and `NOTICE`. 14. **License (`##`)** - **the last section in the file.** It states that the project is licensed under the MIT License, linking `LICENSE`, then carries the license shield, and the link definitions follow it. It closes the file because a license is what a reader checks last and never what they navigate to first.