From f6bf9aea0179bf03a741097a440c4ce3ffe88ee0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 09:58:03 -0700 Subject: [PATCH 1/5] Bucket the backlog and refresh the divergence ledger TODO.md was a flat list of twenty entries that had grown past the point where the next action was visible in it. It is now grouped by the surface each entry changes, since an entry's cost is mostly set by what it touches, and every open issue is named on the entry that covers the same ground. Five issues were verified complete against the tree and are recorded with the commits that fixed them, so closing them cites evidence rather than assertion. Three new entries: the prose_lint scope floor, the rule that an agent never assumes a Docker image is still present locally, and the gh-write-guard newline defect. reports/divergences.md is regenerated. The committed copy predated the router split and still filed three sections under AGENTS.md that now live in GOVERNANCE.md, so it was handing out a work list measured against a tree that no longer exists. The refresh corrected the propagation picture: only two repos carry GOVERNANCE.md at all, so the split never reached the fleet. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 157 +++++++++++++++++++++++++++++------------ reports/divergences.md | 28 ++++++-- 2 files changed, 135 insertions(+), 50 deletions(-) diff --git a/TODO.md b/TODO.md index 37bc6602..5915db98 100644 --- a/TODO.md +++ b/TODO.md @@ -1,45 +1,112 @@ -# TODO - -Running backlog for this repo, kept in a committed file so the guidance survives across environments where agent memory does not. - -- Gate the pattern-detectable half of the representative-data rule in [`prose_lint.py`][prose-lint], meaning an absolute home path (`/home/`, `/Users/`, `C:\Users\`) or a bare drive letter sitting in committed prose, a code comment, or a fixture. [`GOVERNANCE.md`][governance] "Representative Data in Agent-Authored Text" states the rule and says why a check is a floor rather than an answer, so the check is introduced as covering the easy half or it gets read as closing the rule, which is the specific way it would make things worse. Three things to settle when writing it. The exemption carries the whole burden, since the rule's own wording, the `host-setup/` docs, and the audit's examples all quote path shapes in order to describe them, and a wrong exemption hands out a work list that damages correct documents. The leak that motivated the rule was in a pull request comment, which no committed-file linter reads at all, so say what surface the gate covers rather than letting its name imply the rule. And a home path in an operational repo's runbook may be the literal path an operator types, which is the repo's own content rather than an agent quoting the maintainer's environment, so decide whether the finding is scoped by file, by repo type, or left for the author to judge. -- Populate [reports/][reports] for the cataloged repos that still have no audit, since a registry `status` of `cataloged` asserts a result that only a committed report evidences. Eight repos have one. This is paced by maintainer capacity rather than blocked on anything, since repos are brought up to spec as they are worked on, so the entry records the outstanding set rather than a defect. -- Revisit automating the audit, which was explored and deliberately deferred, recorded here so the reasoning is not re-derived from scratch. Three shapes were considered: a scheduled hub-driven audit publishing each report as a workflow artifact, the same thing committing the report back the way the codegen bot updates its own files, and a pull-request hook in each downstream repo that audits itself against the current hub. Three things blocked all of them. Until the fleet reaches stasis with every repo onboarded, a scheduled run reports mostly noise, since a repo mid-onboarding is expected to be non-conformant. The hub has to be stable before downstreams can audit against it, because a hub change lands as fleet-wide findings the same day. And the downstream half is a catch-22, since a self-auditing pull-request hook is CI instrumentation the repos that most need it do not yet carry. The agreed outcome was the on-demand audit that [`AUDIT.md`][audit-doc] describes today. Worth reopening once the fleet is onboarded and the hub goes a stretch without carried-content changes, and the artifact shape is the one to try first, since it produces evidence without committing anything and so cannot generate review load while the noise level is still unknown. -- Canonicalize Python linter-config placement on `pyproject.toml` (one cataloged repo uses standalone `.ruff.toml` + `pyrightconfig.json`), track as a drift finding, fix downstream. -- Consider renaming this repo to reflect the audit-catalog identity (updates badge and link URLs across the fleet). -- Adopt the OCI annotation keys (`org.opencontainers.image.*`) for Docker image metadata across the Docker repos, replacing the ad-hoc and `org.label-schema.*` labels (from #363). -- Sweep `ManagePackageVersionsCentrally` placement to `Directory.Packages.props` fleet-wide (PlexCleaner sets it in `Directory.Build.props`, off the CODESTYLE canonical). -- Finish onboarding hardening (from #310): make the `AUDIT.md` audit a required onboarding step and run the per-type cold-start self-tests tracked in [reports/conformance-matrix.md][matrix] (`STANDUP.md` is already in place). -- Refresh the README (it has gone stale) and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation: it must track what the docs contain. -- Add a linter-only Python project type for codegen/boilerplate Python, code that runs during another tool's build to emit generated source (e.g. ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage (as in PlexCleaner). Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile. -- Add a fleet-standard clang-format config for the `cpp` type: a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`. -- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out. -- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field the way it already sets every other repository setting. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. -- Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. -- Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. -- Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. -- Re-vendor the changed `verbatim` content across the fleet, which is one sweep covering three files. `repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"` and the hub swept it to one sentence per line. In `AGENTS.md`, "Context and Delegation Discipline" carries the wait rule's failure clause and "Where the Rules Live" carries a row for the new section named below. In `GOVERNANCE.md`, "Verification Discipline" carries the rule that a launched process is not a result, "PR Review Etiquette" carries the five outcomes that close a finding, and "Representative Data in Agent-Authored Text" is an entirely new carried section that no downstream repo holds, which the audit reports as a missing section rather than as drift. Every repo already holding a copy of a changed section is byte-mismatched against the hub until it takes the new one, which the audit reports as stale rather than modified. -- Measure review rounds against pull request size, and decide what the number licenses. The recent loops suggest a large change earns a different finding every round while a small one converges in one or two, which would make change size the lever on review cost rather than the reviewer's thoroughness, and would argue for splitting a change before review rather than discovering it through five rounds of findings. The data needs no new instrumentation, since the review history already carries it: for each recent pull request, record the diff size in files and lines, the number of rounds, and the findings per round, counting suppressed findings alongside threaded ones because they are the majority of what these loops produce. The outcome worth having is a threshold [`GOVERNANCE.md`][governance] can state in the branching or review guidance, expressed as the size at which a change is split rather than as advice to keep changes small. Note two confounds before drawing a line from the numbers. A large change is usually also a novel one, so size and unfamiliarity move together and the record should note what kind of change each was. And a round that finds something new is not evidence of a problem by itself, since a round that finds something new is the reviewer working, so the metric to watch is findings that a smaller first cut would have surfaced earlier rather than findings per round on its own. -- Decide where a carried file may name hub-only machinery, since `GOVERNANCE.md` "PR Review Etiquette" points at `scripts/pr_review.py` and the fleet carries the section but not the script. A downstream reader follows that pointer to a path their repo does not have. Either the script joins the carried set, or the rule states the behavior and drops the tool name the way the coordination-reference rule already requires for the template repo itself. -- Make [`prose_lint.py`][prose-lint] assert a floor on its own scope, applying to itself the rule [`GOVERNANCE.md`][governance] already states: a gate that finds nothing is indistinguishable from a gate with nothing to find. A `--diff` run that resolves a non-empty diff and then matches **zero** files has almost certainly failed to scope rather than found a clean change, so it should say so instead of exiting 0. One session produced four separate routes to that same false clean: an unresolvable base widening to a whole-tree scan, a multi-line `paths` input read only to its first newline, a diff taken in one repository while scanning another, and a path under no repository at all. Each was fixed with its own guard, which is the wrong shape, because the fifth route will need a fifth guard and will be found the same way the first four were, by a reviewer rather than by the gate. A floor assertion covers the family. Note the honest limit before building it: a change touching only files the gate does not read (an image, a lock file) legitimately scopes to zero, so the assertion compares against the diff's own file list rather than against zero alone. -- Teach the `sha-pin` check in [`repo_gate.py`][repo-gate] to verify a pin **resolves**, not merely that it is shaped like a SHA. Forty hex characters is a format any fabricated string satisfies, and an agent hand-writing a plausible SHA into a workflow is a real failure mode rather than a hypothetical one. A resolvability check also catches the neighboring case, a pin whose commit was reachable only from a branch that has since been squashed and deleted, which breaks a downstream gate long after the change that caused it. Scope the network call to same-owner repositories, where the fleet's own actions live, and skip rather than fail when the host is offline so the local gate stays usable. Note that the existing `gh-write-guard` hook cannot cover this, since it watches Bash and an editor tool writing the same string into a file never reaches it. -- Add a check that a pull request's **description** does not contradict its own branch. Three stale descriptions in one session generated six review findings between them, each one a reviewer noticing that the body named a commit, a branch, or a behavior the branch no longer carried. The cheap and precise form is to extract SHAs and `uses:` refs quoted in the body and confirm each still appears in the head tree, since those are the claims that go stale silently and the ones a reviewer actually catches. Prose claims are out of scope, and deliberately so: judging those needs a similarity heuristic, which [`spec/section-model.md`][section-model] already rejects for exactly the reason it would fail here. -- Reconsider whether the pre-commit hook should run the doc gates now that they are diff-scoped. [`scripts/README.md`][scripts] records the current decision and its reason, that doc linters stay out of the hook so it stays fast, and that reason was sound when the only mode was a whole-tree sweep. A `--diff` run reads the lines one commit touches and finishes in about a second, so the trade has moved. The failure it would prevent is the most repeated one on record: comment sentences wrapped across lines, caught by CI or by a reviewer after the commit rather than before it, over and over within a single session. Weigh it against the standing preference for a fast hook, and against the risk of a hook that runs the gate from the wrong directory, which is its own false clean. -- Investigate replacing copy-pasted workflow content with cross-repo reuse, now that this repo is public. A public repository's composite actions and reusable workflows can be consumed by any other repository regardless of owner type, so the organization account this pattern was assumed to require is not needed, and the constraint that shaped the current vendor-everything model no longer holds. The catalog under [`catalog/snippets/workflows/`][workflows] is copied into each repo today, which means a fix to a shared job is a sweep across the fleet rather than one edit here, and it is the mechanism by which a defect in a snippet seeds itself into every repo that adopted it. Scope the investigation to which jobs are genuinely identical across repos against which only look similar, since a reusable workflow that needs a long input list to cover per-repo variation is worse than the copy it replaces. Settle the ref policy in the same pass, because consuming hub code at CI time is a floating dependency unless it is pinned, and [`GOVERNANCE.md`][governance] "Action pinning" requires a commit SHA for every action with one documented exception. Note that `uses:` does not accept expressions, so a per-branch ref cannot be selected in the workflow file and any branch-dependent behavior belongs inside the consumed action instead. - - - -[audit]: ./spec/audit.py -[audit-doc]: ./AUDIT.md -[governance]: ./GOVERNANCE.md -[matrix]: ./reports/conformance-matrix.md -[prose-lint]: ./scripts/prose_lint.py -[readme-structure]: ./spec/readme-structure.md -[repo-gate]: ./scripts/repo_gate.py -[scripts]: ./scripts/README.md -[reports]: ./reports/ -[repos]: ./registry/repos.json -[secrets]: ./spec/secrets.json -[section-model]: ./spec/section-model.md -[standup]: ./STANDUP.md -[workflows]: ./catalog/snippets/workflows/ +# TODO + +Running backlog for this repo, kept in a committed file so the guidance survives across environments where agent memory does not. Entries are grouped by the surface they change rather than by priority, since an entry's cost is mostly set by what it touches. Where an open issue covers the same ground it is named on the entry, so the two are read together rather than worked twice. + +Every issue claim below was re-checked against `develop` at `1ed0cc8` on 2026-08-03. An issue records the tree as it was on the day it was filed, so a claim in one is a starting point for a check rather than a finding to act on. + +## Verified Complete, Awaiting Close + +Each of these was checked line by line against the current tree and has nothing left to do here. Closing them is the maintainer's call, and each wants the evidence quoted in the closing comment rather than a bare close. + +- [#509][issue-509] (PhotoCleaner spec questions) is complete on all five items. Item 1, `.github/copilot-instructions.md` now reads "Most of `GOVERNANCE.md` is universal fleet law" and describes `AGENTS.md` as the thin router carrying two byte-locked sections and no repository-specific ones. Item 2, `CODESTYLE.md` "Markdown and Spelling" now names `details` and `summary` as allowed, matching [`.markdownlint-cli2.jsonc`][markdownlint]. Item 3, [`spec/readme-structure.md`][readme-structure] now states the private-repository behavior outright, that the full shape is carried and the GitHub-sourced badges render broken until the flip, which is accepted rather than worked around. Item 4, the same file now says the `HISTORY.md` mirror rule lives in `CODESTYLE.md`, which every repo carries, so the rule has a carried home and the file states only what the audit does with it. Item 5, `WORKFLOW.md` D2.2 now reads "the check exits early while the job still reports success" and spells out why a job-level `if:` would couple the release to smoke through `needs:`. +- [#490][issue-490] (STANDUP has no repo-creation step) is complete on all four suggestions. [`STANDUP.md`][standup] carries a new section 0A, "Hand Over What Only the Maintainer Can Supply", listing the repository, the installed App, the App secret values in both stores, and every declared publish credential and environment. It states that a repo with no remote is not partially stood up but not started, and that a blocking prerequisite is escalated the moment it is found rather than carried, which is the failure mode the issue diagnosed in the agent's own task list. Step 4 now opens with `gh repo view` and carries the ruleset ordering constraint that the issue asked for. +- [#489][issue-489] (Blog carry findings) is closed on all six items by the maintainer's own status table, five fixed and one resolved as needing no change. The single piece of follow-through it leaves is the `repo-config/configure.sh` re-vendor, which is already carried by the re-vendor entry below rather than by this issue. +- [#379][issue-379] (Financial-Modeling fidelity findings) is dispositioned on all six items, and the manifest gap the follow-up raised was fixed by adding `repo-config/configure.sh` and `repo-config/settings.json` to [`spec/files.json`][files]. What holds it open is a downstream acknowledgement and that repo's own re-vendor, neither of which is hub work. +- [#353][issue-353] (`.editorconfig` relaxes all analyzers) is done hub-side, and the tree confirms it, since `dotnet_analyzer_diagnostic.severity` no longer appears in `.editorconfig`. What holds it open is the downstream re-vendor across six C# repos, sequenced in the issue so PhotoCleaner's 362 sites do not gate the other five. That is cross-repo work needing the maintainer to name the repos. + +## Gate Honesty + +The gates in [`scripts/`][scripts] are hub-only and are the fleet's main defense against agent-authored drift, so a gate that reports clean without having read anything is the worst failure available here. The `gh-write-guard` hook in [`host-setup/agent-safety/`][agent-safety] is grouped with them, since it is the same kind of instrument even though it ships per host rather than per repo. These entries are ordered so the scope floor lands before anything relies on a clean verdict. + +- Make [`prose_lint.py`][prose-lint] assert a floor on its own scope, applying to itself the rule [`GOVERNANCE.md`][governance] already states: a gate that finds nothing is indistinguishable from a gate with nothing to find. A `--diff` run that resolves a non-empty diff and then matches **zero** files has almost certainly failed to scope rather than found a clean change, so it should say so instead of exiting 0. One session produced four separate routes to that same false clean: an unresolvable base widening to a whole-tree scan, a multi-line `paths` input read only to its first newline, a diff taken in one repository while scanning another, and a path under no repository at all. Each was fixed with its own guard, which is the wrong shape, because the fifth route will need a fifth guard and will be found the same way the first four were, by a reviewer rather than by the gate. A floor assertion covers the family. Note the honest limit before building it: a change touching only files the gate does not read (an image, a lock file) legitimately scopes to zero, so the assertion compares against the diff's own file list rather than against zero alone. This lands first, because every entry below it produces verdicts that are only worth reading once the gate can prove it read something. +- Clear the [#519][issue-519] prose backlog, starting with `catalog/snippets/**`. Two of the four changes that issue planned have already landed and its headline numbers are stale, so re-measure before quoting anything from it. `comment-wrap` and `comment-case` are now in `DEFAULT_RULES`, and `reports/` is now exempt as a generated tree, which is why the current figures are **534** whole-tree under the default rules and **184** under `catalog/snippets` alone rather than the 668 and 119 the issue records. The snippets lead the fix order for the reason the issue gives, that a non-conformant snippet seeds its violations into every repo that adopts it and the downstream repo is then flagged for content it was handed. What remains undecided is whether the gate itself becomes a carried file rather than a hub-only one, which is the same question the entry below on hub-only machinery asks from the other direction. +- Gate the pattern-detectable half of the representative-data rule in [`prose_lint.py`][prose-lint], meaning an absolute home path (`/home/`, `/Users/`, `C:\Users\`) or a bare drive letter sitting in committed prose, a code comment, or a fixture. [`GOVERNANCE.md`][governance] "Representative Data in Agent-Authored Text" states the rule and says why a check is a floor rather than an answer, so the check is introduced as covering the easy half or it gets read as closing the rule, which is the specific way it would make things worse. Three things to settle when writing it. The exemption carries the whole burden, since the rule's own wording, the `host-setup/` docs, and the audit's examples all quote path shapes in order to describe them, and a wrong exemption hands out a work list that damages correct documents. The leak that motivated the rule was in a pull request comment, which no committed-file linter reads at all, so say what surface the gate covers rather than letting its name imply the rule. And a home path in an operational repo's runbook may be the literal path an operator types, which is the repo's own content rather than an agent quoting the maintainer's environment, so decide whether the finding is scoped by file, by repo type, or left for the author to judge. +- Teach the `sha-pin` check in [`repo_gate.py`][repo-gate] to verify a pin **resolves**, not merely that it is shaped like a SHA. Forty hex characters is a format any fabricated string satisfies, and an agent hand-writing a plausible SHA into a workflow is a real failure mode rather than a hypothetical one. A resolvability check also catches the neighboring case, a pin whose commit was reachable only from a branch that has since been squashed and deleted, which breaks a downstream gate long after the change that caused it. Scope the network call to same-owner repositories, where the fleet's own actions live, and skip rather than fail when the host is offline so the local gate stays usable. Note that the existing `gh-write-guard` hook cannot cover this, since it watches Bash and an editor tool writing the same string into a file never reaches it. +- Add a check that a pull request's **description** does not contradict its own branch. Three stale descriptions in one session generated six review findings between them, each one a reviewer noticing that the body named a commit, a branch, or a behavior the branch no longer carried. The cheap and precise form is to extract SHAs and `uses:` refs quoted in the body and confirm each still appears in the head tree, since those are the claims that go stale silently and the ones a reviewer actually catches. Prose claims are out of scope, and deliberately so: judging those needs a similarity heuristic, which [`spec/section-model.md`][section-model] already rejects for exactly the reason it would fail here. +- Reconsider whether the pre-commit hook should run the doc gates now that they are diff-scoped. [`scripts/README.md`][scripts] records the current decision and its reason, that doc linters stay out of the hook so it stays fast, and that reason was sound when the only mode was a whole-tree sweep. A `--diff` run reads the lines one commit touches and finishes in about a second, so the trade has moved. The failure it would prevent is the most repeated one on record: comment sentences wrapped across lines, caught by CI or by a reviewer after the commit rather than before it, over and over within a single session. Weigh it against the standing preference for a fast hook, and against the risk of a hook that runs the gate from the wrong directory, which is its own false clean. +- Fix the newline parsing in [`gh-write-guard.py`][write-guard], where `_git_subcommand_arglists` ends a `git push` argument list at `&&` but not at a newline, so every token on a later line of the same command is read as a refspec. Measured against the installed hook, `git push -u origin revendor/x` alone resolves to the one branch, while the same push followed by a newline and `gh pr create` with a base of `develop` resolves to five, meaning `revendor/x`, `gh`, `pr`, `create`, and `develop`. The `&&` form resolves correctly, which is what isolates the defect to the newline case. The direction of the error is over-blocking rather than under-blocking, so it is a usability defect rather than a safety hole, and that is exactly why it is worth fixing rather than tolerating: the denial tells the agent it is attempting a direct push to a protected branch that only lands by bypassing a rule with admin power, when the push targets an ordinary feature branch. An agent that believes the message either escalates a push needing no escalation or goes looking for a way around the guard, and teaching a safety hook to cry wolf is how it stops being read. Note the fix reaches every host only when `host-setup/agent-safety/install.sh` is re-run, so it lands with the rollout work in [#365][issue-365] rather than ahead of it, and the entry to write alongside the fix is a self-test case for the newline form, since the existing suite covers the `&&` form and passed throughout. + +## Carried Content and Propagation + +Everything here changes a file the fleet holds a copy of, so each entry costs a hub edit plus a sweep, and the sweep is the part that gets skipped. + +- Re-vendor the changed `verbatim` content across the fleet, which is one sweep covering three files. `repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"` and the hub swept it to one sentence per line. In `AGENTS.md`, "Context and Delegation Discipline" carries the wait rule's failure clause and "Where the Rules Live" carries a row for the new section named below. In `GOVERNANCE.md`, "Verification Discipline" carries the rule that a launched process is not a result, "PR Review Etiquette" carries the five outcomes that close a finding, and "Representative Data in Agent-Authored Text" is an entirely new carried section that no downstream repo holds, which the audit reports as a missing section rather than as drift. Every repo already holding a copy of a changed section is byte-mismatched against the hub until it takes the new one, which the audit reports as stale rather than modified. This sweep is also the follow-through [#489][issue-489] and [#379][issue-379] were waiting on, and the `.editorconfig` line in [#353][issue-353] rides the same visit to each repo. Regenerate [reports/divergences.md][divergences-report] before using it as the work list, since the committed copy is dated 2026-07-22 and therefore predates the router split, which shows in it naming "Repository Boundaries and Write Safety", "Git and Commit Rules" and "Verification Discipline" as `AGENTS.md` sections when all three now live in `GOVERNANCE.md`. A stale ledger is the same hazard as a stale exemption, in that it hands out a work list measured against a tree that no longer exists. +- Drop the unused `GITHUB_TOKEN` grants from the merge-bot workflow, per [#521][issue-521], which the tree confirms is still open, since all four jobs still carry `contents: write` and `pull-requests: write` while every write in the file authenticates with the App token. The finding is least privilege on the highest-blast-radius shape in the fleet, a `pull_request_target` workflow holding an App private key, where the grant is not exploitable today only because no step consumes it. Three things to settle: whether to drop the job-level blocks or set a workflow-level `permissions: {}`, whether the audit should compare permissions at all given the file is `interface` fidelity with only a `requiredJobKeys` contract, and the re-vendor, since every repo carries the file. Note that [`spec/files.json`][files] now declares this workflow at `appliesTo: "*"`, which closes the separate gap [#456][issue-456] raised, that D8.1 graded a file the file spec never required. +- State the rule that an agent never assumes a Docker image is present locally, however recently it pulled one, because a background prune can remove an image between two commands of the same session and the run then fails on something that worked minutes earlier. The prescription is the always-pull flag by default, and an explicit `docker pull :` before use where the flag does not apply. The four documented lint invocations already carry `--pull=always`, in [`GOVERNANCE.md`][governance] "Running the Linters Locally", mirrored in [`OPERATIONS.md`][operations] and both VS Code task snippets, so what is missing is the rule rather than the one-liners: an agent composing an ad-hoc `docker run` drops the flag precisely because it believes the image is already cached. Two things to settle when writing it. Where it lives, since that section is scoped to the four lint tools while the rule covers any container an agent starts, which argues for placing it with the Docker or verification guidance instead, and whether it is carried to the fleet, since every repo runs the same images from the same instructions. Note the limit before writing it, so the flag does not read as the whole answer: `docker run` against a registry tag re-pulls an absent image on its own, so the cases that actually break are a locally built tag with no registry to pull from, and any command that branches on the image being present (`docker image inspect`, `docker images`) rather than simply running it. +- Decide where a carried file may name hub-only machinery, since `GOVERNANCE.md` "PR Review Etiquette" points at `scripts/pr_review.py` and the fleet carries the section but not the script. A downstream reader follows that pointer to a path their repo does not have. Either the script joins the carried set, or the rule states the behavior and drops the tool name the way the coordination-reference rule already requires for the template repo itself. +- Revisit [#305][issue-305], which asks for a propagation path for the rule docs and is the oldest open issue here, because most of the machinery it asked for has since been built and the issue has not been re-scoped to match. The fidelity model, the `spec/files.json` manifest, [`spec/divergences.json`][divergences] with its generated [reports/divergences.md][divergences-report], and [`AUDIT.md`][audit-doc] section 10 together give the canonical-versus-adapted split and the audit path the issue proposed. What is genuinely still missing is the push half, since every one of those detects drift while the sweep that fixes it is manual, which is exactly the entry above. Either re-scope the issue to the push half or close it against the entry above and the audit-automation entry below. +- Investigate replacing copy-pasted workflow content with cross-repo reuse, now that this repo is public. A public repository's composite actions and reusable workflows can be consumed by any other repository regardless of owner type, so the organization account this pattern was assumed to require is not needed, and the constraint that shaped the current vendor-everything model no longer holds. The catalog under [`catalog/snippets/workflows/`][workflows] is copied into each repo today, which means a fix to a shared job is a sweep across the fleet rather than one edit here, and it is the mechanism by which a defect in a snippet seeds itself into every repo that adopted it. Scope the investigation to which jobs are genuinely identical across repos against which only look similar, since a reusable workflow that needs a long input list to cover per-repo variation is worse than the copy it replaces. Settle the ref policy in the same pass, because consuming hub code at CI time is a floating dependency unless it is pinned, and [`GOVERNANCE.md`][governance] "Action pinning" requires a commit SHA for every action with one documented exception. Note that `uses:` does not accept expressions, so a per-branch ref cannot be selected in the workflow file and any branch-dependent behavior belongs inside the consumed action instead. + +## Audit and Spec Model + +- Teach the audit to see content sitting in the wrong file, per [#523][issue-523], which the tree confirms is untouched, since [`spec/audit.py`][audit] still checks file presence, declared-section presence, verbatim hashes, and workflow interface conformance and nothing that reads a heading against a destination. The case that found it is a repo whose `.github/copilot-instructions.md` carried 311 lines under nine headings that [`spec/section-model.md`][section-model] assigns to `ARCHITECTURE.md` and `OPERATIONS.md`, reported as a missing-file letter while the misplacement that caused it was invisible. The shape that fits the engine is structural, collecting the level-two headings of an `intent` file, subtracting the ones [`spec/files.json`][files] declares for it, and comparing the remainder against the headings other destinations declare. The similarity-based version is explicitly rejected by `section-model.md`, and a detector built on it produces findings whose remedy is to delete content. Four things to settle, all in the issue: whether an undeclared heading is a finding at all given a repo may add locally, whether the destination mapping becomes declared data rather than prose, whether it reaches the advisory `ARCHITECTURE.md`, and how many repos are affected, which is measured before the check is designed rather than after it starts reporting. +- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field it already sets every other repository setting from. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. The disposition is recorded on [#509][issue-509], and the ask on the Docker repos is only that the parsing step is not propagated further while the field is pending. +- Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. +- Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. +- Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. +- Canonicalize Python linter-config placement on `pyproject.toml`, since one cataloged repo uses a standalone `.ruff.toml` plus `pyrightconfig.json`. Track it as a drift finding and fix it downstream. +- Populate [reports/][reports] for the cataloged repos that still have no audit, since a registry `status` of `cataloged` asserts a result that only a committed report evidences. Eight repos have one. This is paced by maintainer capacity rather than blocked on anything, since repos are brought up to spec as they are worked on, so the entry records the outstanding set rather than a defect. +- Revisit automating the audit, which was explored and deliberately deferred, recorded here so the reasoning is not re-derived from scratch. Three shapes were considered: a scheduled hub-driven audit publishing each report as a workflow artifact, the same thing committing the report back the way the codegen bot updates its own files, and a pull-request hook in each downstream repo that audits itself against the current hub. Three things blocked all of them. Until the fleet reaches stasis with every repo onboarded, a scheduled run reports mostly noise, since a repo mid-onboarding is expected to be non-conformant. The hub has to be stable before downstreams can audit against it, because a hub change lands as fleet-wide findings the same day. And the downstream half is a catch-22, since a self-auditing pull-request hook is CI instrumentation the repos that most need it do not yet carry. The agreed outcome was the on-demand audit that [`AUDIT.md`][audit-doc] describes today. Worth reopening once the fleet is onboarded and the hub goes a stretch without carried-content changes, and the artifact shape is the one to try first, since it produces evidence without committing anything and so cannot generate review load while the noise level is still unknown. + +## Onboarding and New Types + +- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. Note that section 0A now covers the human-gated prerequisites from [#490][issue-490], so this is the remaining half of that gap rather than the whole of it. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out. +- Add the static-site generator type that [#456][issue-456] has been gathering answers for, once Blog reports its measured shape rather than its expected one. The intake is answered in detail across that issue: dispatch-only publish with the tag cut last after a live check, rsync over SSH into a per-release directory with an atomic symlink swap, one secret plus per-environment variables, `staging` and `production` environments, theme as a Dependabot-tracked submodule, built output never committed, LF line endings under the operational model, and imported content excluded from the prose gates by an explicit provenance table. Two decisions are already settled against the first draft, that the deploy **is** a publish because the pipeline versions it with NBGV and readers consume it, and that the type is named for the generator rather than the transport with the checks phrased so they do not name the generator. The URL parity gate is the check of record, asserting a floor on the golden list length before comparing, since a truncated list makes every check below it pass vacuously. Blog is interim-classified until the type exists, so the deferral clears when this lands. Note two caveats the issue also records and nothing else states: `source-only.detect` is literally `["no build-*-task.yml"]`, so a Hugo build leaf must not be named `build-site-task.yml` or the declaration becomes a false letter, and the `copilot_code_review` rule in both ruleset payloads does not gate a merge today because gated Copilot review is an invite-only beta, which deserves a sentence near the merge gate so no repo reads the rule as the enforcement and relaxes the manual discipline actually holding the line. +- Finish onboarding hardening (from [#310][issue-310]): make the [`AUDIT.md`][audit-doc] audit a required onboarding step and run the per-type cold-start self-tests tracked in [reports/conformance-matrix.md][matrix]. `STANDUP.md` is already in place. +- Add a linter-only Python project type for codegen and boilerplate Python, code that runs during another tool's build to emit generated source (for example ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage, as in PlexCleaner. Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile. +- Add a fleet-standard clang-format config for the `cpp` type, a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`. + +## Host and Environment + +- Finish the [#365][issue-365] rollout, which has two halves and one of them is invisible from here. Four machines are still unchecked, WSL2 Ubuntu, the MacBook Air, and both ThinkPads, plus any headless or cron environment that runs Claude Code with the token. A ticked row means the `CLAUDE.md` text and not the hook, since only running `host-setup/agent-safety/install.sh` deploys both layers, and the proxmox host proved that distinction the hard way by carrying the documentary half alone for eight days on the very machine where the incident originated. Note also the follow-up left open on this host and never applied, that the superseded `## Safety rules` section from [#364][issue-364] still sits above the canonical `agent-safety v1` block in `~/.claude/CLAUDE.md`, so the two overlap. Removing the superseded section is a judgment call on a per-machine file, which is why it was surfaced rather than applied. +- Fill in the remaining cells of the [#483][issue-483] host tooling matrix, which is the reference that makes the per-machine chore above repeatable, so the two close together rather than either alone. Linux, Windows and WSL2 are filled in and verified. **macOS** needs someone on that platform, the **Proxmox** question is whether that host is a shell for agent work or also runs containers, which decides whether `docker` is required there at all, and the engine-inside-the-distro variant of the WSL2 `docker` cell is still unverified since that host only exercised the Docker Desktop integration. Honor the issue's own rule when filling a cell, that an unverified install command is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded. + +## Fleet Chores + +- Measure review rounds against pull request size, and decide what the number licenses. The recent loops suggest a large change earns a different finding every round while a small one converges in one or two, which would make change size the lever on review cost rather than the reviewer's thoroughness, and would argue for splitting a change before review rather than discovering it through five rounds of findings. The data needs no new instrumentation, since the review history already carries it: for each recent pull request, record the diff size in files and lines, the number of rounds, and the findings per round, counting suppressed findings alongside threaded ones because they are the majority of what these loops produce. The outcome worth having is a threshold [`GOVERNANCE.md`][governance] can state in the branching or review guidance, expressed as the size at which a change is split rather than as advice to keep changes small. Note two confounds before drawing a line from the numbers. A large change is usually also a novel one, so size and unfamiliarity move together and the record should note what kind of change each was. And a round that finds something new is not evidence of a problem by itself, since a round that finds something new is the reviewer working, so the metric to watch is findings that a smaller first cut would have surfaced earlier rather than findings per round on its own. +- Refresh the README, which has gone stale, and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation, since it must track what the docs contain. +- Adopt the OCI annotation keys (`org.opencontainers.image.*`) for Docker image metadata across the Docker repos, replacing the ad-hoc and `org.label-schema.*` labels (from [#363][issue-363]). +- Sweep `ManagePackageVersionsCentrally` placement to `Directory.Packages.props` fleet-wide, since PlexCleaner sets it in `Directory.Build.props`, off the CODESTYLE canonical. +- Consider renaming this repo to reflect the audit-catalog identity, which updates badge and link URLs across the fleet. + + + +[issue-305]: https://github.com/ptr727/ProjectTemplate/issues/305 +[issue-310]: https://github.com/ptr727/ProjectTemplate/issues/310 +[issue-353]: https://github.com/ptr727/ProjectTemplate/issues/353 +[issue-363]: https://github.com/ptr727/ProjectTemplate/issues/363 +[issue-364]: https://github.com/ptr727/ProjectTemplate/issues/364 +[issue-365]: https://github.com/ptr727/ProjectTemplate/issues/365 +[issue-379]: https://github.com/ptr727/ProjectTemplate/issues/379 +[issue-456]: https://github.com/ptr727/ProjectTemplate/issues/456 +[issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 +[issue-489]: https://github.com/ptr727/ProjectTemplate/issues/489 +[issue-490]: https://github.com/ptr727/ProjectTemplate/issues/490 +[issue-509]: https://github.com/ptr727/ProjectTemplate/issues/509 +[issue-519]: https://github.com/ptr727/ProjectTemplate/issues/519 +[issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521 +[issue-523]: https://github.com/ptr727/ProjectTemplate/issues/523 + + + +[agent-safety]: ./host-setup/agent-safety/ +[audit]: ./spec/audit.py +[audit-doc]: ./AUDIT.md +[divergences]: ./spec/divergences.json +[divergences-report]: ./reports/divergences.md +[files]: ./spec/files.json +[governance]: ./GOVERNANCE.md +[markdownlint]: ./.markdownlint-cli2.jsonc +[matrix]: ./reports/conformance-matrix.md +[operations]: ./OPERATIONS.md +[prose-lint]: ./scripts/prose_lint.py +[readme-structure]: ./spec/readme-structure.md +[repo-gate]: ./scripts/repo_gate.py +[reports]: ./reports/ +[repos]: ./registry/repos.json +[scripts]: ./scripts/README.md +[secrets]: ./spec/secrets.json +[section-model]: ./spec/section-model.md +[standup]: ./STANDUP.md +[workflows]: ./catalog/snippets/workflows/ +[write-guard]: ./host-setup/agent-safety/gh-write-guard.py diff --git a/reports/divergences.md b/reports/divergences.md index f17f039a..2ae4d905 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -6,7 +6,7 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ### re-vendor -- **.markdownlint-cli2.jsonc** - AudioCleaner, PhotoCleaner, aiopurpleair - Verbatim config held as a hand-modified copy in these three (not a past hub revision). Restore the current canonical. +- **.markdownlint-cli2.jsonc** - AudioCleaner, aiopurpleair _(recorded PhotoCleaner now resolved)_ - Verbatim config held as a hand-modified copy in these three (not a past hub revision). Restore the current canonical. - **repo-config/configure.sh** - ESPHome-NonRoot, LanguageTags, NxWitness, VSCode-Server-DotNetCore, aiopurpleair, homeassistant-purpleair - The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness may keep a thin repo-specific wrapper for its hardcoded Docker Hub secret checks, which the repo-agnostic canonical leaves as a manual-verify note. ### investigate @@ -22,15 +22,33 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ## Untriaged - add a disposition to `spec/divergences.json` -- **AGENTS.md > Repository Boundaries and Write Safety** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical) -- **AGENTS.md > Git and Commit Rules** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical) -- **AGENTS.md > Verification Discipline** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical) +- **AGENTS.md > Context and Delegation Discipline** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical) +- **AGENTS.md > Where the Rules Live** - hand-modified in AudioCleaner, DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair (verbatim canonical) +- **GOVERNANCE.md > Representative Data in Agent-Authored Text** - hand-modified in Financial-Modeling, PhotoCleaner (verbatim canonical) ## Mechanical re-vendor (verbatim stale copies) A past hub revision, not the current canonical - the audit already flags these as DRIFT. Copy the current file down. No judgment needed. -- **.markdownlint-cli2.jsonc** (15): DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, homeassistant-purpleair +- **AGENTS.md > Context and Delegation Discipline** (2): Financial-Modeling, PhotoCleaner +- **AGENTS.md > Where the Rules Live** (2): Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Foundational Principles** (1): Financial-Modeling +- **GOVERNANCE.md > Durable Knowledge and Self-Improvement** (1): Financial-Modeling +- **GOVERNANCE.md > Repository Boundaries and Write Safety** (1): Financial-Modeling +- **GOVERNANCE.md > Git and Commit Rules** (2): Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Branching Model** (1): Financial-Modeling +- **GOVERNANCE.md > Release Model** (1): Financial-Modeling +- **GOVERNANCE.md > Operational Repositories** (1): Financial-Modeling +- **GOVERNANCE.md > Pull Request Title and Commit Message Conventions** (1): Financial-Modeling +- **GOVERNANCE.md > Documentation Style Conventions** (1): Financial-Modeling +- **GOVERNANCE.md > Verification Discipline** (2): Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > PR Review Etiquette** (2): Financial-Modeling, PhotoCleaner +- **GOVERNANCE.md > Communicating with the User** (1): Financial-Modeling +- **GOVERNANCE.md > Workflow YAML Conventions** (1): Financial-Modeling +- **GOVERNANCE.md > Supported Development Platforms** (1): Financial-Modeling +- **GOVERNANCE.md > Editor and Tasks** (1): Financial-Modeling +- **GOVERNANCE.md > Repository Details** (1): Financial-Modeling +- **.markdownlint-cli2.jsonc** (16): DevKitCIoT, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HolidayLights, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, homeassistant-purpleair - **repo-config/configure.sh** (7): ESPHome-Config, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, PlexCleaner, Utilities, Vantage-Config ## Promote candidates (intent uniform -> verbatim) From 943b5648e9f7bda7856abc83b94fdc358bd758dd Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 10:43:55 -0700 Subject: [PATCH 2/5] Drop the hardcoded count from a disposition over a changing repo list The disposition read "in these three" while the report rendered two names, because PhotoCleaner resolved and the generated line drops a resolved repo while the reason text stayed as written. Correcting the number to two would go stale the same way the next time a repo converges, so the count is gone rather than fixed. The fix belongs in spec/divergences.json rather than in the report, since reports/divergences.md is generated and a hand edit there is undone by the next regeneration. Co-Authored-By: Claude Opus 5 (1M context) --- reports/divergences.md | 2 +- spec/divergences.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reports/divergences.md b/reports/divergences.md index 2ae4d905..b57274ad 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -6,7 +6,7 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ### re-vendor -- **.markdownlint-cli2.jsonc** - AudioCleaner, aiopurpleair _(recorded PhotoCleaner now resolved)_ - Verbatim config held as a hand-modified copy in these three (not a past hub revision). Restore the current canonical. +- **.markdownlint-cli2.jsonc** - AudioCleaner, aiopurpleair _(recorded PhotoCleaner now resolved)_ - Verbatim config held as a hand-modified copy rather than a past hub revision. Restore the current canonical. - **repo-config/configure.sh** - ESPHome-NonRoot, LanguageTags, NxWitness, VSCode-Server-DotNetCore, aiopurpleair, homeassistant-purpleair - The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness may keep a thin repo-specific wrapper for its hardcoded Docker Hub secret checks, which the repo-agnostic canonical leaves as a manual-verify note. ### investigate diff --git a/spec/divergences.json b/spec/divergences.json index 9f707bfc..680e5fcd 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -3,7 +3,7 @@ "note": "Curated dispositions for known fleet divergences from the manifest canonicals - the burn-down ledger. spec/fidelity_honesty.py --report joins this against live fleet reality to write reports/divergences.md. A recorded divergence still present renders as a burn-down task with its disposition. A live divergence absent here renders as UNTRIAGED. A recorded divergence no longer live renders as resolved. Edit this file (not the generated report) and regenerate. dispositions cover per-repo divergences from a verbatim or intent canonical - a whole file, or a verbatim section whose unit key is the file path, then a space-greater-space delimiter, then the section name (for example GOVERNANCE.md > Git and Commit Rules). gaps cover files carried by the fleet but absent from spec/files.json. disposition vocabulary: re-vendor (drift-to-fix, copy the current canonical down), track (a gap to add to the manifest), accepted (a legitimate permanent divergence, no action), upstream-candidate (the downstream carries an improvement the hub should adopt, then re-vendor), investigate (recorded, decision pending).", "dispositions": [ { "path": ".editorconfig-checker.json", "repos": ["HomeAutomation-Config", "HolidayLights"], "disposition": "accepted", "reason": "Both carry a legitimate repo-specific Exclude list (HomeAutomation-Config excludes a Vantage/ subtree, HolidayLights excludes .fseq sequence files). The uniform Disable block is carried intent-equivalent. Exclude is inherently repo-local, which is why the unit is intent, not verbatim.", "tracking": null }, - { "path": ".markdownlint-cli2.jsonc", "repos": ["aiopurpleair", "PhotoCleaner", "AudioCleaner"], "disposition": "re-vendor", "reason": "Verbatim config held as a hand-modified copy in these three (not a past hub revision). Restore the current canonical.", "tracking": null }, + { "path": ".markdownlint-cli2.jsonc", "repos": ["aiopurpleair", "PhotoCleaner", "AudioCleaner"], "disposition": "re-vendor", "reason": "Verbatim config held as a hand-modified copy rather than a past hub revision. Restore the current canonical.", "tracking": null }, { "path": "repo-config/configure.sh", "repos": ["NxWitness", "aiopurpleair", "homeassistant-purpleair", "ESPHome-NonRoot", "VSCode-Server-DotNetCore", "LanguageTags"], "disposition": "re-vendor", "reason": "The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness may keep a thin repo-specific wrapper for its hardcoded Docker Hub secret checks, which the repo-agnostic canonical leaves as a manual-verify note.", "tracking": null } ], "gaps": [ From d4e77982d27ec455d93eb1724a816273911807b1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 11:01:23 -0700 Subject: [PATCH 3/5] Queue the rule for what a driftNote may contain when it is written AUDIT.md section 8 governs a note's end of life, saying it records a current deviation and is deleted once resolved, and says nothing about its content at the moment an agent authors one. PhotoCleaner is the worked example: asked to self-audit and reconcile, the agent wrote a completion record, the date a past version published, and a description of the repo conforming to spec. None is a current deviation, and nothing it was given forbids any of the three, so this is an incomplete instruction rather than an agent ignoring one. The detector has the matching gap. It flags a stale note by matching words that assert outstanding work, which is the opposite polarity to all three of those notes, and it only runs when the repo audits clean, which is the state where the problem has already gone. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TODO.md b/TODO.md index 5915db98..a2758554 100644 --- a/TODO.md +++ b/TODO.md @@ -40,6 +40,7 @@ Everything here changes a file the fleet holds a copy of, so each entry costs a ## Audit and Spec Model - Teach the audit to see content sitting in the wrong file, per [#523][issue-523], which the tree confirms is untouched, since [`spec/audit.py`][audit] still checks file presence, declared-section presence, verbatim hashes, and workflow interface conformance and nothing that reads a heading against a destination. The case that found it is a repo whose `.github/copilot-instructions.md` carried 311 lines under nine headings that [`spec/section-model.md`][section-model] assigns to `ARCHITECTURE.md` and `OPERATIONS.md`, reported as a missing-file letter while the misplacement that caused it was invisible. The shape that fits the engine is structural, collecting the level-two headings of an `intent` file, subtracting the ones [`spec/files.json`][files] declares for it, and comparing the remainder against the headings other destinations declare. The similarity-based version is explicitly rejected by `section-model.md`, and a detector built on it produces findings whose remedy is to delete content. Four things to settle, all in the issue: whether an undeclared heading is a finding at all given a repo may add locally, whether the destination mapping becomes declared data rather than prose, whether it reaches the advisory `ARCHITECTURE.md`, and how many repos are affected, which is measured before the check is designed rather than after it starts reporting. +- State what a `driftNote` may contain at the moment one is written, not only what happens to it once the deviation clears, and widen the check that guards it. [`AUDIT.md`][audit-doc] section 8 says a note records a current deviation and is deleted once resolved, which is a rule about a note's end of life and leaves an agent authoring one with no rule about its content. PhotoCleaner is the worked example, in [#537][pr-537]: asked to self-audit and reconcile its notes, the agent wrote a completion record ("baseline onboarding completed"), a historical fact (the date a past version first published), and a description of the repo conforming to the README spec. All three are prose about the past or about compliance rather than a current deviation, and none of them is what the field is for. Read that as an incomplete instruction rather than an agent ignoring one, since nothing it was given forbids any of the three. Two things to settle. The rule wants stating positively at authoring time, that a note names a deviation that is true right now and would still be true if nobody read it, so history, completion, and conformance are each out by construction, and a repo doing exactly what the spec requires earns no note at all. And the detector needs the same widening, because [`spec/audit.py`][audit] currently flags a stale note by matching words that assert *outstanding* work ("pending", "not yet", "missing", "behind"), which is the opposite polarity to all three of PhotoCleaner's notes, and it only runs that check when the repo audits clean, which PhotoCleaner did not until the round that found this. A guard that fires only on one polarity, and only in the state where the problem has already gone, cannot see the failure it exists to catch. Note the limit before building it, since it is the same limit the section model already records: judging whether a sentence describes a current deviation is a semantic call, so the honest target is the shapes that are mechanically recognizable, a past-tense completion verb and an explicit date among them, with the rest left to the rule. - Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field it already sets every other repository setting from. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. The disposition is recorded on [#509][issue-509], and the ask on the Docker repos is only that the parsing step is not propagated further while the field is pending. - Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. - Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. @@ -87,6 +88,10 @@ Everything here changes a file the fleet holds a copy of, so each entry costs a [issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521 [issue-523]: https://github.com/ptr727/ProjectTemplate/issues/523 + + +[pr-537]: https://github.com/ptr727/ProjectTemplate/pull/537 + [agent-safety]: ./host-setup/agent-safety/ From 700bc695b69f403ffa1ba57952964ddb4b06e0fa Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 11:09:15 -0700 Subject: [PATCH 4/5] Restore CRLF on TODO.md The re-bucketing rewrote the whole file rather than editing it in place, and the write emitted LF into a repo whose editorconfig sets crlf as the default for every file with no override. editorconfig-checker caught it in CI, which is the check working, but the cause is worth naming: a whole-file rewrite discards the line endings an in-place edit would have preserved, so the risk scales with how much of a file is replaced rather than with how much of it changed. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 234 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/TODO.md b/TODO.md index a2758554..4d4ea957 100644 --- a/TODO.md +++ b/TODO.md @@ -1,117 +1,117 @@ -# TODO - -Running backlog for this repo, kept in a committed file so the guidance survives across environments where agent memory does not. Entries are grouped by the surface they change rather than by priority, since an entry's cost is mostly set by what it touches. Where an open issue covers the same ground it is named on the entry, so the two are read together rather than worked twice. - -Every issue claim below was re-checked against `develop` at `1ed0cc8` on 2026-08-03. An issue records the tree as it was on the day it was filed, so a claim in one is a starting point for a check rather than a finding to act on. - -## Verified Complete, Awaiting Close - -Each of these was checked line by line against the current tree and has nothing left to do here. Closing them is the maintainer's call, and each wants the evidence quoted in the closing comment rather than a bare close. - -- [#509][issue-509] (PhotoCleaner spec questions) is complete on all five items. Item 1, `.github/copilot-instructions.md` now reads "Most of `GOVERNANCE.md` is universal fleet law" and describes `AGENTS.md` as the thin router carrying two byte-locked sections and no repository-specific ones. Item 2, `CODESTYLE.md` "Markdown and Spelling" now names `details` and `summary` as allowed, matching [`.markdownlint-cli2.jsonc`][markdownlint]. Item 3, [`spec/readme-structure.md`][readme-structure] now states the private-repository behavior outright, that the full shape is carried and the GitHub-sourced badges render broken until the flip, which is accepted rather than worked around. Item 4, the same file now says the `HISTORY.md` mirror rule lives in `CODESTYLE.md`, which every repo carries, so the rule has a carried home and the file states only what the audit does with it. Item 5, `WORKFLOW.md` D2.2 now reads "the check exits early while the job still reports success" and spells out why a job-level `if:` would couple the release to smoke through `needs:`. -- [#490][issue-490] (STANDUP has no repo-creation step) is complete on all four suggestions. [`STANDUP.md`][standup] carries a new section 0A, "Hand Over What Only the Maintainer Can Supply", listing the repository, the installed App, the App secret values in both stores, and every declared publish credential and environment. It states that a repo with no remote is not partially stood up but not started, and that a blocking prerequisite is escalated the moment it is found rather than carried, which is the failure mode the issue diagnosed in the agent's own task list. Step 4 now opens with `gh repo view` and carries the ruleset ordering constraint that the issue asked for. -- [#489][issue-489] (Blog carry findings) is closed on all six items by the maintainer's own status table, five fixed and one resolved as needing no change. The single piece of follow-through it leaves is the `repo-config/configure.sh` re-vendor, which is already carried by the re-vendor entry below rather than by this issue. -- [#379][issue-379] (Financial-Modeling fidelity findings) is dispositioned on all six items, and the manifest gap the follow-up raised was fixed by adding `repo-config/configure.sh` and `repo-config/settings.json` to [`spec/files.json`][files]. What holds it open is a downstream acknowledgement and that repo's own re-vendor, neither of which is hub work. -- [#353][issue-353] (`.editorconfig` relaxes all analyzers) is done hub-side, and the tree confirms it, since `dotnet_analyzer_diagnostic.severity` no longer appears in `.editorconfig`. What holds it open is the downstream re-vendor across six C# repos, sequenced in the issue so PhotoCleaner's 362 sites do not gate the other five. That is cross-repo work needing the maintainer to name the repos. - -## Gate Honesty - -The gates in [`scripts/`][scripts] are hub-only and are the fleet's main defense against agent-authored drift, so a gate that reports clean without having read anything is the worst failure available here. The `gh-write-guard` hook in [`host-setup/agent-safety/`][agent-safety] is grouped with them, since it is the same kind of instrument even though it ships per host rather than per repo. These entries are ordered so the scope floor lands before anything relies on a clean verdict. - -- Make [`prose_lint.py`][prose-lint] assert a floor on its own scope, applying to itself the rule [`GOVERNANCE.md`][governance] already states: a gate that finds nothing is indistinguishable from a gate with nothing to find. A `--diff` run that resolves a non-empty diff and then matches **zero** files has almost certainly failed to scope rather than found a clean change, so it should say so instead of exiting 0. One session produced four separate routes to that same false clean: an unresolvable base widening to a whole-tree scan, a multi-line `paths` input read only to its first newline, a diff taken in one repository while scanning another, and a path under no repository at all. Each was fixed with its own guard, which is the wrong shape, because the fifth route will need a fifth guard and will be found the same way the first four were, by a reviewer rather than by the gate. A floor assertion covers the family. Note the honest limit before building it: a change touching only files the gate does not read (an image, a lock file) legitimately scopes to zero, so the assertion compares against the diff's own file list rather than against zero alone. This lands first, because every entry below it produces verdicts that are only worth reading once the gate can prove it read something. -- Clear the [#519][issue-519] prose backlog, starting with `catalog/snippets/**`. Two of the four changes that issue planned have already landed and its headline numbers are stale, so re-measure before quoting anything from it. `comment-wrap` and `comment-case` are now in `DEFAULT_RULES`, and `reports/` is now exempt as a generated tree, which is why the current figures are **534** whole-tree under the default rules and **184** under `catalog/snippets` alone rather than the 668 and 119 the issue records. The snippets lead the fix order for the reason the issue gives, that a non-conformant snippet seeds its violations into every repo that adopts it and the downstream repo is then flagged for content it was handed. What remains undecided is whether the gate itself becomes a carried file rather than a hub-only one, which is the same question the entry below on hub-only machinery asks from the other direction. -- Gate the pattern-detectable half of the representative-data rule in [`prose_lint.py`][prose-lint], meaning an absolute home path (`/home/`, `/Users/`, `C:\Users\`) or a bare drive letter sitting in committed prose, a code comment, or a fixture. [`GOVERNANCE.md`][governance] "Representative Data in Agent-Authored Text" states the rule and says why a check is a floor rather than an answer, so the check is introduced as covering the easy half or it gets read as closing the rule, which is the specific way it would make things worse. Three things to settle when writing it. The exemption carries the whole burden, since the rule's own wording, the `host-setup/` docs, and the audit's examples all quote path shapes in order to describe them, and a wrong exemption hands out a work list that damages correct documents. The leak that motivated the rule was in a pull request comment, which no committed-file linter reads at all, so say what surface the gate covers rather than letting its name imply the rule. And a home path in an operational repo's runbook may be the literal path an operator types, which is the repo's own content rather than an agent quoting the maintainer's environment, so decide whether the finding is scoped by file, by repo type, or left for the author to judge. -- Teach the `sha-pin` check in [`repo_gate.py`][repo-gate] to verify a pin **resolves**, not merely that it is shaped like a SHA. Forty hex characters is a format any fabricated string satisfies, and an agent hand-writing a plausible SHA into a workflow is a real failure mode rather than a hypothetical one. A resolvability check also catches the neighboring case, a pin whose commit was reachable only from a branch that has since been squashed and deleted, which breaks a downstream gate long after the change that caused it. Scope the network call to same-owner repositories, where the fleet's own actions live, and skip rather than fail when the host is offline so the local gate stays usable. Note that the existing `gh-write-guard` hook cannot cover this, since it watches Bash and an editor tool writing the same string into a file never reaches it. -- Add a check that a pull request's **description** does not contradict its own branch. Three stale descriptions in one session generated six review findings between them, each one a reviewer noticing that the body named a commit, a branch, or a behavior the branch no longer carried. The cheap and precise form is to extract SHAs and `uses:` refs quoted in the body and confirm each still appears in the head tree, since those are the claims that go stale silently and the ones a reviewer actually catches. Prose claims are out of scope, and deliberately so: judging those needs a similarity heuristic, which [`spec/section-model.md`][section-model] already rejects for exactly the reason it would fail here. -- Reconsider whether the pre-commit hook should run the doc gates now that they are diff-scoped. [`scripts/README.md`][scripts] records the current decision and its reason, that doc linters stay out of the hook so it stays fast, and that reason was sound when the only mode was a whole-tree sweep. A `--diff` run reads the lines one commit touches and finishes in about a second, so the trade has moved. The failure it would prevent is the most repeated one on record: comment sentences wrapped across lines, caught by CI or by a reviewer after the commit rather than before it, over and over within a single session. Weigh it against the standing preference for a fast hook, and against the risk of a hook that runs the gate from the wrong directory, which is its own false clean. -- Fix the newline parsing in [`gh-write-guard.py`][write-guard], where `_git_subcommand_arglists` ends a `git push` argument list at `&&` but not at a newline, so every token on a later line of the same command is read as a refspec. Measured against the installed hook, `git push -u origin revendor/x` alone resolves to the one branch, while the same push followed by a newline and `gh pr create` with a base of `develop` resolves to five, meaning `revendor/x`, `gh`, `pr`, `create`, and `develop`. The `&&` form resolves correctly, which is what isolates the defect to the newline case. The direction of the error is over-blocking rather than under-blocking, so it is a usability defect rather than a safety hole, and that is exactly why it is worth fixing rather than tolerating: the denial tells the agent it is attempting a direct push to a protected branch that only lands by bypassing a rule with admin power, when the push targets an ordinary feature branch. An agent that believes the message either escalates a push needing no escalation or goes looking for a way around the guard, and teaching a safety hook to cry wolf is how it stops being read. Note the fix reaches every host only when `host-setup/agent-safety/install.sh` is re-run, so it lands with the rollout work in [#365][issue-365] rather than ahead of it, and the entry to write alongside the fix is a self-test case for the newline form, since the existing suite covers the `&&` form and passed throughout. - -## Carried Content and Propagation - -Everything here changes a file the fleet holds a copy of, so each entry costs a hub edit plus a sweep, and the sweep is the part that gets skipped. - -- Re-vendor the changed `verbatim` content across the fleet, which is one sweep covering three files. `repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"` and the hub swept it to one sentence per line. In `AGENTS.md`, "Context and Delegation Discipline" carries the wait rule's failure clause and "Where the Rules Live" carries a row for the new section named below. In `GOVERNANCE.md`, "Verification Discipline" carries the rule that a launched process is not a result, "PR Review Etiquette" carries the five outcomes that close a finding, and "Representative Data in Agent-Authored Text" is an entirely new carried section that no downstream repo holds, which the audit reports as a missing section rather than as drift. Every repo already holding a copy of a changed section is byte-mismatched against the hub until it takes the new one, which the audit reports as stale rather than modified. This sweep is also the follow-through [#489][issue-489] and [#379][issue-379] were waiting on, and the `.editorconfig` line in [#353][issue-353] rides the same visit to each repo. Regenerate [reports/divergences.md][divergences-report] before using it as the work list, since the committed copy is dated 2026-07-22 and therefore predates the router split, which shows in it naming "Repository Boundaries and Write Safety", "Git and Commit Rules" and "Verification Discipline" as `AGENTS.md` sections when all three now live in `GOVERNANCE.md`. A stale ledger is the same hazard as a stale exemption, in that it hands out a work list measured against a tree that no longer exists. -- Drop the unused `GITHUB_TOKEN` grants from the merge-bot workflow, per [#521][issue-521], which the tree confirms is still open, since all four jobs still carry `contents: write` and `pull-requests: write` while every write in the file authenticates with the App token. The finding is least privilege on the highest-blast-radius shape in the fleet, a `pull_request_target` workflow holding an App private key, where the grant is not exploitable today only because no step consumes it. Three things to settle: whether to drop the job-level blocks or set a workflow-level `permissions: {}`, whether the audit should compare permissions at all given the file is `interface` fidelity with only a `requiredJobKeys` contract, and the re-vendor, since every repo carries the file. Note that [`spec/files.json`][files] now declares this workflow at `appliesTo: "*"`, which closes the separate gap [#456][issue-456] raised, that D8.1 graded a file the file spec never required. -- State the rule that an agent never assumes a Docker image is present locally, however recently it pulled one, because a background prune can remove an image between two commands of the same session and the run then fails on something that worked minutes earlier. The prescription is the always-pull flag by default, and an explicit `docker pull :` before use where the flag does not apply. The four documented lint invocations already carry `--pull=always`, in [`GOVERNANCE.md`][governance] "Running the Linters Locally", mirrored in [`OPERATIONS.md`][operations] and both VS Code task snippets, so what is missing is the rule rather than the one-liners: an agent composing an ad-hoc `docker run` drops the flag precisely because it believes the image is already cached. Two things to settle when writing it. Where it lives, since that section is scoped to the four lint tools while the rule covers any container an agent starts, which argues for placing it with the Docker or verification guidance instead, and whether it is carried to the fleet, since every repo runs the same images from the same instructions. Note the limit before writing it, so the flag does not read as the whole answer: `docker run` against a registry tag re-pulls an absent image on its own, so the cases that actually break are a locally built tag with no registry to pull from, and any command that branches on the image being present (`docker image inspect`, `docker images`) rather than simply running it. -- Decide where a carried file may name hub-only machinery, since `GOVERNANCE.md` "PR Review Etiquette" points at `scripts/pr_review.py` and the fleet carries the section but not the script. A downstream reader follows that pointer to a path their repo does not have. Either the script joins the carried set, or the rule states the behavior and drops the tool name the way the coordination-reference rule already requires for the template repo itself. -- Revisit [#305][issue-305], which asks for a propagation path for the rule docs and is the oldest open issue here, because most of the machinery it asked for has since been built and the issue has not been re-scoped to match. The fidelity model, the `spec/files.json` manifest, [`spec/divergences.json`][divergences] with its generated [reports/divergences.md][divergences-report], and [`AUDIT.md`][audit-doc] section 10 together give the canonical-versus-adapted split and the audit path the issue proposed. What is genuinely still missing is the push half, since every one of those detects drift while the sweep that fixes it is manual, which is exactly the entry above. Either re-scope the issue to the push half or close it against the entry above and the audit-automation entry below. -- Investigate replacing copy-pasted workflow content with cross-repo reuse, now that this repo is public. A public repository's composite actions and reusable workflows can be consumed by any other repository regardless of owner type, so the organization account this pattern was assumed to require is not needed, and the constraint that shaped the current vendor-everything model no longer holds. The catalog under [`catalog/snippets/workflows/`][workflows] is copied into each repo today, which means a fix to a shared job is a sweep across the fleet rather than one edit here, and it is the mechanism by which a defect in a snippet seeds itself into every repo that adopted it. Scope the investigation to which jobs are genuinely identical across repos against which only look similar, since a reusable workflow that needs a long input list to cover per-repo variation is worse than the copy it replaces. Settle the ref policy in the same pass, because consuming hub code at CI time is a floating dependency unless it is pinned, and [`GOVERNANCE.md`][governance] "Action pinning" requires a commit SHA for every action with one documented exception. Note that `uses:` does not accept expressions, so a per-branch ref cannot be selected in the workflow file and any branch-dependent behavior belongs inside the consumed action instead. - -## Audit and Spec Model - -- Teach the audit to see content sitting in the wrong file, per [#523][issue-523], which the tree confirms is untouched, since [`spec/audit.py`][audit] still checks file presence, declared-section presence, verbatim hashes, and workflow interface conformance and nothing that reads a heading against a destination. The case that found it is a repo whose `.github/copilot-instructions.md` carried 311 lines under nine headings that [`spec/section-model.md`][section-model] assigns to `ARCHITECTURE.md` and `OPERATIONS.md`, reported as a missing-file letter while the misplacement that caused it was invisible. The shape that fits the engine is structural, collecting the level-two headings of an `intent` file, subtracting the ones [`spec/files.json`][files] declares for it, and comparing the remainder against the headings other destinations declare. The similarity-based version is explicitly rejected by `section-model.md`, and a detector built on it produces findings whose remedy is to delete content. Four things to settle, all in the issue: whether an undeclared heading is a finding at all given a repo may add locally, whether the destination mapping becomes declared data rather than prose, whether it reaches the advisory `ARCHITECTURE.md`, and how many repos are affected, which is measured before the check is designed rather than after it starts reporting. -- State what a `driftNote` may contain at the moment one is written, not only what happens to it once the deviation clears, and widen the check that guards it. [`AUDIT.md`][audit-doc] section 8 says a note records a current deviation and is deleted once resolved, which is a rule about a note's end of life and leaves an agent authoring one with no rule about its content. PhotoCleaner is the worked example, in [#537][pr-537]: asked to self-audit and reconcile its notes, the agent wrote a completion record ("baseline onboarding completed"), a historical fact (the date a past version first published), and a description of the repo conforming to the README spec. All three are prose about the past or about compliance rather than a current deviation, and none of them is what the field is for. Read that as an incomplete instruction rather than an agent ignoring one, since nothing it was given forbids any of the three. Two things to settle. The rule wants stating positively at authoring time, that a note names a deviation that is true right now and would still be true if nobody read it, so history, completion, and conformance are each out by construction, and a repo doing exactly what the spec requires earns no note at all. And the detector needs the same widening, because [`spec/audit.py`][audit] currently flags a stale note by matching words that assert *outstanding* work ("pending", "not yet", "missing", "behind"), which is the opposite polarity to all three of PhotoCleaner's notes, and it only runs that check when the repo audits clean, which PhotoCleaner did not until the round that found this. A guard that fires only on one polarity, and only in the state where the problem has already gone, cannot see the failure it exists to catch. Note the limit before building it, since it is the same limit the section model already records: judging whether a sentence describes a current deviation is a semantic call, so the honest target is the shapes that are mechanically recognizable, a past-tense completion verb and an explicit date among them, with the rest left to the rule. -- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field it already sets every other repository setting from. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. The disposition is recorded on [#509][issue-509], and the ask on the Docker repos is only that the parsing step is not propagated further while the field is pending. -- Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. -- Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. -- Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. -- Canonicalize Python linter-config placement on `pyproject.toml`, since one cataloged repo uses a standalone `.ruff.toml` plus `pyrightconfig.json`. Track it as a drift finding and fix it downstream. -- Populate [reports/][reports] for the cataloged repos that still have no audit, since a registry `status` of `cataloged` asserts a result that only a committed report evidences. Eight repos have one. This is paced by maintainer capacity rather than blocked on anything, since repos are brought up to spec as they are worked on, so the entry records the outstanding set rather than a defect. -- Revisit automating the audit, which was explored and deliberately deferred, recorded here so the reasoning is not re-derived from scratch. Three shapes were considered: a scheduled hub-driven audit publishing each report as a workflow artifact, the same thing committing the report back the way the codegen bot updates its own files, and a pull-request hook in each downstream repo that audits itself against the current hub. Three things blocked all of them. Until the fleet reaches stasis with every repo onboarded, a scheduled run reports mostly noise, since a repo mid-onboarding is expected to be non-conformant. The hub has to be stable before downstreams can audit against it, because a hub change lands as fleet-wide findings the same day. And the downstream half is a catch-22, since a self-auditing pull-request hook is CI instrumentation the repos that most need it do not yet carry. The agreed outcome was the on-demand audit that [`AUDIT.md`][audit-doc] describes today. Worth reopening once the fleet is onboarded and the hub goes a stretch without carried-content changes, and the artifact shape is the one to try first, since it produces evidence without committing anything and so cannot generate review load while the noise level is still unknown. - -## Onboarding and New Types - -- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. Note that section 0A now covers the human-gated prerequisites from [#490][issue-490], so this is the remaining half of that gap rather than the whole of it. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out. -- Add the static-site generator type that [#456][issue-456] has been gathering answers for, once Blog reports its measured shape rather than its expected one. The intake is answered in detail across that issue: dispatch-only publish with the tag cut last after a live check, rsync over SSH into a per-release directory with an atomic symlink swap, one secret plus per-environment variables, `staging` and `production` environments, theme as a Dependabot-tracked submodule, built output never committed, LF line endings under the operational model, and imported content excluded from the prose gates by an explicit provenance table. Two decisions are already settled against the first draft, that the deploy **is** a publish because the pipeline versions it with NBGV and readers consume it, and that the type is named for the generator rather than the transport with the checks phrased so they do not name the generator. The URL parity gate is the check of record, asserting a floor on the golden list length before comparing, since a truncated list makes every check below it pass vacuously. Blog is interim-classified until the type exists, so the deferral clears when this lands. Note two caveats the issue also records and nothing else states: `source-only.detect` is literally `["no build-*-task.yml"]`, so a Hugo build leaf must not be named `build-site-task.yml` or the declaration becomes a false letter, and the `copilot_code_review` rule in both ruleset payloads does not gate a merge today because gated Copilot review is an invite-only beta, which deserves a sentence near the merge gate so no repo reads the rule as the enforcement and relaxes the manual discipline actually holding the line. -- Finish onboarding hardening (from [#310][issue-310]): make the [`AUDIT.md`][audit-doc] audit a required onboarding step and run the per-type cold-start self-tests tracked in [reports/conformance-matrix.md][matrix]. `STANDUP.md` is already in place. -- Add a linter-only Python project type for codegen and boilerplate Python, code that runs during another tool's build to emit generated source (for example ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage, as in PlexCleaner. Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile. -- Add a fleet-standard clang-format config for the `cpp` type, a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`. - -## Host and Environment - -- Finish the [#365][issue-365] rollout, which has two halves and one of them is invisible from here. Four machines are still unchecked, WSL2 Ubuntu, the MacBook Air, and both ThinkPads, plus any headless or cron environment that runs Claude Code with the token. A ticked row means the `CLAUDE.md` text and not the hook, since only running `host-setup/agent-safety/install.sh` deploys both layers, and the proxmox host proved that distinction the hard way by carrying the documentary half alone for eight days on the very machine where the incident originated. Note also the follow-up left open on this host and never applied, that the superseded `## Safety rules` section from [#364][issue-364] still sits above the canonical `agent-safety v1` block in `~/.claude/CLAUDE.md`, so the two overlap. Removing the superseded section is a judgment call on a per-machine file, which is why it was surfaced rather than applied. -- Fill in the remaining cells of the [#483][issue-483] host tooling matrix, which is the reference that makes the per-machine chore above repeatable, so the two close together rather than either alone. Linux, Windows and WSL2 are filled in and verified. **macOS** needs someone on that platform, the **Proxmox** question is whether that host is a shell for agent work or also runs containers, which decides whether `docker` is required there at all, and the engine-inside-the-distro variant of the WSL2 `docker` cell is still unverified since that host only exercised the Docker Desktop integration. Honor the issue's own rule when filling a cell, that an unverified install command is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded. - -## Fleet Chores - -- Measure review rounds against pull request size, and decide what the number licenses. The recent loops suggest a large change earns a different finding every round while a small one converges in one or two, which would make change size the lever on review cost rather than the reviewer's thoroughness, and would argue for splitting a change before review rather than discovering it through five rounds of findings. The data needs no new instrumentation, since the review history already carries it: for each recent pull request, record the diff size in files and lines, the number of rounds, and the findings per round, counting suppressed findings alongside threaded ones because they are the majority of what these loops produce. The outcome worth having is a threshold [`GOVERNANCE.md`][governance] can state in the branching or review guidance, expressed as the size at which a change is split rather than as advice to keep changes small. Note two confounds before drawing a line from the numbers. A large change is usually also a novel one, so size and unfamiliarity move together and the record should note what kind of change each was. And a round that finds something new is not evidence of a problem by itself, since a round that finds something new is the reviewer working, so the metric to watch is findings that a smaller first cut would have surfaced earlier rather than findings per round on its own. -- Refresh the README, which has gone stale, and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation, since it must track what the docs contain. -- Adopt the OCI annotation keys (`org.opencontainers.image.*`) for Docker image metadata across the Docker repos, replacing the ad-hoc and `org.label-schema.*` labels (from [#363][issue-363]). -- Sweep `ManagePackageVersionsCentrally` placement to `Directory.Packages.props` fleet-wide, since PlexCleaner sets it in `Directory.Build.props`, off the CODESTYLE canonical. -- Consider renaming this repo to reflect the audit-catalog identity, which updates badge and link URLs across the fleet. - - - -[issue-305]: https://github.com/ptr727/ProjectTemplate/issues/305 -[issue-310]: https://github.com/ptr727/ProjectTemplate/issues/310 -[issue-353]: https://github.com/ptr727/ProjectTemplate/issues/353 -[issue-363]: https://github.com/ptr727/ProjectTemplate/issues/363 -[issue-364]: https://github.com/ptr727/ProjectTemplate/issues/364 -[issue-365]: https://github.com/ptr727/ProjectTemplate/issues/365 -[issue-379]: https://github.com/ptr727/ProjectTemplate/issues/379 -[issue-456]: https://github.com/ptr727/ProjectTemplate/issues/456 -[issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 -[issue-489]: https://github.com/ptr727/ProjectTemplate/issues/489 -[issue-490]: https://github.com/ptr727/ProjectTemplate/issues/490 -[issue-509]: https://github.com/ptr727/ProjectTemplate/issues/509 -[issue-519]: https://github.com/ptr727/ProjectTemplate/issues/519 -[issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521 -[issue-523]: https://github.com/ptr727/ProjectTemplate/issues/523 - - - -[pr-537]: https://github.com/ptr727/ProjectTemplate/pull/537 - - - -[agent-safety]: ./host-setup/agent-safety/ -[audit]: ./spec/audit.py -[audit-doc]: ./AUDIT.md -[divergences]: ./spec/divergences.json -[divergences-report]: ./reports/divergences.md -[files]: ./spec/files.json -[governance]: ./GOVERNANCE.md -[markdownlint]: ./.markdownlint-cli2.jsonc -[matrix]: ./reports/conformance-matrix.md -[operations]: ./OPERATIONS.md -[prose-lint]: ./scripts/prose_lint.py -[readme-structure]: ./spec/readme-structure.md -[repo-gate]: ./scripts/repo_gate.py -[reports]: ./reports/ -[repos]: ./registry/repos.json -[scripts]: ./scripts/README.md -[secrets]: ./spec/secrets.json -[section-model]: ./spec/section-model.md -[standup]: ./STANDUP.md -[workflows]: ./catalog/snippets/workflows/ -[write-guard]: ./host-setup/agent-safety/gh-write-guard.py +# TODO + +Running backlog for this repo, kept in a committed file so the guidance survives across environments where agent memory does not. Entries are grouped by the surface they change rather than by priority, since an entry's cost is mostly set by what it touches. Where an open issue covers the same ground it is named on the entry, so the two are read together rather than worked twice. + +Every issue claim below was re-checked against `develop` at `1ed0cc8` on 2026-08-03. An issue records the tree as it was on the day it was filed, so a claim in one is a starting point for a check rather than a finding to act on. + +## Verified Complete, Awaiting Close + +Each of these was checked line by line against the current tree and has nothing left to do here. Closing them is the maintainer's call, and each wants the evidence quoted in the closing comment rather than a bare close. + +- [#509][issue-509] (PhotoCleaner spec questions) is complete on all five items. Item 1, `.github/copilot-instructions.md` now reads "Most of `GOVERNANCE.md` is universal fleet law" and describes `AGENTS.md` as the thin router carrying two byte-locked sections and no repository-specific ones. Item 2, `CODESTYLE.md` "Markdown and Spelling" now names `details` and `summary` as allowed, matching [`.markdownlint-cli2.jsonc`][markdownlint]. Item 3, [`spec/readme-structure.md`][readme-structure] now states the private-repository behavior outright, that the full shape is carried and the GitHub-sourced badges render broken until the flip, which is accepted rather than worked around. Item 4, the same file now says the `HISTORY.md` mirror rule lives in `CODESTYLE.md`, which every repo carries, so the rule has a carried home and the file states only what the audit does with it. Item 5, `WORKFLOW.md` D2.2 now reads "the check exits early while the job still reports success" and spells out why a job-level `if:` would couple the release to smoke through `needs:`. +- [#490][issue-490] (STANDUP has no repo-creation step) is complete on all four suggestions. [`STANDUP.md`][standup] carries a new section 0A, "Hand Over What Only the Maintainer Can Supply", listing the repository, the installed App, the App secret values in both stores, and every declared publish credential and environment. It states that a repo with no remote is not partially stood up but not started, and that a blocking prerequisite is escalated the moment it is found rather than carried, which is the failure mode the issue diagnosed in the agent's own task list. Step 4 now opens with `gh repo view` and carries the ruleset ordering constraint that the issue asked for. +- [#489][issue-489] (Blog carry findings) is closed on all six items by the maintainer's own status table, five fixed and one resolved as needing no change. The single piece of follow-through it leaves is the `repo-config/configure.sh` re-vendor, which is already carried by the re-vendor entry below rather than by this issue. +- [#379][issue-379] (Financial-Modeling fidelity findings) is dispositioned on all six items, and the manifest gap the follow-up raised was fixed by adding `repo-config/configure.sh` and `repo-config/settings.json` to [`spec/files.json`][files]. What holds it open is a downstream acknowledgement and that repo's own re-vendor, neither of which is hub work. +- [#353][issue-353] (`.editorconfig` relaxes all analyzers) is done hub-side, and the tree confirms it, since `dotnet_analyzer_diagnostic.severity` no longer appears in `.editorconfig`. What holds it open is the downstream re-vendor across six C# repos, sequenced in the issue so PhotoCleaner's 362 sites do not gate the other five. That is cross-repo work needing the maintainer to name the repos. + +## Gate Honesty + +The gates in [`scripts/`][scripts] are hub-only and are the fleet's main defense against agent-authored drift, so a gate that reports clean without having read anything is the worst failure available here. The `gh-write-guard` hook in [`host-setup/agent-safety/`][agent-safety] is grouped with them, since it is the same kind of instrument even though it ships per host rather than per repo. These entries are ordered so the scope floor lands before anything relies on a clean verdict. + +- Make [`prose_lint.py`][prose-lint] assert a floor on its own scope, applying to itself the rule [`GOVERNANCE.md`][governance] already states: a gate that finds nothing is indistinguishable from a gate with nothing to find. A `--diff` run that resolves a non-empty diff and then matches **zero** files has almost certainly failed to scope rather than found a clean change, so it should say so instead of exiting 0. One session produced four separate routes to that same false clean: an unresolvable base widening to a whole-tree scan, a multi-line `paths` input read only to its first newline, a diff taken in one repository while scanning another, and a path under no repository at all. Each was fixed with its own guard, which is the wrong shape, because the fifth route will need a fifth guard and will be found the same way the first four were, by a reviewer rather than by the gate. A floor assertion covers the family. Note the honest limit before building it: a change touching only files the gate does not read (an image, a lock file) legitimately scopes to zero, so the assertion compares against the diff's own file list rather than against zero alone. This lands first, because every entry below it produces verdicts that are only worth reading once the gate can prove it read something. +- Clear the [#519][issue-519] prose backlog, starting with `catalog/snippets/**`. Two of the four changes that issue planned have already landed and its headline numbers are stale, so re-measure before quoting anything from it. `comment-wrap` and `comment-case` are now in `DEFAULT_RULES`, and `reports/` is now exempt as a generated tree, which is why the current figures are **534** whole-tree under the default rules and **184** under `catalog/snippets` alone rather than the 668 and 119 the issue records. The snippets lead the fix order for the reason the issue gives, that a non-conformant snippet seeds its violations into every repo that adopts it and the downstream repo is then flagged for content it was handed. What remains undecided is whether the gate itself becomes a carried file rather than a hub-only one, which is the same question the entry below on hub-only machinery asks from the other direction. +- Gate the pattern-detectable half of the representative-data rule in [`prose_lint.py`][prose-lint], meaning an absolute home path (`/home/`, `/Users/`, `C:\Users\`) or a bare drive letter sitting in committed prose, a code comment, or a fixture. [`GOVERNANCE.md`][governance] "Representative Data in Agent-Authored Text" states the rule and says why a check is a floor rather than an answer, so the check is introduced as covering the easy half or it gets read as closing the rule, which is the specific way it would make things worse. Three things to settle when writing it. The exemption carries the whole burden, since the rule's own wording, the `host-setup/` docs, and the audit's examples all quote path shapes in order to describe them, and a wrong exemption hands out a work list that damages correct documents. The leak that motivated the rule was in a pull request comment, which no committed-file linter reads at all, so say what surface the gate covers rather than letting its name imply the rule. And a home path in an operational repo's runbook may be the literal path an operator types, which is the repo's own content rather than an agent quoting the maintainer's environment, so decide whether the finding is scoped by file, by repo type, or left for the author to judge. +- Teach the `sha-pin` check in [`repo_gate.py`][repo-gate] to verify a pin **resolves**, not merely that it is shaped like a SHA. Forty hex characters is a format any fabricated string satisfies, and an agent hand-writing a plausible SHA into a workflow is a real failure mode rather than a hypothetical one. A resolvability check also catches the neighboring case, a pin whose commit was reachable only from a branch that has since been squashed and deleted, which breaks a downstream gate long after the change that caused it. Scope the network call to same-owner repositories, where the fleet's own actions live, and skip rather than fail when the host is offline so the local gate stays usable. Note that the existing `gh-write-guard` hook cannot cover this, since it watches Bash and an editor tool writing the same string into a file never reaches it. +- Add a check that a pull request's **description** does not contradict its own branch. Three stale descriptions in one session generated six review findings between them, each one a reviewer noticing that the body named a commit, a branch, or a behavior the branch no longer carried. The cheap and precise form is to extract SHAs and `uses:` refs quoted in the body and confirm each still appears in the head tree, since those are the claims that go stale silently and the ones a reviewer actually catches. Prose claims are out of scope, and deliberately so: judging those needs a similarity heuristic, which [`spec/section-model.md`][section-model] already rejects for exactly the reason it would fail here. +- Reconsider whether the pre-commit hook should run the doc gates now that they are diff-scoped. [`scripts/README.md`][scripts] records the current decision and its reason, that doc linters stay out of the hook so it stays fast, and that reason was sound when the only mode was a whole-tree sweep. A `--diff` run reads the lines one commit touches and finishes in about a second, so the trade has moved. The failure it would prevent is the most repeated one on record: comment sentences wrapped across lines, caught by CI or by a reviewer after the commit rather than before it, over and over within a single session. Weigh it against the standing preference for a fast hook, and against the risk of a hook that runs the gate from the wrong directory, which is its own false clean. +- Fix the newline parsing in [`gh-write-guard.py`][write-guard], where `_git_subcommand_arglists` ends a `git push` argument list at `&&` but not at a newline, so every token on a later line of the same command is read as a refspec. Measured against the installed hook, `git push -u origin revendor/x` alone resolves to the one branch, while the same push followed by a newline and `gh pr create` with a base of `develop` resolves to five, meaning `revendor/x`, `gh`, `pr`, `create`, and `develop`. The `&&` form resolves correctly, which is what isolates the defect to the newline case. The direction of the error is over-blocking rather than under-blocking, so it is a usability defect rather than a safety hole, and that is exactly why it is worth fixing rather than tolerating: the denial tells the agent it is attempting a direct push to a protected branch that only lands by bypassing a rule with admin power, when the push targets an ordinary feature branch. An agent that believes the message either escalates a push needing no escalation or goes looking for a way around the guard, and teaching a safety hook to cry wolf is how it stops being read. Note the fix reaches every host only when `host-setup/agent-safety/install.sh` is re-run, so it lands with the rollout work in [#365][issue-365] rather than ahead of it, and the entry to write alongside the fix is a self-test case for the newline form, since the existing suite covers the `&&` form and passed throughout. + +## Carried Content and Propagation + +Everything here changes a file the fleet holds a copy of, so each entry costs a hub edit plus a sweep, and the sweep is the part that gets skipped. + +- Re-vendor the changed `verbatim` content across the fleet, which is one sweep covering three files. `repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"` and the hub swept it to one sentence per line. In `AGENTS.md`, "Context and Delegation Discipline" carries the wait rule's failure clause and "Where the Rules Live" carries a row for the new section named below. In `GOVERNANCE.md`, "Verification Discipline" carries the rule that a launched process is not a result, "PR Review Etiquette" carries the five outcomes that close a finding, and "Representative Data in Agent-Authored Text" is an entirely new carried section that no downstream repo holds, which the audit reports as a missing section rather than as drift. Every repo already holding a copy of a changed section is byte-mismatched against the hub until it takes the new one, which the audit reports as stale rather than modified. This sweep is also the follow-through [#489][issue-489] and [#379][issue-379] were waiting on, and the `.editorconfig` line in [#353][issue-353] rides the same visit to each repo. Regenerate [reports/divergences.md][divergences-report] before using it as the work list, since the committed copy is dated 2026-07-22 and therefore predates the router split, which shows in it naming "Repository Boundaries and Write Safety", "Git and Commit Rules" and "Verification Discipline" as `AGENTS.md` sections when all three now live in `GOVERNANCE.md`. A stale ledger is the same hazard as a stale exemption, in that it hands out a work list measured against a tree that no longer exists. +- Drop the unused `GITHUB_TOKEN` grants from the merge-bot workflow, per [#521][issue-521], which the tree confirms is still open, since all four jobs still carry `contents: write` and `pull-requests: write` while every write in the file authenticates with the App token. The finding is least privilege on the highest-blast-radius shape in the fleet, a `pull_request_target` workflow holding an App private key, where the grant is not exploitable today only because no step consumes it. Three things to settle: whether to drop the job-level blocks or set a workflow-level `permissions: {}`, whether the audit should compare permissions at all given the file is `interface` fidelity with only a `requiredJobKeys` contract, and the re-vendor, since every repo carries the file. Note that [`spec/files.json`][files] now declares this workflow at `appliesTo: "*"`, which closes the separate gap [#456][issue-456] raised, that D8.1 graded a file the file spec never required. +- State the rule that an agent never assumes a Docker image is present locally, however recently it pulled one, because a background prune can remove an image between two commands of the same session and the run then fails on something that worked minutes earlier. The prescription is the always-pull flag by default, and an explicit `docker pull :` before use where the flag does not apply. The four documented lint invocations already carry `--pull=always`, in [`GOVERNANCE.md`][governance] "Running the Linters Locally", mirrored in [`OPERATIONS.md`][operations] and both VS Code task snippets, so what is missing is the rule rather than the one-liners: an agent composing an ad-hoc `docker run` drops the flag precisely because it believes the image is already cached. Two things to settle when writing it. Where it lives, since that section is scoped to the four lint tools while the rule covers any container an agent starts, which argues for placing it with the Docker or verification guidance instead, and whether it is carried to the fleet, since every repo runs the same images from the same instructions. Note the limit before writing it, so the flag does not read as the whole answer: `docker run` against a registry tag re-pulls an absent image on its own, so the cases that actually break are a locally built tag with no registry to pull from, and any command that branches on the image being present (`docker image inspect`, `docker images`) rather than simply running it. +- Decide where a carried file may name hub-only machinery, since `GOVERNANCE.md` "PR Review Etiquette" points at `scripts/pr_review.py` and the fleet carries the section but not the script. A downstream reader follows that pointer to a path their repo does not have. Either the script joins the carried set, or the rule states the behavior and drops the tool name the way the coordination-reference rule already requires for the template repo itself. +- Revisit [#305][issue-305], which asks for a propagation path for the rule docs and is the oldest open issue here, because most of the machinery it asked for has since been built and the issue has not been re-scoped to match. The fidelity model, the `spec/files.json` manifest, [`spec/divergences.json`][divergences] with its generated [reports/divergences.md][divergences-report], and [`AUDIT.md`][audit-doc] section 10 together give the canonical-versus-adapted split and the audit path the issue proposed. What is genuinely still missing is the push half, since every one of those detects drift while the sweep that fixes it is manual, which is exactly the entry above. Either re-scope the issue to the push half or close it against the entry above and the audit-automation entry below. +- Investigate replacing copy-pasted workflow content with cross-repo reuse, now that this repo is public. A public repository's composite actions and reusable workflows can be consumed by any other repository regardless of owner type, so the organization account this pattern was assumed to require is not needed, and the constraint that shaped the current vendor-everything model no longer holds. The catalog under [`catalog/snippets/workflows/`][workflows] is copied into each repo today, which means a fix to a shared job is a sweep across the fleet rather than one edit here, and it is the mechanism by which a defect in a snippet seeds itself into every repo that adopted it. Scope the investigation to which jobs are genuinely identical across repos against which only look similar, since a reusable workflow that needs a long input list to cover per-repo variation is worse than the copy it replaces. Settle the ref policy in the same pass, because consuming hub code at CI time is a floating dependency unless it is pinned, and [`GOVERNANCE.md`][governance] "Action pinning" requires a commit SHA for every action with one documented exception. Note that `uses:` does not accept expressions, so a per-branch ref cannot be selected in the workflow file and any branch-dependent behavior belongs inside the consumed action instead. + +## Audit and Spec Model + +- Teach the audit to see content sitting in the wrong file, per [#523][issue-523], which the tree confirms is untouched, since [`spec/audit.py`][audit] still checks file presence, declared-section presence, verbatim hashes, and workflow interface conformance and nothing that reads a heading against a destination. The case that found it is a repo whose `.github/copilot-instructions.md` carried 311 lines under nine headings that [`spec/section-model.md`][section-model] assigns to `ARCHITECTURE.md` and `OPERATIONS.md`, reported as a missing-file letter while the misplacement that caused it was invisible. The shape that fits the engine is structural, collecting the level-two headings of an `intent` file, subtracting the ones [`spec/files.json`][files] declares for it, and comparing the remainder against the headings other destinations declare. The similarity-based version is explicitly rejected by `section-model.md`, and a detector built on it produces findings whose remedy is to delete content. Four things to settle, all in the issue: whether an undeclared heading is a finding at all given a repo may add locally, whether the destination mapping becomes declared data rather than prose, whether it reaches the advisory `ARCHITECTURE.md`, and how many repos are affected, which is measured before the check is designed rather than after it starts reporting. +- State what a `driftNote` may contain at the moment one is written, not only what happens to it once the deviation clears, and widen the check that guards it. [`AUDIT.md`][audit-doc] section 8 says a note records a current deviation and is deleted once resolved, which is a rule about a note's end of life and leaves an agent authoring one with no rule about its content. PhotoCleaner is the worked example, in [#537][pr-537]: asked to self-audit and reconcile its notes, the agent wrote a completion record ("baseline onboarding completed"), a historical fact (the date a past version first published), and a description of the repo conforming to the README spec. All three are prose about the past or about compliance rather than a current deviation, and none of them is what the field is for. Read that as an incomplete instruction rather than an agent ignoring one, since nothing it was given forbids any of the three. Two things to settle. The rule wants stating positively at authoring time, that a note names a deviation that is true right now and would still be true if nobody read it, so history, completion, and conformance are each out by construction, and a repo doing exactly what the spec requires earns no note at all. And the detector needs the same widening, because [`spec/audit.py`][audit] currently flags a stale note by matching words that assert *outstanding* work ("pending", "not yet", "missing", "behind"), which is the opposite polarity to all three of PhotoCleaner's notes, and it only runs that check when the repo audits clean, which PhotoCleaner did not until the round that found this. A guard that fires only on one polarity, and only in the state where the problem has already gone, cannot see the failure it exists to catch. Note the limit before building it, since it is the same limit the section model already records: judging whether a sentence describes a current deviation is a semantic call, so the honest target is the shapes that are mechanically recognizable, a past-tense completion verb and an explicit date among them, with the rest left to the rule. +- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field it already sets every other repository setting from. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. The disposition is recorded on [#509][issue-509], and the ask on the Docker repos is only that the parsing step is not propagated further while the field is pending. +- Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. +- Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. +- Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. +- Canonicalize Python linter-config placement on `pyproject.toml`, since one cataloged repo uses a standalone `.ruff.toml` plus `pyrightconfig.json`. Track it as a drift finding and fix it downstream. +- Populate [reports/][reports] for the cataloged repos that still have no audit, since a registry `status` of `cataloged` asserts a result that only a committed report evidences. Eight repos have one. This is paced by maintainer capacity rather than blocked on anything, since repos are brought up to spec as they are worked on, so the entry records the outstanding set rather than a defect. +- Revisit automating the audit, which was explored and deliberately deferred, recorded here so the reasoning is not re-derived from scratch. Three shapes were considered: a scheduled hub-driven audit publishing each report as a workflow artifact, the same thing committing the report back the way the codegen bot updates its own files, and a pull-request hook in each downstream repo that audits itself against the current hub. Three things blocked all of them. Until the fleet reaches stasis with every repo onboarded, a scheduled run reports mostly noise, since a repo mid-onboarding is expected to be non-conformant. The hub has to be stable before downstreams can audit against it, because a hub change lands as fleet-wide findings the same day. And the downstream half is a catch-22, since a self-auditing pull-request hook is CI instrumentation the repos that most need it do not yet carry. The agreed outcome was the on-demand audit that [`AUDIT.md`][audit-doc] describes today. Worth reopening once the fleet is onboarded and the hub goes a stretch without carried-content changes, and the artifact shape is the one to try first, since it produces evidence without committing anything and so cannot generate review load while the noise level is still unknown. + +## Onboarding and New Types + +- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. Note that section 0A now covers the human-gated prerequisites from [#490][issue-490], so this is the remaining half of that gap rather than the whole of it. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out. +- Add the static-site generator type that [#456][issue-456] has been gathering answers for, once Blog reports its measured shape rather than its expected one. The intake is answered in detail across that issue: dispatch-only publish with the tag cut last after a live check, rsync over SSH into a per-release directory with an atomic symlink swap, one secret plus per-environment variables, `staging` and `production` environments, theme as a Dependabot-tracked submodule, built output never committed, LF line endings under the operational model, and imported content excluded from the prose gates by an explicit provenance table. Two decisions are already settled against the first draft, that the deploy **is** a publish because the pipeline versions it with NBGV and readers consume it, and that the type is named for the generator rather than the transport with the checks phrased so they do not name the generator. The URL parity gate is the check of record, asserting a floor on the golden list length before comparing, since a truncated list makes every check below it pass vacuously. Blog is interim-classified until the type exists, so the deferral clears when this lands. Note two caveats the issue also records and nothing else states: `source-only.detect` is literally `["no build-*-task.yml"]`, so a Hugo build leaf must not be named `build-site-task.yml` or the declaration becomes a false letter, and the `copilot_code_review` rule in both ruleset payloads does not gate a merge today because gated Copilot review is an invite-only beta, which deserves a sentence near the merge gate so no repo reads the rule as the enforcement and relaxes the manual discipline actually holding the line. +- Finish onboarding hardening (from [#310][issue-310]): make the [`AUDIT.md`][audit-doc] audit a required onboarding step and run the per-type cold-start self-tests tracked in [reports/conformance-matrix.md][matrix]. `STANDUP.md` is already in place. +- Add a linter-only Python project type for codegen and boilerplate Python, code that runs during another tool's build to emit generated source (for example ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage, as in PlexCleaner. Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile. +- Add a fleet-standard clang-format config for the `cpp` type, a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`. + +## Host and Environment + +- Finish the [#365][issue-365] rollout, which has two halves and one of them is invisible from here. Four machines are still unchecked, WSL2 Ubuntu, the MacBook Air, and both ThinkPads, plus any headless or cron environment that runs Claude Code with the token. A ticked row means the `CLAUDE.md` text and not the hook, since only running `host-setup/agent-safety/install.sh` deploys both layers, and the proxmox host proved that distinction the hard way by carrying the documentary half alone for eight days on the very machine where the incident originated. Note also the follow-up left open on this host and never applied, that the superseded `## Safety rules` section from [#364][issue-364] still sits above the canonical `agent-safety v1` block in `~/.claude/CLAUDE.md`, so the two overlap. Removing the superseded section is a judgment call on a per-machine file, which is why it was surfaced rather than applied. +- Fill in the remaining cells of the [#483][issue-483] host tooling matrix, which is the reference that makes the per-machine chore above repeatable, so the two close together rather than either alone. Linux, Windows and WSL2 are filled in and verified. **macOS** needs someone on that platform, the **Proxmox** question is whether that host is a shell for agent work or also runs containers, which decides whether `docker` is required there at all, and the engine-inside-the-distro variant of the WSL2 `docker` cell is still unverified since that host only exercised the Docker Desktop integration. Honor the issue's own rule when filling a cell, that an unverified install command is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded. + +## Fleet Chores + +- Measure review rounds against pull request size, and decide what the number licenses. The recent loops suggest a large change earns a different finding every round while a small one converges in one or two, which would make change size the lever on review cost rather than the reviewer's thoroughness, and would argue for splitting a change before review rather than discovering it through five rounds of findings. The data needs no new instrumentation, since the review history already carries it: for each recent pull request, record the diff size in files and lines, the number of rounds, and the findings per round, counting suppressed findings alongside threaded ones because they are the majority of what these loops produce. The outcome worth having is a threshold [`GOVERNANCE.md`][governance] can state in the branching or review guidance, expressed as the size at which a change is split rather than as advice to keep changes small. Note two confounds before drawing a line from the numbers. A large change is usually also a novel one, so size and unfamiliarity move together and the record should note what kind of change each was. And a round that finds something new is not evidence of a problem by itself, since a round that finds something new is the reviewer working, so the metric to watch is findings that a smaller first cut would have surfaced earlier rather than findings per round on its own. +- Refresh the README, which has gone stale, and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation, since it must track what the docs contain. +- Adopt the OCI annotation keys (`org.opencontainers.image.*`) for Docker image metadata across the Docker repos, replacing the ad-hoc and `org.label-schema.*` labels (from [#363][issue-363]). +- Sweep `ManagePackageVersionsCentrally` placement to `Directory.Packages.props` fleet-wide, since PlexCleaner sets it in `Directory.Build.props`, off the CODESTYLE canonical. +- Consider renaming this repo to reflect the audit-catalog identity, which updates badge and link URLs across the fleet. + + + +[issue-305]: https://github.com/ptr727/ProjectTemplate/issues/305 +[issue-310]: https://github.com/ptr727/ProjectTemplate/issues/310 +[issue-353]: https://github.com/ptr727/ProjectTemplate/issues/353 +[issue-363]: https://github.com/ptr727/ProjectTemplate/issues/363 +[issue-364]: https://github.com/ptr727/ProjectTemplate/issues/364 +[issue-365]: https://github.com/ptr727/ProjectTemplate/issues/365 +[issue-379]: https://github.com/ptr727/ProjectTemplate/issues/379 +[issue-456]: https://github.com/ptr727/ProjectTemplate/issues/456 +[issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 +[issue-489]: https://github.com/ptr727/ProjectTemplate/issues/489 +[issue-490]: https://github.com/ptr727/ProjectTemplate/issues/490 +[issue-509]: https://github.com/ptr727/ProjectTemplate/issues/509 +[issue-519]: https://github.com/ptr727/ProjectTemplate/issues/519 +[issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521 +[issue-523]: https://github.com/ptr727/ProjectTemplate/issues/523 + + + +[pr-537]: https://github.com/ptr727/ProjectTemplate/pull/537 + + + +[agent-safety]: ./host-setup/agent-safety/ +[audit]: ./spec/audit.py +[audit-doc]: ./AUDIT.md +[divergences]: ./spec/divergences.json +[divergences-report]: ./reports/divergences.md +[files]: ./spec/files.json +[governance]: ./GOVERNANCE.md +[markdownlint]: ./.markdownlint-cli2.jsonc +[matrix]: ./reports/conformance-matrix.md +[operations]: ./OPERATIONS.md +[prose-lint]: ./scripts/prose_lint.py +[readme-structure]: ./spec/readme-structure.md +[repo-gate]: ./scripts/repo_gate.py +[reports]: ./reports/ +[repos]: ./registry/repos.json +[scripts]: ./scripts/README.md +[secrets]: ./spec/secrets.json +[section-model]: ./spec/section-model.md +[standup]: ./STANDUP.md +[workflows]: ./catalog/snippets/workflows/ +[write-guard]: ./host-setup/agent-safety/gh-write-guard.py From 6f9b9a953d66395595dc2693991629db8f98e4ac Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 11:26:24 -0700 Subject: [PATCH 5/5] Settle the NxWitness wrapper question by measurement The disposition said NxWitness may keep a thin repo-specific wrapper for its hardcoded secret checks. Measured rather than left open: the fork asserts four secret names, and spec/audit.py checks the spec/secrets.json baseline plus that repo's registry requiredSecrets, which is a strict superset of the four and additionally covers CODECOV_TOKEN that the fork misses. Re-vendoring loses no coverage and gains some, so no wrapper is owed. The disposition also now records what actually blocks these repos, which the sweep found the hard way. Five of the six carry ruleset-develop.json and ruleset-main.json against the canonical's develop.json and main.json, so each needs the payload migration before it can take the script at all, and a re-vendor without it leaves apply and check aborting on payloads that do not exist. NxWitness is the fifth, missed at first because its configure.sh was never in its pull request so its payload names went unchecked. Report regenerated so it matches its source. Co-Authored-By: Claude Opus 5 (1M context) --- reports/divergences.md | 2 +- spec/divergences.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reports/divergences.md b/reports/divergences.md index b57274ad..dfa0d43b 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -7,7 +7,7 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur ### re-vendor - **.markdownlint-cli2.jsonc** - AudioCleaner, aiopurpleair _(recorded PhotoCleaner now resolved)_ - Verbatim config held as a hand-modified copy rather than a past hub revision. Restore the current canonical. -- **repo-config/configure.sh** - ESPHome-NonRoot, LanguageTags, NxWitness, VSCode-Server-DotNetCore, aiopurpleair, homeassistant-purpleair - The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness may keep a thin repo-specific wrapper for its hardcoded Docker Hub secret checks, which the repo-agnostic canonical leaves as a manual-verify note. +- **repo-config/configure.sh** - ESPHome-NonRoot, LanguageTags, NxWitness, VSCode-Server-DotNetCore, aiopurpleair, homeassistant-purpleair - The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness needs no wrapper for its hardcoded secret checks, settled by measurement: its fork asserts four secret names, while spec/audit.py checks the spec/secrets.json baseline plus the registry requiredSecrets for that repo, which is a strict superset of the four and additionally covers CODECOV_TOKEN that the fork misses. Re-vendoring therefore loses no coverage and gains some. Five of the six carry ruleset-develop.json and ruleset-main.json against the canonical's develop.json and main.json, so each needs the payload migration before it can take the script at all, and a re-vendor without it leaves apply and check aborting on payloads that do not exist. ### investigate diff --git a/spec/divergences.json b/spec/divergences.json index 680e5fcd..67ba8fef 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -4,7 +4,7 @@ "dispositions": [ { "path": ".editorconfig-checker.json", "repos": ["HomeAutomation-Config", "HolidayLights"], "disposition": "accepted", "reason": "Both carry a legitimate repo-specific Exclude list (HomeAutomation-Config excludes a Vantage/ subtree, HolidayLights excludes .fseq sequence files). The uniform Disable block is carried intent-equivalent. Exclude is inherently repo-local, which is why the unit is intent, not verbatim.", "tracking": null }, { "path": ".markdownlint-cli2.jsonc", "repos": ["aiopurpleair", "PhotoCleaner", "AudioCleaner"], "disposition": "re-vendor", "reason": "Verbatim config held as a hand-modified copy rather than a past hub revision. Restore the current canonical.", "tracking": null }, - { "path": "repo-config/configure.sh", "repos": ["NxWitness", "aiopurpleair", "homeassistant-purpleair", "ESPHome-NonRoot", "VSCode-Server-DotNetCore", "LanguageTags"], "disposition": "re-vendor", "reason": "The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness may keep a thin repo-specific wrapper for its hardcoded Docker Hub secret checks, which the repo-agnostic canonical leaves as a manual-verify note.", "tracking": null } + { "path": "repo-config/configure.sh", "repos": ["NxWitness", "aiopurpleair", "homeassistant-purpleair", "ESPHome-NonRoot", "VSCode-Server-DotNetCore", "LanguageTags"], "disposition": "re-vendor", "reason": "The hub adopted the fleet check mode into the configure.sh canonical (apply plus a payload-driven check). These carry an older design that predates it (five an older check-mode fork with ruleset-*.json filenames and no registry, LanguageTags an older apply-only copy). Re-vendor the new canonical to converge them. The stale copies re-vendor mechanically. NxWitness needs no wrapper for its hardcoded secret checks, settled by measurement: its fork asserts four secret names, while spec/audit.py checks the spec/secrets.json baseline plus the registry requiredSecrets for that repo, which is a strict superset of the four and additionally covers CODECOV_TOKEN that the fork misses. Re-vendoring therefore loses no coverage and gains some. Five of the six carry ruleset-develop.json and ruleset-main.json against the canonical's develop.json and main.json, so each needs the payload migration before it can take the script at all, and a re-vendor without it leaves apply and check aborting on payloads that do not exist.", "tracking": null } ], "gaps": [ { "path": "LICENSE", "disposition": "accepted", "reason": "Each repo owns its license file. The hub does not standardize license text, so it is intentionally outside the manifest.", "tracking": null },