You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
State the Recurring Case in Three Rules That Only State the Common One (#627)
Ships the `ready` cluster carrying
[#578](#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) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: GOVERNANCE.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,7 +58,7 @@ Agent-authored text illustrates with data the agent constructed, never with data
58
58
-**Executing a `develop -> main` promotion safely, two traps both learned the hard way:**
59
59
-**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/<owner>/<repo>/git/refs" -f ref=refs/heads/develop -f sha="$(gh pr view <n> --json headRefOid --jq .headRefOid)"`.
60
60
-**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 <file>`) **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).
61
-
-**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.
61
+
- **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.
62
62
-**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.
63
63
-*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.
64
64
-*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`.
104
104
105
105
-**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.
106
106
-**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.
107
+
- **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.
107
108
-**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`.
108
109
-**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.
109
110
-**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
337
338
338
339
## Communicating with the User
339
340
340
-
-**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.
341
+
- **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.
341
342
-**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.
342
343
- **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.
0 commit comments