Conversation
…631) The README opened straight into prose written for an agent, so a human arriving from GitHub had no path to what they actually ask: how to use this repo, how to deploy the host rules, how to diverge from one, and where to file an issue. It now forks by reader, and it carries the write-grant documentation that `TODO.md` recorded as ready in #629 without doing. ## The README, routed by reader A **Getting Started** table above the Table of Contents states the human/agent split outright, an agent starting at `AGENTS.md` and a human starting at the table, then routes six reader shapes to one destination each. Three new sections fill the gaps, placed in the slots [`spec/readme-structure.md`](./spec/readme-structure.md) already orders them in so the redesign does not fight the hub's own spec: - **Using This Repo** (Installation slot), covering the host guardrails, carrying the rules into a repository, and an honest note that the registry does not transfer, so adopting outside this fleet means running your own hub. - **Diverging From a Rule** (Configuration slot), which separates the two kinds of exception that get confused: a repository diverging from a carried unit, where the fidelity level usually already grants the freedom and the rest is recorded in `spec/divergences.json`, and a write the host guard denies. - **Questions or Issues**, previously absent entirely, saying what an issue needs and that a downstream agent filing here is the normal path rather than an exception. ## The cross-owner write grant The guard denies a `gh` write whose explicit target sits under another owner and names `GH_WRITE_GUARD_ALLOW` in the denial, and no document said how to give one. [`docs/host-setup.md`](./docs/host-setup.md) now carries it where the reader already is when the denial arrives: - The **fork asymmetry** that raises it, where `origin` is your fork and only the half of the work aimed at `upstream` denies. - The channel that works, an `env` block in the checkout's `.claude/settings.local.json`, which scopes the grant to that checkout rather than the host, and the **session restart** that loads it. - The value's exact shape: one string and never a JSON array, split on any run of whitespace or commas, with `owner/*` granting a whole owner and a token carrying no `/` silently granting nothing. Verified against `_granted_targets` and `_target_permitted`. - The **two forms that fail silently**, an inline `VAR=x gh ...` prefix and an `export` in a shell call, neither of which reaches a hook running as its own process. That is the property making a grant a deliberate act outside the session rather than something an agent does for itself once blocked. - A **confirmation that does not involve making the write**. Worth recording: feeding the hook a synthetic payload is not a usable probe from inside a session, because the payload text carries the very write shape the guard matches, so the guard denies the probe command itself. Observed twice while writing this. `host-setup/agent-safety/README.md` carries the mechanics beside the hook description and links across. ## Also here - The **license shield** moves into the closing License section, matching the form PlexCleaner carries. [`spec/readme-structure.md`](./spec/readme-structure.md) still states the old placement under Build and Distribution, so this README is deliberately ahead of the spec, and reconciling the two is the `decision`-state **README Structure Rework** cluster's edit rather than a loose end here. That cluster's `Settled` bullet asserting the old placement as fact is rewritten to record it. - **Proxmox** drops out of every supported-platform list, since it is Linux. The two remaining mentions name the actual Proxmox host in the rollout, which is correct. - `untriaged` joins the cspell word list, since the new Diverging section names the literal value the divergence report renders. - `TODO.md` loses the shipped cluster and its orphaned link definitions, and the stale "Refresh the README" chore becomes the open question this change leaves behind: whether these human entry points belong in the spec so fleet repos are measured on them. The per-section index alternative is recorded as declined, with the reason. ## Verification markdownlint, cspell, editorconfig-checker, `spec/validate.py`, and `scripts/prose_lint.py --diff develop` all clean locally. CRLF preserved in every Markdown file, LF preserved in `install.sh`. Every relative link target and every in-file anchor resolves, checked mechanically. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README spec was advisory: `readme.sections` sat at verdict `intent`, so nothing mechanically checked section presence, order, shields, or link naming. It is now five `letter` checks driven by declared data. Two wanted changes land with it. The License section closes the file and carries the license shield, which 15 of the 16 repos with a License section already did. The first line under the H1 is the tagline, and it alone carries the 100-character link-free rule and the mirrors, so a README may say more about itself above the fold. Settled alongside them from a survey of all 22 non-archived non-fork repos: a Table of Contents in every README, `Use Cases` renamed to `Overview` with no aliases, `3rd Party Tools` required, and `Questions or Issues` required in a public repo. Reference links gain a closed five-group set, a suffix per kind, and canonical names for the destinations every repo has. `spec/third-party-tools.json` declares 40 shared tools so a tool two repos use is linked and described identically in both. Every rule was measured against the fleet before it shipped, and the corpus run corrected the model three times before the pull request opened: shields keyed on alt text flagged three repos for caption wording, a coverage class would have told 7 repos to add a badge nobody chose, and requiring both Docker version shields is wrong for NxWitness, which carries 40 across six images and four channels that are not `develop`. This repo's own README reports zero findings. The fleet backlog the checks report is 214 findings across 22 repos, recorded in `TODO.md`, and no downstream repo is edited here. The Copilot loop ran 14 rounds for 21 findings, all accepted, of which 18 were in suppressed blocks and 6 were defects introduced by earlier fixes on this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`repo_gate.py --check eol` compares .gitattributes with .editorconfig and never compares either against the repository, so both documents can agree perfectly and both be wrong. ptr727/Blog carried two defects in that gap while the gate reported clean: an extensionless shebang script systemd runs unattended, matched by no pin, and two pins naming paths never tracked there. The dead pins are what hid the live one, since a pin that binds nothing still reads as coverage. `eol-coverage` adds the filesystem-facing direction as a separate check, so `--check eol` keeps meaning exactly what it means today. A pin block marked `forward-declared` is exempt from the dead reading, because in a carried baseline a pattern matching nothing is a declaration for whichever consumer adds the file, and the mark travels with the copy. Closes#633.
There was a problem hiding this comment.
Pull request overview
Promotes the develop changeset that reworks the README (including tagline/mirror semantics and audited README structure) and adds a new repository gate for line-ending coverage, then carries the corresponding spec/tooling updates into main.
Changes:
- Updates the README spec and audit tooling to treat the first post-H1 line as the canonical tagline, and adds auditable README structure/link/shield/tool-catalog rules.
- Introduces a new
repo_gate.py --check eol-coveragethat validates.gitattributesEOL pins against the tracked tree (shebang LF enforcement + dead-pin detection withforward-declaredexemptions). - Updates documentation and governance text to align with the new README/tagline and EOL-coverage behaviors.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| TODO.md | Updates backlog entries to reflect the shipped tagline work and the new downstream-observer idea, plus link ref maintenance. |
| spec/validate.py | Adds shape checks for spec/readme-sections.json and spec/third-party-tools.json to avoid audit crashes and silent passes. |
| spec/third-party-tools.schema.json | Adds JSON schema for the shared 3rd party tool catalog. |
| spec/third-party-tools.json | Introduces the shared 3rd party tool catalog (standardized links/descriptions). |
| spec/readme-structure.md | Updates README prose spec (tagline semantics, required sections, link groups, shields, tools catalog rules). |
| spec/readme-sections.schema.json | Adds JSON schema for the declared README section/link/shield model. |
| spec/readme-sections.json | Adds the declared README model consumed by the audit (sections, link groups/naming, shield classes, canonical links). |
| spec/project-types.json | Promotes README checks from intent to letter and adds new README-related checks (sections/shields/links/tools/tagline). |
| spec/audit.py | Implements README structure/shield/link/tool checks and tagline-based mirroring behavior. |
| scripts/test_repo_gate.py | Expands tests to cover eol-coverage, forward-declared pins, and gitattributes-matching correctness. |
| scripts/repo_gate.py | Adds eol-coverage gate (tree-facing EOL pin validation) and supporting helpers. |
| scripts/README.md | Documents the new eol-coverage check and its rationale/constraints. |
| README.md | Reworks the README structure (Getting Started routing, new sections, canonical link groups, license shield placement). |
| host-setup/agent-safety/README.md | Documents cross-owner write grants (GH_WRITE_GUARD_ALLOW) and removes Proxmox mentions. |
| host-setup/agent-safety/install.sh | Updates platform comment (removes Proxmox mention). |
| GOVERNANCE.md | Updates “Repository Details” to define description/tagline semantics and Docker Hub propagation path. |
| docs/host-setup.md | Adds a detailed “Granting a Write the Guard Denies” section and removes Proxmox mention. |
| cspell.json | Adds “untriaged” to the dictionary. |
| CODESTYLE.md | Updates HISTORY.md mirroring rule to mirror the tagline (not the full intro paragraph). |
| AUDIT.md | Updates readme-structure dimension description to reflect the declared model + new checks. |
| .gitattributes | Marks uv.lock/Dockerfile pins as forward-declared to support the new dead-pin coverage check. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
…cense Column (#636) The 3rd Party Tools check required both outer pipes on every table row, which GitHub's Markdown makes optional, so a row written without either was skipped outright and its link, description and ordering all went unread. A table written that way scored clean rather than unread. spec/project-types.json asserted "The section carries no license column" at verdict letter, and nothing read it. The table header is now read for that column, once per table rather than once per row, and it is the only extra column named, since spec/readme-structure.md forbids that one and no other. table_cells requires a separating pipe rather than a leading one, which is what keeps a thematic break out: `---` alone carries no pipe, where treating it as a one-cell delimiter row would have read the paragraph above it as a table header. Measured against all 22 cataloged READMEs, in both directions: 56 findings become 57, the addition being Blog's License column, no verdict is lost, and the leading-pipe widening moves nothing. Neither outer-pipe form appears in the fleet today, so both fixes close a silent blind spot rather than a live miss. Raised by Copilot on promotion pull request #635.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
spec/audit.py:796
readme_shield_findingsclaims retired badge services are reported “wherever it sits”, but the implementation only scans reference definitions (defs.items()). If a repo uses the retired badge as an inline image (), it won’t appear indefsand will be missed even though the URL is rendered in the README.
findings = []
defs = {m.group(1): m.group(2) for m in _LINK_DEF.finditer(unfenced_text(text))}
# A retired badge service is scanned across the whole document rather than per section, since a dead badge is wrong wherever it sits.
# It renders broken rather than absent, which a visitor reads as a failing build rather than as a stale badge.
for dep in model.get("deprecatedShields", []):
for ref, url in sorted(defs.items()):
if dep["match"] in url:
findings.append(("LETTER", f"readme: `[{ref}]` renders {dep['label']}, which is retired - {dep['reason']} (spec/readme-structure.md)"))
readme_shield_findings scanned reference definitions alone for a retired badge service, so a repo writing it as an inline image rendered the dead badge and reported nothing. The comment above the loop claimed it was scanned wherever it sits, and the selftest case carried that name while testing the reference form only, so the claim and its evidence agreed with each other and not with the code. shield_endpoints already resolves both forms and its docstring records the same lesson; this was the one site that had not taken it. Three review rounds refined it. The finding text claimed every matching definition renders the badge, which contradicted the comment directly above it, so the wording now follows which of four shapes it is. Attribution moved from the URL to the reference name, because the same endpoint rendered inline leaves a definition unused and reading the URL credited that render to a reference nothing uses. unfenced_text is computed once, so the definitions and the rendered images cannot be read from two views of the document, and the endpoint set is held as a set named by its namespace. Measured against all 22 cataloged READMEs at every round: shield findings stay at 7 and no verdict or wording moves, since no repo carries either shape. Every new selftest case was A/B tested against the pre-fix function rather than reasoned about. Raised by Copilot as a suppressed finding on promotion pull request #635.
ptr727
commented
Aug 9, 2026
Closing the record on the suppressed finding raised here on
Accepted and fixed on
Measured against all 22 cataloged READMEs at every round: shield findings stay at 7 and no verdict and no wording moves, because all three repos carrying the badge write it as a rendered reference definition. Every new selftest case was A/B tested against the pre-fix function rather than reasoned about. Both findings this pull request produced share one shape, which is why the description now states them together: a claim wider than the code behind it, where a comment, a finding message and a selftest name all agreed with each other and none agreed with the behavior. Neither was introduced by this promotion; both were in |
The host contract checked presence, and both host defects this fleet has hit are version facts on a tool that is installed, answers --version, and looks healthy. spec/host-tools.json declares the floors as data and each records the defect it encodes, so most entries carry none deliberately: a floor nobody can justify becomes a host failure nobody can act on. scripts/host_gate.py reads it and replaces the presence-only line in the docs/host-setup.md verification block. Two floors. A distribution gh in the 2.45.x / 2.46.x range is named by the GitHub CLI maintainers as broken by deprecated GitHub APIs. A git-restore-mtime before 2025.08 calls git whatchanged, which current git refuses, so it restores nothing, prints its ordinary statistics and exits 0, and a deploy keyed on mtimes then ships a full copy and reports success. Note the direction of that second one: a newer git is the trigger rather than the remedy. A repository layers its own host-tools.json over the hub's, so a repo needing ffmpeg, or needing a tool the fleet calls optional, declares that where it is true. Layering is tighten-only, since lowering a floor from inside the repository a floor protects retires the check, and a rejected relaxation is reported rather than dropped. The premise was verified during review. The maintainer installed gh 2.97.0 from the official repository and git-restore-mtime 2025.08, the gate went clean, and both gh limitations OPERATIONS.md carried as permanent behavior were re-tested and are gone. That file and .github/copilot-instructions.md are corrected, the latter naming the version range rather than this host since it reaches the fleet. Neither symptom was visible to a presence check, which is the argument for the whole change. Thirteen review rounds, every finding the same shape: a guard whose stated scope is wider than what it reads.
Uh oh!
There was an error while loading. Please reload this page.
Closes#633.
Promotion of
developat2082547, six squashes since the last one. Merge commit only, no squash and no rebase, and no--delete-branch, since this pull request's head isdevelopitself.What is being promoted
9b73d1a(#631)GH_WRITE_GUARD_ALLOWgrant indocs/host-setup.mdwhere a denied cross-owner write puts the reader.c2ce145(#632)readme.sectionsfromintenttoletterand adds four checks beside it, backed by the newspec/readme-sections.jsonandspec/third-party-tools.json. Settles the tagline rule, which is #577's first half, and closes the License section over the file.0bb43c5(#634)repo_gate.py --check eol-coverage, reading the line-ending pins against the tree rather than only against.editorconfig, in the two directions #633 measured: a shebang file git does not resolve toeol=lf, and a pin matching no tracked file.1e2a4ba(#636)spec/project-types.jsonalready asserted at verdictletterwithout anything reading it.042c853(#637)deprecatedShieldsscan, which read reference definitions alone, and the finding's own wording then mis-attributed a render to an unused reference.2082547(#638)scripts/host_gate.pyandspec/host-tools.json, turning the host contract's presence check into a version gate. Retires twoghworkarounds that were artifacts of a stale distribution package, re-tested on an upgraded host rather than inferred.27 files at 2906 insertions against 101 deletions, of which
spec/andscripts/are 2597, so the prose half of the diff is small.The closing keyword
Closes #633is on this pull request rather than on #634, because GitHub fires a closing keyword only when it merges into the default branch, and #634 merged intodevelop. Merging this closes that issue. The work itself landed in0bb43c5.Issue #577 stays open deliberately. Its tagline half shipped in
c2ce145, andTODO.mdnow reuses the issue for what remains, the README-to-About-panel hop that nothing writes.The two findings this pull request produced
Both are defects in
develop, not in the promotion, and both were fixed ondeveloprather than here, since this pull request's head isdevelop. They share one shape, which is the reason to state them together: a claim wider than the code behind it, where the comment, the finding text and the selftest name all agreed with each other and none agreed with the behavior.third_party_tool_findingsrequired a trailing pipe on every row, so a valid Markdown row without one was skipped and its link, description and ordering all went unread. A table written that way reported zero findings, which scores as clean rather than as unread. Round 2 found the symmetric case on the leading pipe. Also makes honest an assertspec/project-types.jsonalready carried at verdictletter, that the section carries no License column, which nothing read.readme_shield_findingsread reference definitions alone while its comment said the badge is caught "wherever it sits" and its selftest case carried that name while testing one form. An inline badge rendered and reported nothing. Three further rounds corrected the finding's own wording, moved attribution from the URL to the reference name, and settled the four shapes a retired badge can take.Measured across all 22 cataloged READMEs at every round, in both directions: tool findings go 56 to 57, the addition being Blog's License column; shield findings stay at 7 with no verdict and no wording moving. No fleet README carries either blind spot today, so both fixes close a silent gap rather than correct a live report. Stating that plainly is the point: the 17 repos still owing a
3rd Party Toolssection have not written their tables yet.What this owes downstream
Three carried files changed, and each reaches the fleet differently:
GOVERNANCE.md"Repository Details" isverbatim, so every downstream copy is byte-mismatched and the audit reports it as stale. It propagates a rule rather than refreshing a hash: the About description is the tagline alone rather than the paragraph it opens, and Docker Hub receives it from the About panel rather than from the README, so a panel left diverged is carried onward instead of corrected at publish time.CODESTYLE.mditem 4 isintentandwhole, so no audit reports it. A repo on the old copy hasHISTORY.mdmirror the whole intro paragraph, which is wrong for any README that takes the room the tagline rule opens..gitattributesisintentandwhole, and this one couples to the new gate.eol-coverageexempts a pin block markedforward-declared, and a repo holding the pre-0bb43c5copy carries no such mark, so itsuv.lock,Dockerfileand*.Dockerfilepins read as dead the first time the check runs there against a tree with no lockfile and no Dockerfile..github/copilot-instructions.mdisintent, so nothing reports it, and it propagates a correction rather than a refresh. Itsgh pr editentry called the command broken outright; it now names the 2.45.x / 2.46.x range and says to checkgh --versionfirst. A repo left on the old copy has its agents routing around a command that works, which is the inverse of the usual staleness: the old text is not merely out of date, it is now wrong.AUDIT.mdisintentand takes one dimension description;cspell.jsontakes one word;OPERATIONS.mdanddocs/host-setup.mdare this repo's own;spec/andscripts/are hub-hosted tooling and carry nothing.None of these four are recorded in the
TODO.mdre-vendor entry yet, which still lists the batch ending at #629. That entry needs the three above added before the sweep runs, and saying so here rather than editingTODO.mdon this pull request keeps the promotion a merge of whatdevelopalready holds.The conformance backlog this opens
The
readme-structurechecks were run against all 22 cataloged repos before shipping, andTODO.mdrecords what they report: 73 section and shield findings, 57 third-party-tool findings across four repos, 55 link-naming findings with 27 more on grouping, and the retiredbyob.yarr.isbadge on three repos. No repo is edited by the hub, and each lands on its own next visit. This repo reports zero across all four checks.🤖 Generated with Claude Code