From c5e75e5a638343b22412ca9a765b83d2fdceda93 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 08:04:20 -0700 Subject: [PATCH 1/9] Replace the onboarding-branch entry with the sequence that avoids cleanup The previous entry said the initial commits belong on a feature branch and reach develop by squash. Correct, but it still leaves main and develop as things an agent might commit onto and then have to clean up. Blog tried exactly that and was blocked, which is the ruleset working. The better sequence removes the problem instead of managing it. Create main carrying nothing, create develop from main carrying nothing, create the first feature branch from develop and do the whole standup there, add the repo to GitHub and apply the repo config from that branch, then open a normal pull request to develop when ready. Two details the write-up needs and would otherwise get wrong. A git branch cannot exist without a commit, so "carrying nothing" means one signed empty root commit, and section 0's signing window applies to it. And squashing afterwards is not an escape, because non_fast_forward is set on both develop payloads, so the history cannot be rewritten without disabling the ruleset. STANDUP.md currently says nothing about how main and develop come into being, which is the gap that lets an agent commit onto whichever branch it finds. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 882da1ec..462cdc5c 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ Running backlog for this repo, kept in a committed file so the guidance survives - Refresh the README (it has gone stale) and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation: it must track what the docs contain. - Add a linter-only Python project type for codegen/boilerplate Python, code that runs during another tool's build to emit generated source (e.g. ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage (as in PlexCleaner). Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile. - Add a fleet-standard clang-format config for the `cpp` type: a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`. -- Document in [`STANDUP.md`][standup] that the initial onboarding commits belong on a feature branch and reach `develop` by squash, never as direct commits to `develop`. Pre-creating `main` and `develop` is right, and committing onto `develop` from there is the wrong next step. Section 0 already teaches the same shape of trap for signing, that the window closes at the first commit, so branch placement belongs beside it. The two reasons do not carry equally across the workflow models, which is the part worth stating rather than assuming. On a `release` repo `repo-config/develop.json` carries a `pull_request` rule, so a direct commit is simply blocked and an agent discovers the rule by hitting it. On an `operational` repo `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so a direct commit succeeds and nothing corrects it, which is exactly why the instruction has to carry the weight there. The reason that holds for both is that the squash collapses the exploratory onboarding history, which is where PII, secrets and noise commits accumulate, and a public repo treats that history as a hard gate. There is no recovery afterwards, since `non_fast_forward` is set on both `develop` payloads, so history that reaches `develop` cannot be rewritten without disabling the ruleset. Blog hit this during its standup and moved to a feature branch. +- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out. - Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. - Re-vendor `repo-config/configure.sh` across the fleet. The hub swept it to one sentence per line, and it is carried `verbatim` with `appliesTo: "*"`, so every repo already holding a copy is byte-mismatched against the hub until it takes the new one. From 7705c52f5495bde812f2e21e2b6a9fbba1c44fb9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 08:31:41 -0700 Subject: [PATCH 2/9] Key OPERATIONS.md to the need for it, not to the workflow model OPERATIONS.md was declared appliesTo: ["operational"], which binds a content need to a workflow model. Those are unrelated axes. The `operational` model describes where config lives and how a change reaches develop, not whether the repo has runbooks, so the selector read a sufficient condition as a necessary one. Blog is the proof. It moved from operational to release and its runbook needs did not change by one word: same host, same deploy, same staging. Only the commit-flow classification moved. The rationale in section-model.md already generalized. It mandated the file because the convention was emerging unevenly and the same material scattered across ad-hoc names, which has nothing to do with the workflow model. Every repo has operational surface: a package registry needs trusted-publisher setup, an image needs registry credentials, a site needs a deploy and a staging path. So OPERATIONS.md is now appliesTo: "*", and a repo with nothing to say carries a stub of the headings rather than a blank file, because a stub names the destination and its shape where a blank names only the destination. Also states the rule that decides what earns a declared destination at all: a content class is declared when it recurs across repos. Architecture recurs, so ARCHITECTURE.md stays declared, and advisory, since how much of it earns a separate document is contextual. A one-repo need such as a home-device inventory stays the repo's own file, neither declared nor mandated, because the scattering these destinations prevent has no force for content existing in exactly one repo. The hub carried no OPERATIONS.md, so it would have shipped a rule it broke. It now has a real one rather than a stub, covering the gates, the fleet audit, the configure.sh runbook, and the tool caveats this repo has already hit. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 96 +++++++++++++++++++++++++++++++++++++++++++ STANDUP.md | 4 +- spec/files.json | 2 +- spec/section-model.md | 10 +++-- 4 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 OPERATIONS.md diff --git a/OPERATIONS.md b/OPERATIONS.md new file mode 100644 index 00000000..b7bee47b --- /dev/null +++ b/OPERATIONS.md @@ -0,0 +1,96 @@ +# Operations + +How this repository is run. It ships no application code, so its operations are the fleet audit, the local gates that mirror CI, and the script that applies repository configuration. + +## Runbooks + +### Run the gates the way CI runs them + +CI passes explicit `--check` lists, and a bare `prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations: + +```sh +python3 scripts/test_prose_lint.py +python3 scripts/test_repo_gate.py +python3 scripts/test_pr_review.py +python3 spec/audit.py --selftest +python3 scripts/repo_gate.py +python3 scripts/prose_lint.py . --check charset --check dupword --check spelling +python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary +python3 spec/validate.py +``` + +The first prose invocation gates. The second is warn-only and reports the backlog that is corrected as each file is next edited. + +Scope a run to what changed, which matches the correct-as-next-edited rule: + +```sh +python3 scripts/prose_lint.py . --diff origin/develop +``` + +`prose_lint.py` reads only files git tracks, so a new file reports clean until it is staged. The first clean run on an unstaged file is vacuous. + +### Audit the fleet + +```sh +python3 spec/audit.py # every cataloged repo +python3 spec/audit.py # one repo +python3 spec/audit.py --issue +``` + +Findings are a point-in-time snapshot read live over the API. Re-run before acting on one, and quote the run stamp in any issue derived from it. The deterministic subset lives here, and the full letter-and-intent verdict is [AUDIT.md](./AUDIT.md). + +### Apply or verify repository configuration + +```sh +repo-config/configure.sh check / +repo-config/configure.sh apply / +``` + +`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. + +`apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, and creates or updates both branch rulesets. On a repository that has deliberately drifted it silently reasserts the fleet configuration. + +The model argument selects which develop payload is applied, so passing the wrong one applies the wrong ruleset. + +## Backup and Recovery + +The repository is the record, and GitHub holds it. Nothing here keeps state outside git. + +A deleted branch is recoverable from any full clone that still has the commit, which is the recovery path when a branch is deleted while another pull request is based on it: + +```sh +git push origin :refs/heads/ +``` + +Never use `--depth 1` on a clone that will amend or force-push, because a shallow clone severs the merge base and orphans the branch. + +## Logs and Debugging + +Workflow runs are the log. `gh run list --branch ` and `gh run view --log-failed` reach them. + +A local gate reproduces a CI failure exactly, because CI runs the same commands listed under Runbooks against the same committed configuration. Reproduce locally before reading workflow logs. + +## Tool Usage + +The Docker linters pull `:latest` deliberately, so a local run matches whatever CI resolved: + +```sh +docker run --rm --pull=always -v "$PWD:/workdir" davidanson/markdownlint-cli2:latest "**/*.md" +docker run --rm --pull=always -v "$PWD:/workdir" ghcr.io/streetsidesoftware/cspell:latest --no-progress "**/*.md" +``` + +The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action. + +Two `gh` limitations on the current host, both worked around rather than fixed: + +- `gh pr checks --json` does not exist before `gh` 2.50, so a watcher built on it prints nothing and a quiet result reads as a passing one. +- `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos///pulls/ -f base=` instead. + +## Configuration Layout + +- [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 and the 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. diff --git a/STANDUP.md b/STANDUP.md index a5b57c10..8e6d59d9 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -89,10 +89,10 @@ Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's - [`CODESTYLE.md`][codestyle]: the repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. -- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. +- `OPERATIONS.md`: how the repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. - `TODO.md`: the repo's running backlog, per [`spec/readme-structure.md`][readme-structure]. It keeps open work out of the README's section order, where it does not belong and changes on a different cadence from everything around it. -**`OPERATIONS.md` is required on an `operational` repo**, not optional, so it appears in the baseline above with `appliesTo: ["operational"]`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file. It is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. +**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning the headings above with no content under them. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place. diff --git a/spec/files.json b/spec/files.json index 120276c7..241549ac 100644 --- a/spec/files.json +++ b/spec/files.json @@ -8,7 +8,7 @@ { "path": "WORKFLOW.md", "fidelity": "intent", "whole": true, "intentRef": "WORKFLOW.md", "appliesTo": "*" }, { "path": "README.md", "appliesTo": "*" }, { "path": "HISTORY.md", "appliesTo": "*" }, - { "path": "OPERATIONS.md", "appliesTo": ["operational"] }, + { "path": "OPERATIONS.md", "appliesTo": "*" }, { "path": ".github/copilot-instructions.md", "fidelity": "intent", "whole": true, "sections": ["Commit Messages and Pull Request Titles", "Reviewing Carried Fleet Content", "GitHub Copilot Review Runbook"], "placeholders": ["", "", ""], "appliesTo": "*" }, { "path": ".editorconfig", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" }, { "path": ".editorconfig-checker.json", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" }, diff --git a/spec/section-model.md b/spec/section-model.md index 3efaad79..19d33f01 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -63,11 +63,15 @@ A repo's own content is not carried and is not declared here, so extraction need - [`CODESTYLE.md`][codestyle]: a repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. -- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. This is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. +- `OPERATIONS.md`: how a repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. -**`OPERATIONS.md` is required for an `operational` repo**, declared in [`files.json`][files] as `appliesTo: ["operational"]` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. A repo with nothing to say in it still carries it, since an empty file that names the destination is cheaper than rediscovering the destination. +**`OPERATIONS.md` is required for every repo**, declared in [`files.json`][files] as `appliesTo: "*"` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational-model repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. That reasoning never depended on the workflow model. Every repo has operational surface, since publishing to a package registry needs trusted-publisher setup, shipping an image needs registry credentials, and serving a site needs a deploy path and a staging story. A repo with nothing to say still carries the file as a stub, meaning the headings named above with no content under them, because a stub names the destination and its shape where a blank file names only the destination. -`CODESTYLE.md` is carried by every repo already. `ARCHITECTURE.md` stays **advisory**, deliberately: "a code repo" is not one selector token, and mandating it would produce empty files in repos whose design needs no separate document. Promote it if the same unprompted-convention evidence appears for it. +**The workflow model and the need for this file are unrelated axes.** `operational` as a `workflowModel` describes where config lives and how a change reaches `develop`, not whether the repo has runbooks. Keying the file to that selector read a sufficient condition as a necessary one, since an operational-model repo certainly has runbooks while a release repo has them too. Reclassifying a repo between models does not change how much operational surface it has, which is the test that showed the selector was wrong. + +**A destination is declared when its content class recurs across repos.** Software architecture recurs, because every code repo has one, so `ARCHITECTURE.md` is declared. A home-device inventory does not recur, so it stays the repo's own file, neither declared nor mandated. The scattering these destinations prevent is the same material landing under different names in different repos, and that has no force for content existing in exactly one repo. Declaring a destination for a one-repo need would grow this list without bound and still lag whatever the next repo invents. + +`CODESTYLE.md` is carried by every repo already. `ARCHITECTURE.md` stays **declared but advisory**: every code repo has an architecture, and how much of it earns a separate document is contextual, so mandating it would produce empty files where the design needs none. Declared and required are separate questions, and only a universal need answers both. `OPERATIONS.md` is agent-instruction content, so it takes the inline-link exception the markdown rules name, not the reference-style default. `ARCHITECTURE.md` is not on that closed list and follows the reference-style rule. From 4959bc951022931794a89cf2674f2a5a468b3368 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 08:36:28 -0700 Subject: [PATCH 3/9] Write OPERATIONS.md with the endings this repo declares CI's editorconfig-checker step failed because the new file was LF while `[*] end_of_line = crlf` applies. The tooling that created it writes LF, and nothing local caught that before the push. Converted to CRLF, and added the editorconfig-checker invocation to the gate list in the file itself, which is where its absence caused this. The runbook listed every other gate CI runs and omitted the one that failed. This is the same trap Blog reported from the other side: a carried file arrives with the hub's endings, so an LF repo fails its own check on the first run. Here a new file arrived with the tool's endings and failed the hub's. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 195 +++++++++++++++++++++++++------------------------- 1 file changed, 99 insertions(+), 96 deletions(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index b7bee47b..e36c0fbe 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -1,96 +1,99 @@ -# Operations - -How this repository is run. It ships no application code, so its operations are the fleet audit, the local gates that mirror CI, and the script that applies repository configuration. - -## Runbooks - -### Run the gates the way CI runs them - -CI passes explicit `--check` lists, and a bare `prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations: - -```sh -python3 scripts/test_prose_lint.py -python3 scripts/test_repo_gate.py -python3 scripts/test_pr_review.py -python3 spec/audit.py --selftest -python3 scripts/repo_gate.py -python3 scripts/prose_lint.py . --check charset --check dupword --check spelling -python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary -python3 spec/validate.py -``` - -The first prose invocation gates. The second is warn-only and reports the backlog that is corrected as each file is next edited. - -Scope a run to what changed, which matches the correct-as-next-edited rule: - -```sh -python3 scripts/prose_lint.py . --diff origin/develop -``` - -`prose_lint.py` reads only files git tracks, so a new file reports clean until it is staged. The first clean run on an unstaged file is vacuous. - -### Audit the fleet - -```sh -python3 spec/audit.py # every cataloged repo -python3 spec/audit.py # one repo -python3 spec/audit.py --issue -``` - -Findings are a point-in-time snapshot read live over the API. Re-run before acting on one, and quote the run stamp in any issue derived from it. The deterministic subset lives here, and the full letter-and-intent verdict is [AUDIT.md](./AUDIT.md). - -### Apply or verify repository configuration - -```sh -repo-config/configure.sh check / -repo-config/configure.sh apply / -``` - -`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. - -`apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, and creates or updates both branch rulesets. On a repository that has deliberately drifted it silently reasserts the fleet configuration. - -The model argument selects which develop payload is applied, so passing the wrong one applies the wrong ruleset. - -## Backup and Recovery - -The repository is the record, and GitHub holds it. Nothing here keeps state outside git. - -A deleted branch is recoverable from any full clone that still has the commit, which is the recovery path when a branch is deleted while another pull request is based on it: - -```sh -git push origin :refs/heads/ -``` - -Never use `--depth 1` on a clone that will amend or force-push, because a shallow clone severs the merge base and orphans the branch. - -## Logs and Debugging - -Workflow runs are the log. `gh run list --branch ` and `gh run view --log-failed` reach them. - -A local gate reproduces a CI failure exactly, because CI runs the same commands listed under Runbooks against the same committed configuration. Reproduce locally before reading workflow logs. - -## Tool Usage - -The Docker linters pull `:latest` deliberately, so a local run matches whatever CI resolved: - -```sh -docker run --rm --pull=always -v "$PWD:/workdir" davidanson/markdownlint-cli2:latest "**/*.md" -docker run --rm --pull=always -v "$PWD:/workdir" ghcr.io/streetsidesoftware/cspell:latest --no-progress "**/*.md" -``` - -The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action. - -Two `gh` limitations on the current host, both worked around rather than fixed: - -- `gh pr checks --json` does not exist before `gh` 2.50, so a watcher built on it prints nothing and a quiet result reads as a passing one. -- `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos///pulls/ -f base=` instead. - -## Configuration Layout - -- [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 and the 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. +# Operations + +How this repository is run. It ships no application code, so its operations are the fleet audit, the local gates that mirror CI, and the script that applies repository configuration. + +## Runbooks + +### Run the gates the way CI runs them + +CI passes explicit `--check` lists, and a bare `prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations: + +```sh +python3 scripts/test_prose_lint.py +python3 scripts/test_repo_gate.py +python3 scripts/test_pr_review.py +python3 spec/audit.py --selftest +python3 scripts/repo_gate.py +python3 scripts/prose_lint.py . --check charset --check dupword --check spelling +python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary +python3 spec/validate.py +docker run --rm --pull=always -v "$PWD:/check" mstruebing/editorconfig-checker:latest ec +``` + +Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally. + +The first prose invocation gates. The second is warn-only and reports the backlog that is corrected as each file is next edited. + +Scope a run to what changed, which matches the correct-as-next-edited rule: + +```sh +python3 scripts/prose_lint.py . --diff origin/develop +``` + +`prose_lint.py` reads only files git tracks, so a new file reports clean until it is staged. The first clean run on an unstaged file is vacuous. + +### Audit the fleet + +```sh +python3 spec/audit.py # every cataloged repo +python3 spec/audit.py # one repo +python3 spec/audit.py --issue +``` + +Findings are a point-in-time snapshot read live over the API. Re-run before acting on one, and quote the run stamp in any issue derived from it. The deterministic subset lives here, and the full letter-and-intent verdict is [AUDIT.md](./AUDIT.md). + +### Apply or verify repository configuration + +```sh +repo-config/configure.sh check / +repo-config/configure.sh apply / +``` + +`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. + +`apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, and creates or updates both branch rulesets. On a repository that has deliberately drifted it silently reasserts the fleet configuration. + +The model argument selects which develop payload is applied, so passing the wrong one applies the wrong ruleset. + +## Backup and Recovery + +The repository is the record, and GitHub holds it. Nothing here keeps state outside git. + +A deleted branch is recoverable from any full clone that still has the commit, which is the recovery path when a branch is deleted while another pull request is based on it: + +```sh +git push origin :refs/heads/ +``` + +Never use `--depth 1` on a clone that will amend or force-push, because a shallow clone severs the merge base and orphans the branch. + +## Logs and Debugging + +Workflow runs are the log. `gh run list --branch ` and `gh run view --log-failed` reach them. + +A local gate reproduces a CI failure exactly, because CI runs the same commands listed under Runbooks against the same committed configuration. Reproduce locally before reading workflow logs. + +## Tool Usage + +The Docker linters pull `:latest` deliberately, so a local run matches whatever CI resolved: + +```sh +docker run --rm --pull=always -v "$PWD:/workdir" davidanson/markdownlint-cli2:latest "**/*.md" +docker run --rm --pull=always -v "$PWD:/workdir" ghcr.io/streetsidesoftware/cspell:latest --no-progress "**/*.md" +``` + +The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action. + +Two `gh` limitations on the current host, both worked around rather than fixed: + +- `gh pr checks --json` does not exist before `gh` 2.50, so a watcher built on it prints nothing and a quiet result reads as a passing one. +- `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos///pulls/ -f base=` instead. + +## Configuration Layout + +- [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 and the 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. From f449da819b9462e806666f2066beaa8c654a2dde Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 08:41:39 -0700 Subject: [PATCH 4/9] Name the stub headings, and reconcile two counts the review caught Four findings from the Copilot round, all real. "A stub of the headings above" was a dangling reference in both files, since neither named any headings. Both now name the five: Runbooks, Backup and Recovery, Logs and Debugging, Tool Usage, and Configuration Layout. They match this repo's own OPERATIONS.md, which both files now point at as the worked example. section-model.md said "Three topical docs take it" while STANDUP.md said four, the difference being TODO.md. section-model.md was the one missing it, so a reader of the spec had no declared destination for backlog content. It now lists four. The editorconfig-checker line did not match CI's invocation, in a section titled "Run the gates the way CI runs them". It now mirrors CI exactly. Both forms were verified to behave identically against a deliberate violation, so this removes a dependency on the image's default working directory rather than fixing a broken command. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 2 +- STANDUP.md | 4 ++-- spec/section-model.md | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index e36c0fbe..df6935b9 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -17,7 +17,7 @@ python3 scripts/repo_gate.py python3 scripts/prose_lint.py . --check charset --check dupword --check spelling python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary python3 spec/validate.py -docker run --rm --pull=always -v "$PWD:/check" mstruebing/editorconfig-checker:latest ec +docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest ``` Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally. diff --git a/STANDUP.md b/STANDUP.md index 8e6d59d9..70958f6f 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -89,10 +89,10 @@ Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's - [`CODESTYLE.md`][codestyle]: the repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. -- `OPERATIONS.md`: how the repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. +- `OPERATIONS.md`: how the repo is run, under the headings `Runbooks`, `Backup and Recovery`, `Logs and Debugging`, `Tool Usage`, and `Configuration Layout`. - `TODO.md`: the repo's running backlog, per [`spec/readme-structure.md`][readme-structure]. It keeps open work out of the README's section order, where it does not belong and changes on a different cadence from everything around it. -**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning the headings above with no content under them. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. +**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning those five headings with no content under them, for which this repo's own `OPERATIONS.md` is the worked example. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place. diff --git a/spec/section-model.md b/spec/section-model.md index 19d33f01..af7d30aa 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -59,13 +59,14 @@ The set of sections, and each section's fidelity, is itself governed. ## Where repo-specific content goes -A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Three topical docs take it, chosen by what the content *is*: +A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Four topical docs take it, chosen by what the content *is*: - [`CODESTYLE.md`][codestyle]: a repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. -- `OPERATIONS.md`: how a repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. +- `OPERATIONS.md`: how a repo is run, under the headings `Runbooks`, `Backup and Recovery`, `Logs and Debugging`, `Tool Usage`, and `Configuration Layout`. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half. +- `TODO.md`: the repo's running backlog, which keeps open work out of the README's section order where it does not belong and changes on a different cadence from everything around it. -**`OPERATIONS.md` is required for every repo**, declared in [`files.json`][files] as `appliesTo: "*"` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational-model repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. That reasoning never depended on the workflow model. Every repo has operational surface, since publishing to a package registry needs trusted-publisher setup, shipping an image needs registry credentials, and serving a site needs a deploy path and a staging story. A repo with nothing to say still carries the file as a stub, meaning the headings named above with no content under them, because a stub names the destination and its shape where a blank file names only the destination. +**`OPERATIONS.md` is required for every repo**, declared in [`files.json`][files] as `appliesTo: "*"` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational-model repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. That reasoning never depended on the workflow model. Every repo has operational surface, since publishing to a package registry needs trusted-publisher setup, shipping an image needs registry credentials, and serving a site needs a deploy path and a staging story. A repo with nothing to say still carries the file as a stub, meaning those five headings with no content under them, because a stub names the destination and its shape where a blank file names only the destination. This repo's own [`OPERATIONS.md`][operations] is the worked example. **The workflow model and the need for this file are unrelated axes.** `operational` as a `workflowModel` describes where config lives and how a change reaches `develop`, not whether the repo has runbooks. Keying the file to that selector read a sufficient condition as a necessary one, since an operational-model repo certainly has runbooks while a release repo has them too. Reclassifying a repo between models does not change how much operational surface it has, which is the test that showed the selector was wrong. @@ -93,4 +94,5 @@ A repo that carried its governance inside `AGENTS.md` before the router split ho [fidelity-model]: ./fidelity-model.md [files]: ./files.json [governance]: ../GOVERNANCE.md +[operations]: ../OPERATIONS.md [validate]: ./validate.py From 5343e1a08e0a6dd5b37af2f96588340b09e30a4d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 09:07:45 -0700 Subject: [PATCH 5/9] Update the conformance matrix for the widened rule and Blog's shape The matrix still said OPERATIONS.md is required only for an operational repo, with appliesTo: ["operational"], which this PR makes false. Merging without this would have shipped the hub contradicting itself in the document agents read to learn what a shape requires. The operational row now describes what actually distinguishes that shape, that its develop ruleset carries no pull_request rule, so branch discipline rests on the instruction rather than the gate. The same row also still claimed Blog as the next operational standup. Blog is release plus source-only now, so it gets its own row rather than sitting under a model it no longer uses. The new row adds one warn-only dash finding, the `| - |` empty cell every other row also carries. The dash rule reads a table placeholder as prose. Keeping the table's own convention is worth one backlog entry, and the rule gap is the follow-up rather than a reason to write the row differently. Co-Authored-By: Claude Opus 5 (1M context) --- reports/conformance-matrix.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reports/conformance-matrix.md b/reports/conformance-matrix.md index 30d92422..957c4429 100644 --- a/reports/conformance-matrix.md +++ b/reports/conformance-matrix.md @@ -11,6 +11,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug | Shape | Reference repo | Cold-standup | Last audited | First gap / notes | |---|---|---|---|---| | `python` + `source-only` | Financial-Modeling | not-tested | - | Reference for the source-release (dispatch-only) profile; the downstream standup issue is open. | +| `source-only` + `release` | Blog | not-tested | - | Hugo static site (#456), the next standup and the first cold test of `STANDUP.md` step 0. Its deploy target has no declared type yet, so `releaseTrigger` is `none` and `publish[]` is empty until a deploy has actually run. | | `csharp` + `console` | - | not-tested | - | | | `csharp` + `docker` | - | not-tested | - | | | `csharp` + `python` | PlexCleaner | not-tested | - | First mixed-language shape (#339). Python is a stdlib-only `uvx` **scripts** profile subtree (`RegressionTests/`): no `uv.lock`, `pyproject.toml` lint/type config only, mypy checker, `python.uvlock.pinned` + `python.coverage.codecov` N/A; `codecov.yml` stays required for the C# side. Both language rule-sets apply (CODESTYLE.md "Two profiles"). | @@ -19,7 +20,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug | `upstream-wrapper` | - | not-tested | - | Tag from a committed state file, not SemVer2. | | `codegen` | - | not-tested | - | Deterministic matrix over both branches. | | `docs` | ProjectTemplate | not-tested | - | Governance hub; CI is lint-only. | -| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Carries a required `OPERATIONS.md` (`appliesTo: ["operational"]`, presence-checked) for its runbooks. Blog (#456) is the next standup and the first cold test of `STANDUP.md` step 0. | +| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Its `develop` ruleset carries no `pull_request` rule, so the branch discipline rests on the instruction rather than the gate. | ## Composable Targets From b7e42a22538779fa440552f9ad15f86f2fd48b08 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 09:12:42 -0700 Subject: [PATCH 6/9] Mirror the canonical linter invocations, and say where warn-only comes from Two suppressed findings, both real. The runbook called the second prose invocation warn-only without saying that the leniency is CI's, not the command's. The command exits non-zero locally whenever findings exist, and the workflow step sets continue-on-error. A reader following the runbook would have read the expected result as a failure. The markdownlint and cspell lines also drifted from the canonical invocations in GOVERNANCE.md: both omitted --workdir, and cspell read every markdown file rather than README and HISTORY. The narrower cspell scope is deliberate, so the file now carries that reason too, since a reader who does not know it will widen the scope back and get noise no gate acts on. Third round of real findings in this file. It was a lot of new prose written quickly, and the reviews have been earning their place. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index df6935b9..7a9e7af8 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -22,7 +22,7 @@ docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/edito Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally. -The first prose invocation gates. The second is warn-only and reports the backlog that is corrected as each file is next edited. +The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem. Scope a run to what changed, which matches the correct-as-next-edited rule: @@ -78,10 +78,12 @@ A local gate reproduces a CI failure exactly, because CI runs the same commands The Docker linters pull `:latest` deliberately, so a local run matches whatever CI resolved: ```sh -docker run --rm --pull=always -v "$PWD:/workdir" davidanson/markdownlint-cli2:latest "**/*.md" -docker run --rm --pull=always -v "$PWD:/workdir" ghcr.io/streetsidesoftware/cspell:latest --no-progress "**/*.md" +docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir davidanson/markdownlint-cli2:latest "**/*.md" +docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md ``` +Both commands are the canonical invocations from [GOVERNANCE.md](./GOVERNANCE.md). markdownlint reads every markdown file, while cspell reads `README.md` and `HISTORY.md` only. That narrower spelling scope is deliberate, since gating every markdown file would mean padding `cspell.json` with technical terms without end, and broad live spell-check is the editor extension's job. Widening it here produces noise that no gate acts on. + The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action. Two `gh` limitations on the current host, both worked around rather than fixed: From b0e07326d2180ba92cc6e7f189b90df056442547 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 09:17:47 -0700 Subject: [PATCH 7/9] Say which prose_lint modes are limited to tracked files The runbook said prose_lint.py reads only files git tracks. That holds for whole-tree discovery and for --diff, not for an explicit path, which is read whether or not git tracks it. Verified rather than reasoned: an untracked file named directly reports its finding and exits 1, while the same file is invisible to a whole-tree run. The wording mattered more than an ordinary inaccuracy, because it discouraged the one habit that catches a new file's problems before CI does. This PR hit exactly that: OPERATIONS.md was written with the wrong line endings and failed the first CI run. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index 7a9e7af8..14d5d8b2 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -30,7 +30,7 @@ Scope a run to what changed, which matches the correct-as-next-edited rule: python3 scripts/prose_lint.py . --diff origin/develop ``` -`prose_lint.py` reads only files git tracks, so a new file reports clean until it is staged. The first clean run on an unstaged file is vacuous. +Whole-tree discovery reads only files git tracks, so `prose_lint.py .` and `--diff` do not see a new file until it is staged, and a clean whole-tree run proves nothing about an unstaged one. An explicit path is always read, tracked or not, so name a new file directly to check it before staging. ### Audit the fleet From cd27dc89f0e074350c9dbfec81722378cf4f8959 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 09:23:04 -0700 Subject: [PATCH 8/9] Name the scripts by path, and warn that a bare configure.sh applies Three review findings plus two my own re-read caught. The prose referred to `prose_lint.py` while the script is `scripts/prose_lint.py`, so the sentences did not match the commands beside them. Both now use the real path. section-model.md said repo content "is not declared here" and then declared four destinations. The intent was that the hub declares where content goes rather than what it says, so it now says that instead of reading as a contradiction. Two found by re-reading rather than by review. The gate list omitted the jq parse loop that CI runs before validate.py, so the section claiming to mirror CI did not. And the note that `gh pr checks --json` "does not exist before gh 2.50" asserted a version boundary that was never verified. It now states the verified fact, that the installed 2.46.0 has no such flag, and names the working alternative. The configure.sh section now warns that a bare invocation defaults to apply. That is not hypothetical: running the script with no arguments to check whether it was executable performed a live apply against this repo. It was a no-op because the repo was already conformant, confirmed by a clean check afterward, but the runbook should say so before someone else learns it the same way. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 9 ++++++--- spec/section-model.md | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index 14d5d8b2..a490193f 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -6,7 +6,7 @@ How this repository is run. It ships no application code, so its operations are ### Run the gates the way CI runs them -CI passes explicit `--check` lists, and a bare `prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations: +CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations: ```sh python3 scripts/test_prose_lint.py @@ -16,6 +16,7 @@ python3 spec/audit.py --selftest python3 scripts/repo_gate.py python3 scripts/prose_lint.py . --check charset --check dupword --check spelling python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary +for f in registry/*.json spec/*.json repo-config/*.json; do jq empty "$f"; done python3 spec/validate.py docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest ``` @@ -30,7 +31,7 @@ Scope a run to what changed, which matches the correct-as-next-edited rule: python3 scripts/prose_lint.py . --diff origin/develop ``` -Whole-tree discovery reads only files git tracks, so `prose_lint.py .` and `--diff` do not see a new file until it is staged, and a clean whole-tree run proves nothing about an unstaged one. An explicit path is always read, tracked or not, so name a new file directly to check it before staging. +Whole-tree discovery reads only files git tracks, so `python3 scripts/prose_lint.py .` and `--diff` do not see a new file until it is staged, and a clean whole-tree run proves nothing about an unstaged one. An explicit path is always read, tracked or not, so name a new file directly to check it before staging. ### Audit the fleet @@ -49,6 +50,8 @@ repo-config/configure.sh check / repo-config/configure.sh apply / ``` +**Always pass the command.** 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. + `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. `apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, and creates or updates both branch rulesets. On a repository that has deliberately drifted it silently reasserts the fleet configuration. @@ -88,7 +91,7 @@ The `editorconfig-checker` action is setup-only. Using it alone silently skips t Two `gh` limitations on the current host, both worked around rather than fixed: -- `gh pr checks --json` does not exist before `gh` 2.50, so a watcher built on it prints nothing and a quiet result reads as a passing one. +- `gh pr checks` carries no `--json` flag on the installed `gh` 2.46.0, so a watcher built on it prints nothing and a quiet result reads as a passing one. Read the checks from `gh pr view --json statusCheckRollup` instead. - `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos///pulls/ -f base=` instead. ## Configuration Layout diff --git a/spec/section-model.md b/spec/section-model.md index af7d30aa..96605c4a 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -59,7 +59,7 @@ The set of sections, and each section's fidelity, is itself governed. ## Where repo-specific content goes -A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Four topical docs take it, chosen by what the content *is*: +A repo's own content is not carried, and the hub declares where it goes rather than what it says, so extraction needs a predictable destination rather than a judgment call per repo. Four topical docs take it, chosen by what the content *is*: - [`CODESTYLE.md`][codestyle]: a repo's language and formatting conventions beyond the carried rules. - `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions. From a644b6f0d5da298e776d248960f5d9d9713eef05 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 1 Aug 2026 09:53:27 -0700 Subject: [PATCH 9/9] Describe what CI checks, and record the two gaps in it The runbook said DEFAULT_RULES omits comment-wrap, comment-case and sentence-split, which reads as though CI covers all three. CI names sentence-split nowhere, so the sentence overstated the coverage it warned about. Two gaps are CI's rather than this runbook's, and are now recorded rather than silently mirrored. The jq glob covers repo-config/*.json and does not reach repo-config/operational/develop.json, so a malformed operational payload passes the JSON validity step, and validate.py does not parse it either. sentence-split is implemented and carries tests but is named by no invocation, so a rule the repo maintains never runs. Both are left as findings rather than fixed here, since changing what CI runs is a different change from declaring where operational content lives. Both are clean today, so enabling them costs nothing when it happens. Co-Authored-By: Claude Opus 5 (1M context) --- OPERATIONS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index a490193f..380606af 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -6,7 +6,7 @@ How this repository is run. It ships no application code, so its operations are ### Run the gates the way CI runs them -CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations: +CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py ` runs `DEFAULT_RULES`, which omits `comment-wrap` and `comment-case`. A bare run therefore under-reports against what CI checks, and a clean result from it proves less than it appears to. Run the CI invocations: ```sh python3 scripts/test_prose_lint.py @@ -21,6 +21,8 @@ python3 spec/validate.py docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest ``` +Two gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. And `sentence-split` is implemented and tested but named by no invocation, so nothing runs it. + Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally. The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem.