From fdd1e25357751bfd30367eafca3038028113c966 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 11:49:11 -0700 Subject: [PATCH 1/3] State the Recurring Case in Three Rules That Only State the Common One (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships the `ready` cluster carrying [#578](https://github.com/ptr727/ProjectTemplate/issues/578). No closing keyword, since a feature to `develop` pull request never fires one. Each of the three rules is correct about its common case and silent about the case that keeps recurring. The issue's own diagnosis is that downstream agents keep re-deriving them and reporting the result as a discovery, which says the wording does not reach the case rather than that the rule is missing. All three `Checked` anchors were re-read against `develop` at `a706ddb` before anything was written. All three gaps were still real. ## The three widenings **`GOVERNANCE.md` "Branching Model"** now states that an issue closes when its work is verifiably complete, and that the keyword automates the case where completion and promotion coincide rather than adding a second condition to it. Two cases fall outside it and both close by hand: a promotion that merged without the keyword, and **work complete on `develop` with no promotion imminent**, where waiting holds a finished issue open for however long the next promotion takes. The closing comment cites the squash SHA, re-read against the branch rather than trusted from the claim that asked for the close. The old wording licensed a hand-close **only** as a repair after a promotion had already merged without the keyword, which is why the second case had no route. **`GOVERNANCE.md` "Communicating with the User"** now says the clickable link goes in the message that comes **before** the prompt rather than merely alongside it. The rule already got the hard part right, that a prompt renders neither a Markdown link nor a bare URL. What it missed is that a prompt blocks on an answer, so a message emitted after it is read once the answer is already given, which is the one moment the link is no longer any use. One word, `accompanies` to `before`, plus the reason. **`GOVERNANCE.md` "Operational Repositories"** now states when to decline the direct-commit grant. The grant exists for the one-line config edit whose review round costs more than it protects, and that reason stops applying well before a change gets large, so the test is stated as a **shape rather than a line count**: restructuring rather than adjusting a value, several files at once, a device, integration or automation that did not exist before, anything whose failure shows up on the live service rather than in a lint run, or a change the author cannot state in one sentence. This stays guidance by construction. Adding a `pull_request` rule to `repo-config/operational/develop.json` would gate the direct push too and withdraw the very allowance the model exists to give, so nothing mechanical can apply it. ## What sweeping by term found Item 3 swept by term rather than by the instance the issue named turned up **`WORKFLOW.md` section 3** restating the same allowance with the same silence, in the same words: "a change worth reviewing still takes one". It now points at the section that owns the test instead of repeating it, so the rule stays in one place and the second reader still finds it. Item 1's rule also sits in **"Branching Model"**, not the "Git and Commit Rules" the cluster's `Touches` line named. That is where the re-vendor has to look for it, so it is recorded rather than silently corrected. ## Backlog bookkeeping - The shipped cluster is retired from "Work Clusters". - **#557 and #579 leave "Verified Complete, Awaiting Close"**, both having been closed on 2026-08-08, along with four link definitions left orphaned by their removal. - **#578 takes their place** with its closing evidence quoted, so the section is not empty and the issue stays visible while open, which step 1 of "How to Select the Next Item" requires. - The "Fleet Sweeps" re-vendor entry records that this half **propagates a rule rather than refreshing a hash**. Every other carried change queued there is punctuation or capitalization, where a repo on the old copy is correct on the rule and wrong on the bytes. Here it is wrong on the rule, which makes it the half to carry first, and item 3 matters most on the two `operational` repos that can act on it. ## Verification | Gate | Result | | --- | --- | | `prose_lint.py --diff origin/develop .` | clean | | markdownlint-cli2 over `**/*.md` | 0 issues in 44 files | | editorconfig-checker | clean | | CRLF preserved | 0 lines without CR in all three files | Three carried `verbatim` `GOVERNANCE.md` sections change, so the fleet is byte-mismatched until the re-vendor visit. `WORKFLOW.md` is `intent`, so nothing reports its cross-reference at all, which is why it is written down. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- GOVERNANCE.md | 5 +++-- TODO.md | 50 +++++++------------------------------------------- WORKFLOW.md | 3 ++- 3 files changed, 12 insertions(+), 46 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 36d9edc5..886b8bcb 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -58,7 +58,7 @@ Agent-authored text illustrates with data the agent constructed, never with data - **Executing a `develop -> main` promotion safely, two traps both learned the hard way:** - **Never delete `develop`.** A promotion PR's head *is* `develop`, so `gh pr merge --delete-branch` (and the repo's "Automatically delete head branches" toggle, which is why that toggle is [kept off](./repo-config/settings.json)) deletes `develop` itself. Merge a promotion with a plain `gh pr merge --merge`, no `--delete-branch`. If `develop` is ever lost this way, restore it to the merged PR's head SHA, which is still reachable as the merge commit's second parent: `gh api -X POST "repos///git/refs" -f ref=refs/heads/develop -f sha="$(gh pr view --json headRefOid --jq .headRefOid)"`. - **Spurious EOL-only conflicts resolve by taking `develop`.** When develop declared workflow YAML as LF while main is still CRLF, `develop -> main` conflicts *whole-file* on those paths. develop's `required_linear_history` + PR rulesets forbid resolving on `develop` (no merge commit, no force-push), so resolve on a throwaway branch off `main`: `git checkout -b promote/develop-to-main origin/main && git merge origin/develop`, take develop's side for the EOL-conflicted files (`git checkout --theirs `) **after confirming each is content-identical modulo EOL or that develop is a strict superset** (`diff <(git show :2:f|tr -d '\r') <(git show :3:f|tr -d '\r')`), then open that branch -> `main`. Verify no genuine main-only content is dropped (build/test where the repo supports it). -- **Issue-closing keywords (`Closes #N`, `Fixes #N`) go in the `develop -> main` promotion PR, not the feature -> `develop` PR.** GitHub auto-closes an issue only when the closing keyword merges into the **default branch** (`main`). A feature -> `develop` PR merges into `develop`, so the keyword never fires there. Reference the issue in the `develop` PR body if useful, but put the actual closing keyword on the promotion PR. If a `develop` PR merges with the keyword on it, the keyword does nothing and the issue stays open, so put it on the promotion PR body instead. Close the issue by hand citing the squash SHA only when the promotion has already merged without it. +- **Issue-closing keywords (`Closes #N`, `Fixes #N`) go in the `develop -> main` promotion PR, not the feature -> `develop` PR.** GitHub auto-closes an issue only when the closing keyword merges into the **default branch** (`main`). A feature -> `develop` PR merges into `develop`, so the keyword never fires there. Reference the issue in the `develop` PR body if useful, but put the actual closing keyword on the promotion PR. If a `develop` PR merges with the keyword on it, the keyword does nothing and the issue stays open, so put it on the promotion PR body instead. **Closing by hand is the ordinary route wherever the keyword cannot fire, not a repair for a botched promotion.** An issue closes when its work is verifiably complete, and the keyword automates that for the common case where completion and promotion coincide rather than adding a second condition to it. Two cases fall outside the keyword and both close by hand: a promotion that merged without it, and work complete on `develop` with no promotion imminent, where waiting holds a finished issue open for however long the next promotion takes. Cite the squash SHA that completed the work, and re-read that commit against the branch before closing rather than trusting the claim that asked for the close, so the closing comment carries evidence a reader can check. - **Both rulesets intentionally omit "Require branches to be up to date before merging".** The flag is off on `main` and on `develop`, for related but distinct reasons. - *Main:* the check is graph-based: it asks whether main's tip commit is reachable from develop, not whether the two branches have the same content. After any develop -> main release, main's tip is a brand-new merge commit that develop's history doesn't contain. Forward-only develop never adds it (no back-merge of main into develop), so the check would fail on every subsequent release. Other technical workarounds (rebasing develop onto main, or rewriting develop's history) exist but contradict the squash-only develop ruleset and the linearity invariant. - *Develop:* the check stalls bot auto-merge when two bot PRs against develop land within the same window. As soon as the first merges, the second flips to `mergeStateStatus: BEHIND` and GitHub's auto-merge will not fire while strict is on. The merge-bot only *enables* auto-merge on `opened`/`reopened` (see below) and never auto-updates bot branches, and Dependabot's rebase isn't real-time, so the second PR sits OPEN with all checks green indefinitely. Squash mechanics still rebase the diff onto develop's tip on merge, `required_linear_history` still enforces linearity, textual conflicts still block `mergeable: CONFLICTING`, and the required `Check pull request workflow status job` still gates merges. The only thing lost is pre-merge detection of *semantic-but-not-textual* conflicts, which the post-merge develop CI run catches anyway. @@ -104,6 +104,7 @@ The registry `workflowModel` field is `release` (the default) or `operational`. - **Commit configuration directly to `develop`.** There is no feature branch, and the maintainer commits straight to `develop` and *occasionally* opens a `develop -> main` PR to bless a known-good snapshot. The `develop` ruleset drops the PR and status-check gate, so direct signed pushes are allowed (force-push, deletion, and unsigned commits are still blocked) and CI runs on the push as **advisory** feedback that never rejects a commit. - **A PR into `develop` stays available, and CI runs on it.** Dropping the requirement permits the direct push, it does not withdraw the pull request, so a change worth reviewing takes one and both paths into `develop` are legitimate. The lint workflow's `pull_request` trigger therefore names `develop` alongside `main`, since a trigger set naming `main` alone matches nothing on a `develop` PR and that PR would merge with no validation at all, which is the one output [WORKFLOW.md](./WORKFLOW.md) D1.2 forbids on *any* PR. The result is **reported and not required** there, because a required status check on `develop` would gate the direct push too and dissolve the allowance the model is built on. The asymmetry is the point: on the direct-commit path CI can only be advisory after the fact, while on the PR path the change has not landed yet, so validating it is both pre-merge and actionable. +- **Take the pull request whenever the change is not one a reader takes in at a glance and reverts cleanly.** The grant exists for the one-line config edit whose review round costs more than it protects, and that reason stops applying well before a change gets large, so what decides it is the shape of the change rather than a line count. Restructuring rather than adjusting a value, touching several files at once, introducing a device, an integration, or an automation that did not exist before, and anything whose failure shows up on the live service rather than in a lint run are each the pull request case. So is a change the author cannot state in one sentence, which is the same signal read from the other side. This stays guidance by construction, because adding a `pull_request` rule to the [operational `develop` ruleset](./repo-config/operational/develop.json) would gate the direct push too and withdraw the very allowance the model exists to give, so the judgment sits with whoever is about to commit and nothing mechanical will make it for them. - **The `main` promotion gate is unchanged.** The [`main` ruleset](./repo-config/main.json) is shared with `release` repos, so the `develop -> main` PR still **enforces** the required `Check pull request workflow status job`. For an operational repo that check is lint/validation only, meaning editorconfig/EOL plus domain linters (a Home Assistant or ESPHome config validation, a firmware build) and never unit tests, so `develop` stays the live surface and a broken config can never reach `main`. - **Release only by manual dispatch.** Operational repos carry `releaseTrigger: dispatch-only` and run no codegen or auto-publish bots, so they publish **only** on a manual `workflow_dispatch`, the source-only release the publisher already supports (tag + source zip + README + LICENSE, NBGV-versioned), never automatically. The `develop -> main` promotion just blesses a known-good snapshot, and a release is a separate, deliberate dispatch. - **Fleet sync still applies.** Dependabot's dual-target sync and the App-signed merge-bot run on **every** tier, operational included, so both branches stay in sync and a promotion stays a clean forward merge. @@ -337,7 +338,7 @@ Anti-pattern: don't keep flipping the code on the same style point. Flip the rul ## Communicating with the User -- **Reference every pull request as a clickable link.** When you mention a PR on a surface that renders Markdown (chat, a summary, a report), render it as a Markdown link to the PR (`[#123](https://github.com/OWNER/REPO/pull/123)`), never a bare `#123`. The same applies to issues and commits. **The form follows the surface.** Some surfaces link neither a Markdown link nor a bare URL, an interactive prompt's question and option text among them, and pasting a full URL into one of those does not rescue it, since the reader gets a string to copy, which is the outcome this rule exists to prevent. There the reference is a bare `#123`, and the clickable link goes in the message that accompanies the prompt. The test is whether the reader can click it where it is read, not whether it was written in the syntax that works elsewhere. +- **Reference every pull request as a clickable link.** When you mention a PR on a surface that renders Markdown (chat, a summary, a report), render it as a Markdown link to the PR (`[#123](https://github.com/OWNER/REPO/pull/123)`), never a bare `#123`. The same applies to issues and commits. **The form follows the surface.** Some surfaces link neither a Markdown link nor a bare URL, an interactive prompt's question and option text among them, and pasting a full URL into one of those does not rescue it, since the reader gets a string to copy, which is the outcome this rule exists to prevent. There the reference is a bare `#123`, and the clickable link goes in the message that comes **before** the prompt rather than merely alongside it, because the prompt blocks on an answer and a message emitted after it is read once that answer is already given, which is the one moment the link is no longer any use. The test is whether the reader can click it where it is read, not whether it was written in the syntax that works elsewhere. - **Ask for input as a numbered list.** When you need the user to decide or answer, present the questions, and any options, as a numbered list so they can reply per number. A single inline question is fine, and two or more are always numbered. - **Raise work blocked on the user as a direct interactive prompt.** When progress needs a decision, an authorization, or an answer only the user can give, ask for it through the interface's own prompt mechanism, at the point the work stops. Never leave it as prose in a summary: a handoff buried in a paragraph is a handoff that did not happen, because a summary reads as a report of finished work and the one line still waiting on the user is the easiest in it to skim past. The blocked item is the message, not a closing remark on a message about something else. **The options offered are the actions themselves**, and the one that unblocks the work names the action it authorizes ("squash and merge it"), so selecting it is the go-ahead rather than a note to act on later. Offering only ways to wait is the same failure in interactive clothing, since a prompt whose every choice is inaction reports the block rather than clearing it, and where the agent may not perform the authorized action itself, the option says who does it. This supersedes the numbered-list rule above wherever an interactive prompt is available, and the numbered list is the fallback where none is. diff --git a/TODO.md b/TODO.md index 992b904b..7e6955f7 100644 --- a/TODO.md +++ b/TODO.md @@ -37,36 +37,6 @@ One pull request pointing a hub `uses:` at a hub-owned action, so that the resol - **Settled** - The resolvability pass reports what it covered on every run, so the hub's zero is visible rather than silent, which is why this is a separate decision rather than a defect in that pass. - **Settled** - The fleet's `ptr727` pins are live in the downstream repos that consume the action, and `repo_gate.py --root ` from a hub checkout reads them there, so the pass is not idle fleet-wide. -### Three Rules That Leave the Recurring Case Unstated - -One pull request widening three carried [`GOVERNANCE.md`][governance] rules that each state their common case and go quiet on the case that recurs, filed together because they share that shape and land in one re-vendor. - -**State** `ready`. **Touches** [`GOVERNANCE.md`][governance] "Git and Commit Rules", "Communicating with the User", and "Operational Repositories". **Cost** one hub edit plus a carried-section re-vendor, which rides the visit in "Fleet Sweeps". - -- **Say when an issue is closed by hand, not only that the closing keyword belongs on the promotion.** The uncovered case is work complete on `develop` with no promotion imminent. - - **Blocked by** - Nothing. - - **Issue** - [#578][issue-578] item 1. - - **Checked** - `develop` at `b82c1a3` on 2026-08-05, where the rule licenses a hand-close only once a promotion has merged without the keyword. - - **Open** - Nothing. - - **Settled** - Downstream agents keep re-deriving the rule and reporting it as a discovery, which says it is being missed rather than that it is missing, so the discoverability half is not a wording fix. - - **Settled** - The widening says an issue is closed when the work is verifiably complete, citing the squash commit that completed it, and that the promotion keyword is the automation for the common case rather than the only permitted route. - -- **Say that the message carrying the clickable link comes before the prompt it accompanies.** A message emitted after the prompt is not read before the question is answered. - - **Blocked by** - Nothing. - - **Issue** - [#578][issue-578] item 2. - - **Checked** - `develop` at `b82c1a3` on 2026-08-05, where the rule says accompanying rather than preceding. - - **Open** - Nothing. - - **Settled** - The rule already gets the hard part right, that an interactive prompt renders neither a Markdown link nor a bare URL, so the reference inside it is a bare number and the link goes in the message. - - **Settled** - The recurrence is evidence that the wording does not reach the case rather than that the rule is ignored, which is the same diagnosis the entry above reaches. - -- **State that an operational repository still opens a pull request for a large or risky change.** The grant to commit direct to `develop` says nothing about when to decline it. - - **Blocked by** - Nothing. - - **Issue** - [#578][issue-578] item 3. - - **Checked** - `develop` at `3d1a0b1` on 2026-08-06, where `repo-config/operational/develop.json` carries exactly three rules, `deletion`, `non_fast_forward` and `required_signatures`. - - **Open** - What counts as large, stated as a shape rather than a line count, since the property that matters is whether the change can be read at a glance and reverted cleanly. - - **Settled** - The reason the grant exists is the one-line config edit that a review round costs more than it protects, and that reason stops applying well before a change gets large. - - **Settled** - This stays guidance by construction, because adding a `pull_request` rule to the operational ruleset would withdraw the direct-commit grant the model exists to give. - ### The Declared Repository Description One pull request moving the canonical short description into declared data, which settles the second-paragraph ambiguity by construction rather than by writing an extraction rule the same change then deletes. @@ -384,6 +354,7 @@ Regenerate [reports/divergences.md][divergences-report] before using it as the w - **Detail** - [`CODESTYLE.md`][codestyle] is the fifth file, at `intent` rather than `verbatim`, so it reaches the fleet as a rule each repo adopts in its own copy, and the same mixed spelling waits in every downstream tree. - **Detail** - [`.github/copilot-instructions.md`][copilot-instructions] is the sixth, also at `intent`, where "Reply and Thread Resolution Workflow" now leads with the hub's reply helper and keeps the hand-run mutations as the cross-owner and unreachable-hub path. A repo taking the old copy is not broken by it, since the mutations it documents still work, so this rides the visit rather than gating it. - **Detail** - The prose batch rewrote punctuation in five `verbatim` [`GOVERNANCE.md`][governance] sections, "Branching Model", "Release Model", "Documentation Style Conventions", "PR Review Etiquette" and "Workflow YAML Conventions", so every downstream copy of those five is byte-mismatched and the audit reports it as stale. No rule changed meaning, so the re-vendor is a hash refresh rather than a propagation, and a repo taking the old copy is correct on the rule while wrong on the bytes. + - **Detail** - The [#578][issue-578] widening is the one part of this sweep that propagates a rule rather than refreshing a hash, so a repo left on the old copy is wrong on the rule and not merely on the bytes, which makes it the half to carry first. It touches three `verbatim` [`GOVERNANCE.md`][governance] sections, "Branching Model", "Communicating with the User" and "Operational Repositories", and the third of those matters most on the two `operational` repos that can act on it. [`WORKFLOW.md`][workflow] took a cross-reference in the same change and is `intent`, so nothing reports it. - **Detail** - [`WORKFLOW.md`][workflow] is the seventh file and `repo-config/README.md` joins [`CODESTYLE.md`][codestyle] and [`.github/copilot-instructions.md`][copilot-instructions] at `intent`, where a punctuation-only edit produces no hash and therefore no audit finding at all. Nothing reports these, which is why they are recorded here rather than left to the run. `HISTORY.md` is `presence` and is each repo's own changelog, so its one fix owes nothing downstream. - **Carry the `Local Verification` heading into every repository's `OPERATIONS.md`.** The heading leads the file and states what verifying a change there requires, naming the part of the repo's contract CI structurally cannot exercise, and a repo whose gates are entirely in CI says that under it rather than omitting it. @@ -444,15 +415,12 @@ 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. -- **[#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. - - **Closing evidence** - [`GOVERNANCE.md`][governance] "Repository Boundaries and Write Safety" carries the rule that each task runs in its own checkout, in its own directory, on its own feature branch, scoped per task rather than per agent, which adopts the first open question. It names the three commands that cross the boundary while being correct in isolation, and it names the two signals that another task is live in a tree, whose stated response is to stop, which adopts the second. The rule reaches every machine through the same text in [`host-setup/agent-safety/`][agent-safety]. - -- **[#579][issue-579], a repository with no instruction set cannot resolve its own vocabulary.** Complete on the term definitions and on the routing. - - **Fixed by** - `9d85941`. - - **Checked** - `develop` at `9d85941` on 2026-08-06. - - **Closing evidence** - [`AGENTS.md`][agents] "Fleet Bootstrap" names the hub as a defined term and carries the reach rule, and [`README.md`][readme] defines the six words a request is phrased in, the hub, the fleet, standing a repository up, auditing one, closing the review loop, and carried against reached, each naming the file that answers it. The host-wide block under [`host-setup/agent-safety/`][agent-safety] carries the same definition, which is what reaches a repository holding no instruction set at all. +- **[#578][issue-578], three rules that state the common case and leave the recurring one unstated.** Complete on all three items. + - **Fixed by** - The pull request carrying this entry, since the fix and the entry ship in one squash and the closing comment cites that SHA. + - **Checked** - `develop` at `a706ddb` on 2026-08-08, where all three gaps were re-read before the widening was written. + - **Closing evidence** - [`GOVERNANCE.md`][governance] "Branching Model" now states that an issue closes when its work is verifiably complete and that the keyword automates the case where completion and promotion coincide rather than adding a condition to it, naming work complete on `develop` with no promotion imminent as the second hand-close case beside a promotion that merged without the keyword, which answers item 1. "Communicating with the User" now says the message carrying the clickable link comes **before** the prompt rather than merely alongside it, since a prompt blocks on an answer and a later message arrives after that answer is given, which answers item 2. "Operational Repositories" now states when to decline the direct-commit grant, as a shape rather than a line count, and records that it stays guidance because a `pull_request` rule on the operational ruleset would gate the direct push and withdraw the allowance, which answers item 3. + - **Detail** - The item 1 rule sits in "Branching Model" rather than the "Git and Commit Rules" the retired cluster named, which is where the re-vendor has to look for it. + - **Detail** - Sweeping item 3 by term rather than by the instance the issue named found [`WORKFLOW.md`][workflow] section 3 restating the same allowance with the same silence, so it now points at the section that owns the test rather than repeating it, which is one rule in one place and a cross-reference for the second reader. @@ -470,11 +438,9 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [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 -[issue-557]: https://github.com/ptr727/ProjectTemplate/issues/557 [issue-558]: https://github.com/ptr727/ProjectTemplate/issues/558 [issue-577]: https://github.com/ptr727/ProjectTemplate/issues/577 [issue-578]: https://github.com/ptr727/ProjectTemplate/issues/578 -[issue-579]: https://github.com/ptr727/ProjectTemplate/issues/579 [issue-580]: https://github.com/ptr727/ProjectTemplate/issues/580 [issue-585]: https://github.com/ptr727/ProjectTemplate/issues/585 [issue-597]: https://github.com/ptr727/ProjectTemplate/issues/597 @@ -492,7 +458,6 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i -[agent-safety]: ./host-setup/agent-safety/ [agents]: ./AGENTS.md [audit]: ./spec/audit.py [audit-doc]: ./AUDIT.md @@ -508,7 +473,6 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [operations]: ./OPERATIONS.md [project-types]: ./spec/project-types.json [prose-gate]: ./.github/actions/prose-gate/action.yml -[readme]: ./README.md [readme-structure]: ./spec/readme-structure.md [reports]: ./reports/ [repos]: ./registry/repos.json diff --git a/WORKFLOW.md b/WORKFLOW.md index 99005b07..24bdc711 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -62,7 +62,7 @@ flowchart LR develop -->|merge commit, enforced lint CI| main ``` -The direct commit is an **allowance, not a substitute for review**. The ruleset drops the pull-request *requirement*, which permits a direct push without withdrawing the pull request, so a change worth reviewing still takes one and both paths reach `develop` legally. What differs is when validation lands. On the direct-commit path the commit is already on the branch, so CI can only be advisory after the fact, and that is the accepted cost of the model. On the pull-request path the change has not landed, so validation is pre-merge and actionable, which is the moment it is worth the most, and the lint workflow's `pull_request` trigger therefore names `develop` alongside `main` (Section 6). That is what makes **D1.2** hold here, since its input is *any* PR and the operational model is no exception. The check is reported on a `develop` PR rather than required, because a required status check on `develop` binds the direct push too and would dissolve the allowance the model is built on. +The direct commit is an **allowance, not a substitute for review**. The ruleset drops the pull-request *requirement*, which permits a direct push without withdrawing the pull request, so a change worth reviewing still takes one and both paths reach `develop` legally. Which changes those are is stated as a shape rather than a line count in [GOVERNANCE.md "Operational Repositories"][governance-operational-repositories], which owns the test and is the one place it is written, since nothing in a ruleset can apply it. What differs is when validation lands. On the direct-commit path the commit is already on the branch, so CI can only be advisory after the fact, and that is the accepted cost of the model. On the pull-request path the change has not landed, so validation is pre-merge and actionable, which is the moment it is worth the most, and the lint workflow's `pull_request` trigger therefore names `develop` alongside `main` (Section 6). That is what makes **D1.2** hold here, since its input is *any* PR and the operational model is no exception. The check is reported on a `develop` PR rather than required, because a required status check on `develop` binds the direct push too and would dissolve the allowance the model is built on. Their CI is lint/validation only (editorconfig/EOL plus domain linters such as Home Assistant or ESPHome config validation or a firmware build, but **no unit tests**), so the D-guarantees below that assume a build/test pipeline are **N/A** exactly as for `source-only` (Section 6). What binds: the promotion gate, where the `develop -> main` PR must pass the required `Check pull request workflow status job`, and the source-only release on manual dispatch (`releaseTrigger: dispatch-only`; tag + source zip). Branch-model rulesets are specified in [GOVERNANCE.md "Branching Model"][governance-branching-model] and [repo-config/README.md][repo-config-readme], not here. @@ -298,4 +298,5 @@ Each type maps the *applicable* S-scenarios onto its targets. The differences ar [codestyle]: ./CODESTYLE.md [governance-branching-model]: ./GOVERNANCE.md#branching-model +[governance-operational-repositories]: ./GOVERNANCE.md#operational-repositories [repo-config-readme]: ./repo-config/README.md From 92b9fc5c7215928e6bd90079ab2aedfdceb2d4a8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 12:24:58 -0700 Subject: [PATCH 2/3] Read the Reviewer's Node Id Repo-Wide Instead of From the Pull Request (#628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Disposition** `Amends "Re-vendor the changed \`verbatim\` content, which is one sweep covering seven files."` under "Fleet Sweeps". No cluster under "Work Clusters" covered this, and a cluster created and deleted in the same change records nothing, so the observation lands on the entry that already reasons about what each downstream copy owes. No closing keyword, since a feature to `develop` pull request never fires one. ## What was wrong "Triggering and Polling" in the runbook said the reviewer bot's node id is read from an existing Copilot review **on the pull request being reviewed**. Three statements asserted it: - The round-1 paragraph: "The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA). By then a prior review exists, so its bot node id is readable." - The step-1 code comment, whose query was `pullRequest(number: ) { reviews }`. - The paragraph after the recipe: "step 1 needs at least one prior formal review on the PR". A "Cold start" paragraph added later says the opposite in the same section, that the id is the reviewer account's and is stable across every pull request in the repo. The three statements above were never swept, so the section contradicted itself and the strict reading is the one a reader hits first. Acting on it, a pull request that auto-review-on-open never fired on has no way to reach round 1 and gets handed to the maintainer to seed through the UI, which is the hand-off the mutation exists to remove. ## What the tree says Read across the newest eight pull requests in this repo on 2026-08-08, every Copilot review carries the same bot node id. It is an account id, so nothing has to land on a pull request before it can be read. ## The change - The round-1 paragraph drops the two claims above and says a round 1 carrying no review means wait, then request it yourself. - Step 1 queries the 20 newest pull requests with the `first // empty` guard and the `[ -z ]` stop, so it resolves on a pull request with no review of its own. - The per-pull-request claim and the cold-start exception collapse into one statement of the id's scope, which drops the duplicated query block. The pull-request-scoped read stays as a note that it buys nothing over the repo-wide read and fails on exactly the round the repo-wide read handles. - `TODO.md` records the re-vendor. This propagates a procedure rather than refreshing a hash, so a downstream repo left on the old copy is wrong on the rule. The [#578](https://github.com/ptr727/ProjectTemplate/issues/578) bullet claimed to be the sole rule-propagating half of that sweep, which a second one falsifies, so it is widened to two. ## Verification Both corrected blocks were run verbatim against this repo before the prose was written, reads only, and both returned the id. `scripts/prose_lint.py` exit 0, `markdownlint-cli2` 0 issues, `editorconfig-checker` exit 0, and both files were checked line by line for CRLF, `.github/copilot-instructions.md` at 353 of 353 and `TODO.md` at 489 of 489. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) --- .github/copilot-instructions.md | 47 +++++++++++++-------------------- TODO.md | 3 ++- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6ae5c397..6819f95e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -70,24 +70,29 @@ gh api repos///pulls//reviews --jq \ | {round: (if .commit_id == \"$PR_HEAD\" then \"head\" else \"earlier\" end), id}]" ``` -**Round 1 is normally auto-seeded, so poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA). By then a prior review exists, so its bot node id is readable. A missing bot node id on round 1 therefore means "the auto-review has not landed yet - wait and poll," **not** "ask the maintainer to kick it off." +**Round 1 is normally auto-seeded, so poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes, and on some pull requests it never fires at all. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. Where it never lands, drive round 1 with the same `requestReviews` mutation every later round uses, which needs nothing this PR has to produce first. A round 1 carrying no review therefore means "wait, then request it yourself," **not** "ask the maintainer to kick it off." > **The reviewer login differs by API, in three forms rather than two.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer`, with **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]`, **with** the suffix. In a REST **timeline** `review_requested` event the `requested_reviewer` is a third spelling again, login `Copilot` with `type` `Bot`, so a filter written against either of the other two selects nothing there and reports a pull request with requests as having none. Match on the type plus a loose login test rather than on any one spelling, and each query below uses the correct form for its API. ```sh -# 1. PR node id + the Copilot reviewer's bot node id (read from any existing -# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). +# 1. PR node id, plus the reviewer bot's node id read across the repo's recent PRs. +# The bot id is the reviewer account's own, so every PR in the repo carries the same one. +# The reviewer login is `copilot-pull-request-reviewer` in GraphQL. PR_NODE=$(gh pr view --json id --jq '.id') BOT_ID=$(gh api graphql -f query=' { repository(owner: "", name: "") { - pullRequest(number: ) { - reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } + pullRequests(first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { + nodes { reviews(first: 20) { nodes { author { __typename login ... on Bot { id } } } } } } } -}' --jq '[.data.repository.pullRequest.reviews.nodes[] +}' --jq '[.data.repository.pullRequests.nodes[].reviews.nodes[] | select(.author.login == "copilot-pull-request-reviewer") - | .author.id] | first') + | .author.id] | first // empty') +if [ -z "$BOT_ID" ]; then + echo "no Copilot review in the 20 most recent PRs, so widen the window" >&2 + return 1 2>/dev/null || exit 1 # Stop. Do NOT call requestReviews with an empty id. +fi # 2. Re-request a Copilot review on the current head. gh api graphql -f query=' @@ -98,28 +103,9 @@ mutation($pr: ID!, $bot: ID!) { }' -F pr="$PR_NODE" -F bot="$BOT_ID" ``` -The bot node id is read from an existing Copilot **formal** review (`pullRequest.reviews`), so step 1 needs at least one prior formal review on the PR, and the auto-review-on-open normally supplies the first one (it may have **no inline comments**, which still counts, and its bot node id is still readable). Poll for it (give auto-review-on-open a few minutes) before deciding it is missing. - -**Cold start (round 1 not yet landed): read the id repo-wide, not from this PR.** The Copilot reviewer's bot node id is the reviewer bot *account's* node id and is **stable across every PR in the repo**. So a freshly opened PR that has neither a formal review nor an issue comment yet does **not** need UI seeding to bootstrap the id: read it from any prior Copilot review anywhere in the repo, then feed it into the `requestReviews` mutation to drive round 1. Query the **most recent** PRs (`first: 20` with an explicit newest-first order, since plain `last: 20` returns the *oldest* PRs, which may predate Copilot on the repo), and **guard for an empty result**, since an empty `$BOT_ID` means none of the sampled PRs carry a Copilot review. Widen the window (raise the count or paginate) before concluding the repo has never had one and falling back to UI seeding. Never feed an empty id into the mutation: - -```sh -BOT_ID=$(gh api graphql -f query=' -{ - repository(owner: "", name: "") { - pullRequests(first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { - nodes { reviews(first: 20) { nodes { author { __typename login ... on Bot { id } } } } } - } - } -}' --jq '[.data.repository.pullRequests.nodes[].reviews.nodes[] - | select(.author.login == "copilot-pull-request-reviewer") - | .author.id] | first // empty') -if [ -z "$BOT_ID" ]; then - echo "no Copilot review in the 20 most recent PRs - widen the window, else fall back to UI seeding" >&2 - return 1 2>/dev/null || exit 1 # stop; do NOT call requestReviews with an empty id -fi -``` +**The bot node id belongs to the reviewer account, not to a pull request**, and it is the same id on **every PR in the repo**, so nothing has to land on this PR before step 1 can read it. A PR opened a minute ago, with no review and no comment of its own, needs no UI seeding to bootstrap the id and no prior review to source it from: any Copilot review anywhere in the repo carries it. Query the **most recent** PRs, since a plain `last: 20` returns the *oldest* ones, which may predate Copilot on the repo. **Guard for an empty result**, because an empty `$BOT_ID` says only that none of the PRs sampled carry a Copilot review, so widen the window (raise the count or paginate) before concluding the repo has never had one. Never pass an empty id to the mutation. -If Copilot posted **only an issue comment** on this PR and no formal review, you can instead read the id from that comment's author (`pullRequest.comments` -> author `... on Bot { id }`). Manual UI seeding is the last resort, needed only for a repo that has **never** had a Copilot review, so no prior id exists anywhere to read. Use the mutation for every subsequent re-request. +A read scoped to this PR (`pullRequest(number: ) { reviews }`) returns the same id once a review has landed here, and it buys nothing over the repo-wide read while failing on exactly the round the repo-wide read handles. Where the repo's only Copilot artifact is an issue comment rather than a formal review, read the id from that comment's author instead (`pullRequest.comments` -> author `... on Bot { id }`). Manual UI seeding is the last resort, needed only for a repo that has **never** had a Copilot review, so no prior id exists anywhere to read. **Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. @@ -355,6 +341,11 @@ After the final push, sweep-resolve stale older threads for removed code paths. - **Proved against** - the 25 most recent merged pull requests as of `develop` at `756a53e`, the corpus on which the anchored verb form that ships instead raises one reference, and that one true. - **Delete when** - `claims` stops reading a description for commit references. +- **The GraphQL `pullRequests` connection defaults to `states: [OPEN]`, so the bot node id query in "Triggering and Polling" returns nothing in a repository whose Copilot-reviewed pull requests have all merged.** Raised against the repo-wide read, on the ground that a cold start is exactly the case where no open pull request carries a review. + - **Disproved by** - running the connection both ways against this repository while exactly one pull request was open. With `states` omitted, `pullRequests(first: 5, orderBy: { field: CREATED_AT, direction: DESC })` answers `628 OPEN`, `627 MERGED`, `626 MERGED`, `625 MERGED` and `624 MERGED`, so the omitted default is every state rather than `OPEN`. The same call with `states: [OPEN]` answers `628 OPEN` alone, which is the behavior the finding predicts for the first form and is what distinguishes them. The read was first run when this repository had no open pull request at all, and it returned the id from merged ones. + - **Proved against** - the `BOT_ID` query in "Triggering and Polling" in this file, run against this repository's pull request list on 2026-08-08. + - **Delete when** - that query names `states` explicitly, or stops reading pull requests to find the id. + - **"The agent check branches" in `STANDUP.md` section 0 is a subject-verb disagreement, and should read "The agent checks branches".** Raised as a suppressed finding against a line the change under review only touched as diff context. - **Disproved by** - reading the sentence against the snippet it describes. The subject is the noun phrase "the agent check", meaning the check for the signing agent, and "branches" is its verb, which is what the `if [ ... = ssh ]; then ssh-add -L; else gpg --list-secret-keys; fi` line does. The proposed reading needs "branches" as a plural noun, and the paragraph is section 0, before a repository exists, where the alternatives it names are the SSH and GPG forms rather than refs. - **Proved against** - the paragraph following the agent snippet in `STANDUP.md` section 0 on `develop` at `676a2bd`, unchanged since `77be3a3`. diff --git a/TODO.md b/TODO.md index 7e6955f7..0e53970a 100644 --- a/TODO.md +++ b/TODO.md @@ -353,8 +353,9 @@ Regenerate [reports/divergences.md][divergences-report] before using it as the w - **Detail** - Two comment lines in [`.markdownlint-cli2.jsonc`][markdownlint] took the same capitalization, and that file is `verbatim` and `whole`, so every downstream copy is byte-mismatched on a config nothing else changed about. - **Detail** - [`CODESTYLE.md`][codestyle] is the fifth file, at `intent` rather than `verbatim`, so it reaches the fleet as a rule each repo adopts in its own copy, and the same mixed spelling waits in every downstream tree. - **Detail** - [`.github/copilot-instructions.md`][copilot-instructions] is the sixth, also at `intent`, where "Reply and Thread Resolution Workflow" now leads with the hub's reply helper and keeps the hand-run mutations as the cross-owner and unreachable-hub path. A repo taking the old copy is not broken by it, since the mutations it documents still work, so this rides the visit rather than gating it. + - **Detail** - The same file's "Triggering and Polling" reads the reviewer bot's node id across the repo's newest pull requests rather than from the pull request under review, because the id is the reviewer account's own and is identical on every pull request in the repo, read as one value across all eight of the newest here on 2026-08-08. This is the other part that propagates a procedure rather than refreshing a hash, so a repo left on the old copy reads its own runbook as requiring a review on the pull request before the id can be read, and hands round 1 to the maintainer to seed through the UI whenever auto-review-on-open does not fire, which is the hand-off the mutation exists to remove. - **Detail** - The prose batch rewrote punctuation in five `verbatim` [`GOVERNANCE.md`][governance] sections, "Branching Model", "Release Model", "Documentation Style Conventions", "PR Review Etiquette" and "Workflow YAML Conventions", so every downstream copy of those five is byte-mismatched and the audit reports it as stale. No rule changed meaning, so the re-vendor is a hash refresh rather than a propagation, and a repo taking the old copy is correct on the rule while wrong on the bytes. - - **Detail** - The [#578][issue-578] widening is the one part of this sweep that propagates a rule rather than refreshing a hash, so a repo left on the old copy is wrong on the rule and not merely on the bytes, which makes it the half to carry first. It touches three `verbatim` [`GOVERNANCE.md`][governance] sections, "Branching Model", "Communicating with the User" and "Operational Repositories", and the third of those matters most on the two `operational` repos that can act on it. [`WORKFLOW.md`][workflow] took a cross-reference in the same change and is `intent`, so nothing reports it. + - **Detail** - The [#578][issue-578] widening is one of the two parts of this sweep that propagate a rule rather than refreshing a hash, the runbook correction above being the other, so a repo left on the old copy is wrong on the rule and not merely on the bytes, which makes the pair the half to carry first. It touches three `verbatim` [`GOVERNANCE.md`][governance] sections, "Branching Model", "Communicating with the User" and "Operational Repositories", and the third of those matters most on the two `operational` repos that can act on it. [`WORKFLOW.md`][workflow] took a cross-reference in the same change and is `intent`, so nothing reports it. - **Detail** - [`WORKFLOW.md`][workflow] is the seventh file and `repo-config/README.md` joins [`CODESTYLE.md`][codestyle] and [`.github/copilot-instructions.md`][copilot-instructions] at `intent`, where a punctuation-only edit produces no hash and therefore no audit finding at all. Nothing reports these, which is why they are recorded here rather than left to the run. `HISTORY.md` is `presence` and is each repo's own changelog, so its one fix owes nothing downstream. - **Carry the `Local Verification` heading into every repository's `OPERATIONS.md`.** The heading leads the file and states what verifying a change there requires, naming the part of the repo's contract CI structurally cannot exercise, and a repo whose gates are entirely in CI says that under it rather than omitting it. From 6df14c00698e1cf5cb5657c209163a5ae3fb8a4a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 8 Aug 2026 12:38:05 -0700 Subject: [PATCH 3/3] Record the Cluster for Documenting a Cross-Owner Write Grant (#629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Disposition** `New entry`, as a new cluster under "Work Clusters". Nothing in the file reasons about the write-safety kit's documentation, so there was no entry to amend. No closing keyword, since a feature to `develop` pull request never fires one. ## The gap The hook denies a `gh` write whose explicit target is under an owner other than the checkout origin's, and the only way past it is a maintainer grant in `GH_WRITE_GUARD_ALLOW`. Read against `develop` at `92b9fc5`: | Document | What it says about the variable | | --- | --- | | `docs/host-setup.md` "Agent Write-Safety Kit" | Nothing. This is the section a developer follows to install the kit. | | `host-setup/agent-safety/README.md` | Names it once, inside the bullet describing what the hook *denies*. Gives the token format (`owner/repo`, `owner/*` for a whole owner) and not how to set it. | | `README.md` | No host-protection section, only a pointer to `docs/host-setup.md`. | So a reader who hits a denial is told a grant exists and never how to make one. ## What the entry carries - **The channel that works** is an `env` block in the checkout's `.claude/settings.local.json`. It is per project rather than host wide, so a grant made for one checkout does not follow the agent into another repository's sessions. - **The raising case is a fork**, where `origin` is under the maintainer's own owner and `upstream` is the project it forked from. Filing an issue or pull request against the upstream is the cross-owner write and everything aimed at the fork is not, so the grant names the upstream alone. That asymmetry is what a reader meets first, and it is why the section wants a worked example rather than a definition. - **The two forms a reader reaches for first cannot work**: an inline `GH_WRITE_GUARD_ALLOW=owner/repo gh ...` prefix, and an `export` in a shell call. The hook runs as its own process and reads only the environment the session was launched with, which is exactly what makes a grant a deliberate act taken outside the session rather than something an agent can do for itself once blocked. `gh-write-guard.py` already asserts the inline prefix denies, so the behavior is settled and only the explanation is missing. - **Two things to carry beside the example**: that a session restart loads the grant, and a way to confirm one took, since inferring it from a write that no longer denies means learning the answer by making the write. This change records the work. It does not do it, so both documents are untouched here. ## Verification `scripts/prose_lint.py` exit 0, `markdownlint-cli2` 0 issues, `editorconfig-checker` exit 0, `TODO.md` at 507 of 507 CRLF lines. The three new reference definitions sort by reference name, `[host-setup]` after `[governance]`, `[readme]` before `[readme-structure]`, and `[write-guard-readme]` after `[write-guard]`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) --- TODO.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/TODO.md b/TODO.md index 0e53970a..2e74618b 100644 --- a/TODO.md +++ b/TODO.md @@ -318,6 +318,21 @@ The review loop ends by replying on a thread and resolving it, and both halves f - **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. +### The Grant That Unblocks a Cross-Owner Write + +One pull request documenting how a maintainer grants a write target the guard denies. The hook denies a `gh` write whose explicit target is under an owner other than the checkout origin's, and the only way past it is a grant in `GH_WRITE_GUARD_ALLOW`, which no document tells a reader how to give. + +**State** `ready`. **Touches** [`docs/host-setup.md`][host-setup] and [`host-setup/agent-safety/README.md`][write-guard-readme]. **Cost** one pull request, prose only, and hub-only, since the kit is host state rather than carried repo content. + +- **Document the grant where a reader already is when the denial arrives, which is the install section rather than the hook's description.** A denial names the variable, and the two documents that could explain it either never mention it or mention it while describing what the hook refuses, so the reader is told a grant exists and never how to make one. + - **Blocked by** - Nothing. + - **Issue** - None filed. + - **Checked** - `develop` at `92b9fc5` on 2026-08-08, where "Agent Write-Safety Kit" in [`docs/host-setup.md`][host-setup] never names the variable at all, [`host-setup/agent-safety/README.md`][write-guard-readme] names it once inside the bullet listing what the hook denies, giving the token format and not how to set it, and [`README.md`][readme] carries no host-protection section, only a pointer to the first. + - **Detail** - The channel that works is an `env` block in the checkout's `.claude/settings.local.json`, holding `GH_WRITE_GUARD_ALLOW` set to the target. It is per project rather than host-wide, which is the property worth stating, since a grant made for one checkout does not follow the agent into another repository's sessions. + - **Detail** - The case that raised it is a fork, where `origin` is under the maintainer's own owner and `upstream` is the project it forked from. Filing an issue or a pull request against the upstream is the cross-owner write, and everything aimed at the fork is not, so the grant names the upstream alone and the denial appears only on the half that leaves the owner. That asymmetry is the part a reader hits first and the reason a worked example beats a definition here. + - **Detail** - The two forms a reader reaches for first both fail, silently in the sense that the write simply stays denied: an inline `GH_WRITE_GUARD_ALLOW=owner/repo gh ...` prefix, and an `export` in a shell call. The hook runs as its own process and reads only the environment the session was launched with, which is exactly what makes a grant a deliberate act taken outside the session rather than something an agent can do for itself once blocked. The behavior is settled and tested, since [`gh-write-guard.py`][write-guard] already asserts the inline prefix denies, so what is missing is only the explanation. + - **Detail** - Two things worth carrying beside the example: that a session restart is what loads the grant, and a way to confirm one took, since inferring it from a write that no longer denies means learning the answer by making the write. + ## Standalone Chores Small work with no research to preserve, selectable one bullet at a time. @@ -468,12 +483,14 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [divergences-report]: ./reports/divergences.md [files]: ./spec/files.json [governance]: ./GOVERNANCE.md +[host-setup]: ./docs/host-setup.md [markdownlint]: ./.markdownlint-cli2.jsonc [matrix]: ./reports/conformance-matrix.md [merge-bot]: ./.github/workflows/merge-bot-pull-request.yml [operations]: ./OPERATIONS.md [project-types]: ./spec/project-types.json [prose-gate]: ./.github/actions/prose-gate/action.yml +[readme]: ./README.md [readme-structure]: ./spec/readme-structure.md [reports]: ./reports/ [repos]: ./registry/repos.json @@ -487,3 +504,4 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i [workflow]: ./WORKFLOW.md [workflows]: ./catalog/snippets/workflows/ [write-guard]: ./host-setup/agent-safety/gh-write-guard.py +[write-guard-readme]: ./host-setup/agent-safety/README.md