Conversation
Backlog bookkeeping for a question that is settled, and for a number that was doing two jobs. #577 asked that the tagline be stated as a distinct thing from the intro, so a README may carry a second paragraph without the 100-character cap or the mirrors reaching it. That is what shipped in `c2ce145` (#632): `spec/readme-structure.md` item 1 states that further paragraphs below the tagline are free prose read by no mirror, `CODESTYLE.md` item 4 narrows the `HISTORY.md` mirror to the tagline alone, and `spec/audit.py` extracts the first line of the intro region under a self-test that names the two-paragraph case. #577 is closed against that evidence. Grepping the number before closing it found it in a second place. "The Declared Repository Description" had given #577 to its README-to-About hop entry, which is work the issue's body never covers, so closing the issue on its own correct evidence would have left that entry pointing at a closed number. The hop now carries #639 of its own and the entry cites it. The "Recorded for the Maintainer" bullet that held #577 behind the declared description is deleted rather than rewritten, because the action it recorded is taken. Its reasoning did not survive the check either: the extraction rule it feared writing already existed for the About and Docker Hub mirrors, so narrowing the changelog mirror to match it was one line rather than a new parser, which is recorded on the closing comment so it is not re-derived. ## Verification `scripts/prose_lint.py`, `scripts/repo_gate.py` (`eol`, `eol-coverage`, `sha-pin`), `editorconfig-checker` and `markdownlint-cli2` all pass on the working tree. The file is CRLF and stayed CRLF, which `editorconfig-checker` is the gate that would have caught. Closes #577 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Maintenance, in three parts, none of which changes a rule. ## The ledger is regenerated `reports/divergences.md` is generated by a live pass over each repo's ground-truth branch, so a committed copy is only as current as its last run, which was `3f91868` on 2026-08-06. This run moves three rows: `AGENTS.md` "Fleet Bootstrap" is now divergent at Blog and HomeAutomation-Config, and `GOVERNANCE.md` "Verification Discipline" and "Workflow YAML Conventions" widen from one repo to four. The fleet re-vendor sweep reads this file as its work list, so it wanted regenerating before the sweep rather than during it. The reason `TODO.md` gave for regenerating did not survive the run, which is the more useful finding. It said the committed copy still rendered `repo-config/configure.sh` under a re-vendor disposition that the retirement decision replaced. That copy already carried `retire`, so the warning described the decision rather than the file, and a session acting on it would have gone looking for a row that was already correct. The line now states why the file goes stale at all and records what this run moved. ## Two spent claims are cleared The "Verified Complete, Awaiting Close" entry for #578 is deleted, since #578 closed on 2026-08-08. Deleting it loses nothing the fleet still needs: the re-vendor entry under "Fleet Sweeps" already names all three `GOVERNANCE.md` sections the #578 widening touches, and it keeps the `[issue-578]` reference definition in use. The README rework cluster's `State` read `backlog`, which is not one of the four values `TODO.md` defines at the top of the file, so a reader following the selection procedure had nothing to match it against. It is now `decision`, on where `## Build Artifacts` belongs, which is the only thing in that cluster a hub pull request settles. The four conformance entries above it are named as landing on downstream visits, in the sense "Fleet Sweeps" gives that phrase. ## Verification `scripts/prose_lint.py` as CI invokes it (`. --check charset --check semicolon ...`) exits 0, `scripts/repo_gate.py` reports `eol`, `eol-coverage` and `sha-pin` clean, `editorconfig-checker` exits 0, and `markdownlint-cli2` reports 0 issues over both files. Both files are CRLF and stayed CRLF. One note for a future run, since it cost a confusing minute here: pointing `prose_lint.py` at `reports/divergences.md` **by path** reports 12 `dash` violations, because naming a file explicitly bypasses the `GENERATED_TREES` exemption that holds `reports/` out of the corpus. The generator's own phrasing is not an author's, which is what that exemption is for, and the CI invocation scans `.` and is unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…642) `TODO.md` carried a chore to reconsider whether the pre-commit hook should run the doc gates now that they are diff-scoped. Measuring it first, as the entry asked, found the premise was false for this repository. ## There was no hook `git ls-files .husky` returned nothing. `.husky/` held only `_/husky.sh`, which is gitignored, and `core.hooksPath` sat in `.git/config`, which no clone carries. Every commit in this tree ran nothing. Both [`GOVERNANCE.md`](../blob/develop/GOVERNANCE.md) and [`scripts/README.md`](../blob/develop/scripts/README.md) described what "the `.husky/pre-commit` hook" runs. That was true of the fleet convention and not of this tree, which is the shape a reader has no way to catch. The cataloged snippet would not have closed it either: it runs `dotnet husky run`, and there is no .NET here. ## The measurement that answers the original question | Command | Time | | --- | --- | | `prose_lint.py .` whole-tree, as CI runs it | 2.22 s | | `prose_lint.py . --diff HEAD` | 0.13 s | | `prose_lint.py . --diff HEAD~1` | 0.30 s | | `repo_gate.py --check eol` | 0.04 s | The standing decision to keep doc linters out was made against the 2.2-second sweep. What reverses it is the diff scope rather than a change of preference, so both documents now say that rather than simply asserting the new arrangement. ## What is deliberately absent `repo_gate.py --check sha-pin` stays in CI. It measured 0.03 s here only because all 45 pins are under another owner and were read for shape alone. It resolves a same-owner pin against the GitHub API, and a hook that needs a network fails offline and on a slow link. The hook does not source `_/husky.sh`. That file is gitignored and arrives with an npm install this repository does not have, so sourcing it would break the hook in a fresh clone. The `.husky/` path is kept regardless, because the fleet's line-ending rules are written against it by name. The hook reads the working tree rather than the index, so a partially staged file is judged on all of its edits rather than only the staged ones. That is stated in the hook and in `scripts/README.md` rather than glossed, and CI's whole-tree run is the backstop that makes it affordable. ## Line-ending pins `GOVERNANCE.md` "Line Endings" makes both pins mandatory for a repository that ships an extensionless hook, `.gitattributes` for git and `.editorconfig` for the editor, since the git pin alone leaves the editor free to write a CRLF shebang and break it. Both are added. `eol-coverage` now reads 19 LF pins over 19 shebang files, up from 18 over 18. ## Verification The hook rejected its own first draft, on 13 `comment-wrap` findings across the comments I had just written, which is the exact failure the chore named. Fixed and re-run, it exits 0, and it ran on the commit in this pull request. Full CI-equivalent set is clean: `prose_lint.py` with the CI check list exits 0, `repo_gate.py` reports `eol`, `eol-coverage` and `sha-pin` clean, `editorconfig-checker` exits 0, and `markdownlint-cli2` reports 0 issues across 44 files. The hook is committed `100755`, and is LF. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates local tooling and bookkeeping to reflect recent decisions: closes/renumbers backlog references, refreshes the generated divergence ledger, and adds a repo-local pre-commit hook that runs the diff-scoped prose gate plus the repo-wide EOL consistency check (with corresponding documentation and line-ending pins).
Changes:
- Refresh backlog bookkeeping in
TODO.md, including moving the About-hop entry to #639 and recording the ruff-convergence prerequisite before adding a ruff hook gate. - Regenerate
reports/divergences.mdto reflect current verbatim drift counts across the fleet. - Add
.husky/pre-commitand updateGOVERNANCE.md+scripts/README.mdto describe the new hook behavior; pin the extensionless hook to LF in.gitattributesand.editorconfig.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| TODO.md | Backlog bookkeeping updates: renumbering/closure notes and adding a ruff-convergence chore before enabling ruff in hooks. |
| scripts/README.md | Updates script documentation to reflect that the pre-commit hook now runs two doc gates (diff-scoped prose gate + repo-wide EOL check). |
| reports/divergences.md | Regenerated divergence ledger reflecting updated drift counts/rows. |
| GOVERNANCE.md | Updates local-lint guidance to describe the hook running formatting + diff-scoped doc gates (and what remains CI-only). |
| .husky/pre-commit | Adds a repo-local pre-commit hook running prose_lint.py --diff HEAD and repo_gate.py --check eol. |
| .gitattributes | Pins .husky/pre-commit to LF to prevent CRLF shebang breakage. |
| .editorconfig | Pins .husky/pre-commit to LF at the editor layer to match .gitattributes enforcement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Answers the one finding on the promotion pull request #643, which cannot take a fix commit of its own: its head is `develop`, so the correction reaches it by landing here first. > The header comment says the hook runs doc checks "on the commit's own scope", but the script later documents (and implements) scope as the working tree diff against HEAD (not the index / staged content). Accepted. The header named the staged content where the hook evaluates the working tree diff against `HEAD`. The body of the same file already said so, and said why, so the header was the single line disagreeing both with the implementation and with the three sentences under it. That is the worse shape of the two, since a reader who stops at the header gets a wrong answer that the file itself contradicts further down. It now says each gate runs at the scope that fits it, which is what the file documents and what #642 settled: the prose gate is diff-scoped against `HEAD`, and the line-ending check is repo-wide because it reads `.gitattributes` against `.editorconfig` and takes no file list. The hook ran on this commit and exits 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y Turn On (#644) The mechanically safe slice of the ruff backlog `TODO.md` recorded, plus the host floor that decides how much of that slice is safe. > **Note on this description.** It originally described the first commit, which declined `UP017` on the ground that no python3 floor was declared. The maintainer then set the floor at 3.13, which reverses that decision, and the second commit takes the rewrite instead. The body below describes the change as it now stands. The superseded reasoning is kept in the commit history rather than here, since a description that argues for the opposite of its own diff is worse than one that is merely terse. ## The ruff slice This repository declares `[tool.ruff]` in `pyproject.toml` and `spec/project-types.json` declares `python.ruff.config`, yet no workflow runs ruff and the tree has never passed it. The full backlog is 106 findings plus a formatter that would rewrite 10449 lines across all 13 substantial Python files. This takes the auto-fixable findings only: import ordering, `re.M` spelled `re.MULTILINE`, two slice-to-`removeprefix` rewrites, an explicit f-string conversion, an unused `noqa`, a pair of extraneous parentheses, and `UP017`. The 70 findings needing judgment stay open, as does the formatter. Each fix pass exposed the next, so the set was run to convergence rather than once. `UP017` rewrote the call sites and left `timezone` imported but unused in three files, `F401` removed those imports, and that left three import blocks unsorted for `I001`. A hand-picked rule set does not settle in one pass the way the full set does. ## The python3 floor, at 3.13 `spec/host-tools.json` declared `"minimum": null` for python3, on the stated ground that no floor had been measured. That was not true of the tree: `str.removeprefix` and `str.removesuffix` are called in `spec/audit.py` and `scripts/prose_lint.py` on `develop`, which is a 3.9 requirement that had been there unmeasured and undeclared, and is exactly the gap that entry's own wording warns about. The entry now declares `3.13` with the `source` block the schema requires once a floor exists. It is a **target** floor rather than a **measured** one, and the `why` says so outright: `pyproject.toml` sets ruff to `py313` and mypy to `3.13`, so the lint and type results this repo gates on describe 3.13 and describe no other interpreter, and a run below the floor is unverified rather than known broken. The two measured requirements are recorded beside it at 3.9 and 3.11, since those are what an older interpreter actually fails on, as an `AttributeError` at the call rather than at startup. That distinction did not exist before this change, and four places stated the single-kind rule that a floor exists only where a version is known to break a documented procedure. All four now carry both kinds: the note in `spec/host-tools.json`, the `minimum` description in `spec/host-tools.schema.json`, `docs/host-setup.md` "Where a Tool Comes From, and How Old It May Be", and `scripts/README.md`. Sweeping by the term rather than by the instance is what found the last two. ## The test that failed, which is the test working `scripts/test_host_gate.py` asserted the floor set as exactly `gh` and `git-restore-mtime`, and it failed on this change. That assertion exists so adding a floor is a deliberate edit in the test rather than a silent one in the data, and it did that job. It now asserts the three-name set, and a second case asserts that a target floor identifies itself in its `why`, so a reader does not go hunting for a defect report that does not exist. ## Verification All 551 tests pass, `spec/validate.py` exits 0, `scripts/host_gate.py` exits 0 reporting `python3 3.13.5 meets the 3.13 floor`, and both the audit and write-guard self-tests report `SELFTEST PASS`. Every `.py` line ending is unchanged, checked by recording all 13 files' endings before and after and diffing the lists, since ruff rewrites in place and this repository pins endings per path. `prose_lint.py` with the CI check list exits 0, `repo_gate.py` reports `eol`, `eol-coverage` and `sha-pin` clean, `editorconfig-checker` exits 0, and `markdownlint-cli2` reports 0 issues across 44 files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/test_host_gate.py:17
scripts/test_host_gate.pymanipulatessys.pathbefore importinghost_gate, so ruff's defaultE402(import not at top of file) will flag this once ruff is enforced. Sincepyproject.tomlalready configures ruff, keep the existing pattern but add an explicit suppression so this file doesn't add new debt during ruff convergence.
sys.path.insert(0, str(Path(__file__).resolve().parent))
import host_gate
docs/host-setup.md:33
- This section says
python3carries a 3.13 target floor andscripts/host_gate.pywill enforce it as a hard minimum. The contract table above still says "Python 3", which can mislead a host on 3.12 into thinking it meets the requirement until the gate fails. Update the table row to reflect the enforced floor (3.13+).
Presence is the weaker half of this contract. Both host defects this fleet has actually hit are **version** facts on a tool that is installed, answers `--version`, and looks healthy, so the table above cannot see either one. [`spec/host-tools.json`][host-tools] carries the floors as data and records the defect each one encodes, and [`scripts/host_gate.py`][host-gate] reads it. A floor is one of two kinds and names its own kind in the `why` it carries. A **measured** floor sits above a version known to break a documented procedure, which is what both `gh` and `git-restore-mtime` carry. A **target** floor names the version the repo's own toolchain is configured for, which is what `python3` carries at 3.13, where `pyproject.toml` sets ruff and mypy to that version, so a lower interpreter is unverified rather than known broken and the entry says exactly that. Everything else carries none, deliberately: a floor nobody can justify becomes a host failure nobody can act on.
|
Answering both suppressed findings from the round on The contract table, accepted and widened
Correct, and it is not confined to the row it names. The table answers presence, and a tool below its floor answers
That is in #646, since a promotion pull request's head is the branch being promoted and cannot carry a fix of its own. The
|
…646) Answers one of the two suppressed findings on the promotion pull request #643, which cannot take a fix of its own since its head is `develop`. > This section says `python3` carries a 3.13 target floor and `scripts/host_gate.py` will enforce it as a hard minimum. The contract table above still says "Python 3", which can mislead a host on 3.12 into thinking it meets the requirement until the gate fails. Accepted, and widened to the class. The table answers presence, and a tool below its floor answers `--version` exactly like one above it, so every column reported a host as fine while `host_gate.py` failed it. That was **already** true of `gh` before this session: a host on `2.46` reads the `gh --version` row, concludes it qualifies, and then fails the floor that has been declared since it was added. Patching only the row the finding named would have left the same trap one row up, so the fix is a `Floor` column covering both. The kind is named beside the number, since the distinction decides what a failing host can do next. A **measured** floor gives them a defect to point at, and a **target** floor does not. `git-restore-mtime` carries a floor and is deliberately absent from the table, because it is optional and no procedure here needs it, which the prose below the table already explains. ## The number is now restated in two places, so a test holds them together Copying a value out of `spec/host-tools.json` into prose creates exactly the drift this column exists to prevent: the data moves, the table does not, and a host reads the stale number and stops. A test now reads the shipped `docs/host-setup.md` and asserts every **required** tool's declared floor appears in a table row. It was written before the table was correct, to confirm it fails rather than assuming it would, and the first failure it produced was a real finding rather than a bug in the test: `git-restore-mtime declares a floor and has no row in the contract table`. That is what scoped the assertion to required tools, and the reason is recorded in the test rather than left to the next reader. Confirmed it catches the case it exists for, by rewriting the table to `3.12` and running it: ```console AssertionError: False is not true : python3 declares 3.13 and no table row states it ``` ## Verification 552 tests pass, `spec/validate.py` and `scripts/host_gate.py` exit 0, `prose_lint.py` with the CI check list exits 0, `repo_gate.py` reports all three checks clean, `editorconfig-checker` exits 0, and `markdownlint-cli2` reports 0 issues across 44 files. The hook caught a wrapped comment sentence in the new test before it landed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.husky/pre-commit:37
- The hook invokes
python3directly, but docs/host-setup.md notes that on native Windows the python.org install registerspy(andpython) but notpython3, so this hook will fail for Windows contributors running Git hooks via Git Bash. Consider falling back topy -3whenpython3is not on PATH so the hook matches the documented host contract probes.
python3 scripts/prose_lint.py . --diff HEAD
# The eol check is repo-wide rather than diff-scoped, and it is here because it is already fast.
# It reads `.gitattributes` against `.editorconfig` for the whole repository and takes no file list.
# At about 0.04 seconds there is nothing to scope, so scoping it would only make it wrong.
|
Answering the suppressed finding from the round on
Accepted, and fixed before this promotion rather than after it, in #647 against This is the repository contradicting itself rather than an oversight about an untested platform. Reproduced the shape rather than reasoning about Windows from Linux, on a PATH carrying a stand-in launcher and no $ PATH="$D" sh .husky/pre-commit # fixed hook
[ok ] eol 0 issue(s)
exit=0
$ PATH="$D" sh "$D/old-pre-commit" # the hook as this promotion carries it
old-pre-commit: 32: python3: not found
exit=127Being precise about the failure, since it changes how urgent this is: exit 127 is non-zero, so the current hook blocks the commit rather than passing it falsely. It is unusable on such a host rather than silently wrong, which is the better of the two failures and still not one to promote. It is fixed on |
Answers the suppressed finding on the promotion pull request #643, which cannot take a fix of its own since its head is `develop`. > The hook invokes `python3` directly, but docs/host-setup.md notes that on native Windows the python.org install registers `py` (and `python`) but not `python3`, so this hook will fail for Windows contributors running Git hooks via Git Bash. Accepted. This is the repository contradicting itself: `docs/host-setup.md` states the hazard in its own paragraph, and `spec/host-tools.json` already encodes the answer as its probe order, `[["python3", "--version"], ["py", "-3", "--version"]]`. The hook restated a name the contract says is wrong on one supported platform. It now follows the same order rather than naming an interpreter. ## Checked on a PATH that has `py` and no `python3` Rather than reasoning about Windows from Linux, the shape was reproduced: a PATH carrying `git`, `sh` and a stand-in for the Windows launcher, and no `python3` at all. ```console $ PATH=$D command -v python3 || echo NO NO $ PATH="$D" sh .husky/pre-commit [ok ] eol 0 issue(s) exit=0 ``` The previous version on that same PATH: ```console $ PATH="$D" sh "$D/old-pre-commit" old-pre-commit: 32: python3: not found exit=127 ``` So this fixes a real break rather than hardening against a hypothetical one. Worth being precise about the failure it was: exit 127 is non-zero, so the old hook **blocked** the commit rather than passing it falsely. The defect is that it was unusable on such a host, not that it was silently wrong. ## With neither interpreter present ```console pre-commit: no python3 and no py -3 on PATH, so the doc gates did not run. pre-commit: see docs/host-setup.md 'What a Host Must Provide'. exit=1 ``` It names what did not run and points at the contract, and exits non-zero. A hook that cannot run its gates must not report a clean commit, which is the same rule `prose_lint.py` follows when it announces a dropped rule rather than going quiet. ## Verification 552 tests pass, `prose_lint.py` with the CI check list exits 0, and `repo_gate.py` reports `eol`, `eol-coverage` and `sha-pin` clean. The hook ran on this commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
spec/host-tools.json:53
- The python3 tool's
whytext says older interpreters fail with an AttributeError "at the call rather than at startup", but spec/audit.py and scripts/pr_review.py now importUTCfromdatetime, which raises ImportError at import time on Python < 3.11. Updating this wording will keep the host contract accurate about the actual failure mode.
"why": "Every script here is standard library only, so a bare interpreter is enough and no package floor exists. The floor is the toolchain target rather than a measured breakage one version below it, which is the one entry here that reads that way and says so rather than implying a defect nobody found. pyproject.toml sets ruff target-version to py313 and mypy python_version to 3.13, so what those tools report describes 3.13 and describes no other interpreter, and a run below the floor is unverified rather than known broken. Neither tool runs in CI, which gates the standard-library script tests and the prose and repo gates and nothing else, so this floor is a configuration choice rather than an enforced result, and a host failing it has no CI failure to point at. Two hard requirements are measured and both sit lower: str.removeprefix and str.removesuffix need 3.9 and are called in spec/audit.py and scripts/prose_lint.py, and datetime.UTC needs 3.11 and is used in spec/audit.py, scripts/pr_review.py and their tests. Those are what an older interpreter actually fails on, as an AttributeError at the call rather than at startup. The name rather than the version is what differs per platform, which the second probe covers.",
|
Answering the suppressed finding from the round on
Accepted. The sentence collapsed two requirements into one failure mode and was wrong for one of them. Both failures were constructed rather than described, since naming the symptom is the sentence's whole job: import-time : ImportError -> cannot import name '...' from 'datetime'
call-time : AttributeError -> 'str' object has no attribute 'removeprefix'This matters more than a wording nit because of where the text is read. It also has a common cause with the finding two rounds ago about "the lint and type results this repo gates on". Both are the same entry claiming more than was checked, in the one field whose stated purpose is to keep a floor from becoming folklore. The entry is now three corrections deep, and each correction came from reading it against the tree rather than from re-reading the sentence. The fix is on |
Answers the suppressed finding on the promotion pull request #643, which cannot take a fix of its own since its head is `develop`. > The python3 tool's `why` text says older interpreters fail with an AttributeError "at the call rather than at startup", but spec/audit.py and scripts/pr_review.py now import `UTC` from `datetime`, which raises ImportError at import time on Python < 3.11. Accepted. The sentence collapsed two requirements into one failure mode and was wrong for one of them. `str.removeprefix` is a method call and fails as the text described. `datetime.UTC` arrives through a module-level import: ```console $ grep -n "from datetime import" spec/audit.py scripts/pr_review.py scripts/test_pr_review.py spec/audit.py:33:from datetime import UTC, datetime scripts/pr_review.py:74:from datetime import UTC, datetime scripts/test_pr_review.py:20:from datetime import UTC, datetime, timedelta ``` So on an older interpreter that module raises before any of it runs. Constructed both failures rather than describing them, since the whole point of the sentence is to name the symptom a reader is looking at: ```console import-time : ImportError -> cannot import name '...' from 'datetime' call-time : AttributeError -> 'str' object has no attribute 'removeprefix' ``` The entry now names each requirement with its own mode, and says the import failure is the one a host meets first, because the script does not start at all. This matters more than a wording nit because of where the text is read. `why` is what `scripts/host_gate.py` prints to a host that just failed the floor. Telling that reader to expect an `AttributeError` partway through a run, when what they will actually see is an `ImportError` before anything runs, sends them looking for a different problem. ## Verification `spec/validate.py` exits 0, `scripts/host_gate.py` exits 0 reporting `python3 3.13.5 meets the 3.13 floor`, 552 tests pass, and `prose_lint.py` with the CI check list exits 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
GOVERNANCE.md:371
- The opening sentence says the
.husky/pre-commithook runs language formatting, but this repo's committed hook currently runs only doc gates (it explicitly documents the formatting half as absent). Consider rewording so the hook's behavior is described accurately even for repos mid-convergence (doc gates always; formatting once the tree passes/enables it).
- **The `.husky/pre-commit` hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to what the commit changes rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out, since it resolves a same-owner pin against the GitHub API and a hook that needs a network fails offline. A repo enables the hook per clone with `git config core.hooksPath .husky`, and CI remains the authoritative run either way.
Three commits, all backlog bookkeeping and local tooling. No rule changes meaning, and nothing here alters what CI gates.
#640, the tagline question. #577 asked that the tagline be a distinct thing from the intro so a README may carry a second paragraph without the cap or the mirrors reaching it. That shipped in
c2ce145(#632), so the issue is closed against it. Grepping the number before closing foundTODO.mdhad also given #577 to a different entry, the README-to-About hop, which the issue's body never covers. That hop now carries #639 of its own.#641, the divergence ledger.
reports/divergences.mdis a live pass over each repo's ground-truth branch, so a committed copy is only as current as its last run. Regenerating moved three rows:AGENTS.md"Fleet Bootstrap" is now divergent at Blog and HomeAutomation-Config, andGOVERNANCE.md"Verification Discipline" and "Workflow YAML Conventions" widened from one repo to four. The reasonTODO.mdgave for regenerating did not survive the run and is corrected. Two spent backlog claims went with it.#642, the pre-commit hook. The chore was to reconsider whether the hook should run the doc gates now that they are diff-scoped. Measuring first found this repository had no hook at all, while
GOVERNANCE.mdandscripts/README.mdboth described what one runs. There is now a committed.husky/pre-commitrunning the prose gate diff-scoped and the line-ending check repo-wide, with the.gitattributesand.editorconfigpins thatGOVERNANCE.md"Line Endings" makes mandatory for an extensionless hook.The formatting half the fleet convention names is deliberately absent, and that is the finding worth carrying: this repository declares
[tool.ruff]inpyproject.tomlandspec/project-types.jsondeclarespython.ruff.config, yet no workflow runs ruff and the tree does not pass it, at 13 of 57 files unformatted and 106 lint errors. A ruff step would have blocked every commit from the moment it landed. TheGOVERNANCE.mdbullet now states the condition rather than a tool list, that a repo adds each half once its tree passes that half, and the measurement is recorded as aTODO.mdchore.Carried content in this promotion
GOVERNANCE.md"Running the Linters Locally" changed, and that section is carriedverbatim, so every downstream copy is byte-mismatched until the next fleet visit. It rides the re-vendor entry already tracked underTODO.md"Fleet Sweeps" rather than owing a sweep of its own.🤖 Generated with Claude Code