Conversation
The `.editorconfig` / `.gitattributes` / `.gitignore` entry from `TODO.md`, taken now because `Blog` is carrying the baseline this week and these are the first files it copies. `TODO.md` states the reason itself: until they are fixed, a new repo learns the shape the rules forbid. ## Result **36 findings gone.** `.gitignore` was already clean, so the 44 that `TODO.md` claimed was stale, most likely because #500 removed several `.editorconfig` findings on the way past. | File | Before | After | Fidelity | | --- | --- | --- | --- | | `.editorconfig` | 22 | 0 | `intent` | | `.gitattributes` | 14 | 0 | `intent` | | `.gitignore` | 0 | 0 | repo-owned | Both swept files are `intent` fidelity, so a downstream copy is **adapted rather than byte-matched**. Unlike #501 this obliges no re-vendor. ## A recommendation I am reversing In #501 I said a **command-synopsis exemption** in `prose_lint.py` was probably the better long-term answer than labeling each block. Having done this sweep, I no longer think so, and this PR labels instead. Two bare commands appear here: ``` # git config --global core.autocrlf false # git ls-files --eol ``` The difference from the bare-URI case in #500 is that **relabeling a command is easy and improves it**, where relabeling a URI is impossible without corrupting the address: ``` # Configure with: git config --global core.autocrlf false # Inspect with: git ls-files --eol ``` The URI needed an exemption because there was no legal fix. A command has one, and it reads better. On top of that, "a comment body that is a command invocation" has no crisp form the way `^scheme://...$` does, and a fuzzy exemption inside a gate silently swallows real prose. So the rule stays as it is. ## What was corrected, by shape - **Wrapped prose** rewritten one sentence per line, which is the bulk of the 36. - **Comments opening on `git` or `uv`** restructured rather than capitalized, per GOVERNANCE's tooling-casing rule. For example `# uv regenerates uv.lock ...` became `# The uv tool regenerates uv.lock ...`. - **Bare commands** labeled, as above. ## Fixed in passing, since these files were open A mid-sentence semicolon in `.gitattributes` and several spaced hyphens in both files. GOVERNANCE bans both in agent-authored prose, and neither was reported here because the `semicolon` and `dash` rules are still markdown-only. Leaving them would have carried the banned construction into every repo copying these files. ## No setting or pattern changed The whole point is that this is comment-only, so it is evidenced rather than asserted: - **Every non-comment line in both files is byte-identical**, checked by diffing the files with comment lines stripped. - **`repo_gate`'s `eol` check still passes.** That check cross-validates the two files against each other, asserting every path pinned LF in `.gitattributes` has the matching `.editorconfig` override, so it would catch a mangled pattern or section header. ## Verification ``` prose_lint .editorconfig .gitattributes --check comment-wrap --check comment-case clean prose_lint . --check charset --check dupword --check spelling clean prose_lint . --diff HEAD clean, all default rules python3 scripts/repo_gate.py eol 0, sha-pin 0 python3 scripts/test_prose_lint.py 141 tests, OK python3 spec/validate.py OK, 21 cataloged ``` Tree-wide warn-only backlog moves **1152 -> 1116**. ## TODO.md Drops the two swept entries, and adds the **fleet re-vendor of `configure.sh`** that #501 obliges, which was previously recorded only in the PR body. The `README.md` entry is reworded: it was deferred because two edits to that file were in flight, and both have since landed, so it is now free to take. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lf (#505) Found by running the live audit against the hub itself while checking that the tooling handles the shape `Blog` is about to become. Two defects in one check, both in `spec/audit.py`'s carried-file template-reference rule. ## 1. The stated reason is false The check justified itself this way, in the code comment, the finding text, and the `recurring.norepoxref` assertion in `spec/project-types.json`: > private, so it 404s for the carrying repo's readers **This repository has been public since 2026-07-26**, confirmed live: ``` $ gh api repos/ptr727/ProjectTemplate --jq '{private, visibility}' {"private": false, "visibility": "public"} ``` So a reference does not 404, and that half of the justification is dead. **The rule itself survives**, on the second reason `project-types.json` already gave: the coordination flow is machinery a consumer should not see, so a carried file states the behavior rather than the destination. The wording now rests on that alone, corrected in all three places that carried the dead premise. This matters beyond tidiness. A rule whose stated reason is checkably false is a rule the next agent argues with, and a downstream repo carrying the assertion inherits the false claim. ## 2. It fired on the hub itself Auditing `ProjectTemplate` reported its own `AGENTS.md` as drift for naming the repository it lives in: ``` DRIFT carried: AGENTS.md references the template repo by name or link ... ``` That is a false positive. The hub's carried files are the **source**, and naming itself there is correct and unavoidable. A downstream repo naming it is still flagged, which is the actual point of the rule. The fix reuses the guard the section check at the same layer already uses (`entry.get("name") != HUB_NAME`), whose comment states the same rationale: *"Skip the hub itself: its copies are the source."* The idiom existed; this check had simply omitted it. ## Verification The check reads the repo over the API, so it is not reachable from the offline `--selftest`. The evidence is the live audit before and after, not a unit test. **Before:** ``` == ProjectTemplate (source-only, docs; release) @ main@6501479 == DEFECT settings: has_discussions live=False expected=True DRIFT carried: AGENTS.md references the template repo by name or link (private - 404s ...) 1 repo(s) audited; 1 defect/letter/error finding(s). ``` **After:** ``` == ProjectTemplate (source-only, docs; release) @ main@6501479 == DEFECT settings: has_discussions live=False expected=True 1 repo(s) audited; 1 defect/letter/error finding(s). ``` The false DRIFT is gone and the real defect is untouched. ``` python3 spec/audit.py --selftest SELFTEST PASS python3 spec/validate.py OK, 21 cataloged python3 scripts/test_prose_lint.py 141 tests, OK python3 scripts/repo_gate.py eol 0, sha-pin 0 prose_lint --check charset --check dupword --check spelling clean ``` `spec/audit.py` carries a large pre-existing comment backlog. This PR does not sweep it, but the rewritten block leaves the file slightly better rather than worse: **105 -> 103**. ## A separate finding this surfaced, for the maintainer `has_discussions` is `false` on this repository while `repo-config/settings.json` and `configure.sh` expect `true` for a public repo. It is a real settings drift, not a tooling bug, and `repo-config/configure.sh apply` would set it. Enabling Discussions is a visible change to the repository, so it is left for the maintainer rather than applied here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `README.md` entry from `TODO.md`. It was deferred rather than dropped because two edits to that file were in flight, and both have since landed, so it is now free to take. > **Stacked on #503**, which also edits the `TODO.md` entry this PR removes. Merge #503 first and GitHub retargets this to `develop` automatically. Branching from `develop` instead would have conflicted on that line for no gain. ## Result **12 findings gone**: 6 `dash` and 6 `semicolon`, both constructions GOVERNANCE bans in agent-authored prose. ``` before: dash 6, semicolon 6 (12) after: 0 ``` Each was recast the way the rule prescribes: a spaced hyphen becomes a comma, a preposition, or a subordinate clause, and a prose semicolon becomes a comma or two sentences. **No content changed, only its construction.** ## Two worth naming **The Source Code and Versioned Releases bullets** read `- **Label**: [link] - explanation`. That is *not* the `- **Label** - explanation` separator the dash rule exempts, because the label already ends in a colon, so the dash was a genuine clause break rather than structural punctuation. Both now read `[link] for explanation`. **The Python bullet** carried both faults in a single line: ``` - Configure ruff and a type checker in `pyproject.toml` - pyright strict, or mypy in CI with pyright editor-only; whichever runs in CI is the gate. ``` It splits into two sentences, which resolves the dash and the semicolon together. ## Verification ``` prose_lint README.md (dash, semicolon, charset, charset-unknown, dupword, spelling, sentence-split) clean prose_lint TODO.md (same set) clean prose_lint . --diff HEAD clean, all default rules markdownlint-cli2 (docker, README + TODO) 0 issues cspell (docker, README + TODO) 0 issues python3 scripts/test_prose_lint.py 144 tests, OK python3 spec/validate.py OK, 21 cataloged ``` markdownlint and cspell were run because CI gates `README.md` with both, and this PR rewrites sentences rather than only comments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…anup (#506) Replaces the onboarding-branch entry added in #504 with the sequence Pieter proposed after watching `Blog` hit the problem. ## What changed and why The #504 entry said the initial commits belong on a feature branch and reach `develop` by squash. That is correct, but it still treats `main` and `develop` as branches an agent might commit onto and then have to clean up. `Blog` tried to squash its standup work back onto the `main` and `develop` it had already committed to, and **was blocked**, which is the ruleset doing its job. The proposed sequence removes the problem rather than managing it: 1. Create `main`, carrying nothing. 2. Create `develop` from `main`, carrying nothing. 3. Create the first feature branch from `develop` and do the **whole** standup there. 4. Add the repo to GitHub and apply the repo config while still on that branch. 5. 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. ## Two details the write-up would otherwise get wrong **A git branch cannot exist without a commit.** "Carrying nothing" therefore means exactly one signed empty root commit (`git commit --allow-empty`), not a literally empty ref. Section 0's signing window applies to that commit like any other, so it has to be signed before it is made. **Squashing afterwards is not an escape hatch.** `non_fast_forward` is set on **both** `develop` payloads, so once history reaches `develop` it cannot be rewritten without disabling the ruleset. That is why `Blog` was blocked rather than merely inconvenienced. ## The gap this fills `STANDUP.md` says nothing about how `main` and `develop` come into being. Section 0 covers signing before the first commit, section 0A covers the human-gated prerequisites, and section 2 covers carrying the baseline files, but the branch bootstrap between them is undocumented. An agent with no instruction commits onto whichever branch it finds itself on. Worth noting the protection is **uneven**, which the entry records: | Payload | Rules | Direct commit to `develop` | | --- | --- | --- | | `repo-config/develop.json` (release) | adds `pull_request`, `required_linear_history`, `required_status_checks`, `copilot_code_review` | blocked | | `repo-config/operational/develop.json` | `deletion`, `non_fast_forward`, `required_signatures` only | **succeeds** | On an operational repo the ruleset alone would not stop a direct commit, so only the instruction stands between it and an unfixable history. That is the argument for documenting the sequence rather than relying on the gate. **Correction, after this PR was opened:** `Blog` is `release`, not `operational`. I had classified it wrongly, and Pieter corrected it. The test is where the config lives, since `operational` is for config that sits on the actual server, and Blog builds and pushes releases out instead. So Blog's own `develop` *is* PR-gated and the ruleset does block it. That strengthens the entry rather than weakening it: the uneven-protection point stands for every genuinely operational repo, and the bootstrap sequence is what keeps `main` and `develop` clean in both models. The reason it matters on a public repo: 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. ## Verification ``` prose_lint TODO.md (dash, semicolon, charset, charset-unknown, dupword, spelling, sentence-split) clean prose_lint . --check charset --check dupword --check spelling clean markdownlint-cli2 (docker) 0 issues cspell (docker) 0 issues ``` `cspell` rejected "unreviewed" on the first pass. Rephrased rather than adding a word to `cspell.json`, since one entry's worth of dictionary growth is not worth a phrasing that had an ordinary alternative. Backlog only. The `STANDUP.md` change itself is the work this entry describes, not this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the `OPERATIONS.md` half of the topical-destination question Pieter raised, and states the rule that decides what earns a declared destination at all. ## The defect `OPERATIONS.md` was declared `appliesTo: ["operational"]`, which binds a **content** need to a **workflow model**. Those are unrelated axes. `operational` describes where config lives and how a change reaches `develop`. It says nothing about whether the repo has runbooks, so the selector read a sufficient condition as a necessary one. **`Blog` is the proof.** It was reclassified from `operational` to `release` today and its runbook needs did not change by one word: same host, same deploy, same staging, same rsync. Only the commit-flow classification moved. A doc requirement that appears and disappears on an unrelated axis is mis-keyed. **The existing rationale already generalized.** `spec/section-model.md` justified mandating the file like this: > 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 Nothing there depends on the workflow model. Every repo has operational surface: publishing to a package registry needs trusted-publisher setup, shipping an image needs registry credentials, serving a site needs a deploy path and a staging story. ## The change - `OPERATIONS.md` is `appliesTo: "*"`, required everywhere, still **presence-checked only**, so its content stays entirely the repo's own. - A repo with nothing to say carries a **stub of the headings**, not a blank file. Same cost, strictly more useful: a stub names the destination *and its shape*, where a blank names only the destination. - Both `spec/section-model.md` and `STANDUP.md` now say explicitly that the workflow model and this file are unrelated, so nobody re-derives the link. ## The rule this also states **A destination is declared when its content class recurs across repos.** - Software architecture recurs, because every code repo has one, so `ARCHITECTURE.md` stays **declared**, and **advisory**, because how much of it earns a separate document is contextual. - A home-device inventory (ESPHome-Config's `devices.md`) 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*. That has no force for content existing in exactly one repo, and declaring a destination for a one-repo need would grow the list without bound while still lagging whatever the next repo invents. This also dissolves an asymmetry that had been accidental rather than decided: `OPERATIONS.md` mandatory and `ARCHITECTURE.md` advisory are consistent once **declared** and **required** are separate questions. Both are declared. Only the universal one is required. ## The hub now complies with its own rule `ProjectTemplate` carried no `OPERATIONS.md`, so this PR would have shipped a rule the hub itself broke. It now has a **real one, not a stub**, covering what this repo has actually hit: running the gates the way CI runs them (including that a bare `prose_lint.py` runs `DEFAULT_RULES` and under-reports), the fleet audit, the `configure.sh` runbook with its blast radius stated, branch recovery via `git push origin <sha>:refs/heads/<branch>`, and the two live `gh` limitations on this host. ## Fleet impact, stated rather than discovered This makes the file required for 18 more repos. Measured live: | | Count | Repos | | --- | --- | --- | | Have it | 3 | ESPHome-Config, Financial-Modeling, HomeAssistant-Config | | Missing | 18 | the rest, including ProjectTemplate | This PR fixes the hub, leaving **17 repos that will report a new `LETTER file: OPERATIONS.md absent` finding** on the next audit. That is the intended consequence of widening the requirement, not a surprise, and a stub satisfies it. It is worth knowing before the next fleet audit reads noisier than the last one. ## Verification ``` python3 scripts/test_prose_lint.py 144 tests, OK python3 scripts/test_repo_gate.py 23 tests, OK python3 scripts/test_pr_review.py 27 tests, OK python3 spec/audit.py --selftest SELFTEST PASS python3 scripts/repo_gate.py eol 0, sha-pin 0 python3 spec/validate.py OK, 21 cataloged prose_lint --check charset --check dupword --check spelling clean prose_lint --diff HEAD (dash, semicolon, comment rules, charset, dupword, spelling) clean markdownlint-cli2 (docker, the three docs) 0 issues ``` `cspell` was run and reports words in `OPERATIONS.md` and pre-existing ones in `STANDUP.md`. None are gated: the CI cspell step covers `README.md` and `HISTORY.md` only, deliberately, because gating all markdown would mean endlessly padding `cspell.json` with technical terms. Nothing was added to the dictionary. ## Not in this PR The advisory audit dimension for **misplaced** content in undeclared markdown, which is the other half of the discussion. It needs `spec/audit.py` to enumerate unmatched markdown and `AUDIT.md` to carry the agent's classification step, and it is cleaner as its own change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records the README shape rework Pieter asked for, measured rather than described. Backlog only, no spec change here. ## What was compared `ptr727/PlexCleaner` `README.md` (hand-crafted, the wanted shape), this repo's `README.md`, and `spec/readme-structure.md`. ## Four concrete divergences | # | PlexCleaner | Spec / this repo | | --- | --- | --- | | 1 | `**Binary Releases**`, pre-compiled executables | `**Versioned Releases**`, fixed for every repo | | 2 | License shield at the **bottom**, in a closing `## License` section right before the link definitions | License shield in the **top** `### Build Status` block | | 3 | "See Release History for complete release notes and older versions." | "See Release History for the full history." | | 4 | Channel bullets and shields vary per deliverable | One fixed channel list | **1 and 4 are the same underlying point.** The label depends on what ships. PlexCleaner ships executables so the channel is Binary Releases, while this repo ships source archives so Versioned Releases is right *here*. That makes the label a **per-channel table** rather than one canonical string, which is also what the `readme-structure` audit dimension needs in order to check a repo against its own declared types (GitHub binaries, Docker Hub, NuGet, PyPI each with a distinct bullet label and shield set). **3 carries a caveat.** PlexCleaner writes it as an inline link: ```markdown See [Release History](./HISTORY.md) for complete release notes and older versions. ``` The reference-style rule in `GOVERNANCE.md` forbids inline targets outside the four agent-instruction files. So the entry says to adopt the **wording** and keep the **reference form**, rather than copying the line whole. ## The section order is recorded as a separate question Deliberately not folded into the item above, because it affects every repo and the audit rather than one file's contents. PlexCleaner places `## Questions or Issues` immediately after the Table of Contents, where the spec orders it **ninth**. It also carries sections the spec names nowhere: Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, Sample Media Files. Under the recurrence rule landed in #507, **those unnamed sections are correctly repo-specific and stay undeclared**, so they are not evidence that the spec's list is short. The open question is only the *position* of the sections the spec already names, and that is Pieter's call rather than something to infer from one repo. ## Verification ``` prose_lint . --check charset --check dupword --check spelling clean prose_lint TODO.md --check dash --check semicolon --check charset-unknown --check sentence-split clean markdownlint-cli2 (docker, canonical invocation) 0 issues editorconfig-checker (docker, canonical) clean python3 spec/validate.py OK, 21 cataloged ``` The spelling gate caught `labelled` in the first draft of this entry and it was corrected to `labeled`. Worth noting because it is the gate doing its job on the same commit that documents gates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Reported by the Blog agent on #456 and confirmed here. This one has teeth: a repo can pass the file-presence audit and still be functionally broken. ## The gap `spec/files.json` requires `.github/dependabot.yml` on **every** repo (`appliesTo: "*"`) and never required the workflow that merges the pull requests it opens. A grep for `merge-bot` across the whole baseline returns **nothing**. Everything else assumes it exists: - `WORKFLOW.md:197` **D8.1 is a graded behavioral contract** for it, and audit reports grade repos on it. `reports/vscode-server-dotnetcore/audit.md` already marks a repo `fail`/`drift` for "no merge-bot workflow". - `GOVERNANCE.md:54` links it as an in-repo path and makes it the mechanism the dual-target bot model depends on. - `GOVERNANCE.md:298` names it as one of two documented `cancel-in-progress: false` exceptions. - `WORKFLOW.md:199` ties the tracker's branch prefix to "the merge-bot's hard-coded `<prefix>-<base>` head/base pairs, or auto-merge silently never fires." **A graded contract with no required file.** ## How it presented Blog carried all 23 applicable baseline files and `configure.sh check` exited 0, which is the definition of conformant. Dependabot then opened its first pull request and it had nowhere to go. The gap is invisible to a presence audit because the missing file is not in the list being checked, and the symptom appears days later as bot pull requests quietly accumulating. The `repo-setup` dimension also asserts the App secrets are provisioned while nothing checks the consumer, so a repo can hold valid App credentials that nothing consumes. ## The change ```json { "path": ".github/workflows/merge-bot-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["merge-dependabot", "disable-auto-merge-on-maintainer-push"] }, "intentRef": "WORKFLOW.md#d8---bots--automation", "appliesTo": "*" } ``` **`interface` fidelity**, because a repo legitimately prunes jobs for bots it does not have. Blog vendored `merge-dependabot` and `disable-auto-merge-on-maintainer-push` and dropped `merge-codegen` and `merge-upstream-version`, having neither a codegen workflow nor an upstream tracker, and a job keyed on a bot that never opens a pull request would be dead code reading as coverage. **The two required keys are the universal ones**: `merge-dependabot`, since `dependabot.yml` applies to every repo, and `disable-auto-merge-on-maintainer-push`, which D8.1 names directly. The other two stay optional. Verified the contract is actually enforced rather than decorative: `audit.py:701` reads `item.get("contract")` and `check_interface` at `:340` iterates `requiredJobKeys`. ## Fleet impact, measured **15 of 21 repos already carry the file**, so this ratifies a de facto standard rather than inventing one. | | Repos | | --- | --- | | Have it (15) | ESPHome-Config, ESPHome-NonRoot, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, ProjectTemplate, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair | | Missing (6) | AudioCleaner, DevKitCIoT, EspDinIoT, Financial-Modeling, HolidayLights, PhotoCleaner | Those six have Blog's silent pile-up today. ## One thing worth deciding separately **Absence of an `interface` unit reports as `DRIFT`, not `LETTER`.** Confirmed live against a missing repo: ``` DRIFT interface: .github/workflows/merge-bot-pull-request.yml absent on main, cannot verify its contract ``` `audit.py:690` explains why: "a workflow's naming is more variable than a carried config, so absence is a hint to verify." That is reasonable in general and consistent with `test-pull-request.yml`, the other universally-required interface file. For this file it may understate the case, since absence is not a naming variation, it is the bot not running. Whether an `interface` unit at `appliesTo: "*"` should report `LETTER` on absence is a separate engine question, deliberately not changed here. ## Verification ``` python3 spec/validate.py OK, 21 cataloged python3 spec/audit.py --selftest SELFTEST PASS python3 scripts/test_prose_lint.py 144 tests, OK python3 scripts/repo_gate.py eol 0, sha-pin 0 spec/audit.py ProjectTemplate no merge-bot finding (the hub carries it) spec/audit.py AudioCleaner reports the new DRIFT, as intended ``` The first draft used `intentRef: WORKFLOW.md#d8---bot-pull-requests`, an anchor that does not exist. Nothing caught it, since anchors in the spec are unvalidated. Corrected to `#d8---bots--automation`, matching `project-types.json`, and the unvalidated-anchor gap is worth its own look. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers finding 3 of #509, raised by the PhotoCleaner agent. ## The gap `spec/readme-structure.md` stated **no behavior** for a private repository. shields.io cannot read one, so every GitHub-sourced badge in **Build Status** and **Releases** renders broken, and section 2 reads as unconditional. With nothing declared, each repo decided separately: PhotoCleaner carries Docker Hub shields only and planned to add the GitHub ones at the visibility flip. ## The decision **The structure does not vary by visibility.** A private repo carries the same sections and the same shield set as a public one, written in full, and the broken badges are accepted rather than worked around. The reasoning is that visibility is **temporary by design**. A repo is usually private precisely *while* it is being made presentable, which is exactly when its README is written. Keying the shape to visibility therefore means writing the README twice and owing work at the flip, on the one axis guaranteed to change. Holding it constant means: - going public is a visibility change and nothing else, with no README work owed - the audit has **one** shape to accept rather than two, and no visibility-conditional branch - the cost is bounded and temporary: broken badges on a repo that, being private, has almost no readers ## What this means for PhotoCleaner Its README should carry the GitHub build and release shields now rather than at the flip. They will render broken until it goes public, which is the accepted cost. No urgency, and nothing else about its README changes. ## Verification ``` prose_lint spec/readme-structure.md (dash, semicolon, charset, charset-unknown, dupword, spelling, sentence-split) clean markdownlint-cli2 (docker, canonical invocation) 0 issues editorconfig-checker (docker, canonical) clean ``` Prose only. No schema, audit, or workflow change, and no fleet impact beyond removing a decision each repo was making on its own. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restores the forward-only invariant after I merged #508 without checking its base branch. ## What happened #508 was opened against **`main`**, not `develop`. I verified its content, its checks, and its review threads, and merged it without ever reading `baseRefName`. It landed as `048a442` on `main` alone. The branching model is feature to `develop` by squash, then `develop` to `main` by promotion merge. A feature PR merged straight to `main` puts content on `main` that `develop` does not have, which is exactly the divergence the forward-only rule exists to prevent. Left alone, the next promotion either conflicts or silently reverts the audit refresh, since `develop` would carry the older `reports/photocleaner/audit.md`. ## The fix `048a442` cherry-picked onto `develop`. Confirmed it is the **only** content commit `main` holds that `develop` lacks: ``` $ git log --oneline origin/develop..origin/main 048a442 Refresh the PhotoCleaner audit report and driftNotes (#508) 6501479 Promote the prose-gate fixes and the configure.sh sweep to main (#502) ... 100+ further entries, all promotion merges ``` Everything below `048a442` is a promotion merge commit, main-only by construction, which is topology rather than drift. After this merges, the two branches carry identical content again and the next promotion is clean. ## Two separate errors, worth naming **Mine**: I ran the full merge gate, checks, review coverage, unresolved threads, and an independent re-audit of the report's claims, and never checked which branch the PR targeted. A base-branch check belongs in that gate, and it was not in it. **The PR's**: a downstream conformance PR against the hub targeted `main`. That is the same class as the report-authorship question #508 raised, an agent filling a gap the instructions never addressed, and it argues for `AUDIT.md` stating the base branch explicitly alongside who may author a report. ## Verification ``` python3 spec/validate.py OK, 21 cataloged registry/repos.json parses prose_lint reports/photocleaner/audit.md --check charset --check dupword --check spelling clean editorconfig-checker (docker, canonical) clean ``` Content-identical to `048a442`, so no review of the report's substance is re-opened here. It was verified against a live `spec/audit.py PhotoCleaner` run before #508 merged, and every substantive claim was corroborated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the governance question #508 raised: may a downstream repo open a pull request against the hub to write its own audit report? ## Why there was a vacuum `AUDIT.md` said the audit is read-only and produces a report under `reports/`, and `AUDIT.md` section 10 said "the hub audits and the agent applies the fixes via target PRs". Neither ever said **who writes the report**, so an agent filled the gap, reasonably. ## The answer: the hub authors it The maintainer's call, and the argument that settles it is **efficiency, not principle**: > consider you want it in develop, and you want to audit it anyway, you are going to run the same effort, and the result would be the same if you agree with their audit That is correct and it dismantles the case I had been making. I argued a submitted report preserves downstream context the hub would otherwise rebuild. But **verifying the judgment dimensions is the audit**: confirming a verdict like "analyzers enforced, `TreatWarningsAsErrors` present" means reading the same files the audit reads. Only the deterministic subset is cheap. So a submitted report saves the **writing up**, which is the trivial part once the audit is done. The independence argument survives as a second reason, since a repo grading itself produces a claim rather than evidence, but on its own it would only have justified verifying a submission rather than declining one. ## A third reason #508 demonstrated **A submitted report is stale by construction.** It is a snapshot of one hub revision arriving at a later one. #508 was authored against hub `6501479` and merged at `839ecde`, and before adopting it I had to reconcile two findings that did not exist when it was written: `OPERATIONS.md` became universal in #507, and the merge-bot workflow was declared in #512. That reconciliation is inherent to the workflow, not incidental. ## What downstream still does, and it matters Unchanged, and it is where downstream context is worth most: - **File findings about the hub as issues.** The opposite of self-certification, and it has surfaced several real hub defects today: the merge-bot workflow that no spec required while `WORKFLOW.md` D8.1 graded it, `copilot-instructions.md` describing a pre-split `AGENTS.md`, `CODESTYLE.md` contradicting the markdownlint config, and D2.2 wording that nearly caused a real regression. - **Apply fixes to its own repo** per section 10. ## Also names the base branch `#508` targeted **`main`**, and I merged it without checking `baseRefName`. That put content on `main` that `develop` lacked, breaking the forward-only invariant, and needed a forward-port in #513. The branching model was already stated in `GOVERNANCE.md`, but not in the place an agent opening a conformance pull request against the hub would be reading. It is now stated here too. ## Verification ``` prose_lint AUDIT.md --check charset --check dupword --check spelling --check charset-unknown clean markdownlint-cli2 (docker, canonical) 0 issues editorconfig-checker (docker, canonical) clean python3 spec/validate.py OK, 21 cataloged ``` Prose only, no engine change. `AUDIT.md` is carried at `intent`, so downstream repos pick this up on their next re-vendor. ## What this does not solve Thirteen of twenty-one cataloged repos still have no committed audit report, tracked in `TODO.md`. This decision means the hub writes them, which is a capacity question rather than a governance one. Accepting unverified submissions would have closed the gap by lowering the bar rather than by doing the audits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records a decision that was carried only in conversation, at the maintainer's request. ## The gap `AUDIT.md` already states the **outcome**: the deterministic subset is "owner-initiated, run on demand when onboarding a repo, on suspected drift, or before fleet-wide changes." What it does not state is that **automation was explored and deliberately deferred**, or why. So the next person to notice that 13 of 21 repos have no committed report re-derives the whole analysis, and quite possibly reaches a different answer for reasons already rejected. ## What was considered, and what blocked it Three shapes: 1. A **scheduled hub-driven audit** publishing each report as a workflow artifact. 2. The same, **committing the report back**, the way the codegen bot updates its own files. 3. A **pull-request hook in each downstream repo**, auditing itself against the current hub. Three things blocked all three: - **Noise.** Until the fleet reaches stasis with every repo onboarded, a scheduled run reports mostly noise, because a repo mid-onboarding is *expected* to be non-conformant. Findings that are correct and not actionable train people to ignore the channel. - **Hub stability.** The hub has to be stable before downstreams can audit against it, since a hub change lands as fleet-wide findings the same day. Today is the worked example: `OPERATIONS.md` becoming universal in #507 created 17 findings, and declaring the merge-bot workflow in #512 created 6 more. - **A catch-22 on the downstream shape.** A self-auditing pull-request hook is CI instrumentation, and the repos that most need auditing are exactly the ones that do not carry it yet. The agreed outcome was the on-demand audit `AUDIT.md` describes today. ## Why the entry is actionable rather than a note It records the **preconditions** that would make reopening worthwhile (the fleet onboarded, and the hub going a stretch without carried-content changes) and **which shape to try first**: the artifact, because it produces evidence without committing anything, so it cannot generate review load while the noise level is still unknown. That makes the first experiment cheap and reversible. ## One correction to an existing entry The `reports/` entry read as a defect. It is not. Per the maintainer, the gap is **paced by capacity**, with repos brought up to spec as they are worked on, so the entry now says it records the outstanding set rather than a failure. This matters because #515 just put report authorship with the hub, and an entry that reads as an accusation invites re-opening a decision that was made deliberately. ## Verification ``` prose_lint TODO.md (dash, semicolon, charset, charset-unknown, dupword, spelling, sentence-split) clean markdownlint-cli2 (docker, canonical) 0 issues editorconfig-checker (docker, canonical) clean ``` Backlog only. The `[audit]` reference already pointed at `spec/audit.py`, so the new pointer to the document uses a separate `[audit-doc]` definition rather than overloading it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers the four remaining spec questions from #509, raised by the PhotoCleaner agent. Finding 3 landed separately in #511. Each was verified against the current tree before being changed. ## 1. `.github/copilot-instructions.md` described the pre-split `AGENTS.md` The carried `Reviewing Carried Fleet Content` section said most of `AGENTS.md` is fleet law and that two of its sections describe this repository's own directory tree and devcontainer. Verified wrong in **both** halves against `spec/files.json`: - `AGENTS.md` declares exactly two sections, both `verbatim`, and no repo-specific ones. - `GOVERNANCE.md` holds the sixteen rule sections **and** the two `intent` ones, `Devcontainer` and `Repository Layout`. It is carried at `intent`, so every repo inherits the stale description, and a reviewer following it looks for byte-locked rule text in the wrong file. Now names `GOVERNANCE.md` as the fleet-law file and describes `AGENTS.md` as the thin router carrying two byte-locked sections of its own. ## 2. `CODESTYLE.md` contradicted `.markdownlint-cli2.jsonc` on MD033 `CODESTYLE.md` said HTML elements are flagged. `.markdownlint-cli2.jsonc:10` sets `"MD033": { "allowed_elements": ["details", "summary"] }`, and the config's own comment documents the exception the prose denied: "The details and summary elements are allowed for GitHub collapsibles, which have no markdown equivalent." The prose now matches the config, naming the two allowed elements and why. ## 3. The `HISTORY.md` mirror rule had no carried home The rule lived only in `spec/readme-structure.md`, which is **hub-only** and appears in no `spec/files.json` entry, so no repo carries it. A repo could not read the rule it was being graded against, and PhotoCleaner wrote a local copy into its own `CODESTYLE.md` for want of a destination, which is the duplication the fleet normally avoids. The maintainer's call was to move it where it can be audited, so it becomes item 4 of `CODESTYLE.md` "Markdown and Spelling", which every repo carries whole. `spec/readme-structure.md` now states only **what the audit does** with the rule rather than restating it, so there is one authority and one pointer rather than two copies. ## 4. `WORKFLOW.md` D2.2 "skipped on smoke" was ambiguous D2.2 said the gate "is **skipped on smoke**", naming the *validation*. A Copilot review on PhotoCleaner#29 read it as the **job status** and proposed a job-level `if: !inputs.smoke`. That would have been a real regression rather than a style difference: `github-release` carries `validate-release` in its `needs`, and a skipped need skips the dependent, so the release job would have been coupled to smoke through a second path on top of the `if:` it already carries. The canonical was right and the wording invited the misreading. It now says the check exits early while the job still reports success, and states why the distinction matters. ## Verification ``` markdownlint-cli2 (docker, canonical, **/*.md) 42 files, 0 issues editorconfig-checker (docker, canonical) clean prose_lint (charset, dupword, spelling, charset-unknown) on all four files clean python3 spec/validate.py OK, 21 cataloged python3 spec/audit.py --selftest SELFTEST PASS ``` `spec/readme-structure.md` carried no links at all, so the new pointer to `CODESTYLE.md` is its first. It is written reference-style with a definition block at the bottom, per the rule that same file states, rather than inline. Three of the four are carried files (`.github/copilot-instructions.md`, `CODESTYLE.md`, `WORKFLOW.md`), so downstream repos pick these up on their next re-vendor. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Promotes a set of already-merged develop changes to main that update fleet-wide carried governance/docs and the hub’s spec/audit requirements, so downstream repos pick up corrected canonical text and new declared requirements on their next re-vendor.
Changes:
- Makes
OPERATIONS.mduniversally required (appliesTo: "*") and adds the hub’sOPERATIONS.mdrunbook content. - Declares the merge-bot workflow in
spec/files.jsonatinterfacefidelity and updates audit/spec/docs to match. - Applies multiple carried-file documentation corrections and comment-shape cleanups, plus adds tests to enforce comment cleanliness for verbatim-carried files.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| WORKFLOW.md | Clarifies D2.2 smoke behavior to avoid misreading job-level skips vs check early-exit. |
| TODO.md | Updates backlog items and adds additional references relevant to audit automation and fleet follow-ups. |
| STANDUP.md | Updates OPERATIONS.md requirement and describes stub heading expectations and rationale. |
| spec/section-model.md | Updates declared destinations (adds TODO.md), revises OPERATIONS.md requirement, and adds worked-example link. |
| spec/readme-structure.md | Declares README structure invariant across repo visibility and moves HISTORY mirror rule authority to CODESTYLE.md. |
| spec/project-types.json | Updates the carried-content “no coordination reference” assertion to match current rationale and audit behavior. |
| spec/files.json | Makes OPERATIONS.md universal and declares .github/workflows/merge-bot-pull-request.yml as an interface baseline unit. |
| spec/audit.py | Fixes carried-file template-reference check rationale and skips the hub itself to avoid self-flagging. |
| scripts/test_prose_lint.py | Adds tests asserting verbatim-carried files are comment-clean and that verbatim declarations are non-empty and exist. |
| reports/photocleaner/audit.md | Refreshes the PhotoCleaner audit report snapshot and associated findings/notes. |
| reports/conformance-matrix.md | Adds Blog as a reference shape and updates operational-model notes now that OPERATIONS.md is universal. |
| registry/repos.json | Updates PhotoCleaner drift notes to reflect current state, remaining defects, and visibility implications. |
| README.md | Recasts a few sentences to remove banned constructions and improves formatting consistency. |
| OPERATIONS.md | Adds the hub’s operations/runbook documentation for running gates, auditing, and applying repo config. |
| CODESTYLE.md | Corrects MD033 guidance to match markdownlint config and adds the HISTORY.md mirror rule. |
| AUDIT.md | Clarifies audit report authorship and explicitly states hub-authored reports and base-branch targeting. |
| .github/copilot-instructions.md | Corrects carried-file guidance to reflect the AGENTS/GOVERNANCE split. |
| .gitattributes | Comment-only sweep to improve prose shape and label bare commands. |
| .editorconfig | Comment-only sweep to improve prose shape and clarify line-ending rationale. |
Suppressed comments (1)
OPERATIONS.md:97
- This command example uses
<owner>/<repo>,<number>, and<branch>placeholders inside a shell command. As written, it will be parsed as input redirection if someone copies it, which is especially likely in an operations runbook. Prefer paste-safe placeholders (e.g.[owner/repo],[number],[branch]) and quote the path argument.
- `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos/<owner>/<repo>/pulls/<number> -f base=<branch>` instead.
Fixes the two placeholder findings Copilot raised on the promotion #517, plus the six more of the same class. This has to land on `develop` first: #517's head **is** `develop`, and `develop` carries a `pull_request` rule, so the promotion cannot be fixed in place. ## The defect `OPERATIONS.md` wrote shell placeholders as `<owner>/<repo>`. A POSIX shell parses `<` as input redirection, so pasting one is a syntax error, in a file whose entire purpose is being pasted: ``` $ repo-config/configure.sh check <owner>/<repo> bash: syntax error near unexpected token `newline' ``` The bracket form pastes cleanly, and `STANDUP.md` already writes the same command that way: ``` repo-config/configure.sh check [owner/repo] [release|operational] ``` So the runbook was both unpastable and the odd one out against the repo's own convention. ## Scope: eight, not two Copilot reported two, one inline and one suppressed. Looking for the whole class found eight across six shell blocks: the `prose_lint.py` invocation, both `configure.sh` lines, the branch-recovery `git push`, both `gh run` examples, the `gh api` base-retarget, and two `spec/audit.py` calls. ## Verified by parsing, not by eye Every `sh` block was run through `bash -n`: ``` 6 sh blocks, 0 with syntax errors ``` That check earned its place. A first pass grepping `<[a-z|-]*>` reported zero remaining and the parse test still failed block 3, because `<RepoName>` is uppercase and the pattern was lowercase-only. Checking the property directly caught what checking a proxy for it missed. ## Verification ``` bash -n over every sh block 6 blocks, 0 errors grep -cE '<[A-Za-z|_-]+>' OPERATIONS.md 0 editorconfig-checker (docker, canonical) clean markdownlint-cli2 (docker, canonical) 0 issues prose_lint OPERATIONS.md (charset, dupword, spelling, charset-unknown, dash, semicolon, sentence-split) clean ``` Once this merges, #517 picks it up automatically, since its head is `develop`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes twelve commits to
main. Three of them change carried files, so downstream repos read the stale text until this lands, which is what makes the promotion the delivery step rather than bookkeeping.What this delivers to the fleet
Carried-file corrections, picked up by every repo on its next re-vendor:
.github/copilot-instructions.mdAGENTS.md, wrong in both halves.GOVERNANCE.mdholds the rule sections and the twointentones;AGENTS.mdcarries two verbatim sections and none repo-specificCODESTYLE.mddetailsandsummary; also gains theHISTORY.mdmirror rule, moved from a hub-only spec file so a repo can read the rule it is graded againstWORKFLOW.md.editorconfig,.gitattributesrepo-config/configure.shmainSpec and requirement changes:
OPERATIONS.mdappliesTo: "*", keyed to the need rather than toworkflowModel. Measured: 3 of 21 repos carry it, so 17 will report a newLETTER file:finding. Intended, and a headed stub satisfies it..github/workflows/merge-bot-pull-request.ymlatinterfacefidelity, closing a graded contract (WORKFLOW.mdD8.1) that no spec required. 15 of 21 already carry it, so this ratifies a de facto standard; the other 6 report a newDRIFT, and those six have Dependabot pull requests piling up with nothing to merge them.mainand I merged it without checking.Backlog and reports: #504, #506, #510, #516 record the README shape rework, the branch-bootstrap step, the declared-description design, and why the audit is on demand.
Fleet consequence, stated before it lands
Two owed follow-ups this promotion creates or carries:
repo-config/configure.sh, already owed from Sweep the carried configure.sh comments to one sentence per line #501, since it isverbatimwithappliesTo: "*"and every repo holding a copy is byte-mismatched.OPERATIONS.mdletters and 6 merge-bot drifts. Both are new requirements rather than repo regressions.Verification
Run on
developate4c76bc:Every one of the twelve was merged green with a Copilot review covering its head SHA.
🤖 Generated with Claude Code