Conversation
Closes #456. Closes #558. Blog has been running a Hugo build with an rsync-over-SSH deploy to a host we own, and nothing governed it. `WORKFLOW.md`'s `Output Seam by Destination` had no row for a filesystem on our own host, `registry/repos.schema.json`'s `target` enum had no member for it, and Blog stood cataloged as `source-only` behind two interim driftNotes recording the deferral. ## What lands **The `hugo` type** (`spec/project-types.json`), nine checks. Only `hugo.build.strict` names the generator, where a generator-specific flag *is* the letter. The rest are phrased generator-agnostically, so promoting them to a shared type when a second generator arrives is a registry edit rather than a rewrite. `spec/type-model.md` grows a **Generators** section stating that rule and why there is no `static-site` to `hugo` hierarchy at one member. **The `self-hosted` target and the `deploy-ssh` mechanism.** What a repo builds and where the result lands stay separate axes, so an SMB or S3 publisher later is a new *mechanism*, not a new type. **D4.6 and D5.6.** D4.6 requires the deploy to assert *which release* and *which environment* answered, polling to a bounded timeout, rather than trusting the transport's exit status. D5.6 requires the prune of a durable destination to be asserted on the host that was written to. **A reference leaf pair** in `catalog/snippets/workflows/`, plus the 5A addendum, scenarios S12/S13, and a section 6 walkthrough. **Blog reclassified** to `["hugo", "source-only"]` with both publish targets. ## Three questions a reviewer will ask **Does D4.3 need extending?** No. It enumerates how a repo reaches the tag-only release shape, and this destination adds no fourth route: the deploy is a separate dispatch that touches no release, and Blog reaches tag-only through the source-only route already listed. **Why are `requires` and `stores` empty on `deploy-ssh`?** Not for want of credentials. They are per-environment GitHub Environment secrets, which neither `validate.py` nor `audit.py` can enumerate. `validate.py:200` would force any `requires` name into the repo's `requiredSecrets`, and `audit.py:615` unions that into the **actions** store expectation, so listing them guarantees a false DEFECT on a correctly configured repo. The new optional `environments` block records the names as operator documentation and says plainly that it is not a gate, so a clean audit is not evidence an environment is configured. Extending the `stores` enum instead was measured and rejected: `audit.py:606` seeds `required_by_store` with two keys and `:610` indexes it unguarded, so an unknown store raises `KeyError` for every repo whose `publish[]` maps to that mechanism. Follow-up folded into `TODO.md`'s locally-required-secrets entry, since it is the same missing axis. **Why is the leaf concrete rather than parameterized?** An eleven-input generalized transport leaf was proposed and rejected. The other twelve leaves take `ref`/`branch`/`smoke` and nothing else; reuse is by vendored copy, so a copier edits a `run:` line for free; the hub never executes these files, so eleven parameters would be untested surface presented as canonical; and naming it for the transport is the mistake the type name avoids. The two outputs were kept, since Blog currently has none and no caller can record what shipped. ## Two checks Blog fails today Recorded as driftNotes naming their check id, so the next audit retires them mechanically. | Check | What is wrong | Filed | | --- | --- | --- | | `hugo.vendored.provenance` | The vendored theme is 125 tracked files with no `.gitmodules`, no recorded upstream ref, and no Dependabot ecosystem covering it. | ptr727/Blog#28 | | `hugo.generator.pinned` | The generator version and checksum are pinned in two workflows with nothing asserting they agree. | ptr727/Blog#29 | Both are fixed in ptr727/Blog#30. ## A third check was wrong, and the second commit fixes it `hugo.deploy.retention` and D5.6 originally required the deploy to prune the destination and assert the count on the host. **Blog cannot, and should not be made to.** Its deploy credential is a forced `rsync` command confined write-only, so the server never acts as sender and the key can neither delete a release nor read the destination back to count one. Blog's own ownership table already assigns release-prune timers to the host, which is the correct resolution rather than a gap. As first written the check was unsatisfiable for exactly the repos that confine their credentials properly, and the only way to pass it was to widen a deliberately narrow key. That trades a real confinement boundary for a green check, so the check was wrong rather than the design. Retention is now bounded by a **declared count with one side recorded as owning the prune**: the deploy asserts it where its credential can observe the destination, the host owns it where the credential cannot. What the guarantee still rejects is a prune against a local scratch tree, a best-effort prune, and neither side owning it, since each then assumes the other prunes. The reference leaf keeps the assert-in-pipeline shape and says when to delete the step. Blog passes the corrected version, so its retention driftNote is dropped and ptr727/Blog#27 is closed as invalid. Worth noting as evidence for the type: a check written from the hub's side alone, against a real repo, was wrong on first contact in a way only the repo could reveal. ## The record was wrong, and is corrected rather than deleted `TODO.md`'s intake entry predicted three things that are wrong against what Blog actually runs, and writing the type from the prediction would have encoded requirements the repo does not meet: | Predicted | Measured | | --- | --- | | theme as a Dependabot-tracked submodule | vendored, no upstream ref recorded | | generator at `latest`, not pinned | pinned by version **and** SHA256 | | tag cut last, after the live check | deploy is a separate dispatch; the release is untouched | ## Drive-by fixes - `spec/scope-model.md`'s project-type token table was missing `cpp`, pre-existing. - `STANDUP.md`'s new-type procedure never mentioned the registry `target` enum, which is exactly what the first repo declaring a new destination fails `validate.py` on. It also now warns that a leaf must not be named `build-*-task.yml`, since `source-only.detect` is literally that string. ## Verification | Gate | Result | | --- | --- | | `spec/validate.py` | 22 cataloged, 0 backlog, clean | | `spec/audit.py --selftest` | PASS | | `scripts/repo_gate.py` | eol 0, sha-pin 0 | | `scripts/prose_lint.py --diff develop` | clean (the 522-violation tree backlog is #519's, untouched) | | markdownlint-cli2 | 0 issues in 44 files | | actionlint | clean | | editorconfig-checker | only untracked `.artifacts/` and `Tests/obj/` build output | Selector resolution verified directly: Blog resolves to `{hugo, source-only, release, dispatch-only, pull}`, the deploy leaf is selected, `build-release-task.yml` is correctly **not** selected, the `release` develop payload is selected over the operational one, and `self-hosted` routes to `deploy-ssh` with an empty `requires` so no secret finding is manufactured. ## Not in this PR - The three Blog-side fixes, filed and fixed downstream. - A fresh `reports/blog/audit.md`; the current one predates the deploy and its staleness is noted in the conformance matrix. - Real environment-scoped store support in `audit.py`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GOVERNANCE.md` "Communicating with the User" governed the *form* of a question (a numbered list) and said nothing about *where* it goes. So a decision only the maintainer can make could satisfy the section while sitting in the last paragraph of a summary, which is where it dies: a summary reads as a report of finished work, the one line still waiting on the user is the easiest in it to skim past, and the item ends up owned by neither side while each believes the other has it. ## What lands **One bullet in "Communicating with the User".** Work blocked on the user is raised as a **direct interactive prompt with selectable options**, at the point the work stops, never as prose in a summary. The blocked item is the message rather than a closing remark on a message about something else. It **narrows rather than replaces** the numbered-list bullet above it: the list stays the form wherever the interface offers no prompt mechanism, so an agent on a plain-text surface is still covered. **A second bullet-level clause: the options offered are the actions themselves.** The first draft required the prompt and left its options unspecified, which a prompt offering only ways to wait satisfies while clearing nothing. The option that unblocks the work now names the action it authorizes, so selecting it is the go-ahead, and where the agent may not perform that action itself the option says who does. **A third change, to the clickable-link bullet above them: the form follows the surface.** That rule named one syntax, and a markdown link is clickable only where markdown renders. An interactive prompt links neither the markdown nor a bare URL, so following the rule as written puts an unclickable string in front of the reader, which is the outcome it exists to prevent. Its ban on a bare `#123` is now scoped to a surface that renders markdown, and where none does, the reference is the bare `#123` and the clickable link goes in the accompanying message. Found by using the new prompt rule and watching the link fail in it. **The closing-keyword rule moves from "Release Model" to "Branching Model", unchanged.** That rule was queued for restatement in this PR, and it was already law: closing keywords go on the `develop -> main` promotion PR, because GitHub fires them only on a merge into the default branch, with a hand-close citing the squash SHA as the fallback. Nothing is added for it, since a second statement of a rule is the failure mode rather than the fix. What was wrong was its address. It turns on develop-versus-default-branch mechanics that "Branching Model" states and "Release Model" does not, and an agent writing a feature PR body reaches for the branch section, so it now follows the promotion-execution traps beside the other `develop -> main` mechanics. The move commit carries the line byte-identical, so it reads as a move rather than as a rewrite. A later commit then formats its branch names as code, matching the section it now sits in, which is a change to the rule's text and is deliberately kept out of the move commit. ## For the reviewer Both sections are `verbatim` fidelity in `spec/files.json`, so every fleet repo carries the older copies until re-vendored, and an audit run against a downstream repo before that reports both sections as divergent. A tree-wide grep for the moved rule's distinctive phrasing (`issue-closing`, `closing keyword`, `Fixes #N`) finds no other site restating or cross-referencing it, so no anchor or citation goes stale with the move. ## Verification `prose_lint` (both CI invocations), `repo_gate.py`, `spec/validate.py`, `test_prose_lint.py`, markdownlint-cli2, and editorconfig-checker all clean after each commit. The added line carries the file's CRLF endings. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Fixes the false clean reported in #562: `scripts/pr_review.py` printed `suppressed=0` over a review body carrying `### Suppressed comments (2)`, so the gate that decides whether a review round is answered reported a clean round over findings no thread will ever surface. ## Cause The reviewer moved the section from its own `<details>` wrapper to a markdown heading nested inside the `Review details` wrapper, and `suppressed_blocks()` missed it twice over: - The primary path matched the heading against the wrapper's `<summary>`, which now reads `Review details`. - The fallback that exists for exactly this case scanned `DETAILS.sub('', body)`, which deletes every `<details>` block, including the one the heading now sits in. ## Fix Each region (every wrapper's contents, plus what is left outside them all) is scanned line by line for the heading, so the section is found as its own wrapper's `<summary>`, as a markdown heading nested inside another wrapper, or bare in the body. Neither old shape is retargeted away, since both appear across the rounds of a single pull request. A block now starts at its own heading, so `finding_count()` reads the heading's own `(N)` rather than the wrapper's, which would have floored two findings to one. ## Verification `python3 -m unittest discover -s scripts` passes (247 tests), with four cases added for the nested shape: that it reports at all, that the count is the heading's own, that both shapes report together in one run, and that the neighbouring file-summary wrapper is still not read as a finding. Replayed over the reviewer's own bodies on the last seventeen pull requests of this repository, the new parse recovers **6 findings across 5 rounds** that previously read as clean (on #546 and #561), and changes no count that was already right. The runbook section in `.github/copilot-instructions.md` gains the same fact, since its rule to match more than one *phrasing* did not say the section also *moves*, and a filter reading a wrapper's `<summary>` reports zero on the nested shape. Related: #562 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers the live half of #565, the six suppressed Copilot findings the digest defect fixed in #562 hid. A suppressed finding reaches no thread, so the digest was the only place it could have surfaced, and it reported `suppressed=0` for five rounds across two pull requests. ## The two prose defects Both are in `TODO.md`, both raised on #546 round 4, both still present on `develop`. | Entry | Was | Now | | --- | --- | --- | | downstream surface | `does not fail loudly, it quietly audits itself` | two sentences | | reading a local clone | `answers instead what that clone last saw` | `answers a different question, which is what that clone last saw` | ## The capitalization convention The reviewer asked for the proper noun on one bullet of #561, and that bullet is now internally consistent in lowercase. So the live question is not the line but the file-wide mix: `GOVERNANCE.md` alone carried **17 lowercase against 7 capitalized**, and the same mix is vendored fleet-wide, which is why settling it per file settles it nowhere. **`CODESTYLE.md` "Markdown and Spelling" gains item 5.** Prose capitalizes the format's name. Lowercase is for the strings a machine reads and nothing else: a tool or package name (`markdownlint`, `yzhang.markdown-all-in-one`), a settings key (`markdown.extension.toc.levels`), a heading anchor (`#markdown-and-spelling`), an identifier in code, and a file extension. What it settles is the mix rather than either spelling, since a file carrying both gives the next author no default and a reviewer a finding to raise on whichever one it wrote last. It lands in `CODESTYLE.md` because every repo carries that file, so the convention arrives with it rather than being re-decided per repo. **The sweep corrected 40 occurrences across 18 files** in documents, code comments, docstrings, and two `spec/audit.py` output strings. No identifier changed: the pattern skips a word adjacent to `.`, `-`, `_`, `[`, `#`, or a paren, so `heading_texts(markdown)` and `markdown.splitlines()` were left alone, and the result was read line by line before applying. ## For the reviewer Four `verbatim` regions changed, so every downstream repo is byte-mismatched until re-vendored: - Three `GOVERNANCE.md` sections by one word each, "Documentation Style Conventions", "Communicating with the User", and "Repository Details". - Two comment lines in `.markdownlint-cli2.jsonc`, which is `verbatim` and `whole`, so a config nothing else changed about now reports as drifted. `CODESTYLE.md` is `intent`, so its new item reaches the fleet as a rule each repo adopts in its own copy rather than as bytes to match. `TODO.md`'s re-vendor entry records all of this and now names **five files rather than three**, so the next sweep does not have to rediscover it from a diff that reads cosmetic. The three findings #565 records as already resolved were re-verified against the tree and need no change here. ## Verification | Gate | Result | | --- | --- | | `prose_lint.py` gating rules (charset, dupword, spelling) | clean tree-wide | | `test_prose_lint.py`, `test_repo_gate.py`, `test_pr_review.py` | pass (157 prose tests) | | `spec/audit.py --selftest` | PASS | | `scripts/repo_gate.py` | eol 0, sha-pin 0 | | `spec/validate.py` | 22 cataloged, 0 backlog, clean | | markdownlint-cli2 | 0 issues in 44 files | | cspell | 0 issues | | editorconfig-checker | clean | Every changed file kept its own line endings, which was checked after a first attempt flattened CRLF to LF and was reverted before anything was committed. `prose_lint.py --diff` reports **five warn-only findings** (four `comment-wrap`, one `dash`) on the three lines this diff touched in `spec/audit.py`, `spec/validate.py`, and `spec/fidelity-model.md`. All five predate this change and belong to the #519 backlog, where those files carry 101 and 8 violations respectively. Correcting the one line of each that this diff touches would leave the file no more conformant while burying a one-word sweep in rewrapped comments. ## Not in this PR - A `prose_lint` rule for the convention. The legitimate lowercase uses are the whole difficulty, so a checker is a design question rather than a follow-through, and stating the rule is what #565 asked for. - The fleet re-vendor, which is `TODO.md`'s entry and needs the maintainer to name the repos. Related: #565 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ate (#568) Resolves the audit-tooling half of #563, filed from `ptr727/Blog` after its re-integration. All four of that issue's asks, plus a defect in one of the fixes it proposes. ## The gate that suppressed the check `spec/audit.py` wrapped the whole freshness check in `if not findings:`. The rationale was sound in isolation, in that a clean repo has no outstanding work for a pending-marker note to describe, but the consequence is that **any** repo carrying one finding it cannot clear has its entire `driftNotes` list exempted. Blog carries exactly that, the `carried: AGENTS.md references the template repo` finding tracked in #552, so its notes were never checked however clean the rest of the audit ran. The repo with open findings is where a stale note is most likely, which is the inverse of what the gate produced. Both shapes are now evaluated on every run, worded by context rather than suppressed: | Note | Audit clean | Findings open | | --- | --- | --- | | prose marker (`pending`, `still`, ...) | contradicted outright, as before | raised as which of the open findings it means | | names a check id | surfaced for a hand decision | surfaced for a hand decision | Measured live, this is one added advisory across the fleet. Of 53 notes on 22 repos, one carries a marker (MediaTools, `pending fleet-wide ratification`), and it now reads `while 25 finding(s) are open - confirm it describes one of them rather than closed work`. Under the old gate it was silent. ## The check-id matcher, and why it is not anchored #563 proposes `\(([a-z]+\.[a-z.]+)\)$`. That pattern matches **neither of the two notes it was written for**, because both end the sentence after the paren: > ... so it cannot be moved or diffed against upstream (hugo.vendored.provenance)**.** Run over the whole registry it matches zero notes on zero repos, which is indistinguishable from a fleet carrying no such note. That is the silent-narrowing shape `GOVERNANCE.md` "Verification Discipline" names: a pattern that matches less still exits zero. `CHECK_ID_RE` is therefore unanchored, and the self-test covers the trailing-period case, the mid-sentence case, an id absent from the catalog, an id whose type the repo does not declare, and a parenthesized version string as the false positive the shape has to exclude. **Watched failing**: re-anchoring the pattern to `$` turns five cases red and the suite reports `SELFTEST FAIL`. ## What the audit will not claim Resolving the id is as far as a tool can honestly go here, because `spec/audit.py` never reads `spec/project-types.json` at all. So the audit checks that the id exists and that the repo declares its type, then hands the check itself to the auditor. The finding clears when the note is deleted, which is the retirement path #560 promised, made visible rather than left to a matcher that could never fire. That same fact is now stated where an agent reads a run rather than only in the issue: `AUDIT.md` section 4, the console line printed for a clean repo, the `--issue` body preamble, and `OPERATIONS.md`. Adding a check to `project-types.json` changes what an auditor must judge and changes no tool's output, and silence from a tool that was never looking reads exactly like a pass. ## Blog's two notes are dropped Both deviations closed in ptr727/Blog#30 and are on Blog's ground-truth `main` (`2b132e4`), verified by reading that branch rather than trusting the issue: - `hugo.vendored.provenance` - `themes/README.md` records the upstream repository, commit `154d006e`, its upstream date, `git describe`, the license location, and both local edits. - `hugo.generator.pinned` - the version and SHA256 are declared once at `.github/actions/install-hugo/action.yml:26-27`, and `validate-task.yml:76` and `deploy-site-task.yml:79` both consume that composite action. ## Verification | Gate | Result | | --- | --- | | `spec/audit.py --selftest` | PASS, and FAIL on the anchored matcher | | `spec/validate.py` | 22 cataloged, 0 backlog, clean | | `spec/audit.py Blog MediaTools` (live) | behaves as described above | | markdownlint-cli2 | 44 files, 0 issues | | cspell (README, HISTORY) | 0 issues | | editorconfig-checker | clean | | `scripts/prose_lint.py` | no new violations on any touched file, net -2 | `Refs` rather than `Closes`, since a closing keyword cannot fire from a `develop`-targeted pull request. #563 is closed by hand with evidence once this merges. Filed by an agent in `ptr727/Blog`, resolved here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…haped (#569) Resolves #567, raised from ptr727/Blog#33 after the deploy transport retest. Documentation only, no behavior change. ## The problem the contract created The deploy contract guarantees a ref gate: a production deploy is refused unless the dispatched ref is the default branch. **A gate is shown to fail closed only by tripping it**, so the verification is one `workflow_dispatch` of the production environment from a non-default branch, expecting the run to fail at the gate job. The agent that owns the pipeline is refused that dispatch by its harness, and #567 records that conversational authorization did not lift it: the maintainer authorized the call explicitly, in session, and the identical call was refused again. The staging dispatch was allowed and the same text inside an `echo` was allowed, so the refusal is a judgment about the action rather than the subcommand or the ref. That judgment is defensible and this change does not ask for it to be relaxed. What the contract produced was the worst arrangement of it. The check is required, the agent cannot perform it, and nothing said so. The two predictable outcomes are an agent that drops the check and reports the deploy verified, and an agent that re-expresses the refused call as a raw `gh api -X POST .../dispatches`, which is a mutation fired as a workaround. ## What changes **`WORKFLOW.md` section 5C** gains the ref gate as a live probe, named as the maintainer's step, with the evidence to capture. The evidence is the point: the gate job's conclusion, its error text naming the expected and the received ref, every downstream job recorded as **skipped** rather than passed, and the deployment count against the production environment unchanged. A gate that fails open and a gate nobody tripped produce the same empty run history, so "we have never seen it fail" is not evidence about the one control standing between a mis-dispatch and the live site. The agent prepares the command and reads all four back afterwards; it does not fire it. The same split covers a probe acting on the deploy host directly, an outbound SSH exercising a forced command among them, which #567 records failing the same way. **`GOVERNANCE.md` "Repository Boundaries and Write Safety"** gains the rule the re-shaping needs, and it is the natural home: that section already bans a write fired as a probe and a write consuming a fabricated id, and this is the third way a write goes somewhere nobody approved. It states that a harness refusal and the maintainer's permission are different things, that the second does not lift the first, that a refused write is never re-attempted through a different API surface or tool or rephrasing, and that writing oneself a permission rule is self-authorization whatever was said. Two routes remain and both are the maintainer's. The closing clause is the general form of #567's own complaint: where a required verification can only be performed by a call the agent is refused, the document requiring it says so and names who runs it, since a check that is mandatory and unperformable is dropped silently and then reported as done. Grepped `GOVERNANCE.md`, `AGENTS.md`, and `.github/copilot-instructions.md` for both claims before adding either. Neither was stated anywhere, so this duplicates nothing and contradicts nothing. **`TODO.md`** adds the section to the fleet re-vendor entry. "Repository Boundaries and Write Safety" is `verbatim` fidelity in `spec/files.json`, so every downstream repo now carries an older copy and an audit before the sweep reports it as stale. It joins the existing list rather than opening a new item. ## Verification | Gate | Result | | --- | --- | | markdownlint-cli2 | 44 files, 0 issues | | cspell (README, HISTORY) | 0 issues | | editorconfig-checker | clean | | `scripts/prose_lint.py` | 0 violations on all three files, before and after | | `spec/validate.py` | clean | `Refs` rather than `Closes`, since a closing keyword cannot fire from a `develop`-targeted pull request. #567 is closed by hand with evidence once this merges. Filed by an agent in `ptr727/Blog`, resolved here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…571) The 2026-08-03 report graded Blog as source-only against a hub that had no static-site type, and recorded that it was due a re-run once the deploy existed. This is that run, replacing the file in whole per AUDIT.md section 8. Run stamp `audit run 2026-08-05T21:57:38Z | hub 01507a0`, read at Blog `main@2b132e4`. Verdict operational, no defect. All nine `hugo.*` checks are judged for the first time and pass, each cited to file:line. The three deviations the first run recorded are closed: ptr727/Blog#27 (the remote release tree was never pruned), #28 (the vendored theme recorded no upstream ref), and #29 (the generator pin was duplicated across two workflows). Two drift classes stay open, both the hub having advanced past what the repo carries: seven stale verbatim units and 17 prose findings. One needs sequencing, since the `Closes #N` rule moved out of Release Model into Branching Model in #563, so a re-vendor taking the new Release Model without the new Branching Model drops the rule rather than leaving it stale. The run also surfaced a hub defect. `spec/audit.py` flags a carried file naming the hub anywhere in it, with no exemption for the byte-locked AGENTS.md "Fleet Bootstrap" section, whose first sentence must name the hub because saying where the canonical rules live is that section's function. The finding is unclearable by construction and fires on any repo carrying the current canonical, so it spreads with the carry. Recorded as escalation 1 with the fix; not applied here, since it changes the runner rather than a report. The matrix takes the run's date and findings. Ten clause-joining semicolons and three spaced hyphens elsewhere in the file are fixed in the same pass. The 17 remaining `dash` findings are deliberately left: every one is a `| - |` empty-table-cell placeholder, the notation the file's own header documents, so they are a prose_lint.py exemption gap rather than divergent content, systemic across 46 cells in 8 report files. Copilot review closed over two rounds, second covering the head. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #570. ## The defect `scripts/prose_lint.py` judged the semicolon exemption over the whole span, which for a Markdown bullet is the entire list item: ```python listish = span.count(';') > 1 or ':' in span.split(';')[0] if listish and ',' in span: continue ``` A colon anywhere before the first semicolon therefore marked the whole bullet a list, and every semicolon later in it was exempt however plainly it joined two independent clauses. The colon and the semicolon did not have to be near each other, or related at all. Measured over the 32 Markdown files in the gate's scope, the exemption was covering **62 spans holding 120 semicolons across 9 files** while the rule reported **0** of them, reproducing the issue's figures exactly. That is the shape `GOVERNANCE.md` "Verification Discipline" names: a pattern that silently matches less still exits zero. ## The fix **The sentence is the unit**, because that is where a list lives. `sentences()` splits a span at its sentence boundaries and each one is judged on its own, so a genuine enumeration keeps its separators while an independent clause later in the same bullet is still examined. The boundary reuses the run-on rule's guards, so an initial, an abbreviation, and a dotted identifier end nothing. It also accepts a terminator that closes inside emphasis or a bracket: reading a bare `. ` left `.**` and `.)` joining a bullet's every sentence back into one span, which would have left the defect in place for exactly the bullets it appears in. **The colon arm was measured before being kept.** The issue asked whether a colon should confer the exemption at all. Dropping it reported 14 further lines, and reading them, they are genuine colon-introduced lists whose items carry commas, which is the standard use `GOVERNANCE.md` names, for example `Match the heading style: title case with short bind words (a, an, the, of); hyphenated compounds capitalize both parts.` So the arm is scoped rather than removed. **Both spellings of the bullet-opener colon are now dropped.** `LABEL_COLON` matched `**Label**:` only, so `**Label:**` still announced a list it never announced. One line tree-wide was affected, and it is a splice. ## Verdict diff, old against new on the same tree | | before | after | | --- | --- | --- | | `semicolon` findings, whole tree | 20 | 64 | | Previously reported, now silent | | **0** | | Every other rule | | byte-identical | The 44 newly reported sit in 33 lines across 7 files. Per `GOVERNANCE.md`, existing prose is corrected as each file is next edited rather than swept, and the issue itself asks that the exemption be fixed and proven before anything acts on the work list, so no prose is changed here. The measured list goes to #519. ## Tests Six cases added to `TestSemicolon2`, each one proving a decision rather than restating the code: - a colon in an earlier sentence no longer exempts a later splice, the issue's own example - a sentence closing inside emphasis or a bracket still ends, so `.**` and `.)` split - a series in one sentence does not exempt the next - a colon-introduced list whose items carry commas stays exempt, which is why the arm was kept - `- **D3:**` and `- **D3**:` are one construct - an abbreviation does not end a sentence, since splitting at `e.g.` would cut a list in half All 253 script tests pass. `markdownlint`, `editorconfig-checker`, `repo_gate.py`, `spec/validate.py` and the diff-scoped prose gate are clean, and `ruff` reports nothing new against the pre-change baseline. ## Two corrections found while auditing the surfaces that describe this gate - `OPERATIONS.md` claimed `DEFAULT_RULES` omits `comment-wrap` and `comment-case`, which it has not since they were added. The real difference is the exit code, not the coverage. - The `TODO.md` backlog figure read 534 against a measured 520 before this change, so it is refreshed to 559 with a note that a gate fix moves it as readily as a prose fix does. ## Fleet effect `.github/actions/prose-gate` reads `prose_lint.py` from hub `develop` for every non-`main` run, so once this merges, fleet PRs gate on the corrected rule against their changed lines immediately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by the Blog re-audit in #571 and recorded there as escalation 1. ## The contradiction The rule bans a reference to the template repo in any carried file, because the coordination flow is machinery a consumer should not see. `AGENTS.md` "Fleet Bootstrap" is byte-locked fleet-wide and its first sentence names the hub by path, because saying where the canonical rules live is that section's entire function. It is what an agent reads in a repository whose carried copies are stale, partial, or absent, which is precisely when nothing else present can say it. The two rules therefore contradicted each other. A repository carrying the current canonical was flagged for holding a section the hub requires it to hold, byte for byte, and the only way to clear the finding was to edit that section and fail the verbatim check instead. **The finding was unclearable by construction, and it was going to spread rather than stay a curiosity**, since every repository picks it up as the carry propagates. Two repositories had reached it already. ## Three surfaces stated the rule, so three change | Surface | Change | | --- | --- | | `spec/audit.py` | A new `template_ref_outside_verbatim` helper excises a file's verbatim sections before looking for the name | | `GOVERNANCE.md` "Documentation Style Conventions" | Carries the exception, its reason, and its boundary | | `recurring.norepoxref` in `spec/project-types.json` | The same, since that is the check an auditor judges by hand | A tool fix that left the written rule contradicting it would have fixed nothing, which is why all three move together. The exemption is **derived from the declared verbatim list rather than hardcoded to one heading**, so a future verbatim section that must name the hub is covered without a second fix. **The exception stops at the region boundary.** A repository naming the destination in prose it owns is still flagged, which is the point. A reference that reaches a verbatim section is a defect in the canonical, fixed once at the source rather than reported against every repository carrying it. ## Verification **Six offline cases in `--selftest`**, covering the exempt and non-exempt positions, a document with nothing declared verbatim, a re-cased heading, and a CRLF document. The last matters because `extract_section` normalizes EOLs while carried files are CRLF on this fleet, so excision has to survive that. **Old versus new predicate across all 21 non-hub cataloged repositories.** It clears exactly two findings, both `AGENTS.md`, on the two repositories carrying the current Fleet Bootstrap, and keeps the other eleven: ```text HomeAutomation-Config AGENTS.md True False <== CLEARED Blog AGENTS.md True False <== CLEARED Utilities .github/copilot-instructions.md True True kept NxWitness AGENTS.md True True kept ... (11 kept) ``` Live runs agree. Blog loses the false positive with its seven genuine verbatim drifts unchanged, and Utilities keeps its genuine one. ## Incidental The scan moved into the file loop, so it reads content already fetched for the section checks instead of re-fetching all three files per repository, and reads the same selector-resolved verbatim list those checks were judged against. The conformance matrix note claiming the check has no exemption is updated in the same pass rather than left to contradict the code. The Blog report is deliberately **not** edited: it is a point-in-time run snapshot, and the run-stamp discipline in AUDIT.md section 8 says a later run supersedes the file rather than an edit revising it. Note that `GOVERNANCE.md` "Documentation Style Conventions" is verbatim fleet law, so every downstream repository's copy goes stale on merge and is due a re-vendor. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Promotes the accumulated develop changes to main, updating the hub's governance/spec manifests and audit/review tooling so fleet repositories can be audited and reviewed correctly (notably: suppressed Copilot findings parsing, static-site deploy modeling, and driftNote/audit gating behavior).
Changes:
- Extend the workflow/spec model to cover Hugo/static-site deploys to a self-hosted filesystem target (new target enum + deploy mechanism + interface workflow snippets + new
hugotype checks). - Fix and harden review/audit tooling: correctly detect suppressed Copilot findings even when nested/moved; always evaluate
driftNotes; and exempt verbatim regions from the template-reference scan. - Settle/standardize “Markdown” capitalization across docs, comments, and tool output strings.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| WORKFLOW.md | Documents the self-hosted filesystem deploy seam, deploy verification (D4.6), retention ownership (D5.6), scenarios (S12/S13), and probe guidance. |
| STANDUP.md | Clarifies onboarding steps for new types/targets (including schema enum updates) and reinforces OPERATIONS.md/leaf naming constraints. |
| spec/validate.py | Updates validation error strings to use “Markdown” consistently. |
| spec/type-model.md | Adds “Generators” guidance for generator-vs-transport separation (supports hugo type design). |
| spec/section-model.md | Aligns prose with “Markdown” capitalization and inline-link exception wording. |
| spec/secrets.schema.json | Adds an environments documentation block shape to the secrets schema. |
| spec/secrets.json | Adds deploy-ssh mechanism and maps self-hosted target to it; expands note to explain env-scoped secret limitations. |
| spec/scope-model.md | Updates the project-type token table (adds cpp, hugo). |
| spec/readme-structure.md | Updates prose to “Markdown” capitalization in readme-shape spec. |
| spec/project-types.json | Introduces the hugo type with deploy/retention/verification expectations and updates cross-cutting “no repo xref” rule text. |
| spec/files.json | Adds interface expectations for .github/workflows/deploy-site*.yml for hugo repos. |
| spec/fidelity-model.md | Updates fidelity model prose to “Markdown” capitalization. |
| spec/fidelity_honesty.py | Updates report-rendering docstring to “Markdown” capitalization. |
| spec/audit.py | Fixes driftNote evaluation gating; adds check-id driftNote resolution; moves template-ref scan into the file loop with verbatim-section excision and new selftests; clarifies audit output scope. |
| scripts/test_prose_lint.py | Adds regression coverage for sentence-scoped semicolon exemption and label-colon variants; updates “Markdown” capitalization in docstrings/comments. |
| scripts/test_pr_review.py | Adds fixtures/tests for suppressed findings nested inside “Review details” wrapper and combined-shape handling. |
| scripts/README.md | Updates the prose-lint documentation to reflect sentence-scoped semicolon exemptions and label-colon handling. |
| scripts/prose_lint.py | Scopes semicolon list exemptions to sentences (not whole bullets), improves label-colon stripping, and adds sentence splitting helper. |
| scripts/pr_review.py | Fixes suppressed-findings extraction to handle moved/nested headings and count extraction from the section heading itself. |
| reports/conformance-matrix.md | Updates shape definitions and adds hugo + self-hosted target coverage notes. |
| reports/blog/audit.md | Replaces the Blog audit report with the re-audit against the new hugo type and deploy model. |
| registry/repos.schema.json | Extends the target enum to include self-hosted. |
| registry/repos.json | Reclassifies Blog as hugo + source-only and adds self-hosted publish target; updates driftNotes accordingly. |
| OPERATIONS.md | Clarifies gate invocation differences and audit-run interpretation (deterministic subset vs hand-judged checks). |
| GOVERNANCE.md | Adds harness-refusal/non-reshaping write-safety rule; moves issue-closing keyword rule; adds coordination-reference exception for verbatim regions; scopes “clickable link” rule to Markdown-rendering surfaces; adds interactive prompt requirement. |
| CODESTYLE.md | Establishes “Markdown” capitalization convention and applies it in the general rules. |
| catalog/snippets/workflows/README.md | Registers the new deploy-site workflow snippets in the catalog index. |
| catalog/snippets/workflows/deploy-site.yml | Adds the dispatch entry-point workflow for environment-scoped deploys with early ref gating and shared validation. |
| catalog/snippets/workflows/deploy-site-task.yml | Adds the reusable deploy task workflow (generator pinning, env assertion, rsync deploy, optional prune step, live verification). |
| AUDIT.md | Clarifies what spec/audit.py does/does not evaluate, adds hugo detection guidance, and expands driftNote reconciliation rules. |
| .markdownlint-cli2.jsonc | Updates comments to use “Markdown” capitalization. |
| .github/copilot-instructions.md | Updates runbook guidance to note suppressed-findings sections can move/nest and must be matched by heading location, not wrapper summary alone. |
ptr727
added a commit
that referenced
this pull request
Aug 6, 2026
…e onboarding-feedback backlog (#582) Promotes the two commits `develop` has carried since the previous promotion (#574) to `main`. Merge commit only, no squash, and `develop` is not deleted. ## What lands - **#575** Require an explicit `--repo` on `scripts/pr_review.py`, since the old default silently digested a pull request in the hub when a run in another repository omitted it, and lead the digest summary with the repository it read so a misdirected run is visible in its own output. - **#576** Fold the two-day onboarding-feedback pass into `TODO.md`: ten new entries, three amendments that each supply the second example their entry was reasoning from one instance without, and two recurring cases traced to underspecified wording in rules that already exist. ## Issues closed None. Neither constituent pull request carried a closing keyword, and the four issues #576 filed (#577, #578, #579, #580) record work that is still open rather than work this promotion completes. ## Verification `git merge-tree --write-tree origin/main origin/develop` reports no conflict, so this promotion needs no throwaway resolution branch, and the tree diff against `main` is exactly the five files the two commits touch. Both constituent pull requests merged green with their review loops closed.
This was referenced Aug 8, 2026
ptr727
added a commit
that referenced
this pull request
Aug 8, 2026
) Closes #623. **Disposition** `Amends "Find out which file a partial round skips, and why re-requesting never clears it."` in the "A Programmatic Reading of a Copilot Review" cluster. #623 was filed after this repository's last `TODO.md` edit and appeared nowhere in the file, so selection step 1 was false until this change. ## What was wrong The `COVERAGE_IS_PARTIAL` caveat, `scripts/README.md` and the runbook all said the reviewer names no file list in these rounds, so which file went unread cannot be read from the API. Measured over 348 Copilot review bodies on the newest 120 pull requests here and 121 on Blog, that is false. 91 bodies carry a `| File | Description |` table, and every table row in the corpus belongs to one of those tables. ## What the measurement says the table is worth The issue proposed reading a table that names every changed file as evidence that the count is a reporting artifact. It cannot carry that, and the corpus is what says so. | round | states | names | reading | | --- | --- | --- | --- | | #476 | 10 of 11 | all 11 | table contradicts the count | | #592 | 2 of 3 | all 3 | table contradicts the count | | #479 | 16 of 17 | 16, omitting `GOVERNANCE.md` | table corroborates the count and names the file | | #609 | 61 of 62 | 50 of 62 | short by 12 where the count is short by 1 | | #574 | 33 of 33, full | 32, omitting `TODO.md` | a short table on a fully covered round | | #606 | 7 of 7 | names `GOVENANCE.md` | a path no diff carries | On Blog every table names exactly the changed set, on all seven partial rounds and on every full round alike. A reading identical under both outcomes discriminates neither, so a full table is reported as corroborating nothing rather than as a miscount. #574 kills the converse, since a short table sits on a round that read everything. ## What ships - `Q_FULL` selects the pull request's own `files`, windowed at 100 with `hasNextPage` carried, since a path outside a short window reads exactly like a path the reviewer left out and the record holds a pull request of 301 changed files. - `file_table` reads the table, quotations dropped for the reason the coverage line's are. - `head_table` takes it from any round covering the current head and never from a round before a push. Three of the four partials here carry their table on the pre-push round, describing a diff that push replaced, and comparing that against the current changed files would name a file unreviewed on a stale list. Thirteen commits here carry more than one round, and on one of them a round with a table sits beside a round without, so which of the two the verdict reads must not decide whether a table is found. - `table_against_diff` prints one sentence and decides nothing. It names an omitted file only where the table is short by exactly what the counts leave unread **and** names nothing outside the diff, that second guard existing because #606's typo would otherwise push the real `GOVERNANCE.md` into the omissions and report it as the file nobody reviewed. - Exit `42` is unchanged in every arm, and the caveat keeps its maintainer-decision framing, which is the issue's third suggestion. ## Verified against live data #479 the reviewer's own file table omits exactly the 1 file the counts leave unread, naming GOVERNANCE.md Blog #60 the reviewer's own file table names all 6 changed files, ... so it corroborates nothing #476/#592/#609 no round covering this head carries a file table ## Gates 228 `test_pr_review` cases, plus `test_prose_lint`, `test_repo_gate`, `spec/audit.py --selftest`, `spec/validate.py`, both `prose_lint.py` invocations, `markdownlint-cli2` and `editorconfig-checker`, all green locally. Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes the nine commits
develophas carried since the previous promotion (#555) tomain. Merge commit only, no squash, anddevelopis not deleted.What lands
scripts/pr_review.pystops reporting a clean round over findings that reach no thread.CODESTYLE.md, and fix the twoTODO.mdprose defects the digest defect had hidden.driftNoteon every run rather than only on an otherwise clean audit, and state what the audit does not evaluate.ptr727/Blogagainst the hugo type and bump the conformance matrix.Issues closed
Fixes #562. Closes #565.
The closing keywords sit on this promotion rather than on the feature pull requests, because GitHub fires them only on a merge into the default branch. #456, #558, #563, #567, and #570 are already closed against their merged fixes.
Verification
git merge-treereports no conflict betweenorigin/mainandorigin/develop, so this promotion needs no throwaway resolution branch. Every constituent pull request merged green with its review loop closed.