Skip to content

Promote the OPERATIONS.md requirement, the merge-bot declaration, and the carried-file corrections to main - #517

Merged
ptr727 merged 13 commits into
mainfrom
develop
Aug 1, 2026
Merged

Promote the OPERATIONS.md requirement, the merge-bot declaration, and the carried-file corrections to main#517
ptr727 merged 13 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

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:

PR File Correction
#514 .github/copilot-instructions.md Described the pre-split AGENTS.md, wrong in both halves. GOVERNANCE.md holds the rule sections and the two intent ones; AGENTS.md carries two verbatim sections and none repo-specific
#514 CODESTYLE.md Said MD033 flags HTML elements while the config allows details and summary; also gains the HISTORY.md mirror rule, moved from a hub-only spec file so a repo can read the rule it is graded against
#514 WORKFLOW.md D2.2 "skipped on smoke" read as the job rather than the validation, and a review acting on it proposed a change that would have coupled the release to smoke
#503 .editorconfig, .gitattributes 36 comment findings in the files a new repo copies first
#501 (earlier) repo-config/configure.sh Already on main

Spec and requirement changes:

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:

  1. Re-vendor repo-config/configure.sh, already owed from Sweep the carried configure.sh comments to one sentence per line #501, since it is verbatim with appliesTo: "*" and every repo holding a copy is byte-mismatched.
  2. The next fleet audit reads noisier, by 17 OPERATIONS.md letters and 6 merge-bot drifts. Both are new requirements rather than repo regressions.

Verification

Run on develop at e4c76bc:

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
editorconfig-checker (docker, canonical)                       clean

Every one of the twelve was merged green with a Copilot review covering its head SHA.

🤖 Generated with Claude Code

ptr727 and others added 12 commits August 1, 2026 07:50
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>
Copilot AI review requested due to automatic review settings August 1, 2026 19:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md universally required (appliesTo: "*") and adds the hub’s OPERATIONS.md runbook content.
  • Declares the merge-bot workflow in spec/files.json at interface fidelity 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.

Comment thread OPERATIONS.md Outdated
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>
Copilot AI review requested due to automatic review settings August 1, 2026 20:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 38d79e1 into main Aug 1, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants