Skip to content

Promote Prose Gate, Review Tooling, Unattended Worker, and Install Failure Reporting Fixes to Main - #2816

Merged
ptr727 merged 24 commits into
mainfrom
develop
Oct 11, 2026
Merged

ptr727 merged 24 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Promotes these pull requests from develop to main:

Closes #2285
Closes #2820
Closes #2818
Closes #2809
Closes #2807
Closes #1659
Closes #2796
Closes #2798
Closes #2163
Closes #1984
Closes #2614
Closes #2767
Closes #2781
Closes #2741
Closes #2775
Closes #1665
Closes #2770
Closes #2738
Closes #2758
Closes #2739

🤖 Generated with Claude Code

ptr727 and others added 16 commits October 10, 2026 21:22
…ound (#2765)

## Summary

On a pull request into a branch other than the default, a Copilot round
on the head that states no coverage, has none carried to it, and carries
no file table no longer displaces an attested local pass. `status` and
`wait` now read `review_on_head=yes coverage=local` there and exit 0,
rather than `coverage=unstated` and exit 45.

The attested reading stays narrow:

- A Copilot round on record that states or appears to state partial
coverage still blocks it, per Merge Gate item 2.
- A head round that carries its own file table still blocks it, even one
naming fewer files than the diff changed, since that table is the
round's own word that it skipped a file.
- A promotion, a truncated review history, and a refusal on the head
still block it, as before.

The help text, `scripts/README.md`, `pr-review-conduct` Merge Gate item
2, and `.github/copilot-instructions.md` now name `coverage=local`, the
field both attested readings share.

## Tests

- Table-less head round plus attestation: `status` and `wait` exit 0
with `coverage=local`.
- The same head with no attestation still exits 45.
- A partial-coverage round on record plus attestation is not covered.
- A head round whose table skips a changed file plus attestation stays
at 45.

Each new test fails against the code before its fix. The full suite
passes (`python3 -m unittest discover -s tests`).

## Local strict review

Three recorded passes ran. Every `introduced` finding was fixed, except
one declined with evidence: `wait` on an attested head beside a round
returns 0 while checks are still running. That matches what an ordinary
Copilot-covered head already does. Only the held path, where `wait`
requested nothing, polls checks. No finding is left open.

Closes on promotion: #2739

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…an State Coverage (#2768)

## Summary

`scripts/pr_review.py status` read at most 100 changed files, so a pull
request changing more, a develop -> main promotion above all, exited 45
however complete its Copilot round's file table was. The full read now
follows the `files` connection's cursor up to GitHub's documented
3000-file ceiling, so the round's own table is compared against the
whole change set.

- `follow()` is one cursor walk, shared by the new `page_files()` and
the existing `unresolved_threads()`.
- Paging runs in `full_read`, on the payload it is handed and on each
re-read, so the digest and exit code always read a paged list while
`wait`'s decision-only `Q_FULL` reads spend no page. `digest`'s own
fallback read goes through `full_read` too.
- A list reaching 3000 files stays marked cut short and is compared
against nothing, as a list past the old 100-file window was.
- A page read at a different head leaves the list unread under
`FILES_UNREAD`. `files_shortfall` reports it, so `full_read` takes the
read again on its existing schedule rather than exiting 45 on a
transient cause. A page that fails to read ends the run like any other
failed read.
- The digest, `table_against_diff`, the UNSTATED message, and
`scripts/README.md` now name the ceiling or the moved head rather than a
100-file window.

Tests construct a 160-file pull request read across two pages and reach
a coverage verdict (exit 0). Other cases cover a push between pages and
the ceiling, each exiting 45 with its own reason, a moved head being
read again, `full_read` paging a payload it was handed (the `wait`
path), and a bare `gql(Q_FULL)` read not paging. Each was shown to fail
with the code it covers reverted.

Closes on promotion: #2758

## Local review findings

Every `introduced` finding the local passes raised is fixed on this
branch. The last pass, over the current head, raised none.

Declined with evidence: capping the read at the reviewer's 300-file
limit rather than 3000. #2758's expected outcome names the REST ceiling
of 3000, and the reviewer's limit is not a contract this script
controls.

Filed along the way: #2767, a carried table is still bounded by the REST
compare endpoint's 300-file cap in `changed_at`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…in the Prose Gate (#2771)

## Summary

`LABEL_COLON` in the prose gate strips a list item's opening bold label
before the semicolon check reads the line. It kept its own marker set
(`-`, `*`, `N.`) and read only `**` emphasis, so a bullet opened with
`+` or `N)`, or a label spelled `__Label__:`, `__Label:__`, or
`***Label***:`, kept its label. That label then announced a list and
exempted the splice after it, so one bullet got two verdicts depending
on how it was spelled.

- `LIST_MARKER_TOKEN` is now the one marker set, and both `LIST_MARKER`
and `LABEL_COLON` build on it.
- `LABEL_COLON` accepts every strong-emphasis spelling, `**`/`***` and
`__`/`___`, with the colon inside or outside the emphasis, and the
closing delimiter must match the opening one.
- A new test feeds 8 marker spellings by 7 label spellings and asserts
one verdict for all 56. On the old regex 46 of them fail. A second test
pins that an unmarked or mismatched label is not stripped.

The semicolon findings across the tree's Markdown are identical before
and after the change.

## Local review

One `local-strict-review` pass, recorded. It raised no finding on this
change. It raised one pre-existing finding: `LABEL_DASH` has the same
narrower marker and emphasis set for the dash rule. That is filed as
#2770 rather than widened into this change.

Later passes raised findings this change introduced. Two were fixed in
this push: the shared token had landed between `LABEL_COLON` and its
rationale comment, and a bold-italic label with a single outer mark
(`_**Label**_:`, `*__Label__*:`) was not stripped.

### Findings still open after the edit budget

- **introduced**: `LABEL_COLON` does not strip a bold label whose inner
italic holds the colon, `**_Label:_**` or `__*Label:*__`. The
colon-outside forms `**_Label_**:` and `__*Label*__:` are stripped, so
this nesting still gets two verdicts. No tree line uses it. **Fixed in
8bf2438**, an optional inner italic mark matched on both sides. A later
pass's design finding narrowed the strong delimiter to exactly two marks
in 4a1df84, since the outer mark already spells a triple run. The pass
over 4a1df84 raised nothing.

Closes on promotion: #2738

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved prose linting to recognize emphasized labels in bulleted and
numbered lists, including varied emphasis styles and colon placement.
* Expanded detection coverage for blockquoted lists and added checks to
avoid treating unmarked or mismatched labels as bullets.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ker and Bold Spelling (#2773)

## Summary

`LABEL_DASH` in the prose gate exempts a list item's opening `-
**Label** - explanation` separator from the dash rule. It kept its own
marker set (`-`, `*`, `N.`) and read only `**` emphasis. So a bullet
opened with `+` or `N)`, or a label spelled `__Label__` or
`***Label***`, reported the separator that the `-` and `**` spelling of
the same bullet did not.

- `LIST_MARKER_TOKEN` moves above `LABEL_DASH`, and `LABEL_DASH` now
builds on it.
- The strong-emphasis alternation `LABEL_COLON` carried moves into one
shared `STRONG_LABEL_TOKEN`, and both patterns use it. A conditional
group carries whether the colon sits inside the emphasis, which is the
one thing `LABEL_COLON` needs that `LABEL_DASH` does not.
- A new test feeds 8 marker spellings by 11 label spellings and asserts
the separator is exempt and a later dash on the line still counts. On
the old regex 68 of those subtests fail. A second test pins that an
unmarked or mismatched label is not exempt.

## Verification

- 400,000 random strings give identical `LABEL_COLON.sub("")` output
before and after. The new `LABEL_DASH` matches every line the old one
matched, at the same end offset.
- The tree's `dash` and `semicolon` findings are identical before and
after the change.
- `test_prose_lint` passes in full (427 tests) from the repository root.
ruff format, ruff check, mypy, the prose gate, the eol gate, and
`spec/validate.py` pass.

## Local review

One `local-strict-review` pass, recorded. It raised no finding. The
reviewer independently re-ran the equivalence fuzz on Python 3.12, 3.13,
and 3.14.

Closes on promotion: #2770

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

The tests pinning the prose gate's `comment-added` override wiring
matched text over a whole file, so commenting out the action step's
`ALLOW_COMMENTS` env entry, or moving the workflow's `allow-comments:`
input from `Check prose step` to a sibling step's `with:`, left the
suite green while the override never reached the script.

- A small standard-library reader, `yaml_step` and `yaml_value` in
`tests/test_prose_lint.py`, reads each value under its own step by
indentation, as `spec/audit.py`'s workflow readers do, so the suite
stays off PyYAML.
- The override's wiring is now asserted structurally at each hop: the
action input's default, the step's `env.ALLOW_COMMENTS`, the prose
step's `with['allow-comments']`, the label step's `id`,
`PAYLOAD_LABELED`, `GH_TOKEN`, and `LABELS_PATH`, and the resolved-base
step's `id` and the prose step's `with.base`.
- The three copies of run-block extraction in the file now share one
`step_script(path, name)` reader that slices the named step first, so a
step added ahead of the one named can no longer be run in its place.
- One case pins the reader itself against both mutations from the issue.

## Verification

- Each mutation named in the issue, plus a flipped input default, a
replaced `base:` input, and a commented-out `GH_TOKEN`, was applied to
the tree and fails at least one test, then reverted.
- The full `tests` suite passes (2484 tests, 6 skipped), along with ruff
format and check, mypy, the prose gate diff-scoped including
`sentence-length`, and `spec/validate.py`.
- Three local strict review passes are recorded. The sibling sites in
other test files that the second and third passes found are filed as
#2775.

## Remaining local review findings

- `introduced`: the `yaml_value` docstring calls its result "the plain
scalar", while it returns the raw text after the key's colon with quotes
kept, which the input-default assertion relies on (`"'false'"`). A
caller trusting the wording gets a loud failure rather than a silent
pass. Left for the review loop after the second edit round was spent,
where Copilot raised it too, and fixed in
a0fb90c.

Closes on promotion: #1665

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

Moves the bounded step reader into `tests/workflow_steps.py` and reads
every unbounded step-script site through it, so a named step with no
`run:` fails the harness instead of running the following step's script.

- `step_script_text` looks for `run:` only at the step's own key level
and accepts `|`, `|-`, and `|+`.
- Six sites in `tests/test_release_guards.py` and `decision_script` in
`tests/test_publish_plan.py` now use it.
- `tests/test_workflow_steps.py` constructs a workflow whose named step
has no `run:` and asserts the reader fails.

Closes on promotion: #2775

Local strict review: 0 findings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…eout (#2782)

## Summary

`pr_review.py wait --min-rounds N` waits for a round past the Nth, so N
is the `rounds=` count a `status` read before a re-request. That
counting is by design: the help already said "exceed N", and the
`head_review_done` docstring and its tests state it. A caller passing
one above the count waits for the round after the one it wanted, and the
timeout printed a bare `status=PENDING` that reads exactly like no round
at all. This takes the issue's documented arm and makes the timeout say
why.

- The `--min-rounds` help states N is the count already seen, with a
worked example.
- At the timeout, where a round landing during the wait covers the head
but none past N does, the `status=PENDING` line names the round counts
before and after and the position it waits for. Otherwise the bare line
stays.
- `scripts/README.md` states what the flag counts.
- Tests: the issue's shape (three rounds on the head, a fourth landing
mid-wait, N=3 returns and N=4 times out with the line), a late
superseded-commit round keeping the bare line with and without an older
round on the head, and a reviewer that never answers keeping the bare
line.

Filed along the way: #2780 (the flag name reads as a count to reach,
options to remove the trap) and #2781 (the repo-wide quota arm outranks
a round on the head at the timeout, pre-existing).

Closes on promotion: #2741

## Local strict review

Seven passes recorded, the last raising 0 findings. The README overclaim
the fifth pass listed as `introduced` is fixed in 83f16c7, and a test
gap the sixth raised in 43d85b8. The pre-existing finding that the
repo-wide quota arm shadows the new line is filed as #2781.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…eading (#2784)

## Summary

`pr_review.py wait` decided its repo-wide quota arm, exit `47` with
`status=COPILOT_QUOTA_EXHAUSTED_REPO_WIDE`, from repository history read
when the wait began. At the timeout that arm ranked above the final read
of the pull request, so a genuine round covering the head below
`--min-rounds` still printed that the head carried no Copilot review of
its own.

- A new `standing_quota` reads the repo-wide quota signal as this pull
request's own read leaves it. It is `None` where a genuine round covers
the head. Otherwise it runs `quota_signal` over the history merged with
this pull request's own reviews, so a review of it on any head submitted
after the refusal answers that refusal, the way one anywhere in the
repository would.
- `wait` calls it on the opening snapshot, which gates the auto-request
skip and the poll skip, and on the final read, which gates the `47` arm.
A round landing during the wait that `--min-rounds` does not credit now
reads as the existing min-rounds `PENDING` line, exit `30`. A push
moving the head off the reviewed commit still reaches `47`.
- `history_entry` and `newest_first` are pulled out of
`_copilot_history_window` so the merge spells the entry and the order
the same way.
- The `wait` help text says all of this, that the auto-request skips
only a head covered past `--min-rounds`, and that `48` ranks under `46`
and above `47`.
- Tests cover the issue's constructed example on the head and on a
superseded head, a round already on the head, a push off the reviewed
head, a newer genuine round and a newer file-count refusal on a
superseded head, and an off-head round newer than the refusal at the
opening read. Each was proven by reverting the clause it targets.

## Local strict review

The local passes' findings were each fixed in a later push, and the last
pass over the current head found none. One was declined: a pass asked to
keep skipping the request where a head's round predates a quota refusal
elsewhere. The issue rules `47` out for a head carrying a genuine round,
and skipping the request and poll there leaves the wait ending at once
on a bare `30` that no later wait changes, so the request runs as usual.

Closes on promotion: #2781

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#2790)

## Summary

On a pull request that changes 300 files or more, `scripts/pr_review.py`
refused every carry, both for a coverage statement and for a file table.
The compare endpoint caps its file list at 300, so `changed_at` returned
None at both commits. `status` then exited 45 and told the reader to
re-run, which could never help.

- I checked whether paging gets past the cap, using read-only GETs on a
308-file comparison. Page 1 returned 300 files and page 2 returned none,
whatever `per_page` was set to. The compare's file list does not page.
- A tree diff does get past the cap. It reads the recursive tree of the
commit and of its merge base, and compares them by path, mode and object
id. On that same comparison it named exactly the 326 paths that `git
diff --no-renames --name-only` names.
- When both compares are at the cap, `changed_at` now returns
`Capped(merge_base)`, with the merge base taken from the compare it
already read. `carry_holds` then reads both commits' change sets through
`tree_changed_at`. Both sides are always read the same way, because the
compare lists a rename once while the tree diff lists both paths.
- When only one side is at the cap, the carry is refused as a different
set. The other side's list is complete and has fewer than 300 entries,
so the two sets cannot be equal.
- A tree that GitHub truncates (at about 100,000 entries) reads as
unread. No fleet repository comes close to that size.
- `scripts/README.md` now describes the tree-based read.

Tests: the constructed case from the issue now passes. A 400-file
promotion whose first round has a table, and whose later round has no
table on an unchanged change set, carries the table. Separate cases
cover a coverage statement carried past the cap, a change set that
moved, the cap on either side alone, a failed or truncated tree read, a
capped compare that names no merge base, and a rename. With the tree
reading disabled, 4 of the new cases fail.

The local strict review took two passes. The first raised 8 findings,
and I fixed 7 of them. The remaining one is filed as #2789:
`spec/audit.py` reads trees the same way, and it is a file this change
does not touch. The second pass raised nothing.

Closes on promotion: #2767

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Carry comparisons now handle changes that exceed GitHub’s 300-file
comparison limit by checking the affected file sets directly when both
comparisons are capped.
* Comparisons remain undecided when tree data is missing, unreadable,
truncated, or only one comparison reaches the limit.
* **Documentation**
* Clarified how carry comparisons work when both changes reach the file
limit.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…2792)

## Summary

Closes the gaps #2614 still had open in `unattended-handoff` "The
Worker". #2638 already closed the promotion-body race (gap 3) and added
the before-merge `closedAt` re-read, so this change covers the other
three.

- **A blocked-promotion read that cannot run returns `STOP`.** Before
this, a failed `decision` read counted as "not blocked" and the worker
went on to the promotion. Now a read that did not run says nothing
either way, and a `handoff.py chain` refusal counts as such a read.
- **A parked lane holds the promotion until the lane closes, whether its
decision is answered or not.** Answering a decision closes the issue and
takes the `decision` label off, so the open-`decision` read stops seeing
it before the parked lane has applied the answer. Step 3 now also reads
the open handoffs. It walks each other `auto-*` track with `handoff.py
chain`, reading every link's body and comments. Where a parking comment
names a decision issue that says it blocks the open promotion, the round
narrows to develop, whatever that issue's state. The before-merge
re-read repeats both reads. It narrows only on an open `decision` issue
or a lane hold, so #2638's `closedAt` park can still be reached.
- **Worker step 1 walks its own lane back the whole way.** It lists the
links with `chain`, raising `--limit` only while the cap is its one
notice, and reads each link with `gh issue view --json body,comments`.
Any other notice, or a nonzero exit, returns `STOP`. A lane parked and
handed back more than once keeps its earlier answer in an older link.
- **Step 5 names the maintainer-only action that parks:** a review stall
with a human or team reviewer requested, as `pr-review-conduct`
"Expected review loop" sends it. A stall still pending after the
post-clear wait, and a `REQUEST_NOT_RECORDED` wait, return `STOP`
instead, since a later round on another issue would hit the same
reviewer. This follows "Return Lines".

Generated copies were rebuilt with `scripts/build_dist.py`, and
`--check` is clean.

## Local review

Five recorded `local-strict-review` passes at the `frontier` tier.

- **Round 1 (3 findings, all fixed):**
  - The re-read counted a closed decision as a hold.
  - The lane read looked back only one link.
- A stall after a cleared request had no stated route between parking
and `STOP`.
- **Round 2 (2 findings, both fixed):**
  - The worker's own lane was still read only one link back in step 1.
  - A `chain` refusal was left undefined.
- **Round 3:** no findings.
- **Round 4, on the fix for the Copilot finding about `chain` (2
findings, both fixed):**
- Notices other than the `--limit` cap, and a nonzero exit, were not
routed to `STOP`.
- `gh issue view --comments` prints no body when piped, so links are now
read with `--json body,comments`.
- **Round 5:** no findings.

The opt-in `prose_lint.py --check sentence-length` check flags 12
sentences over 25 words in steps 1, 3 and 5. Each is class `style`, and
they are declined as preferences, since that check is not part of the
default gate and `fleet-code-review` "Review the Change" item 5 omits
preferences.

## Verification

- `scripts/prose_lint.py --diff origin/develop`: clean.
- `scripts/build_dist.py --check`: clean.
- `spec/validate.py`: OK.
- `scripts/repo_gate.py --check eol`: clean.
- markdownlint, editorconfig-checker and cspell through
`scripts/docker_lint.py`: clean.

Closes on promotion: #2614

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…2794)

## Summary

Before this change, the `unattended-handoff` loop had no path for the
orchestrator's own dispatch failing, so a transient service error ended
an unattended run that could otherwise have continued. This change
applies the maintainer's recorded answer on #1984: option 1 with option
2's report line.

- **Transient dispatch errors are retried.** On an error naming a
temporary condition, the orchestrator waits on one bounded background
timer of about ten minutes and then re-dispatches the same brief. The
orchestrator judges the condition from the error's own text, for example
an overload, a rate limit, or a check reporting a transient failure. It
spends at most three waits on one dispatch.
- **A dispatch that never ran is not counted as dispatched**, so a
retried worker brief cannot trip the repeat stop.
- **Any other failure ends the run at its own end state.** That covers a
non-transient error, a dispatch still failing after the third wait, and
a timer that cannot start. The final message names this end state on its
own line, separate from `STOP`, with the failing seat and the last
error.
- The generated distributions are regenerated with
`scripts/build_dist.py`.

## Verification

- `python3 scripts/build_dist.py --check` passes.
- `scripts/prose_lint.py --diff` passes, both alone and with `--check
sentence-length`.
- `spec/validate.py` passes, and the Docker linters pass.
- Two `local-strict-review` passes were recorded. The first raised 9
findings: 8 long sentences, and a transient test narrower than the
maintainer's answer. All 9 are fixed. The second pass raised none.

Closes on promotion: #1984

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

A `.py` file the running interpreter cannot parse was read in silence by
the gate's parser-based rules: `comment-wrap` and `comment-case` fell
back to a line scan, `comment-added` and the comment half of `issue-ref`
read no comment, the docstring half of `issue-ref` read no docstring,
and before 3.12 the tokenizer reads a 3.12 f-string as an open string
and swallows the comments after it without refusing anything. The run
then printed the same scope line a clean tree prints. Newer grammar
under an older interpreter reaches this through every local surface that
runs the gate on a host interpreter.

`check_file` now asks `ast` once, through a new cached `python_tree`,
for any `.py` a rule in `PARSED_RULES` read, and the scope line names
every file it refused:

```text
scope: 1 file(s) read, whole tree, gate local <sha>, 1 .py file(s) Python 3.11 cannot parse, so their comments and docstrings may be misread: 'newer.py'
```

The `ast` parse is the test even for a rule that only tokenizes, because
of the f-string case above. The exit code is unchanged, since a file the
repository's own interpreter parses is not a defect in the file.

A leading byte order mark is dropped before both the `tokenize` and the
`ast` read, since `ast` refuses one in a string on every interpreter and
the tokenizer reads it as code before a first-line comment. `quoted`
takes an optional cap, which replaces the five-name cut the unread-diff
refusal built by hand.

## Verification

- Tests construct files no interpreter tokenizes or parses, so they hold
under whichever interpreter runs the suite: each parser-based rule names
both, a parsing file, a run with no parser-based rule, and a non-UTF-8
file are not named, the list is cut at five, the diff-scoped line
carries the clause, and a byte order mark neither names the file nor
hides a first-line comment or a docstring reference.
- Removing the naming check, the parse gate on `PARSED_RULES`, either
byte order mark drop, or the first-line column adjustment each fails at
least one test.
- Constructed files run under Python 3.11 and 3.13: a `type Alias = int`
above a docstring naming an issue, and a 3.12 f-string above a wrapped
comment. Under 3.11 each is named and the run exits 0, under 3.13 the
finding is reported.
- Full suite (`python -m unittest discover -s tests`) passes, and the
pre-commit gate set is clean.

Filed along the way: #2796, a file whose bytes are not UTF-8 is skipped
in silence, and #2798, a byte order mark hides a first-line comment in
the languages other than Python. Both are pre-existing.

Closes on promotion: #2163

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… Language (#2800)

## Summary

A file that starts with a UTF-8 byte order mark had its first-line
comment read as trailing by the prose gate in every language other than
Python. `extracted_comments` judges a comment leading by `not
line[:at].strip()`, and U+FEFF is not whitespace, so `comment-added` and
`issue-ref`, which read leading comments only, dropped it.

The mark was already being removed per reader, at three Python sites,
and readers nobody patched kept it. A Markdown file opening with a
marked code fence, for one, had its fence state inverted for the whole
file. So `check_file` now drops the mark once, right after decoding, and
the per-reader removals go away. The character-set rule reports U+FEFF,
so it is given the mark on its own, where a fence opening line 1 cannot
hide it.

## Tests

- One case per comment syntax the gate reads, through `check_file` and
`issue-ref`: `#` in Python, shell, YAML, TOML, HCL, PowerShell, and
KiCad files, `//` and `/* */` in C-like files, `/* */` in CSS, `<!--
-->`, `<# #>`, and `;`. Each fails with the removal reverted.
- A Markdown file whose marked first line opens a fence reads the fence
correctly.
- The character-set rule still reports the mark exactly once, including
when line 1 opens a fence, and fails if that report is removed.

Closes on promotion: #2798

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved prose checks for files with a leading byte-order mark.
First-line comments and code comments are now checked more consistently,
and enabled charset rules report the mark as a line-one finding.
* Updated checks for issue references, added comments, semicolons in
Markdown code fences, and unknown charset characters.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

The prose gate read no line of a file whose bytes are not UTF-8, and
said nothing about it. `check_file` caught the decode error and returned
no findings, so every rule skipped the file while the run printed the
same scope line a tree of readable files prints.

`check_file` now collects such a file in a new `undecoded` list,
whatever rules ran, together with a file whose read fails after
discovery, since that is skipped the same way. `main` passes the list to
`scope_note`, which names the files on the scope line the way #2797
names a Python file the interpreter cannot parse, capped at five names.
The exit code is unchanged, per the issue's done criterion, so a run
that skipped a file is now distinguishable from a clean one without
failing a tree that legitimately holds one.

A whole-tree run over this repository names no file, so the change adds
nothing to the hub's own verdict.

## Tests

- `test_a_file_the_gate_cannot_decode_is_named_whatever_rule_ran` builds
the issue's constructed `latin.py`, and the same bytes as `latin.md`,
and asserts the scope line names it under a bare run, `dupword`, and
`charset`.
- `test_the_diff_scoped_line_names_an_undecoded_file_too` covers the
`--diff` form.
- `test_a_file_that_cannot_be_opened_is_collected_as_undecoded` covers
the read-failure path.
- `test_a_file_that_decodes_is_not_named_as_undecoded` guards the clean
case.

With the `prose_lint.py` change reverted, the first three fail (7
failures and 1 error across subtests), and with it the full
`tests/test_prose_lint.py` suite passes.

## Local review

Two recorded `local-strict-review` passes. The first raised three
findings. Two were fixed in the second commit: the `scope_note`
docstring now states its cause the right way round, and the existing
not-UTF-8 test now uses the shared `LATIN` fixture rather than a second
copy of its bytes. The third, folding `unparsed` and `undecoded` into
one reason-keyed collector, is declined on evidence. The two collectors
fill under different conditions at the append site (`unparsed` only for
a `.py` whose parse fails where a `PARSED_RULES` rule ran, `undecoded`
on any read failure whatever ran) and print different clauses, so a
keyed collector removes no per-reason code and swaps two typed
parameters for string keys. The second pass raised no findings and
accepted the decline.

Closes on promotion: #2796

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mment (#2808)

## Summary

- `NOT_PROSE` in the prose gate was an unanchored alternation read with
`.search`, so a comment that merely named a tool (`prettier`, `eslint`,
`Copyright`, `shellcheck`, `noqa`, and the rest) anywhere in a sentence
was exempt from `comment-wrap`, `comment-case`, and `comment-added`
together. Every name is now anchored at the body's start, as
`TOOL_DIRECTIVE` already is, and each word-like name ends at a word
boundary, so `noqable` or `eslintrc` opening a sentence is prose.
- `noqa` and `type: ignore` may still trail prose behind their own `#`,
and `pylint:` anywhere in the comment, the forms flake8 and ruff,
pyright, and pylint read.
- flake8's file-level exemption is matched in every spelling flake8
reads, `flake8:` or `flake8=` in any letter case, since anchoring
dropped it from the old `noqa`-anywhere match.
- `comment_wrap_findings` now matches the `.py` suffix
case-insensitively, as `comment_bodies` already did.
- The narrowed exemption newly reports one tracked line, a two-sentence
comment in `.markdownlint-cli2.jsonc`, which is split here. The rest of
the tracked tree is clean under the whole-tree run.
- The `TOOL_DIRECTIVE` comment now states which comment rules each
directive list exempts a comment from, in place of two lines describing
a hazard the anchoring removed.

## Verification

- `TestANamedToolIsStillProse` pins the three constructed comments from
#1659, directives opening the body (ESLint's `-- description`, a ruff
noqa reason, and each flake8 spelling among them), directives trailing
prose, near-match words, the version anchor, and an uppercase `.PY`
suffix.
- Each part was mutation-checked: reverting the fix, dropping each
trailing alternative, dropping either word boundary, unanchoring the
version, and narrowing the flake8 spelling each fail at least one case.
- Full suite `python3 -m unittest discover -s tests` passes, along with
ruff, mypy, the whole-tree prose gate, the EOL gate, and
`spec/validate.py`.

## Local-review findings and their outcomes

- `# flake8: noqa` newly reported: fixed in 2e9e93b, and its `=` and
mixed-case spellings in 102a12e.
- A trailing `# type: ignore` judged as prose although pyright reads it:
fixed in 2e9e93b.
- No case pinning the version anchor: fixed in 2e9e93b.
- A pylint pragma trailing prose without its own `#`: fixed in 9f5acdd.
- A header such as `# (c) Copyright 2024 Example Corp` now draws
`comment-added`, where the merge base exempted any comment containing
`Copyright`: no change. #1659 requires `Copyright` mid-sentence to be
prose, and `comment-added` is the one rule it reaches, which the
`comments` label answers.

## Filed along the way

- #2804: the uppercase Markdown suffix is read as Markdown in one place
and not in the others.
- #2805: a comment opening on a tool's name stays exempt though it
continues as prose. A sentence guard was tried on this branch and
regressed real directive forms, which that issue records.
- #2806: `comment_wrap_findings` duplicates `comment_bodies`' Python
dispatch with a different fallback.
- #2807: a trailing `noqa` or `pylint` directive exempts the prose in
front of it, and applies outside Python as well.
- #2809: `noqa` and `cSpell` directives are read in one letter case
though their tools read any.

The change edits comment lines in code and config, so it carries the
`comments` label.

Closes on promotion: #1659

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Prose comments that mention tool names or directive markers are now
checked consistently, while supported directives retain their intended
exemptions.
* Python comment handling now applies consistently to files with
uppercase or lowercase extensions.
* **Documentation**
* Clarified that the shared ignore list applies fleet-wide and that
repositories can configure local exclusions alongside their content.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…TF-8 (#2811)

## Summary

Two problems surfaced in one run of the menu's "Upgrade the host tools"
task on Windows.

**A failed install said nothing about why.** A Git for Windows upgrade
failed with `winget exited -1978335226` and no reason. The reason was in
the installer log winget names: the silent Inno Setup installer answered
its own files-in-use dialog with Cancel because a background GnuPG key
daemon, which Git for Windows ships and gpg starts for commit signing,
was still running from Git's tree. The old holder check matched only a
process named after the tool, so it found nothing.

On a failed install, `install-tools.ps1` now:

- prints winget's code in hex beside the decimal (`0x8A150006`), the
form winget documents its codes under,
- reads the matching installer log newer than the attempt
(`Get-InstallerFailureReason`) and prints its reason entries,
continuation lines joined and the home folder masked,
- names every process running from the tool's install tree
(`Get-ToolHolder`, `Get-InstallRoot`), with a `Stop-Process` command on
its own line, but only where a log entry uses the in-use wording or
winget's code is a sharing violation or access denial, so an unrelated
failure does not blame every open Git Bash. Python's tree is read from
the managed line's interpreter rather than from the `py` launcher.

**Docker's status printed as mojibake.** `docker desktop stop` and
`start` print a UTF-8 check mark, which rendered as three code page 437
characters. The menu runs each task in a child pwsh piped through
itself, so the fix is in `menu.ps1`: the console is switched to UTF-8
for the menu's lifetime and restored on exit, which a child pwsh
inherits at startup. `Invoke-Utf8Run` covers a direct console run of
`install-tools.ps1`, and leaves redirected output alone so a captured
log keeps its bytes intact. A menu-shaped parent and child chain decoded
`U+0393 U+00A3 U+00F4` before the change and `U+2713` after it.

## Open findings from the local strict review

The pre-push edit budget was spent with these two still open. Both are
`introduced`, and both are fixed in a879bd3, which a further local
strict review pass found clean.

1. `introduced`, fixed in a879bd3: `Get-ToolHolder` reads `.Source` off
`$null` when the probe is not on PATH, a terminating error under
StrictMode that aborts the run and, for docker, skips
`Exit-DockerMaintenance`.
2. `introduced`, fixed in a879bd3: the in-use code list omits winget's
own `0x8A150101` (package in use) and `0x8A150103` (file in use).

The remaining decimal-only `winget exited` sites in other files are
filed as #2810.

## Test plan

- `python3 -m unittest tests.test_install_tools` on Windows: new
`TestWindowsInstallFailureReason` and `TestWindowsInstallFailureReport`
cover the log parsing, the log age and id filters, install-root
resolution, both branches of `Invoke-Utf8Run`, and each
`Write-InstallFailure` branch.
- The failure report rendered against a real Git installer log on a
fleet host printed the files-in-use reason and named only the key
daemon.
- PSScriptAnalyzer, editorconfig-checker, ruff, mypy, and the prose gate
pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Copilot AI lite review requested due to automatic review settings October 11, 2026 15:56
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ccda327f-742f-4882-9a95-809a23347b71

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

## Summary

- `NOT_PROSE` in the prose gate exempted a whole comment body carrying a
trailing `# noqa`, `# type: ignore`, or `pylint:` directive, so the
prose in front of the directive was judged by none of `comment-wrap`,
`comment-case`, and `comment-added`.
- The trailing alternatives move out of `NOT_PROSE` into
`TRAILING_DIRECTIVE`. A new `without_trailing_directive` cuts the
directive off the body before both comment rules read it,
`comment_wrap_findings` doing so before `config_run_lines`, so a
directive's code ending the body cannot make prose read as a
configuration run.
- The trailing forms apply only in a `.py` file, since no other
language's checker reads them, so a TOML, YAML, or shell comment
mentioning one mid-sentence is judged as prose.
- The test that pinned the old whole-body exemption is replaced by tests
for each constructed case in the issue and its comment, a directive-only
comment still drawing nothing, and the configuration-run ordering. Each
new case fails against the code before the fix.
- A regression sweep of the comment rules over the tree reports no
change outside the intended one.

Closes on promotion: #2807

## Remaining local review findings

The local strict review's edit budget was spent on the first push and
again on the second. The two docstring findings listed from the first
push are fixed in ed6f9d7. The three `introduced` findings below,
listed from the second push, are fixed in
686a95b, whose own local pass found
nothing.

- `introduced`: `.github/actions/prose-gate/prose_lint.py`,
`comment_wrap_findings` keeps the cut body as the previous line for the
continuation check, so a full-line `.py` comment naming a directive
mid-sentence, such as one ending `where needed.` after a backticked `#
noqa: E501`, is cut to a fragment with no full stop and a lowercase next
line is reported as its wrap.
- `introduced`: `.github/actions/prose-gate/prose_lint.py`, no test pins
the leading `\b` in `\bpylint:`.
- `introduced`: `.github/actions/prose-gate/prose_lint.py`, no test pins
the `\s*` in `type:\s*ignore`, the spaceless form mypy also reads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.20805% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.26%. Comparing base (eb848c4) to head (4035555).
⚠️ Report is 363 commits behind head on main.

Files with missing lines Patch % Lines
spec/workflow_reuse.py 0.00% 9 Missing ⚠️
spec/fidelity_honesty.py 0.00% 6 Missing ⚠️
spec/resolve_description.py 25.00% 6 Missing ⚠️
scripts/pr_review.py 95.90% 5 Missing ⚠️
spec/validate.py 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2816      +/-   ##
==========================================
+ Coverage   59.94%   60.26%   +0.32%     
==========================================
  Files          16       16              
  Lines        8390     8489      +99     
==========================================
+ Hits         5029     5116      +87     
- Misses       3361     3373      +12     
Flag Coverage Δ
python-3.13 60.26% <81.20%> (+0.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

🟡 Changes recommended

Address the critical coverage-pagination bug and moderate Windows diagnostic failure before approval.

1 open finding
What changed in this PR

Promotes fixes for prose validation, review tooling, unattended workers, Windows installation diagnostics, and shared test helpers from develop to main.

Changes:

  • Adds bounded workflow-step readers and tests.
  • Improves prose-gate parsing, encoding, reporting, and review coverage handling.
  • Updates unattended-worker guidance, Windows diagnostics, UTF-8 handling, documentation, and generated skills.

Review findings:

  • Critical: scripts/pr_review.py can mark a full FILES_CAP page complete and incorrectly pass coverage.
  • Moderate: Windows process-path diagnostics can abort on protected processes.
  • Nit: .agents/skills/unattended-handoff/SKILL.md contains an extra “on”.
File Description
tests/​workflow_steps.py Shared bounded workflow readers
tests/​test_workflow_steps.py Workflow reader tests
tests/​test_release_guards.py Uses shared reader
tests/​test_publish_plan.py Uses shared reader
tests/​test_prose_lint.py Prose-gate tests
tests/​test_pr_review.py Review-tooling tests
tests/​test_install_tools.py Windows installer tests
scripts/​README.md Review-tool documentation
scripts/​pr_review.py Paging, coverage, quota, and attestation logic
host-setup/​windows/​install-tools.ps1 Installation diagnostics and UTF-8 handling
host-setup/​menu.ps1 Console encoding handling
.markdownlint-cli2.jsonc Lint configuration
.github/​skills/​unattended-handoff/​SKILL.md Generated unattended-worker guidance
.github/​skills/​pr-review-conduct/​SKILL.md Generated review guidance
.github/​copilot-instructions.md Review guidance
.github/​actions/​prose-gate/​prose_lint.py Prose parsing and scope reporting
.claude-plugin/​fleet-skills/​skills/​unattended-handoff/​SKILL.md Claude skill distribution
.claude-plugin/​fleet-skills/​skills/​pr-review-conduct/​SKILL.md Claude skill distribution
.claude-plugin/​fleet-skills/​.source-digests/​unattended-handoff Source digest
.claude-plugin/​fleet-skills/​.source-digests/​pr-review-conduct Source digest
.agents/​skills/​unattended-handoff/​SKILL.md Canonical unattended-worker guidance
.agents/​skills/​pr-review-conduct/​SKILL.md Canonical review guidance

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/pr_review.py
@ptr727

ptr727 commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Answers to the two findings in Copilot's review on 0754530 that the review body lists under "Review findings" with no thread of their own (2).

  1. host-setup/windows/install-tools.ps1:496, "Moderate: Windows process-path diagnostics can abort on protected processes." Disproven. Reading Process.Path on a process the session cannot inspect returns $null rather than raising, and the filter's own $_.Path -and guard skips those processes. Measured on a fleet host under Set-StrictMode -Version Latest with $ErrorActionPreference = 'Stop', unelevated (the script runs unelevated by design): Get-Process -ErrorAction SilentlyContinue returned 438 processes, 189 of them with an unreadable path, each read returned null, and the scan completed with 9 matches. The same claim was raised and declined with this evidence on Name Why a Windows Tool Install Failed and Print Docker's Status in UTF-8 #2811 (thread).
  2. .agents/skills/unattended-handoff/SKILL.md:40, "Nit: ... contains an extra 'on'." Disproven. The file holds no doubled "on", whether searched line by line or with line breaks joined. The only candidate is lines 40-41, "whatever tier it runs / on.", where the line wrap separates "runs" from "on" and the sentence reads correctly.

## Summary

- `NOT_PROSE` in the prose gate matched `noqa` and `cSpell`
case-sensitively, though flake8 reads `noqa` in any letter case and
CSpell reads its in-document directive in any letter case. A `//
cspell:ignore vendorid` in a `.ts` file drew `comment-case` and
`comment-added`, and a `# NOQA: E501` in a `.py` file drew
`comment-added`.
- `noqa` now matches in any case. The CSpell alternative becomes
`(?i:c?spell|spell-?checker):`, which covers `cspell:`, `spell:`,
`spell-checker:` and `spellchecker:` in any case, as CSpell's own
`(?:spell-?checker|c?spell)::?` pattern reads them. It requires the
colon, since CSpell reads the directive only with one, so a comment
opening a sentence on the tool's name is still prose. No comment in the
tree opens on a CSpell name without its colon, so the narrowing changes
no existing result.
- `TRAILING_DIRECTIVE` reads a trailing `# NOQA` the same way, so the
prose in front of it is still cut and judged.
- Every other tool name matches exactly as before.
- New tests pin each spelling, the trailing forms, and the colon
requirement. With the `prose_lint.py` change reverted, 11 subtests fail.

The local review pass also found that `nopep8` has the same case defect.
It is filed as #2818 and left out of this change, which its handoff
scoped to `noqa` and CSpell.

Closes on promotion: #2809

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Prose checks now recognize supported `noqa`, `type: ignore`, and
spell-check directives regardless of letter case.
* Spell-check tool names without the required directive format continue
to be checked as prose.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

🔵 Needs a closer look

A moderate prose-gate directive-matching issue remains unresolved.

0 open findings

1 resolved since last review

🧠 Review effort: Lite

@ptr727

ptr727 commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Answer to the headline of Copilot's review on c0a0eef, which opens on "Needs a closer look", counts no finding, and opens no thread (1): "A moderate prose-gate directive-matching issue remains unresolved."

Deferred to #2818 and #2805. The headline names no file or line, so this answers the directive-matching gaps that remain open after this promotion, each already filed where the fix has to land:

The two directive gaps this promotion does fix close when it merges: #2807 (a trailing noqa or pylint directive exempting the prose in front of it, #2814) and #2809 (noqa and cSpell read in one letter case, #2819).

ptr727 and others added 2 commits October 11, 2026 10:16
## Summary

- `refusing_review()` returned the newest refusal on the head even where
a later genuine Copilot round on that head had answered it. `wait`
reaches that read whenever no round past `--min-rounds` covers the head.
A genuine round at or below the baseline therefore left a spent quota
refusal to report exit `46`, while the digest printed `refusal=no`.
- It now reads the newest review on the head and returns it only where
that review is a refusal. Its docstring no longer says the caller
handles the spent case.
- `local_cover` reads the same function, so a refusal a later round on
the head spends no longer blocks an attested local pass. Its docstring
says so, and a new test pins that shape.

## The exit code for the issue's shape

The issue expected `45` from `wait --min-rounds 2` on [quota refusal on
`H`, genuine round on `H`]. With the fix that wait exits `30` with the
line naming the baseline (`--min-rounds 2 waits for round 3 or later`),
because `head_review_done` counts a round only past `--min-rounds`, and
`N` is the `rounds=` a `status` read before the re-request, which was
`1` there. `wait --min-rounds 1` and `status` both exit `45`
(`COVERAGE_IS_UNSTATED`). The new test asserts all three, and none
reports `COPILOT_QUOTA_EXHAUSTED`.

## Verification

- With the `pr_review.py` change reverted, the `--min-rounds 2` subtest
fails `30 != 46` and the `local_cover` test fails `False is not true`.
- Full suite green, ruff, mypy, prose gate, and `spec/validate.py`
clean.
- The local review pass found a pre-existing `scripts/README.md` wording
mismatch this change did not cause, filed as #2824.

Closes on promotion: #2820

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

- pycodestyle reads `nopep8` in any letter case, since it compiles its
inline pattern `# no(?:qa|pep8)\b` with `re.I`. The prose gate's
`TOOL_DIRECTIVE` matched `nopep8` only in lower case, so `# NOPEP8` and
`# NoPep8: E501` in a `.py` file were judged as new prose comments and
drew `comment-added`.
- `TOOL_DIRECTIVE` now reads `nopep8` in any case.
- `TRAILING_DIRECTIVE` did not list `nopep8` at all, so prose followed
by a trailing `# nopep8` on the same line was not cut. It now cuts a
trailing `nopep8` in any case, as it already did for `noqa`.
- The `without_trailing_directive` docstring names `nopep8` beside
`noqa` and `type: ignore`.
- New constructed tests pin both leading spellings, two trailing forms,
and a trailing directive standing alone. With the `prose_lint.py` change
reverted, 5 subtests fail.

Closes on promotion: #2818

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…2815)

## Summary

The rule against editing a loaded `.code-workspace` file applied
wherever the editing agent ran, so agents in a standalone console kept
handing trivial workspace edits back to the maintainer. A VS Code reload
drops only a session that VS Code hosts. The rule now binds an agent
running inside VS Code, in its extension or its integrated terminal, and
an agent in a standalone console edits the file directly, even while a
window has it loaded.

- `GOVERNANCE.md` "Verification Discipline": scopes the rule to an agent
hosted by VS Code. It keeps the exemption for a file no window has
loaded, and asks the maintainer only when the agent cannot tell whether
the rule binds it.
- `GOVERNANCE.md` "Editor and Tasks": the pointer bullet now states the
same split.
- `agent-conduct` Skill: regenerated, since it carries "Verification
Discipline" as an include.

## Verification

- `scripts/build_dist.py --check` reports the generated distributions
are current.
- `prose_lint.py --diff` against the merge base reports no violations.
- `spec/validate.py` passes.
- The markdownlint container reports 0 errors.
- Three recorded `local-strict-review` passes ran. The first two raised
findings: the dropped not-loaded exemption, an unreliable
environment-variable sign, and an over-broad fallback. Each was fixed.
The final pass raised none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

🟡 Changes recommended

Unresolved moderate prose-gate and Windows installer issues remain, along with minor documentation wording fixes.

1 open finding

🧠 Review effort: Lite

Comment thread .github/actions/prose-gate/prose_lint.py Outdated
ptr727 and others added 3 commits October 11, 2026 11:03
## Summary

This records HomeAssistant-Config's incidental Python as a registry
`driftNote`, which suppresses the audit's undeclared-type advisory for
it, per `spec/type-model.md`'s undeclared-but-detected row.

`spec/audit.py HomeAssistant-Config` reported this advisory in a resync
run (`audit run 2026-10-11T14:06:55Z | hub 54e96f8`):

> DRIFT registry: tracked .py file(s) present on the tree but types
omits 'python'.

The repository tracks two root helpers that Home Assistant runs at
runtime:

- `resize_snapshot.py` is a `shell_command`. Its only third-party import
is Pillow, which comes from the Home Assistant environment.
- `github_repo_status.py` is a `command_line` sensor and uses only the
standard library.

The repository has no `pyproject`, no declared dependencies, and no
tests. Declaring the `python` type would ask for a Python project the
repository does not have, so the note gives that reason with the
`(python.directories.declared)` token.

## Verification

- `python3 spec/validate.py` passes.
- `python3 spec/audit.py --branch develop HomeAssistant-Config` no
longer reports any `registry:` finding, and no other finding changes.
- The diff is one line and preserves the file's formatting.
- Two local strict review passes were run. The first found that the note
called both scripts stdlib, and that is fixed. The second found nothing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…2832)

Adds the CODESTYLE.md General "Command-Line Arguments" rule (every CLI
script answers `--help` and refuses unknown arguments before it reads or
writes anything) and the `tests/test_cli_usage.py` gate that enforces
it, and brings the existing scripts into line.

Closes on promotion: #2285

Known items, each with its class:

- `spec/audit.py` runs `git config --get remote.origin.url` on import,
so scripts importing it read before parsing arguments (introduced,
tracked in #2829).
- The three agent-safety hooks run by name are exempted in
`NOT_COMMAND_LINE` (introduced, tracked in #2786).
- `--check sentence-length` hits on CODESTYLE.md Shell paragraph
sentences (pre-existing).
- Local review findings on the gate's soundness (unknown-flag assertion
accepts a "usage" substring, ignored paths and env passthrough not
covered, detached grandchildren unseen, a leading-hyphen NAME now read
as an option in `spec/resolve_description.py`) (introduced, deferred to
#2831).

Local gates run: ruff format and check, mypy, prose gate,
`repo_gate.py`, `spec/validate.py`, `build_dist.py --check`, and the
full unittest suite (2550 tests, OK).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adds `nopep8` (any letter case) to the prose gate's `NOT_PROSE` pattern
beside `noqa`, so a comment line opening on it is exempt from
comment-wrap, comment-case and comment-added exactly as `noqa` is.
Removes the `nopep8` alternative from `TOOL_DIRECTIVE`, which was
unreachable once `NOT_PROSE` is tested first.

This answers an open Copilot finding on the develop to main promotion
PR.

Known items, each with its class:

- The `nopep8` exemption gives up the sentence-end guard
`is_tool_directive` applied, and is not limited to Python files. This
matches how `noqa` already behaves (intended parity, not a regression).
- No test row pins the lowercase `# nopep8` spelling for comment-case
(introduced, minor; the existing rows are capitalised).

Closes on promotion: none (finding on the promotion PR only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

🟡 Changes recommended

The Windows CLI contract test skips Windows coverage, and two documents use the wrong review field name.

1 open finding
1 resolved since last review

🧠 Review effort: Lite

Comment thread tests/test_cli_usage.py
@ptr727

ptr727 commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Merge Gate item 2, satisfied by the promotion case, on head 4035555. pr_review.py status exits 45 here, and that is the only reason it does not report the promotion ready. 13/13 checks pass and every thread is resolved.

How each constituent pull request's coverage was stated on its own feature -> develop pull request:

Merging with the maintainer's explicit go-ahead to merge and release.

@ptr727

ptr727 commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Coverage evidence for merging at head 4035555.

Copilot's round on this head states no changed-file coverage (pr_review.py exit 45), so each constituent pull request's own coverage was read at its merged head:

A frontier-tier local review of #2815's squash commit 5bfd54c then ran in its place and raised two findings, filed as #2835. The maintainer chose to merge this promotion with #2815 as is and track both findings there. Item 2's standing promotion case does not hold because of #2815, so this merge rests on the maintainer's explicit permission, given for this head.

All review threads on this pull request are resolved, and all 13 checks pass.

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