Skip to content

Key OPERATIONS.md to the need for it, not to the workflow model - #507

Merged
ptr727 merged 9 commits into
developfrom
feature/operations-universal-destination
Aug 1, 2026
Merged

Key OPERATIONS.md to the need for it, not to the workflow model#507
ptr727 merged 9 commits into
developfrom
feature/operations-universal-destination

Conversation

@ptr727

Copy link
Copy Markdown
Owner

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:

CountRepos
Have it3ESPHome-Config, Financial-Modeling, HomeAssistant-Config
Missing18the 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

ptr727and others added 2 commits August 1, 2026 08:04
…anup
The previous entry said the initial commits belong on a feature branch and
reach develop by squash. Correct, but it still leaves main and develop as
things an agent might commit onto and then have to clean up. Blog tried
exactly that and was blocked, which is the ruleset working.
The better sequence removes the problem instead of managing it. Create main
carrying nothing, create develop from main carrying nothing, create the first
feature branch from develop and do the whole standup there, add the repo to
GitHub and apply the repo config from that branch, then open a normal pull
request to develop when ready.
Two details the write-up needs and would otherwise get wrong. A git branch
cannot exist without a commit, so "carrying nothing" means one signed empty
root commit, and section 0's signing window applies to it. And squashing
afterwards is not an escape, because non_fast_forward is set on both develop
payloads, so the history cannot be rewritten without disabling the ruleset.
STANDUP.md currently says nothing about how main and develop come into being,
which is the gap that lets an agent commit onto whichever branch it finds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OPERATIONS.md was declared appliesTo: ["operational"], which binds a content
need to a workflow model. Those are unrelated axes. The `operational` model
describes where config lives and how a change reaches develop, not whether the
repo has runbooks, so the selector read a sufficient condition as a necessary
one.
Blog is the proof. It moved from operational to release and its runbook needs
did not change by one word: same host, same deploy, same staging. Only the
commit-flow classification moved.
The rationale in section-model.md already generalized. It mandated the file
because the convention was emerging unevenly and the same material scattered
across ad-hoc names, which has nothing to do with the workflow model. Every
repo has operational surface: a package registry needs trusted-publisher
setup, an image needs registry credentials, a site needs a deploy and a
staging path.
So OPERATIONS.md is now appliesTo: "*", and a repo with nothing to say carries
a stub of the headings rather than a blank file, because a stub names the
destination and its shape where a blank names only the destination.
Also states the rule that decides what earns a declared destination at all: a
content class is declared when it recurs across repos. Architecture recurs, so
ARCHITECTURE.md stays declared, and advisory, since how much of it earns a
separate document is contextual. A one-repo need such as a home-device
inventory stays the repo's own file, neither declared nor mandated, because
the scattering these destinations prevent has no force for content existing in
exactly one repo.
The hub carried no OPERATIONS.md, so it would have shipped a rule it broke.
It now has a real one rather than a stub, covering the gates, the fleet audit,
the configure.sh runbook, and the tool caveats this repo has already hit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI 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

This PR updates the fleet documentation model so OPERATIONS.md is required based on the recurring need for operational/runbook content rather than being keyed to the operational workflow model, and adds an OPERATIONS.md to ProjectTemplate so the hub complies with the baseline it defines.

Changes:

  • Make OPERATIONS.md universally required by changing spec/files.json from appliesTo: ["operational"] to appliesTo: "*".
  • Update spec/section-model.md and STANDUP.md to explicitly state that workflow model and operational documentation needs are orthogonal, and document the “declared destination” rule.
  • Add a new OPERATIONS.md runbook for this repository and update TODO.md to track a related STANDUP improvement.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
TODO.mdReplaces the prior STANDUP-related backlog item with a more explicit branch-bootstrap step proposal.
STANDUP.mdUpdates onboarding guidance to reflect OPERATIONS.md being required for all repos and clarifies the workflow-model independence.
spec/section-model.mdUpdates the destination/requirement rationale for OPERATIONS.md and documents the rule for when destinations are declared.
spec/files.jsonMakes OPERATIONS.md a universal baseline requirement (appliesTo: "*") instead of workflow-model-scoped.
OPERATIONS.mdAdds an operations/runbook document for ProjectTemplate covering gates, audit, repo-config application, and related tooling notes.

Comment threadSTANDUP.md Outdated
Comment threadspec/section-model.md Outdated
CI's editorconfig-checker step failed because the new file was LF while
`[*] end_of_line = crlf` applies. The tooling that created it writes LF, and
nothing local caught that before the push.
Converted to CRLF, and added the editorconfig-checker invocation to the gate
list in the file itself, which is where its absence caused this. The runbook
listed every other gate CI runs and omitted the one that failed.
This is the same trap Blog reported from the other side: a carried file
arrives with the hub's endings, so an LF repo fails its own check on the first
run. Here a new file arrived with the tool's endings and failed the hub's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 1, 2026 15:36

CopilotAI 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 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

OPERATIONS.md:20

  • The editorconfig-checker command here doesn’t match the CI invocation in .github/workflows/validate-task.yml (missing --workdir /check and adding an extra ec argument). Since this section is explicitly “Run the gates the way CI runs them”, it should mirror the workflow command exactly to avoid local runs behaving differently.
docker run --rm --pull=always -v "$PWD:/check" mstruebing/editorconfig-checker:latest ec

spec/section-model.md:66

  • This section says “Three topical docs take it” but the destination list omits TODO.md (which STANDUP.md includes as a declared destination for repo-specific backlog content). This inconsistency can lead to agents scattering TODO/backlog content under other filenames.
A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Three topical docs take it, chosen by what the content *is*:
- [`CODESTYLE.md`][codestyle]: a repo's language and formatting conventions beyond the carried rules.
- `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions.
- `OPERATIONS.md`: how a repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half.

Four findings from the Copilot round, all real.
"A stub of the headings above" was a dangling reference in both files, since
neither named any headings. Both now name the five: Runbooks, Backup and
Recovery, Logs and Debugging, Tool Usage, and Configuration Layout. They match
this repo's own OPERATIONS.md, which both files now point at as the worked
example.
section-model.md said "Three topical docs take it" while STANDUP.md said four,
the difference being TODO.md. section-model.md was the one missing it, so a
reader of the spec had no declared destination for backlog content. It now
lists four.
The editorconfig-checker line did not match CI's invocation, in a section
titled "Run the gates the way CI runs them". It now mirrors CI exactly. Both
forms were verified to behave identically against a deliberate violation, so
this removes a dependency on the image's default working directory rather than
fixing a broken command.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 1, 2026 15:41
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Closing the loop on the two suppressed findings, both verified before accepting and both fixed in f449da8.

1. The editorconfig-checker line did not match CI. Real, and it mattered in that section.

CI runs:

docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest

I had written -v "$PWD:/check" with a trailing ec and no --workdir. In a section titled "Run the gates the way CI runs them", that is exactly the drift the section exists to prevent, so it now mirrors CI byte for byte.

I checked whether my form was actually broken rather than assuming either way, by dropping a deliberate LF violation into the tree and running both:

mine: ec-probe.md: Not all lines have the correct end of line character -> 1 errors found
CI's: ec-probe.md: Not all lines have the correct end of line character -> 1 errors found

Identical. The image's default working directory is already /check and ec is its default command, so my form was not vacuous and the earlier local pass was real. The fix removes a dependency on those image defaults rather than repairing a broken command, which is worth stating precisely so the change is not read as a bigger correction than it is.

2. "Three topical docs" against STANDUP.md's four. Real, and section-model.md was the wrong one.

STANDUP.md listed four including TODO.md, spec/section-model.md listed three and omitted it. A reader of the spec therefore had no declared destination for backlog content, which is precisely the scattering that section exists to prevent. section-model.md now lists four.

Both of these were pre-existing in one half and introduced by me in the other, so I have not tried to separate blame. They are fixed where they sit.

CopilotAI 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 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

spec/section-model.md:73

  • This PR updates the rule so OPERATIONS.md is required for every repo, but reports/conformance-matrix.md still states the old behavior (it says OPERATIONS.md is required only for workflowModel: operational with appliesTo: ["operational"]). That leaves the hub docs internally inconsistent and can mislead readers using the conformance matrix.

Please update the conformance-matrix row (currently reports/conformance-matrix.md:22) to reflect the new appliesTo: "*" requirement (and any similar references, if added later).

**`OPERATIONS.md` is required for every repo**, declared in [`files.json`][files] as `appliesTo: "*"` and checked for presence only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own. It is mandatory rather than advisory because the convention was already emerging unevenly: of the four operational-model repos, two wrote one unprompted and the others scattered the same material across ad-hoc names, which is the improvisation these destinations exist to prevent. That reasoning never depended on the workflow model. Every repo has operational surface, since publishing to a package registry needs trusted-publisher setup, shipping an image needs registry credentials, and serving a site needs a deploy path and a staging story. A repo with nothing to say still carries the file as a stub, meaning those five headings with no content under them, because a stub names the destination and its shape where a blank file names only the destination. This repo's own [`OPERATIONS.md`][operations] is the worked example.
**The workflow model and the need for this file are unrelated axes.** `operational` as a `workflowModel` describes where config lives and how a change reaches `develop`, not whether the repo has runbooks. Keying the file to that selector read a sufficient condition as a necessary one, since an operational-model repo certainly has runbooks while a release repo has them too. Reclassifying a repo between models does not change how much operational surface it has, which is the test that showed the selector was wrong.
**A destination is declared when its content class recurs across repos.** Software architecture recurs, because every code repo has one, so `ARCHITECTURE.md` is declared. A home-device inventory does not recur, so it stays the repo's own file, neither declared nor mandated. The scattering these destinations prevent is the same material landing under different names in different repos, and that has no force for content existing in exactly one repo. Declaring a destination for a one-repo need would grow this list without bound and still lag whatever the next repo invents.

The matrix still said OPERATIONS.md is required only for an operational repo,
with appliesTo: ["operational"], which this PR makes false. Merging without
this would have shipped the hub contradicting itself in the document agents
read to learn what a shape requires.
The operational row now describes what actually distinguishes that shape, that
its develop ruleset carries no pull_request rule, so branch discipline rests
on the instruction rather than the gate.
The same row also still claimed Blog as the next operational standup. Blog is
release plus source-only now, so it gets its own row rather than sitting under
a model it no longer uses.
The new row adds one warn-only dash finding, the `| - |` empty cell every
other row also carries. The dash rule reads a table placeholder as prose.
Keeping the table's own convention is worth one backlog entry, and the rule
gap is the follow-up rather than a reason to write the row differently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 1, 2026 16:07
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Third round's suppressed finding was real and is fixed in 5343e1a. Verified before accepting, as always with a suppressed one.

reports/conformance-matrix.md:22 still read:

Carries a required OPERATIONS.md (appliesTo: ["operational"], presence-checked) for its runbooks.

This PR makes that false. Merging without the fix would have shipped the hub contradicting itself in the document an agent reads to learn what a shape requires, which is worse than the original defect.

The operational row now states what actually distinguishes that shape: its develop ruleset carries no pull_request rule, so branch discipline rests on the instruction rather than the gate.

Checking that row also caught something Copilot did not flag. It still claimed Blog as the next operational standup, and Blog moved to release plus source-only earlier today. Blog now has its own row rather than sitting under a model it no longer uses.

One deliberate cost: the new row adds a single warn-only dash finding, the | - | empty cell that every other row in the table also carries. The rule reads a table placeholder as prose. Writing my row differently to satisfy a warn-only rule would make the table inconsistent for no gain, so the row keeps the table's convention and the rule gap is a follow-up. That is the same judgment applied in #503, where a command comment got a label because relabeling improved it, and in #500, where a URI got an exemption because no edit could fix it.

CopilotAI 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 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

OPERATIONS.md:25

  • This says the second prose invocation is "warn-only", but the command shown here will still exit non-zero locally on findings. In CI it’s warn-only because the workflow runs this step with continue-on-error: true (see .github/workflows/validate-task.yml:76-78). Clarifying that distinction will avoid readers treating a failing local run as unexpected.
The first prose invocation gates. The second is warn-only and reports the backlog that is corrected as each file is next edited.

OPERATIONS.md:82

  • These Docker commands differ from the documented canonical invocations (missing --workdir /workdir and running cspell over all **/*.md). As written, they may not run from the mounted directory, and they don’t mirror CI’s cspell scope (README.md + HISTORY.md only). Aligning with GOVERNANCE.md:338-345 keeps the runbook consistent and avoids extra non-gated spelling noise.
docker run --rm --pull=always -v "$PWD:/workdir" davidanson/markdownlint-cli2:latest "**/*.md"
docker run --rm --pull=always -v "$PWD:/workdir" ghcr.io/streetsidesoftware/cspell:latest --no-progress "**/*.md"

…s from
Two suppressed findings, both real.
The runbook called the second prose invocation warn-only without saying that
the leniency is CI's, not the command's. The command exits non-zero locally
whenever findings exist, and the workflow step sets continue-on-error. A
reader following the runbook would have read the expected result as a failure.
The markdownlint and cspell lines also drifted from the canonical invocations
in GOVERNANCE.md: both omitted --workdir, and cspell read every markdown file
rather than README and HISTORY. The narrower cspell scope is deliberate, so
the file now carries that reason too, since a reader who does not know it will
widen the scope back and get noise no gate acts on.
Third round of real findings in this file. It was a lot of new prose written
quickly, and the reviews have been earning their place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 1, 2026 16:12
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Fourth round's two suppressed findings, both real, both fixed in b7e42a2.

1. "Warn-only" attributed the leniency to the wrong thing. The command exits non-zero locally whenever findings exist. It is warn-only in CI because the step sets continue-on-error: true, verified at .github/workflows/validate-task.yml:77. A reader following the runbook would have read the expected result as a failure. The file now says where the leniency comes from.

2. The Docker lines had drifted from the canonical invocations. Checked against GOVERNANCE.md:338-345: both omitted --workdir /workdir, and cspell read **/*.md where the canonical form reads README.md HISTORY.md. Both now match byte for byte.

The cspell scope is the more useful half of that. Reading every markdown file is not just non-canonical, it produces findings no gate acts on, since the CI cspell step covers those two files only. So the file now carries the reason as well as the command, because a reader who does not know why the scope is narrow will widen it back and generate noise. That reason is the same one the workflow comment gives: gating every markdown file would mean padding cspell.json with technical terms without end, and broad live spell-check is the editor extension's job.

This is the third round to find real problems in OPERATIONS.md. It was a large amount of new prose written quickly, and a runbook that misdescribes its own tooling is worse than no runbook, so the rounds have been earning their place rather than polishing.

CopilotAI 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 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

OPERATIONS.md:33

  • prose_lint.py can be invoked on an explicit file path (even if untracked), so saying it "reads only files git tracks" is only accurate for whole-tree discovery modes (e.g., prose_lint.py . / --diff). As written, this can mislead a reader into thinking an untracked file can never be checked locally.
`prose_lint.py` reads only files git tracks, so a new file reports clean until it is staged. The first clean run on an unstaged file is vacuous.

The runbook said prose_lint.py reads only files git tracks. That holds for
whole-tree discovery and for --diff, not for an explicit path, which is read
whether or not git tracks it.
Verified rather than reasoned: an untracked file named directly reports its
finding and exits 1, while the same file is invisible to a whole-tree run.
The wording mattered more than an ordinary inaccuracy, because it discouraged
the one habit that catches a new file's problems before CI does. This PR hit
exactly that: OPERATIONS.md was written with the wrong line endings and failed
the first CI run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Fifth round's suppressed finding, real, fixed in b0e0732. Verified empirically rather than by reading the code, since the claim was about behavior:

$ printf 'The the duplicated word here.\n' > untracked-probe.md # untracked
$ python3 scripts/prose_lint.py untracked-probe.md --check dupword
untracked-probe.md:1: dupword: duplicated word 'The' -> exit 1
$ python3 scripts/prose_lint.py . --check dupword
(untracked-probe.md not seen)

Exactly as you said. discover() short-circuits on p.is_file() before consulting git ls-files, so an explicit path is always read. The tracked-files limit applies to whole-tree discovery and --diff only.

This one mattered more than an ordinary inaccuracy. As written it told a reader that a new file cannot be checked before staging, which discourages the single habit that catches a new file's problems before CI does. This PR is the worked example: OPERATIONS.md was created with LF against a CRLF-default repo and failed the first CI run. Advice that steers someone away from checking a new file locally is advice that reproduces that failure.

The line now separates the two modes and says to name a new file directly.

CopilotAI review requested due to automatic review settings August 1, 2026 16:17

CopilotAI 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 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

OPERATIONS.md:33

  • Same issue later in this section: prose_lint.py . isn’t a valid path/command in this repo; the actual script is scripts/prose_lint.py. This affects copy-pasteability of the guidance.
Whole-tree discovery reads only files git tracks, so `prose_lint.py .` and `--diff` do not see a new file until it is staged, and a clean whole-tree run proves nothing about an unstaged one. An explicit path is always read, tracked or not, so name a new file directly to check it before staging.

OPERATIONS.md:9

  • OPERATIONS.md refers to running prose_lint.py <file>, but the script in this repo is scripts/prose_lint.py (there is no top-level prose_lint.py). As written, the command name is misleading and may not work for readers.

This issue also appears on line 33 of the same file.

CI passes explicit `--check` lists, and a bare `prose_lint.py <file>` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations:

spec/section-model.md:62

  • This sentence says repo-specific content “is not declared here”, but this very section immediately declares the standard destinations (CODESTYLE.md, ARCHITECTURE.md, OPERATIONS.md, TODO.md). Dropping that clause would avoid a self-contradiction for downstream readers.
A repo's own content is not carried and is not declared here, so extraction needs a predictable destination rather than a judgment call per repo. Four topical docs take it, chosen by what the content *is*:

Three review findings plus two my own re-read caught.
The prose referred to `prose_lint.py` while the script is
`scripts/prose_lint.py`, so the sentences did not match the commands beside
them. Both now use the real path.
section-model.md said repo content "is not declared here" and then declared
four destinations. The intent was that the hub declares where content goes
rather than what it says, so it now says that instead of reading as a
contradiction.
Two found by re-reading rather than by review. The gate list omitted the jq
parse loop that CI runs before validate.py, so the section claiming to mirror
CI did not. And the note that `gh pr checks --json` "does not exist before gh
2.50" asserted a version boundary that was never verified. It now states the
verified fact, that the installed 2.46.0 has no such flag, and names the
working alternative.
The configure.sh section now warns that a bare invocation defaults to apply.
That is not hypothetical: running the script with no arguments to check
whether it was executable performed a live apply against this repo. It was a
no-op because the repo was already conformant, confirmed by a clean check
afterward, but the runbook should say so before someone else learns it the
same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 1, 2026 16:23

CopilotAI 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 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

OPERATIONS.md:19

  • The JSON validity loop doesn’t include repo-config/operational/develop.json, so it misses one of the repo-config JSON files that CI carries and validates elsewhere.
for f in registry/*.json spec/*.json repo-config/*.json; do jq empty "$f"; done

OPERATIONS.md:9

  • OPERATIONS.md says a bare prose_lint.py run under-reports because DEFAULT_RULES omits sentence-split, but CI also doesn’t run sentence-split (see .github/workflows/validate-task.yml). As written, this implies CI checks sentence-split when it doesn’t, which is misleading.
CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py <file>` runs `DEFAULT_RULES`, which omits `comment-wrap`, `comment-case` and `sentence-split`. A bare run therefore under-reports and a clean result from it proves less than it appears to. Run the CI invocations:

The runbook said DEFAULT_RULES omits comment-wrap, comment-case and
sentence-split, which reads as though CI covers all three. CI names
sentence-split nowhere, so the sentence overstated the coverage it warned
about.
Two gaps are CI's rather than this runbook's, and are now recorded rather than
silently mirrored. The jq glob covers repo-config/*.json and does not reach
repo-config/operational/develop.json, so a malformed operational payload
passes the JSON validity step, and validate.py does not parse it either.
sentence-split is implemented and carries tests but is named by no invocation,
so a rule the repo maintains never runs.
Both are left as findings rather than fixed here, since changing what CI runs
is a different change from declaring where operational content lives. Both are
clean today, so enabling them costs nothing when it happens.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 1, 2026 16:53

CopilotAI 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 6 out of 6 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 12697a6 into developAug 1, 2026
7 checks passed
@ptr727
ptr727 deleted the feature/operations-universal-destination branch August 1, 2026 17:00
ptr727 added a commit that referenced this pull request Aug 1, 2026
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>
ptr727 added a commit that referenced this pull request Aug 1, 2026
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>
ptr727 added a commit that referenced this pull request Aug 1, 2026
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>
ptr727 added a commit that referenced this pull request Aug 1, 2026
… the carried-file corrections to main (#517)
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**:
- **#507** makes `OPERATIONS.md` `appliesTo: "*"`, keyed to the need
rather than to `workflowModel`. Measured: 3 of 21 repos carry it, so
**17 will report a new `LETTER file:` finding**. Intended, and a headed
stub satisfies it.
- **#512** declares `.github/workflows/merge-bot-pull-request.yml` at
`interface` fidelity, closing a graded contract (`WORKFLOW.md` D8.1)
that no spec required. 15 of 21 already carry it, so this ratifies a de
facto standard; the other **6 report a new `DRIFT`**, and those six have
Dependabot pull requests piling up with nothing to merge them.
- **#505** stops the audit flagging the hub against itself and drops a
premise that was false since this repo went public.
- **#511** fixes the README shape across repository visibility.
- **#515** puts audit-report authorship with the hub and names the base
branch, after a downstream conformance PR targeted `main` and I merged
it without checking.
- **#513** forward-ports that PR's content, restoring the forward-only
invariant.
**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 #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](https://claude.com/claude-code)
Sign up for freeto 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

@ptr727