Conversation
Closes the last piece of [#519](#519), which is now closed with its evidence quoted on the issue. ## Why now The prose backlog reached zero at `20916ad`. `semicolon` and `dash` were warn-only because a whole-tree gate on them would have failed every run while several hundred findings sat in the tree. That reason has expired, and a warn-only tier over a clean tree is how the backlog grows back. Measured on this branch before writing the change, and again after: ```text python3 scripts/prose_lint.py . --check charset --check semicolon --check dash --check dupword \ --check spelling --check comment-wrap --check comment-case --check home-path 0 violation(s) across 0 file(s) ``` ## `home-path` gated nothing anywhere, which is the substantive fix `home-path` has been in `DEFAULT_RULES` since it was written, so a bare local run has always included it. It was named by neither CI step, so the rule that catches an absolute home path naming a real account ran on a developer's machine and gated nothing in CI. That is the pattern-detectable sliver of [`GOVERNANCE.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/GOVERNANCE.md) "Representative Data in Agent-Authored Text", the section that exists because real paths carrying real names reached a public comment. [`OPERATIONS.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/OPERATIONS.md) already recorded the hole, correctly and in detail. This closes it and drops the record, rather than leaving a runbook describing a gap that no longer exists. ## What stays warn-only, and why that is not an oversight `charset-unknown` alone. A finding there names a character no tier covers, and classifying one is a fleet-law edit rather than something the change that happened to type it can fix. Blocking on it would make an un-tiered character an unmergeable change instead of a question for the maintainer. The step keeps `continue-on-error: true` and is renamed from "Report prose backlog step" to "Report unclassified characters step", since there is no backlog left for it to report. `sentence-split` remains named by no invocation and stays that way here. It is deliberately outside `DEFAULT_RULES`, so promoting it is a separate decision rather than a consequence of this one, and `OPERATIONS.md` still records it as a gap. ## The composite action is unaffected [`.github/actions/prose-gate`](https://github.com/ptr727/ProjectTemplate/blob/develop/.github/actions/prose-gate/action.yml) runs `python3 "$SCRIPT" --diff "$BASE"` with no `--check` list, so it already gates the full default set, `home-path` included, over the lines a change touches. A downstream caller sees no behavior change from this pull request. Only the hub's own whole-tree steps move. ## Documentation kept level with the change - [`OPERATIONS.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/OPERATIONS.md) carries the two CI invocations verbatim so a local run matches CI rather than exceeding it, so both lines are updated, the three-gap paragraph becomes two, and the sentence describing the second invocation as a backlog report is corrected. - [`scripts/README.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/scripts/README.md) claimed five rules gate and the rest report. It now states the tree is zero and every default rule gates but one. It also names `home-path` for the first time, including the limit that it closes a sliver of its section and nothing more, because the exposure that section exists for was name-shaped and no pattern finds a name. - `TODO.md` loses the #519 entry and its link definition. ## Three corrections the closing comment carries rather than this diff The `TODO.md` entry being removed held three wrong figures, each re-measured before the issue was closed: | Claim in the entry | Measured | | --- | --- | | Fixed by `f7a6a13` (snippets) | Not a valid object in this repository. The snippets batch is `b002fac` (#600). | | 557 across 45 to zero, 184 in snippets | 553 across 44 to zero, batches 181, 241, 90 and 41, with today's checker run at every point | | #573 accounts for 37 of the 38 carried findings | 38 of 38. Pre-#573 checker reports 0 and post-#573 reports 38 over the identical bytes at `69688ec`. | ## Verification | Check | Result | | --- | --- | | New gating invocation, whole tree | 0 violations across 0 files | | `charset-unknown`, whole tree | 0 violations across 0 files | | `python3 scripts/test_prose_lint.py` | 198 tests, OK | | `python3 scripts/repo_gate.py` | `eol` 0, `sha-pin` 0 | | `actionlint` | exit 0 | | `editorconfig-checker` | exit 0 | | `python3 spec/validate.py` | 22 cataloged, 0 backlog repos classify cleanly | The four edited files keep their declared line endings, CRLF for the three Markdown files and LF for the workflow, which `editorconfig-checker` confirms. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Promotes the develop changes that make the hub’s whole-tree prose gate block on the full default rule set (with charset-unknown remaining warn-only), and updates the corresponding documentation and tracking notes so local verification guidance matches CI behavior.
Changes:
- Update
validate-task.ymlto gate the whole default prose rule set (includinghome-path) and keepcharset-unknownwarn-only as a dedicated reporting step. - Refresh ops/docs (
OPERATIONS.md,scripts/README.md) to reflect that the prose backlog is zero and that CI now gates the default rule set whole-tree. - Update
TODO.mdto remove the closed #519 entry and add a new tracked work cluster about review-loop resolve/hand-off mechanics.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| TODO.md | Removes the closed #519 item and adds a new work cluster documenting review-loop resolve/hand-off gaps. |
| scripts/test_prose_lint.py | Adjusts test documentation wording to reflect diff-scoped reporting behavior. |
| scripts/README.md | Updates prose_lint documentation to describe whole-tree gating and home-path/charset-unknown behavior. |
| OPERATIONS.md | Updates local verification instructions to match current CI prose gate invocations and rationale. |
| .github/workflows/validate-task.yml | Expands the gating prose check list to cover default rules whole-tree, leaving charset-unknown warn-only. |
💡 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.
Fixes a defect the [#621](#621) promotion's review found. A promotion's head is `develop`, so the fix cannot land on that pull request and takes its own branch, which the promotion then carries. ## The defect The gating step's comment claimed every default prose rule gates, one line above the comment naming `charset-unknown` as the one that reports instead. Both cannot be true, and the second is. ```text # Every default prose rule is clean tree-wide, so each one gates rather than reporting a backlog. # `charset-unknown` is clean too and still reports, for the reason the step below gives. ``` ## Where the overstatement came from It is the previous fix's. Round 2 of [#620](#620) narrowed "every prose rule" to "every default prose rule", to stop the claim covering `sentence-split`, which sits outside `DEFAULT_RULES` and gates nowhere. That narrowing walked straight into `charset-unknown`, which sits inside `DEFAULT_RULES` and still does not gate. | Rule | In `DEFAULT_RULES` | Gates in CI | | --- | --- | --- | | `charset`, `semicolon`, `dash`, `dupword`, `spelling`, `comment-wrap`, `comment-case`, `home-path` | yes | yes | | `charset-unknown` | yes | no, warn-only by design | | `sentence-split` | no | no | One phrase cannot carry both exclusions. The line now states the count and lets the next line name which rule it is. ## Swept rather than patched at the reported line The same shape was in `scripts/README.md`, where one sentence said being clean is what lets CI gate them and the next sentence excepted one of them. That instance was not reported and is corrected here, since a claim written in two places drifts in two places. ## Verification | Check | Result | | --- | --- | | Gating invocation, whole tree | 0 violations across 0 files | | `python3 scripts/test_prose_lint.py` | 198 tests, OK | | `actionlint` | exit 0 | | `editorconfig-checker` | exit 0 | --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
scripts/test_prose_lint.py:1410
- Grammar: "A parse that returns too little makes a diff-scoped run silently stop reporting" is missing the auxiliary verb, which makes the docstring read ungrammatically.
The repo policy is that existing prose is corrected as each file is next edited rather than
swept, and this parse is the whole mechanism behind it. A parse that returns too little makes
a diff-scoped run silently stop reporting, and one that returns too much reports the backlog
as if the change introduced it.
The kit configured `settings.json` for the hook alone, so a permission rule added on one machine reached none of the others. The installer now carries the rules it owns, which makes re-running it the way those rules reach every host. ## What changed `install.py` gains a `MANAGED_PERMISSIONS` list, each entry a prefix and a rule, merged in the same pass that registers the hook. The settings file is still read once and written once, so the hook and the rules land together or not at all. | Step | Before | After | | --- | --- | --- | | 1 | self-test the hook | unchanged | | 2 | register the `PreToolUse` hook | unchanged | | 3 | write the `CLAUDE.md` blocks | **merge `permissions.allow`** | | 4 | | write the `CLAUDE.md` blocks | ## Why a prefix rather than a plain append A re-run drops every rule under the prefix before adding the current one, which is the same strip-then-register the hook registration already uses. A rule whose spelling changes therefore updates in place rather than accumulating beside the version it replaced, and the prefix bounds what the installer owns so a rule written by hand outside it is never touched. ## The one rule it carries today `Bash(python3 scripts/pr_review.py:*)`, for the review loop's reply and resolve. Driving that loop by hand needs a raw GraphQL mutation, which is the shape that reached a stranger's repository. `scripts/pr_review.py` queries the thread id itself, takes no argument an id fits in, and refuses to resolve a thread whose reply did not land, so allowing the script is narrower than allowing the mutation it replaces. These widen rather than restrict, so they are their own component for the same reason the `fleet-bootstrap` block is separate from the `agent-safety` one: the safety half's own text says nothing in it widens a permission. Keeping a grant in its own step preserves that, where folding it into the hook registration would not. ## Verification, run against throwaway `CLAUDE_HOME` trees | Case | Result | | --- | --- | | Fresh install, no `settings.json` at all | `added`, list holds the one rule | | Re-run, unchanged | `already current`, no duplicate rule, hook count still 1 | | One older spelling present | `updated, superseding 1` | | Two older spellings present | `updated, superseding 2`, collapsed to one rule | | Unrelated rules and other keys | `Bash(git -C * log *)`, `additionalDirectories` and `effortLevel` all preserved | | `settings.json` is not valid JSON | exits 1, names the file, writes nothing | Gates on this branch: prose gate 0 whole-tree, `gh-write-guard.py --selftest` PASS, `repo_gate.py` clean, `editorconfig-checker` exit 0, `markdownlint-cli2` 0 issues. The prose gate caught two wrapped comment sentences in the new code before the first push, which is the gate promoted to blocking earlier today doing the job it was promoted for. ## What this does not do It installs nothing anywhere. Running it on each host stays the maintainer's action, tracked by the per-machine rollout in [#365](#365). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Promotes three changes, 7 files. The second and third exist because this promotion's own review found defects in the first, and a promotion's head is
develop, so each took its own branch and its own review before this could carry it.26af061039be28674a27aWhat lands on
mainThe prose gate now blocks. The backlog reached zero at
20916ad, so the warn-only tier thatsemicolonanddashwere given while several hundred findings sat in the tree has expired. Both block now, andhome-pathjoins them.home-pathis the substantive half. It had been inDEFAULT_RULESsince it was written, so every bare local run included it, and it was named by neither CI step. The rule that catches an absolute home path naming a real account therefore ran on a developer's machine and gated nothing in CI, which is the pattern-detectable sliver ofGOVERNANCE.md"Representative Data in Agent-Authored Text".OPERATIONS.mdhad recorded the hole, and this closes it and drops the record.charset-unknownstays warn-only, deliberately. A finding there names a character no tier covers, and classifying one is a fleet-law edit rather than something the change that typed it can fix.The agent-safety installer carries permission rules. It configured
settings.jsonfor the hook alone, so a rule added on one machine reached none of the others. Each rule is declared as a prefix and a rule, ownership requires a delimiter after the prefix so a hand-written rule for a different script is never claimed, and a re-run updates a changed rule in place. The list holds one rule today, forscripts/pr_review.py. The installer also refuses a malformedsettings.jsonat every level it descends rather than raising a traceback partway through its edits.What is unchanged
The
prose-gatecomposite action runs--diffwith no--checklist, so it already gated the full default set over the lines a change touches. A downstream caller sees no behavior change from this promotion. Only the hub's own whole-tree steps move.Nothing is installed on any machine by this. Running the installer per host stays the maintainer's action, tracked by #365.
One property of the gate worth stating on
mainvalidate-task.ymlisworkflow_call, invoked bytest-pull-request.ymlonpull_requesttomainanddevelop. There is no push trigger, so a merge commit never runs it. The promoted invocation was exercised by this pull request's own CI run on26af061, whereCheck prose stepandReport unclassified characters stepboth appear and both pass.Review record
Twenty-one Copilot rounds across the three pull requests, 5 on #620, 2 on #622, and 12 on #624, plus 2 here. Several rounds reported no comments while carrying findings in a
<details>block, which is the shape a thread-polling loop reads as a pass.Worth recording rather than hidden in the count: nearly every finding was in a claim the change made about itself rather than in what the code does. A count that included the rule it was counting against, a scope claim an absolute path would not have fixed, a "read once" that read twice, and a "nothing was written" printed after the hook was already on disk.
Verification
Run on
developat674a27a:charset-unknownandsentence-split, whole treepython3 scripts/test_prose_lint.pypython3 scripts/repo_gate.pyeol0,sha-pin0python3 host-setup/agent-safety/gh-write-guard.py --selftestpython3 spec/validate.pyeditorconfig-checkerCLAUDE_HOMEMerge with a merge commit rather than a squash, and without
--delete-branch, since this pull request's head isdevelop.