Repository navigation
Conversation
…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)
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
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. Comment |
## 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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 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.pycan mark a fullFILES_CAPpage complete and incorrectly pass coverage. - Moderate: Windows process-path diagnostics can abort on protected processes.
- Nit:
.agents/skills/unattended-handoff/SKILL.mdcontains 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.
|
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).
|
## 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>
|
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 |
## 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>
## 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>


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