Skip to content

Promote develop to main: the prose gate now blocks on every default rule - #621

Merged
ptr727 merged 3 commits into
mainfrom
develop
Aug 8, 2026
Merged

Promote develop to main: the prose gate now blocks on every default rule#621
ptr727 merged 3 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

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.

CommitPull requestWhat it does
26af061#620Gates every default prose rule whole-tree, closing #519
039be28#622Names the one default rule that does not gate, found by this promotion's review
674a27a#624Carries the permission rules the review loop needs in the host installer

What lands on main

The prose gate now blocks. The backlog reached zero at 20916ad, so the warn-only tier that semicolon and dash were given while several hundred findings sat in the tree has expired. Both block now, and home-path joins them.

home-path is the substantive half. It had been in DEFAULT_RULES since 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 of GOVERNANCE.md "Representative Data in Agent-Authored Text". OPERATIONS.md had recorded the hole, and this closes it and drops the record.

charset-unknown stays 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.json for 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, for scripts/pr_review.py. The installer also refuses a malformed settings.json at every level it descends rather than raising a traceback partway through its edits.

What is unchanged

The prose-gate composite action runs --diff with no --check list, 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 main

validate-task.yml is workflow_call, invoked by test-pull-request.yml on pull_request to main and develop. 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 on 26af061, where Check prose step and Report unclassified characters step both 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 develop at 674a27a:

CheckResult
Gating invocation, whole tree0 violations across 0 files
charset-unknown and sentence-split, whole tree0 violations across 0 files
python3 scripts/test_prose_lint.py198 tests, OK
python3 scripts/repo_gate.pyeol 0, sha-pin 0
python3 host-setup/agent-safety/gh-write-guard.py --selftestPASS
python3 spec/validate.py22 cataloged, 0 backlog repos classify cleanly
editorconfig-checkerexit 0
Installer, fresh CLAUDE_HOMEhook registered, permission rule added

Merge with a merge commit rather than a squash, and without --delete-branch, since this pull request's head is develop.

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>
CopilotAI lite review requested due to automatic review settings August 8, 2026 14:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml to gate the whole default prose rule set (including home-path) and keep charset-unknown warn-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.md to 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
FileDescription
TODO.mdRemoves the closed #519 item and adds a new work cluster documenting review-loop resolve/hand-off gaps.
scripts/test_prose_lint.pyAdjusts test documentation wording to reflect diff-scoped reporting behavior.
scripts/README.mdUpdates prose_lint documentation to describe whole-tree gating and home-path/charset-unknown behavior.
OPERATIONS.mdUpdates local verification instructions to match current CI prose gate invocations and rationale.
.github/workflows/validate-task.ymlExpands 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.

Comment thread.github/workflows/validate-task.yml Outdated
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>
CopilotAI review requested due to automatic review settings August 8, 2026 15:50

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
CopilotAI review requested due to automatic review settings August 8, 2026 17:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ptr727