From 66c8adb66c0f63c28c166174e5b7255680893362 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 07:12:24 -0700 Subject: [PATCH 1/5] Gate Every Prose Rule Now That the Backlog Is Zero Closes the last piece of #519. The whole-tree count reached zero at `20916ad`, so `semicolon` and `dash` no longer need the warn-only tier they were given while a backlog existed. Both now block, alongside the five rules that already did. `home-path` joins them, and that is the substantive fix rather than a tidy-up. It has been in `DEFAULT_RULES` since it was written, yet was named by neither CI step, so the pattern-detectable half of the representative-data rule ran on a bare local invocation and gated nothing in CI. `OPERATIONS.md` recorded that hole, and this closes it and drops the record. `charset-unknown` stays warn-only, deliberately. A finding there names a character no tier covers, and classifying one is a fleet-law edit rather than something the change that typed it can fix. The `prose-gate` composite action is unaffected. It runs `--diff` with no `--check` list, so a downstream caller already gates the full default set over the lines a change touches. Verification, all on this branch: the new gating invocation exits 0 whole-tree, `charset-unknown` reports 0 findings, `test_prose_lint.py` passes 198 tests, `repo_gate.py` is clean, `actionlint` exits 0, `editorconfig-checker` exits 0, and `spec/validate.py` reports OK. Also removes the #519 entry from `TODO.md`, now that the issue is closed with its evidence quoted. Three figures in that entry were wrong and are corrected in the closing comment rather than carried forward: a commit id that resolves to nothing, a batch total that mixed checkers, and a 37-of-38 attribution that is 38 of 38 when both checkers run over the same bytes. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/validate-task.yml | 12 ++++++------ OPERATIONS.md | 10 +++++----- TODO.md | 7 ------- scripts/README.md | 4 ++-- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 92c65133..873caa39 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -68,13 +68,13 @@ jobs: - name: Check repo gates step run: python3 scripts/repo_gate.py - # The charset, duplicate-word, spelling and comment rules are clean tree-wide, so they gate. - # Every other prose rule reports in the step below without gating. + # Every prose rule is clean tree-wide, so each one gates rather than reporting a backlog. + # `charset-unknown` is clean too and still reports, for the reason the step below gives. - name: Check prose step - run: python3 scripts/prose_lint.py . --check charset --check dupword --check spelling --check comment-wrap --check comment-case + run: python3 scripts/prose_lint.py . --check charset --check semicolon --check dash --check dupword --check spelling --check comment-wrap --check comment-case --check home-path # Warn-only, and visible rather than absent: an unrun check is one nobody acts on. - # The backlog is corrected as each file is next edited, or cleared in a deliberate batch. - - name: Report prose backlog step + # A finding here names a character no tier covers, and classifying it is a fleet-law edit rather than a prose fix. + - name: Report unclassified characters step continue-on-error: true - run: python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --summary + run: python3 scripts/prose_lint.py . --check charset-unknown --summary diff --git a/OPERATIONS.md b/OPERATIONS.md index 29e621ba..a6e34069 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -8,7 +8,7 @@ What verifying a change here requires, including the part CI cannot perform. The ### Run the gates the way CI runs them -CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists plus `home-path`. What differs is the exit code rather than the coverage: CI gates on `charset`, `dupword`, `spelling`, `comment-wrap` and `comment-case` and reports the other three warn-only, where a bare run exits non-zero on any of the nine. `sentence-split` is in neither and is asked for by name. Run the CI invocations: +CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists together. What differs is the exit code rather than the coverage: CI gates on eight of the nine and reports `charset-unknown` warn-only, where a bare run exits non-zero on any of the nine. `sentence-split` is in neither and is asked for by name. Run the CI invocations: ```sh python3 scripts/test_prose_lint.py @@ -17,18 +17,18 @@ python3 scripts/test_pr_review.py python3 spec/audit.py --selftest python3 host-setup/agent-safety/gh-write-guard.py --selftest python3 scripts/repo_gate.py -python3 scripts/prose_lint.py . --check charset --check dupword --check spelling --check comment-wrap --check comment-case -python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --summary +python3 scripts/prose_lint.py . --check charset --check semicolon --check dash --check dupword --check spelling --check comment-wrap --check comment-case --check home-path +python3 scripts/prose_lint.py . --check charset-unknown --summary for f in registry/*.json spec/*.json repo-config/*.json; do jq empty "$f"; done python3 spec/validate.py docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest ``` -Three gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. And `sentence-split` is implemented and tested but named by no invocation, so nothing runs it. The third is `home-path`, which is in `DEFAULT_RULES` and so runs on every bare local run, yet is named by neither CI list, so the pattern-detectable half of the representative-data rule gates nothing in CI. It is clean tree-wide today, which is why the gap is a hole rather than a backlog. +Two gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. The second is that `sentence-split` is implemented and tested but named by no invocation, so nothing runs it. Run the `editorconfig-checker` line before pushing a new file, and before pushing an existing file that a script rewrote rather than an editor. This repository defaults to CRLF and most tooling writes LF, so a new file fails that check on its first CI run rather than locally. A scripted rewrite is the same hazard on a file that was already correct, since reading and rewriting a whole file in text mode converts every line ending in it, which no prose or Markdown gate reports. -The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, or cleared in a deliberate batch, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem. +The first prose invocation gates. The second reports a character that no tier covers, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem. Scope a run to what changed, which matches the correct-as-next-edited rule: diff --git a/TODO.md b/TODO.md index d265ffe8..c767fe7b 100644 --- a/TODO.md +++ b/TODO.md @@ -418,12 +418,6 @@ Actions on issues that are the maintainer's to take, each carrying its evidence Each was checked against the tree and has nothing left to do anywhere. Closing is the maintainer's call, and each wants the evidence quoted in the closing comment rather than a bare close. -- **[#519][issue-519], the hub's own tree does not pass the prose gate it ships.** Complete on the prose and on both questions. - - **Fixed by** - `f7a6a13` (snippets), `c9c92dd` (comments), `d791930` (hub-only Markdown), and the carried batch on `prose/carried-semicolons`. - - **Checked** - `develop` at `d791930` on 2026-08-07, where `python3 scripts/prose_lint.py --summary` reported 41 across 6 files, and 0 across 0 with the carried batch applied. - - **Closing evidence** - The whole-tree figure went 557 across 45 to zero, in four batches split by surface, being 184 in `catalog/snippets/`, 241 in non-Markdown comments, 90 in hub-only Markdown and 41 in the six carried files. Question 1 is answered by `reports/` being exempt as a generated tree, and question 2 by the snippets leading, since a non-conformant snippet seeds its violations into every repo that adopts it. - - **Closing evidence** - The issue's claim that the governance files were clean, and that this was therefore not a carry problem, was true of the checker of the day and false of the tree. Today's checker reports 38 findings against the same six files as they stood at `69688ec`, the commit the issue measured, while that commit's own checker reports zero. Scoping the list exemption to a sentence rather than a whole bullet accounts for 37 of the 38, because a colon anywhere ahead of the first semicolon had exempted every semicolon after it. The carry problem was real throughout and invisible, which is the stale-exemption hazard running in the loose direction. - - **[#557][issue-557], the agent-isolation rule and its two open questions.** Complete on the rule and on both questions. - **Fixed by** - `9d85941`. - **Checked** - `develop` at `9d85941` on 2026-08-06. @@ -447,7 +441,6 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 [issue-489]: https://github.com/ptr727/ProjectTemplate/issues/489 [issue-509]: https://github.com/ptr727/ProjectTemplate/issues/509 -[issue-519]: https://github.com/ptr727/ProjectTemplate/issues/519 [issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521 [issue-523]: https://github.com/ptr727/ProjectTemplate/issues/523 [issue-550]: https://github.com/ptr727/ProjectTemplate/issues/550 diff --git a/scripts/README.md b/scripts/README.md index 7b9188c4..248527ea 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -18,7 +18,7 @@ uvx coverage@latest run --source=. -m unittest discover -s scripts && uvx covera ## `prose_lint.py` -Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. +Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. It carries one rule from elsewhere in that document, `home-path`, which comes from "Representative Data in Agent-Authored Text" and catches an absolute home path naming a real account. That rule closes the pattern-detectable sliver of its section and nothing beyond it, since the exposure the section exists for was name-shaped and no pattern finds a name. The tiers decide by context rather than by a flat ban. Tier 1 carries no meaning its ASCII form loses and always flags. Tier 2 is an operator, kept next to a figure or another operator and replaced between words, so a threshold table reads as the range it is. Tier 3 is a unit or scientific symbol whose ASCII form would be a lie and never flags. Developer-typed characters such as emoji are preserved regardless of tier, and an un-tiered one is still reported as `charset-unknown` until it is classified. @@ -30,7 +30,7 @@ Run it scoped to changed lines, matching the standing rule that existing prose i python3 scripts/prose_lint.py . --diff origin/develop ``` -Whole-tree (`python3 scripts/prose_lint.py .`) reports the legacy backlog as well, which is informational rather than a gate. `charset`, `dupword`, `spelling`, `comment-wrap` and `comment-case` are clean tree-wide, so CI gates those five and reports the rest warn-only. +Whole-tree (`python3 scripts/prose_lint.py .`) reports zero, so a finding is a line the change under review wrote rather than backlog it inherited. CI gates every rule in the default set on that basis, except `charset-unknown`, which reports warn-only because classifying a character is a fleet-law edit rather than a prose fix. The default rule set covers comment shape (`comment-wrap` and `comment-case`) alongside the prose rules. It did not, which meant a run nobody parameterized reported clean on a wrapped comment while the rule read as enforced, and comment shape is the most frequently regressed rule in agent-authored work. Reading the backlog it exposes needs no flag now, and gating it needed `--diff` while the tree carried several hundred of them. That backlog is cleared, so both comment rules gate whole-tree, and `--diff` is now about scoping a run rather than about surviving one. From 3523f603775cd5f0f01070a908e88803ddbe0a71 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 07:17:34 -0700 Subject: [PATCH 2/5] Correct the Two Places That Still Describe a Warn-Only Backlog Copilot found `scripts/README.md` contradicting itself: the section head now says every default rule gates, while its closing paragraph still listed `semicolon` and `dash` as warn-only with a backlog to sweep. Rewritten to state what the tree-wide zero buys, and to point at where the `charset-unknown` exception is already explained rather than restating it. Swept by term rather than by instance, which found a second hit the finding did not name: `test_prose_lint.py` calls the warn-only step the consumer of the `--diff` scope. It never was, since both hub steps read the whole tree and the diff-scoped consumer is the `prose-gate` composite action. That is pre-existing rather than introduced here, and it is corrected in the same pass. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/README.md | 2 +- scripts/test_prose_lint.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 248527ea..d7ed8201 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -72,7 +72,7 @@ A comment sentence also has to start with a capital, which `comment-case` checks **A comment whose whole body is a URI is a reference rather than a sentence**, and neither rule applies to it. It cannot be capitalized or restructured without corrupting the address it exists to carry, so before the exemption every repo carrying a reference block inherited a finding no edit could answer. Consecutive reference lines are separate addresses rather than one sentence wrapping, which is why the exemption also stops the line below a URI from reading as its continuation. A URI inside a sentence is still prose, so the exemption requires the whole body to be the address and nothing else. -`charset`, `dupword`, `spelling`, `comment-wrap`, and `comment-case` are clean tree-wide and gate CI. `charset-unknown`, `semicolon`, and `dash` run as one warn-only CI step, so the remaining backlog is visible without blocking and is corrected as each file is next edited, or cleared in a deliberate batch. +Every rule above is clean tree-wide, which is what lets CI gate them over the whole tree rather than over a diff. `charset-unknown` reports instead of blocking, for the reason the tier discussion above gives. ## `repo_gate.py` diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index cbe6b2ad..96765401 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -1406,7 +1406,7 @@ class TestChangedLines(unittest.TestCase): The repo policy is that existing prose is corrected as each file is next edited rather than swept, and this parse is the whole mechanism behind it. A parse that returns too little makes - the warn-only step silently stop reporting, and one that returns too much reports the backlog + a diff-scoped run silently stop reporting, and one that returns too much reports the backlog as if the change introduced it. """ From 809da2f85de79ba368664116006de0d6bc3b97bf Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 07:22:16 -0700 Subject: [PATCH 3/5] Say Default Prose Rule Where the Claim Is About the Default Set Round two raised two suppressed findings, both the same shape and both correct. The workflow comment claimed every prose rule is clean and gating, which overstates it: `sentence-split` is a prose rule, is clean tree-wide, and gates nowhere, because it sits outside `DEFAULT_RULES` on purpose. `OPERATIONS.md` said `sentence-split` is asked for by name a few lines above saying nothing runs it. Both readings are true and they read as a contradiction, so the first now says nothing below runs it and a local run reaches it only by naming it. Sweeping the phrasing rather than the two named lines found a third instance in `scripts/README.md`, where every rule above carried the same overstatement. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/validate-task.yml | 2 +- OPERATIONS.md | 2 +- scripts/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 873caa39..96451b0e 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -68,7 +68,7 @@ jobs: - name: Check repo gates step run: python3 scripts/repo_gate.py - # Every prose rule is clean tree-wide, so each one gates rather than reporting a backlog. + # Every default prose rule is clean tree-wide, so each one gates rather than reporting a backlog. # `charset-unknown` is clean too and still reports, for the reason the step below gives. - name: Check prose step run: python3 scripts/prose_lint.py . --check charset --check semicolon --check dash --check dupword --check spelling --check comment-wrap --check comment-case --check home-path diff --git a/OPERATIONS.md b/OPERATIONS.md index a6e34069..d6cc9bc4 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -8,7 +8,7 @@ What verifying a change here requires, including the part CI cannot perform. The ### Run the gates the way CI runs them -CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists together. What differs is the exit code rather than the coverage: CI gates on eight of the nine and reports `charset-unknown` warn-only, where a bare run exits non-zero on any of the nine. `sentence-split` is in neither and is asked for by name. Run the CI invocations: +CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists together. What differs is the exit code rather than the coverage: CI gates on eight of the nine and reports `charset-unknown` warn-only, where a bare run exits non-zero on any of the nine. `sentence-split` is in neither, so nothing below runs it and a local run reaches it only by naming it. Run the CI invocations: ```sh python3 scripts/test_prose_lint.py diff --git a/scripts/README.md b/scripts/README.md index d7ed8201..044abd86 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -72,7 +72,7 @@ A comment sentence also has to start with a capital, which `comment-case` checks **A comment whose whole body is a URI is a reference rather than a sentence**, and neither rule applies to it. It cannot be capitalized or restructured without corrupting the address it exists to carry, so before the exemption every repo carrying a reference block inherited a finding no edit could answer. Consecutive reference lines are separate addresses rather than one sentence wrapping, which is why the exemption also stops the line below a URI from reading as its continuation. A URI inside a sentence is still prose, so the exemption requires the whole body to be the address and nothing else. -Every rule above is clean tree-wide, which is what lets CI gate them over the whole tree rather than over a diff. `charset-unknown` reports instead of blocking, for the reason the tier discussion above gives. +Every rule in the default set is clean tree-wide, which is what lets CI gate them over the whole tree rather than over a diff. `charset-unknown` reports instead of blocking, for the reason the tier discussion above gives. ## `repo_gate.py` From 0e4a1c2fac1df71db8fe60be78013f3c91f2aca6 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 07:26:22 -0700 Subject: [PATCH 4/5] Name the One Checkout That Turns the Home-Path Rule Off Round three raised one suppressed finding, and it is correct. The new `home-path` paragraph read as though the rule always runs, where `prose_lint.py` drops it in an operational repository, whose runbook carries the literal path an operator types rather than data an agent observed. Verified in the code rather than taken on trust. The skip is real and announced on stderr, and it keys on which repository-configuration payload the checkout carries. The hub carries both, being the template for each, so `operational_checkout` reads False here and the rule does gate on this repository, which is what makes the CI promotion in this pull request meaningful rather than a rule that quietly excuses itself. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index 044abd86..b5d23876 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -18,7 +18,7 @@ uvx coverage@latest run --source=. -m unittest discover -s scripts && uvx covera ## `prose_lint.py` -Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. It carries one rule from elsewhere in that document, `home-path`, which comes from "Representative Data in Agent-Authored Text" and catches an absolute home path naming a real account. That rule closes the pattern-detectable sliver of its section and nothing beyond it, since the exposure the section exists for was name-shaped and no pattern finds a name. +Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. It carries one rule from elsewhere in that document, `home-path`, which comes from "Representative Data in Agent-Authored Text" and catches an absolute home path naming a real account. That rule closes the pattern-detectable sliver of its section and nothing beyond it, since the exposure the section exists for was name-shaped and no pattern finds a name. It is the one rule a checkout can turn off: an operational repository's runbook carries the literal path an operator types, so a run there drops `home-path` and says so on stderr rather than going quiet. A repository states its own model by which configuration payload it carries, and the hub carries both, so it reads as a release repository and the rule gates here. The tiers decide by context rather than by a flat ban. Tier 1 carries no meaning its ASCII form loses and always flags. Tier 2 is an operator, kept next to a figure or another operator and replaced between words, so a threshold table reads as the range it is. Tier 3 is a unit or scientific symbol whose ASCII form would be a lie and never flags. Developer-typed characters such as emoji are preserved regardless of tier, and an un-tiered one is still reported as `charset-unknown` until it is classified. From 6fa593b8ccdaa55cd704620c96a9ddd9ce5dd330 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 07:41:13 -0700 Subject: [PATCH 5/5] Record the Blocked Resolve and the Thread a Node Id Cannot Locate Adds a work cluster for the two failures this pull request's own review loop hit at its last step, which the runbook has no shape for. The resolve mutation was refused by the agent harness's permission layer before any request left the machine, seconds after a reply carrying the identical thread id had succeeded and returned a comment url. The runbook's dead-path list is entirely GitHub's own refusals, so a local refusal matches none of them and reads as a bad identifier, which invites the one response a blocked write must never get. Handing the resolve over then failed a second time. `Q_THREADS` selects a thread's id, resolved state, path and first comment, and not its `url`, so the digest names a thread by a `PRRT_` node id that appears nowhere in the GitHub interface. Verified against the query rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/TODO.md b/TODO.md index c767fe7b..76ab186d 100644 --- a/TODO.md +++ b/TODO.md @@ -322,6 +322,29 @@ One pull request, after a survey, deciding whether anything stands between this - **Settled** - The reviewer counts the file and does not read it, rather than losing it earlier. The stated denominator equals the API's own `changedFiles` on **103 of 104** pull requests, the exception being one whose branch shrank between rounds. - **Settled** - Splitting remains a real remedy for a feature branch and is unavailable for a promotion, whose head is `develop`, so a promotion carrying a partial round is a maintainer decision by construction. +### A Resolve the Loop Cannot Perform and a Thread Nobody Can Find + +The review loop ends by replying on a thread and resolving it, and both halves failed on one pull request in ways the runbook describes nowhere. The resolve mutation was refused by the agent harness's own permission layer before any request left the machine, seconds after the reply mutation carrying the identical thread id had succeeded, so the refusal was neither GitHub's nor the id's. Handing the resolve to the maintainer then failed a second time, because the digest names a thread by its `PRRT_` node id, that id appears nowhere in the GitHub interface, and the person asked to resolve it could not find what to click. + +**State** `ready`. **Touches** `scripts/pr_review.py`, the runbook section in [`.github/copilot-instructions.md`][copilot-instructions], and [`OPERATIONS.md`][operations]. **Cost** one pull request, since the query change is one field and the runbook change is one paragraph. + +- **Carry a thread's own web address beside its node id, so a resolve can be handed to a person.** `Q_THREADS` selects `id`, `isResolved`, `path`, `line` and the first comment's `author` and `body`, and not its `url`, so the digest can name a thread and cannot point at it. Selecting `url` and printing it beside the id makes the hand-off one click. + - **Blocked by** - Nothing. + - **Checked** - `develop` at `0e4a1c2` on 2026-08-08, reading `Q_THREADS` in `scripts/pr_review.py` against the digest line that consumes it. + - **Detail** - The two identifiers are not interchangeable and neither is derivable from the other without a query. A `PRRT_` node id is what a mutation takes, and a `#discussion_r` fragment is what the web page anchors on. + - **Detail** - The evidence is [#620][pr-620], where a thread was handed over by node id and the reply was that it could not be found. + +- **Give the runbook a shape for a write the harness refuses, which it currently has none for.** Its list of dead paths is entirely GitHub's own refusals, a silent no-op, a 422, and the wrong bot login for the API in use, so a local refusal matches none of them and reads as a bad identifier, which invites the retry a blocked write must never get. + - **Blocked by** - Nothing. + - **Checked** - `develop` at `0e4a1c2` on 2026-08-08, against the known-non-working-paths list in the runbook. + - **Detail** - The distinguishing evidence is that a reply on the same thread id, in the same session, had already succeeded and returned a comment url, so the identifier was demonstrably good. + - **Detail** - What cleared it was a permalink and a human click, and the durable remedy is a permission rule in host settings. That is host state rather than repo content, so it belongs in the runbook as a note rather than in a committed configuration file. + +- **Confirm a resolve by re-reading the thread rather than by the mutation returning.** `reply` already exits 63 where the resolve did not report the thread resolved, which is the right shape, and a loop driving `gh api` by hand gets no exit code at all and so cannot notice. The rule worth writing down is that the state is the evidence. + - **Blocked by** - Nothing. + - **Checked** - `develop` at `0e4a1c2` on 2026-08-08, reading the exit-code table in the `scripts/pr_review.py` module docstring. + - **Detail** - This is the failure the suppressed-findings count already exists for, where a step that stopped running reads exactly like a step that passed. + ## Standalone Chores Small work with no research to preserve, selectable one bullet at a time. @@ -457,6 +480,7 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [pr-591]: https://github.com/ptr727/ProjectTemplate/pull/591 +[pr-620]: https://github.com/ptr727/ProjectTemplate/pull/620