@erclx/canon 4.119.0 → 4.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/create-skill/SKILL.md +2 -1
  4. package/claude/skills/git-pr/REQUIREMENT.md +1 -0
  5. package/claude/skills/git-pr/SKILL.md +34 -4
  6. package/claude/skills/review-pr/SKILL.md +13 -242
  7. package/claude/skills/review-pr/references/close-out.md +30 -0
  8. package/claude/skills/review-pr/references/post.md +145 -0
  9. package/claude/skills/review-pr/references/scope.md +82 -0
  10. package/claude/skills/role-orchestrator/REQUIREMENT.md +9 -16
  11. package/claude/skills/role-orchestrator/SKILL.md +9 -153
  12. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -193
  13. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +22 -0
  14. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +85 -0
  15. package/claude/skills/role-orchestrator/references/orchestrator-output.md +61 -0
  16. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  17. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +56 -0
  19. package/claude/skills/role-orchestrator/references/orchestrator-review-fallback.md +18 -0
  20. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +1 -1
  21. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  22. package/docs/agents/commands.md +2 -1
  23. package/docs/agents/counts.md +1 -1
  24. package/docs/agents/index.md +8 -3
  25. package/docs/agents/pr-evidence.md +19 -3
  26. package/docs/agents/pr-local.md +102 -0
  27. package/docs/agents/pr-preview.md +2 -2
  28. package/docs/agents/records-push.md +61 -0
  29. package/docs/agents/records-validate.md +73 -0
  30. package/docs/agents/records.md +3 -119
  31. package/docs/agents/sessions-transfer.md +91 -0
  32. package/docs/agents/sessions.md +3 -82
  33. package/docs/agents/skills-audit.md +2 -2
  34. package/docs/agents/teach-authoring.md +112 -0
  35. package/docs/agents/teach.md +3 -103
  36. package/docs/target-projects.md +1 -1
  37. package/docs/workflow/ai-workflow.md +1 -1
  38. package/docs/workflow/visual-design-workflow.md +1 -1
  39. package/governance/rules/claude/570-skill.md +2 -1
  40. package/package.json +1 -1
  41. package/scripts/core/check-ignore-parity.sh +1 -1
  42. package/scripts/lib/sandbox-git.sh +1 -1
  43. package/src/commands/pr.ts +324 -2
  44. package/src/context/classify/extract.ts +2 -2
  45. package/src/counts/scan.ts +1 -1
  46. package/src/design/tokens.ts +1 -1
  47. package/src/gate/stages.ts +2 -2
  48. package/src/pr/evidence.ts +42 -2
  49. package/src/pr/local.ts +125 -0
  50. package/standards/index.md +1 -0
  51. package/standards/skill-requirement.md +72 -0
  52. package/standards/skill.md +34 -152
  53. package/tooling/astro/reference.md +1 -1
  54. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +32 -0
package/README.md CHANGED
@@ -162,8 +162,8 @@ Each domain carries an entry written for someone maintaining the toolkit rather
162
162
  - [Governance rules](canon/context/governance/index.md)
163
163
  - [Standards](canon/context/standards/index.md)
164
164
  - [Snippets](canon/context/snippets.md)
165
- - [Tooling stacks](canon/context/tooling.md)
166
- - [Design system](canon/context/design.md)
165
+ - [Tooling stacks](canon/context/tooling/index.md)
166
+ - [Design system](canon/context/design/index.md)
167
167
  - [Slides](canon/context/slides.md)
168
168
  - [Transcripts](canon/context/transcripts.md)
169
169
  - [Sandbox](canon/context/sandbox/index.md)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.119.0",
4
+ "version": "4.120.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -8,6 +8,7 @@ description: Creates a new `SKILL.md` in `.claude/skills/`. Use when asked to "c
8
8
  Read these files in parallel:
9
9
 
10
10
  - `${CLAUDE_SKILL_DIR}/../../standards/skill.md`: skill structure, skill types, frontmatter fields, invocation rules
11
+ - `${CLAUDE_SKILL_DIR}/../../standards/skill-requirement.md`: what `REQUIREMENT.md` must answer, its sections, and its template
11
12
  - `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: banned words, punctuation, and formatting for skill body text
12
13
  - The `write-human` skill: voice, rhythm, and sentence construction for skill body text
13
14
 
@@ -22,7 +23,7 @@ Read these files in parallel:
22
23
  - Does the body carry a procedure a session would get wrong from first principles? A one-line wrapper around a single command needs no skill.
23
24
  - Will anything invoke it other than the author typing its name? Carry this one into the drafted `REQUIREMENT.md`'s `Must not` section as a review criterion instead, since nothing can answer it before the skill has run.
24
25
  2. Draft the full `SKILL.md` from the user's description, stating the current rule alone. An incident and the date it happened belong in the requirement's `Gap`, not in the body or a reference.
25
- 3. Draft the sibling `REQUIREMENT.md` from what the skill is for, in the shape the standard states. Write the gaps from the user's description rather than from the drafted body, since a requirement derived from the body records whatever the draft overfitted to.
26
+ 3. Draft the sibling `REQUIREMENT.md` from what the skill is for, in the shape the requirement standard states. Write the gaps from the user's description rather than from the drafted body, since a requirement derived from the body records whatever the draft overfitted to.
26
27
  4. Confirm the skill name and both files with the user before writing
27
28
  5. Write to `.claude/skills/<name>/SKILL.md` and `.claude/skills/<name>/REQUIREMENT.md`
28
29
  6. Run `canon claude skills audit --json` and read `findings.datedProvenance` for rows under `.claude/skills/<name>/`. Move each row's incident into `REQUIREMENT.md` under `Gap`, cut the date from the file the row names, and re-run until no row names the folder.
@@ -30,6 +30,7 @@ A lookup that resolves by head branch alone carries its own failure. A branch na
30
30
  - Report a refused label, since a warning nothing surfaces leaves the run indistinguishable from one that labelled
31
31
  - Put a preview address on a pull request that changes a rendered surface when the project's deploy can mint one, since screenshots and a checklist leave a reviewer nothing to click into before merging
32
32
  - Bound the wait on that preview and finish the chain without the link on a timeout, since a slow deploy must not hold the ship
33
+ - Put the branch's running local server on the same pull request when this worktree is serving one, and remove it when the pull request closes, since a reviewer on the operator's machine can interact with the branch and a link that outlives it points at whatever runs there next
33
34
 
34
35
  ## Must not
35
36
 
@@ -178,15 +178,35 @@ When it exists, scan it against `${CLAUDE_SKILL_DIR}/../../standards/publish.md`
178
178
 
179
179
  Where it lands is decided by the evidence step below rather than here, since a checklist reads next to the screenshots it annotates and posting it on its own is the fallback for a branch that changed no screenshot.
180
180
 
181
+ ### Find the local server
182
+
183
+ A reviewer on the operator's machine can open the branch's running dev server, which screenshots and a checklist cannot stand in for. Ask the verb for the server this worktree is running before the evidence step, so the first evidence comment already carries the link:
184
+
185
+ ```bash
186
+ canon pr local --json
187
+ ```
188
+
189
+ The verb reads the listening sockets on this machine, keeps those whose process runs inside this worktree, and reports the lowest port serving an HTML page. A server in another worktree or in the main checkout is never reported, since it shows a different branch. `canon docs pr-local` states the contract. Read `reason` on the record rather than the exit code.
190
+
191
+ - `ok`: hold the record's `url` for the evidence step below.
192
+ - `no-server` or `no-listener-reader`: hold the reason for the closing report, pass no link, and move on. Do not start a server here, since one started inside the ship chain has no owner to stop it.
193
+ - Anything else, including no record at all: move on silently. An installed binary older than the verb answers with an unknown-subcommand error, which is a skip rather than a stop.
194
+
195
+ Pass `--local` to the evidence step only on an `ok` here. The binary answering `ok` is the one carrying the flag, so a binary lacking the verb never meets a flag it would reject.
196
+
197
+ With both `--checklist` and `--local`, a diff carrying no evidence image still renders `ok`, with the link opening a comment the checklist closes, so a reviewer gets both in one place. A local link with neither an evidence image nor a checklist still reports `no-evidence`, which keeps a branch that changed no rendered surface from getting a comment holding a link and nothing else.
198
+
199
+ A seeded workflow replaces the line with a note once the pull request closes, so the link does not outlive the branch it points at.
200
+
181
201
  ### Post the evidence comparison
182
202
 
183
- Run the verb once against the number the pull request step above resolved, passing `--checklist` when the step above found a file and leaving it off when it did not:
203
+ Run the verb once against the number the pull request step above resolved. Pass `--checklist` when the checklist step found a file, and `--local` when the step above returned `ok`. Leave each flag off otherwise:
184
204
 
185
205
  ```bash
186
- canon pr evidence <number> --checklist <main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md --json
206
+ canon pr evidence <number> --checklist <main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md --local <url> --json
187
207
  ```
188
208
 
189
- One call answers both questions because a checklist does not decide `no-evidence`. The verb reports `no-evidence` on a diff carrying no evidence image whether or not a checklist came with it, so the branch below reads the same `reason` it would have read without the flag, and the checklist is folded in only on the path that has a comparison to fold it into.
209
+ One call answers both questions because a checklist does not decide `no-evidence`. The verb reports `no-evidence` on a diff carrying no evidence image whether or not a checklist came with it, so the branch below reads the same `reason` it would have read without the flag, and the checklist is folded in only on the path that has a comparison to fold it into. The local link is the one exception, stated in the step above.
190
210
 
191
211
  Pass `--checklist` only for a file that exists. The verb refuses as `unreadable-checklist` on a path it cannot read or one holding nothing, which is a caller bug rather than a transient failure, so stop and repair the path rather than posting a body with the checklist silently dropped.
192
212
 
@@ -211,7 +231,7 @@ Any other `reason` is one of the mirrored git refusals (`gh-missing`, `gh-failed
211
231
 
212
232
  ### Post the UI checklist alone
213
233
 
214
- Run this step only when the evidence step above did not carry the checklist, meaning it reported `no-evidence` or one of the git refusals, and a checklist file exists. Post it as its own comment on `<number>`:
234
+ Run this step only when the evidence step above did not carry the checklist, meaning it reported `no-evidence` or one of the git refusals, and a checklist file exists. A local link sends the checklist through the evidence step instead, so this step runs on a branch with no evidence image only when the local step found no server. Post it as its own comment on `<number>`:
215
235
 
216
236
  ```bash
217
237
  gh pr comment <number> --body-file <main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md
@@ -251,6 +271,8 @@ The verb dispatches the project's Pages deploy on the pull request's branch and
251
271
  canon pr evidence <number> --preview <url> --json
252
272
  ```
253
273
 
274
+ The hosted address takes the first line and a local link already on the comment moves to the second, carried forward with no flag.
275
+
254
276
  - `no-deploy`: the project deploys nothing a dispatch can start. Say nothing and move on.
255
277
  - Any other `reason`, being `unfenced`, `no-alias`, `run-failed`, `timeout`, or a mirrored `gh` refusal: report it with the record's `message` and move on without stopping the chain. A missing link costs the reviewer a click, and a held ship costs the whole chain.
256
278
 
@@ -298,4 +320,12 @@ Add a line when the preview step returned an address:
298
320
 
299
321
  `🔗 Preview: <url>`
300
322
 
323
+ Add a line when the evidence step posted the local link:
324
+
325
+ `🖥️ Local preview: <url>`
326
+
327
+ Add a line instead when the local step refused as `no-server` or `no-listener-reader` and the evidence or checklist step posted something, since that pull request changes a rendered surface and a reviewer could have used the link. To add it later, start the server, run `canon pr local --json`, then run `canon pr evidence <number> --local <url> --json`, adding `--checklist` when one is still owed, and post the body the way the evidence step does. `git-followup` only carries a line forward, so it cannot add one the comment never had:
328
+
329
+ `🖥️ No local preview: <reason>`
330
+
301
331
  Do not add any other text.
@@ -15,8 +15,14 @@ review against the whole change, and every later pass reads only the commits
15
15
  added since. The heading reports state rather than pass number: a pass carrying
16
16
  anything owed takes `## Review`, and `## Review closed` covers a pass carrying
17
17
  none, so the most recent comment's heading reports whether any work is owed.
18
- Owed is a finding at any severity, a Testing question, or a reviewer request nobody has answered, defined once at Step 4. Every pass is this skill, and which one it is gets detected from
19
- the thread rather than named by the caller.
18
+ Every pass is this skill, and which one it is gets detected from the thread
19
+ rather than named by the caller.
20
+
21
+ The threshold is stated here and nowhere else, and every other surface acting on it cites this skill rather than restating the grades. One rule governs both the heading and the dispatch: a pass carrying anything owed takes `## Review` and owes a dispatch to the session holding the branch, and a pass carrying nothing at all takes `## Review closed` and owes none. Owed covers a finding at any severity, a Testing question, and a reviewer request nobody has answered alike, which is what keeps the two halves from separating. Sending that dispatch is `role-orchestrator`'s step rather than this one, which posts and stops. Post the open heading whether it is the first pass or the fourth. A pull request thread then reads as `## Review`, the worker's answer under `## Review response` from `review-address`, another `## Review` while anything stays open, and `## Review closed` when nothing does.
22
+
23
+ Both of this skill's headings anchor as a section distinct from human threads, and neither invents beyond what the whole set already states. That set is six headings across three families, stated here once so `role-orchestrator`'s poll and every reply-posting skill cite it rather than carry a copy. The review family, `## Review` and `## Review closed`, belongs to this skill alone. The reply family, `## Review response`, `## Rebase`, and `## Post-review findings`, belongs to `review-address`. The third family is a single heading, `## Evidence`, which `canon pr evidence` posts through `git-pr` and `git-followup` and which answers no comment already on the thread, so the poll excludes it from the unclassified count without reading it as a reply that owes a re-review.
24
+
25
+ The first reply heading answers a finding this skill posted, the second reports a stale branch resolved without one, and the third carries a finding a worker produces after a close-out rather than in answer to one already on the thread, since a finding produced late is still a finding. A comment posted under a heading outside these six reaches the poll as unclassified rather than as silence, so an invented seventh heading is a gap the next run reports instead of one it repeats.
20
26
 
21
27
  One live verdict stands at a time. A pass that would repeat a standing
22
28
  close-out rewrites that comment in place rather than posting beside it, so a
@@ -53,79 +59,7 @@ Coding standards from `.claude/rules/` are auto-loaded by Claude Code.
53
59
 
54
60
  ## Step 2: scope the read
55
61
 
56
- Find the commit the last pass covered and the verdict it posted:
57
-
58
- ```bash
59
- canon pr review-state <number> --json
60
- ```
61
-
62
- The fields are `<prior-oid>` off `commit`, `<prior-heading>` off `heading`, and `<prior-at>` off `readAt // submittedAt`. The commit scopes the read below, the heading feeds the repeat guard at the end of this step, and the instant scopes the reply query further down, so one call answers all three rather than three reads of the same review. Branch on the record rather than on the exit code, which a shell function wrapping `canon` can flatten to zero.
63
-
64
- `source` says which stamp answered. `marker` is the pass's own read-time record and is the authority. `fallback` is a pass posted before this mechanism shipped, so its commit is whatever the head was when GitHub recorded the review rather than what that session read, and a push inside its compose window is invisible. `none` is a thread carrying no pass at all.
65
-
66
- Do not read `commit.oid` or `submittedAt` off `gh pr view --json reviews` here. Both are stamped at submission, so a push landing between a pass's read and its post moves them onto a commit that pass never saw, and this step then scopes the delta past it and reports it covered.
67
-
68
- A target whose CLI predates the verb meets a missing subcommand rather than a record. Fall back there to the jq below, which reads the stamps and carries the defect above, and say the fallback answered so a reader can tell a marker read from a stamped one:
69
-
70
- ```bash
71
- gh pr view <number> --json reviews --jq '[.reviews[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review" or . == "## Review closed")] | last | select(. != null) | ((.commit.oid // "") + "\t" + (.body | split("\n")[0] | rtrimstr("\r")) + "\t" + (.submittedAt // ""))'
72
- ```
73
-
74
- The three fields are `<prior-oid>`, `<prior-heading>`, and `<prior-at>`. Keep the `select(. != null)` guard, since the string concatenation aborts jq on the null an empty selection returns, and an aborted command reaches the session as an error rather than as the empty result the first-pass branch reads.
75
-
76
- Match the first line for equality against the two headings this skill posts. A prefix test also matches `## Review response` and any heading merely starting with those words, which would scope the pass to whatever commit that comment carried. The `\r` trim covers a body composed in the GitHub web editor, which stores CRLF.
77
-
78
- A `source` of `none`, or an empty result from the fallback, is a first pass. Read the whole change:
79
-
80
- ```bash
81
- gh pr diff <number>
82
- ```
83
-
84
- ```bash
85
- gh pr diff <number> --name-only
86
- ```
87
-
88
- A commit is a later pass. Fetch the pull request head so both commits are local:
89
-
90
- ```bash
91
- git fetch -q origin pull/<number>/head
92
- ```
93
-
94
- A failed fetch stops the skill: `❌ Could not fetch the PR head. Retry once the remote is reachable.` Do not fall through to the full pass. A fetch failure and a rebase both leave the prior commit unreachable, and the fallback below states a rebase as fact on the pull request, so conflating the two publishes a claim the skill never checked.
95
-
96
- Then test that the prior commit still reaches the head:
97
-
98
- ```bash
99
- git merge-base --is-ancestor <prior-oid> <headRefOid>
100
- ```
101
-
102
- On exit zero, review `<prior-oid>..<headRefOid>` and nothing else. `git diff` and `git log --oneline` over that range are the whole read, because the first pass already covered everything behind it. A non-zero exit means the branch was rebased or force-pushed, so the delta is undefined rather than empty. Fall back to the full pass above and say so in the body.
103
-
104
- A commit is its own ancestor, so an unchanged head passes that test too, with an empty range. When `<prior-oid>` equals `<headRefOid>`, decide whether this pass has anything to add before reading anything else, since the empty range itself cannot answer that:
105
-
106
- ```bash
107
- gh pr view <number> --json comments --jq '[.comments[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Rebase" or . == "## Post-review findings") | select(.createdAt > "<prior-at>")] | last | .url // empty | split("-") | last'
108
- ```
109
-
110
- `<prior-at>` is the instant Step 2 resolved above, which is the prior pass's `readAt` where it wrote one. Reading `submittedAt` off the thread here instead is the same submission-time defect on the time axis: a reply posted inside that pass's compose window sorts before the stamp and reads as already answered, when in fact the pass had stopped reading before it landed.
111
-
112
- Scope the replies to those newer than the prior pass, never to every reply the thread carries. A pass answering the newest reply and a pass answering an older one derive the same third segment (Step 4), so an unscoped read hands a re-run after a close-out the name its own prior pass already wrote. That is the collision this case exists to prevent, reached without a rebase or an error.
113
-
114
- Read the number off `.url`. The `id` field carries a GraphQL node id, which the thread never displays. Keep the `// empty` guard, since `split` aborts jq on the null an empty selection returns, and an aborted command reaches the session as an error rather than as the empty result the stop below reads.
115
-
116
- An empty result means no reply arrived since the prior pass, so the head is unchanged and this pass has nothing new to add. Stop here, before Step 3 or Step 4 run: `❌ The head is unchanged since the prior pass on <short-sha>. Nothing new to review.` This is the earliest point every path crosses, which is why the check sits here rather than inside Step 4's filename derivation. A path that decides there is nothing to add never reaches a step reached only when composing a body, so a stop written there is a stop a shortcut path can route around.
117
-
118
- A non-empty result carries the comment id Step 4 needs for the third filename segment. A `## Review response` or a `## Rebase` reply answers a finding already argued or reports a stale branch resolved without one, so read it for what the worker changed or accepted, treat an accepted finding as closed rather than restating it, and skip the diff and file reads below. This is the entire read on a repeated head for either heading, since the empty range above has nothing in it to say whether a prior finding landed and neither reply needs anything more to answer that.
119
-
120
- A `## Post-review findings` reply carries no argued finding behind it, since it asserts a new defect rather than answering one, and this pass is its first independent reader. Restating it as a finding without opening anything is repeating the worker's claim rather than checking it. Read the file the comment names at `<headRefOid>`, the same `git show <headRefOid>:<path>` read Step 3 already runs to confirm a ticked box, and confirm the defect before it becomes a finding of this pass's own.
121
-
122
- A moved head has its own way to add nothing, which the ancestor test cannot see. When `<prior-heading>` reads `## Review closed`, the standing verdict already reports the branch clear, so a pass over the new commits that raises nothing posts a comment saying what the one above it said.
123
-
124
- The producing shape is narrow. A prior pass tells the author a change is their own call, the author makes it, and the delta reaching the next pass has nothing left to say by construction.
125
-
126
- Carry `<prior-heading>` into Step 4 and run Step 3 unchanged. This guard cannot stop the pass the way the unchanged-head stop above does, because that stop reads an empty range and this one turns on what the pass carries, which is Step 3's output. Deciding ahead of the review would swallow the pass that does find something, and that is the costlier error, so the rule is stated beside its sibling and executed where the heading is picked.
127
-
128
- Read each changed file in scope. Skip deleted files. Run reads in parallel.
62
+ Read `${CLAUDE_SKILL_DIR}/references/scope.md` for finding the commit and verdict the prior pass left, the unchanged-head stop, and the range this pass reads. Read it before Step 3, since its stop ends the pass ahead of any review.
129
63
 
130
64
  ## Step 3: review
131
65
 
@@ -151,11 +85,11 @@ A `reason` of `no-claims` means the reader resolved no path out of a section tha
151
85
 
152
86
  Compose this on the body read Step 4 already performs rather than opening a second one. That step scans the title and body for a leaked phase label, and both reads answer from the same text at the same head.
153
87
 
154
- Ask rather than grade. Whether a human is genuinely required is a reading the branch author may hold a reason this session cannot see, so the question carries no severity and enters no count. What it does carry is the heading and the dispatch, on the rule Step 4 states, because a question the author never receives corrects nothing and the author is the only party who can answer it. Answering it closes it, and the answer may be that the requirement holds.
88
+ Ask rather than grade. Whether a human is genuinely required is a reading the branch author may hold a reason this session cannot see, so the question carries no severity and enters no count. What it does carry is the heading and the dispatch, on the rule the opening states, because a question the author never receives corrects nothing and the author is the only party who can answer it. Answering it closes it, and the answer may be that the requirement holds.
155
89
 
156
90
  Read `## For the reviewer` the same way, bounded to the bullets under that heading rather than the Summary or the Technical Context around it. Those two carry the author's argument for the change, and reading them while judging it is most of what an independent pass exists to avoid, so the read stops at the section itself.
157
91
 
158
- Answer each bullet in the body, on the same terms as a Testing question: no severity, no count. A request nobody can answer stays owed under the heading and dispatch Step 4 states, keeping the thread open exactly as an unanswered Testing box does. One the pass does answer carries no further weight, since the answer is discharged in the same comment that carries it.
92
+ Answer each bullet in the body, on the same terms as a Testing question: no severity, no count. A request nobody can answer stays owed under the heading and dispatch the opening states, keeping the thread open exactly as an unanswered Testing box does. One the pass does answer carries no further weight, since the answer is discharged in the same comment that carries it.
159
93
 
160
94
  On a first pass, confirm each `**Review focus:**` item the plan names against the diff, since the planning session named the inputs that would break the change before any code existed and no other reader holds that list. Report the items in a `**Review focus**` block after the `**For the reviewer**` block, one bullet per item naming what confirmed it, such as a test, a guard, or a branch in the code. The block carries no severity and enters no count. An item the pass cannot confirm becomes a `should-fix` finding under the file that should have handled it, so a reader can tell an item checked from one skipped. A plan whose section reads `None identified.`, or a branch with no plan, takes no block. The section binds the first pass only: a later pass checks whether the finding an unconfirmed item became has landed, like any other prior finding, and never re-raises a confirmed item against a delta that did not touch it.
161
95
 
@@ -165,174 +99,11 @@ A later pass applies the same axes to the delta, and adds one check the first pa
165
99
 
166
100
  A prior finding can also be settled by argument rather than by a fix. A reply naming the plan question that already declined it, or a constraint this session could not see, withdraws the finding or moves its grade. State that outcome in the body under the finding it changes, naming the fact that produced it, whether the argument arrived on the thread or through the channel that carried the dispatch. Dropping the finding from this body instead leaves a reader unable to tell a withdrawal from an oversight, and the reasoning goes with the session that heard it. A withdrawal removes the finding from the count, so a pass that withdrew every finding it carried is a close-out. Write the withdrawal and its cause into that body rather than taking the short close-out line Step 4 supplies, which reports prior findings addressed and would credit a fix nobody made.
167
101
 
168
- Use severity: `critical` (blocks merge), `should-fix` (fix before merge), `minor` (blocks nothing). The ladder ranks a finding and decides nothing about who acts on it, since every grade takes the open heading and owes a dispatch under the threshold Step 4 states.
102
+ Use severity: `critical` (blocks merge), `should-fix` (fix before merge), `minor` (blocks nothing). The ladder ranks a finding and decides nothing about who acts on it, since every grade takes the open heading and owes a dispatch under the threshold the opening states.
169
103
 
170
104
  ## Step 4: post to the PR
171
105
 
172
- Write the comment to `.canon/tmp/pr/review/body-<number>-<short-sha>.md` at the main worktree root, not the current worktree, which the rest of this step calls `<body-file>`. Resolve that root and send the write as a heredoc, both the way `session-worktree` states. The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post, and the head commit stops a second pass overwriting the first one's body.
173
-
174
- Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a name the folder already holds.
175
-
176
- When `<prior-oid>` from Step 2 equals `headRefOid`, the head repeats and the folder already holds `body-<number>-<short-sha>.md`. Add a third segment taking the id of the reply Step 2 resolved, giving `body-<number>-<short-sha>-r<comment-id>.md`, which is `<body-file>` on that path. That satisfies both prohibitions above rather than carving an exception into either. Step 2 already stopped the pass when that resolution came back empty, so reaching this line means the comment id is in hand.
177
-
178
- The comment is a rendered-for-human GitHub surface, so load the `write-human` skill for voice and follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for the banned words: cut editorializing, and keep every sentence load-bearing. Match this shape on a first pass:
179
-
180
- ```markdown
181
- ## Review
182
-
183
- X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
184
-
185
- **`path/to/file.ext`**
186
-
187
- - **should-fix**: what breaks and the fix, in two or three sentences.
188
- - **minor**: finding.
189
-
190
- **What is right**
191
-
192
- - bounded confirmation.
193
-
194
- 🤖 Reviewed by Claude Code
195
-
196
- <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
197
- ```
198
-
199
- A stale ticked box goes in a `**PR body**` block, in place of a `**`path/to/file.ext`**` block and ahead of every one of those, since it precedes the code the diff carries rather than sitting inside it.
200
-
201
- Run `canon labels scan --title "<title>" --body "<body>" --head <headRefName>` against the PR under review, since Step 1 already holds all three and this pass is the last human-shaped gate before merge. A hit lands in the same `**PR body**` block, `should-fix`, naming each finding the scan returns: a token quoted from the title or body for a phase label, a board identifier, a session link, or an unspelled word, and the broken rule's name, `structure`, a casing issue, or `length`, for a title-format hit. This reads the pull request being reviewed, distinct from the comment this pass is about to post, which the scan later in this step still covers.
202
-
203
- A later pass carrying findings keeps that shape and changes only the summary line:
204
-
205
- ```markdown
206
- ## Review
207
-
208
- Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-fix, Z minor.
209
-
210
- **`path/to/file.ext`**
211
-
212
- - **should-fix**: what breaks and the fix, in two or three sentences.
213
-
214
- 🤖 Reviewed by Claude Code
215
-
216
- <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
217
- ```
218
-
219
- A Testing box the Step 3 check raised goes in a `**Testing**` block placed after the file blocks, one bullet per box, each quoting the box and naming what would drive it. It carries no severity and enters no count, and it is still something owed, so a pass carrying one takes `## Review` and the full body rather than either ✅ line. Say so on the summary line as `plus N testing question(s)`, since the three counts read as zero and would otherwise report the pass as silent.
220
-
221
- ```markdown
222
- **Testing**
223
-
224
- - `- [ ] <the box as written>` names no capability the agent lacks. `scripts/sandbox/run.sh <arm>` drives it. Was there a reason to leave it?
225
- ```
226
-
227
- Keep it to the boxes the check raised. Restating a box whose stated requirement holds teaches the branch author to skip the block.
228
-
229
- Every `## For the reviewer` bullet Step 3 read goes in a `**For the reviewer**` block placed after the Testing block, one bullet per request, each followed by its answer or, where the pass could not answer it, by what would settle it. It carries no severity and enters no count.
230
-
231
- An unanswered bullet is owed the same way an unanswered Testing box is, so a pass carrying one takes `## Review` and the full body rather than either ✅ line. A bullet the pass answered is not owed, since the answer is discharged in the same comment that carries it. A pass still posting a numeric summary line, because a finding, a Testing question, or an unanswered bullet already forces one, says so there as `plus N reviewer request(s)`. The all-answered close-out below carries the block in place of that line and needs no addition to it.
232
-
233
- ```markdown
234
- **For the reviewer**
235
-
236
- - Confirm the 401 and 403 split reads correctly for the public API. Confirmed — `AuthService.authenticate()` returns 401 for an expired token and 403 for a missing scope, and both paths are covered under `## Testing`.
237
- ```
238
-
239
- The threshold is stated here and nowhere else, and every other surface acting on it cites this skill rather than restating the grades. One rule governs both the heading and the dispatch: a pass carrying anything owed takes `## Review` and owes a dispatch to the session holding the branch, and a pass carrying nothing at all takes `## Review closed` and owes none. Owed covers a finding at any severity, a Testing question, and a reviewer request nobody has answered alike, which is what keeps the two halves from separating. Sending that dispatch is `role-orchestrator`'s step rather than this one, which posts and stops. Post the open heading whether it is the first pass or the fourth. A pull request thread then reads as `## Review`, the worker's answer under `## Review response` from `review-address`, another `## Review` while anything stays open, and `## Review closed` when nothing does.
240
-
241
- Keying either half on the grade was measured wrong: across 8 findings on one archived pass, 3 were posted as minor and 2 of those were defects a worker fixed rather than recorded, so a floor at should-fix loses real fixes to a grade that runs low. Splitting the two halves so the dispatch fired lower than the heading was the other candidate, and it left a thread reading closed while work was owed on it. The Testing question was first written to sit outside both, which is that same split reached from the other side, and it left the one party who could answer the question with no route to it.
242
-
243
- The cost is that the merge decision no longer reads off the heading alone, since an open heading covers a minor as well as a critical. Take it from the counts on the summary line, which is where they already sit. No summary line reports the merge as unblocked under either heading, since a thread reading open cannot also report that nothing blocks it.
244
-
245
- A minor the dispatched worker declines is what needs a surface that survives the merge, rather than every minor, since one that gets fixed on the branch needs no durable record. Write a declined minor into the `## Findings` section of the task the branch closes, which is where the queue-refill sweep already routes a finding that changes another task. A declined finding left on the thread alone is lost the moment the pull request merges.
246
-
247
- Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises a finding of any grade.
248
-
249
- Both of this skill's headings anchor as a section distinct from human threads, and neither invents beyond what the whole set already states. That set is six headings across three families, stated here once so `role-orchestrator`'s poll and every reply-posting skill cite it rather than carry a copy. The review family, `## Review` and `## Review closed`, belongs to this skill alone. The reply family, `## Review response`, `## Rebase`, and `## Post-review findings`, belongs to `review-address`. The third family is a single heading, `## Evidence`, which `canon pr evidence` posts through `git-pr` and `git-followup` and which answers no comment already on the thread, so the poll excludes it from the unclassified count without reading it as a reply that owes a re-review.
250
-
251
- The first reply heading answers a finding this skill posted, the second reports a stale branch resolved without one, and the third carries a finding a worker produces after a close-out rather than in answer to one already on the thread, since a finding produced late is still a finding. A comment posted under a heading outside these six reaches the poll as unclassified rather than as silence, so an invented seventh heading is a gap the next run reports instead of one it repeats. Do not append the PR number, which GitHub already renders above the comment.
252
-
253
- Name the scope in every summary line after the first pass, since a reader cannot otherwise tell a narrow read from a full one. When the fallback in Step 2 fired, replace the commit count with `Re-reviewed the full change, the prior pass's commit is no longer on the branch`.
254
-
255
- Budget the body. State each finding as the failure and the fix in two or three sentences, not a paragraph of reasoning.
256
-
257
- Omit files with no findings. Do not lecture on process. The integration, contract, and consumer lenses stay, but as findings, not asides.
258
-
259
- The `What is right` section is optional, capped at three bullets, and included only when it changes the merge decision. Drop it otherwise and let the summary line carry the approval.
260
-
261
- Close the body with `🤖 Reviewed by Claude Code` on its own line so the review reads as an independent machine pass, not a human sign-off.
262
-
263
- ### The marker every body carries
264
-
265
- End every body with this line, carrying `<headRefOid>` and `<read-at>` from Step 1 verbatim:
266
-
267
- ```markdown
268
- <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
269
- ```
270
-
271
- Every body this step writes carries it, with no exception: the full body under either heading, both ✅ close-out lines, a withdrawal body, and the `PUT` rewrite at the end of this step. A body missing it reads as a pre-marker pass, so the next reader falls back to the stamps and the pass loses the coverage it actually had.
272
-
273
- Last is load-bearing rather than tidy. `canon pr review-state` reads the last non-empty line and searches nowhere else, so a marker written above the footer is a marker the next pass does not see. That position is also what lets a finding quote the format safely, including inside a fenced block, since a quotation is never the line the reader takes.
274
-
275
- It is what the review is scoped from. GitHub stamps `commit.oid` and `submittedAt` when a review is submitted, not when it was read, so a push landing in the compose window moves both onto a commit this pass never opened and the next pass reads that commit as covered. The marker is the read-time record those two fields are not, and `canon pr review-state` is the one place it is parsed, so Step 2 here and the orchestrator poll read one answer rather than each carrying a copy of the format.
276
-
277
- Write it as a comment rather than as prose so a reader of the thread never meets it. HTML comments render as nothing on GitHub, which is why the fact travels here rather than in a footer line a person would have to be told to ignore.
278
-
279
- It is inert data, so the `publish.md` scan below and `canon labels scan` have nothing to fire on. Confirm that against the posted body rather than assuming it, since a phase label or a board identifier appearing inside a commit sha is not a shape either scan was written against.
280
-
281
- Before posting, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the body. The hook skips `.canon/tmp/`, so this scan is the only gate ahead of the post. A finding phrased against an internal phase label is what the label half of the scan catches here. This repository reads the same text again once posted, on `phase-label-gate.yml`'s `pull_request_review` trigger, which is what closes the gap this scan leaves open for a review a person writes and posts by hand with no scripted step in front of it. That workflow now reaches every target on the `base` stack, seeded at `tooling/base/configs/.github/workflows/phase-label-gate.yml` invoking the published CLI rather than this checkout's own source tree, so a target's coverage extends past this pre-post scan to the same post-trigger re-read this repository gets.
282
-
283
- Do not run the command below when `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed. That pass replaces the standing comment rather than adding one, under `### A close-out that repeats the standing one` at the end of this step. Posting first and reaching that section afterward leaves two close-outs both naming the new head, which is worse than the pair the guard exists against.
284
-
285
- ```bash
286
- gh pr review <number> --comment --body-file .canon/tmp/pr/review/body-<number>-<short-sha>.md
287
- ```
288
-
289
- A pass carrying nothing at all takes `## Review closed` and a short body, with the footer line included either way. On a first pass, post `✅ No findings. Reviewed against project docs and the board.` On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
290
-
291
- A pass carrying only minors is an ordinary finding-carrying pass, so it takes the open heading and the full shape rather than either short line, since the minors have to be readable and neither line reports them. A pass carrying only Testing questions, or only an unanswered reviewer request, takes the same route for the same reason. Keep whichever scope sentence the pass owes on the summary line:
292
-
293
- ```markdown
294
- ## Review
295
-
296
- 0 critical, 0 should-fix, Z minor. Reviewed against project docs and the board.
297
-
298
- **`path/to/file.ext`**
299
-
300
- - **minor**: finding, and the fix it wants.
301
-
302
- 🤖 Reviewed by Claude Code
303
-
304
- <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
305
- ```
306
-
307
- A pass that closed by withdrawing a finding rather than by reading its fix takes neither ✅ line, per the withdrawal rule in Step 3. Both claim a fix landed, and the second names it, so posting either over a withdrawal credits work nobody did on the one comment a reader treats as the verdict. Write the withdrawal and the fact that settled it in place of the canned line, keeping the heading and the footer.
308
-
309
- A pass whose only content is a `## For the reviewer` block with every bullet answered, and that owes nothing else, takes the same shape: `## Review closed`, the block in place of the canned line, and the footer. The heading reports what the branch author still owes rather than what the pass did, and an answer discharged in the same comment owes nothing back.
310
-
311
- Post a close-out even when there is nothing to report. A review left with no closing comment reads as one nobody answered.
312
-
313
- ### A close-out that repeats the standing one
314
-
315
- When `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed, the thread already holds this verdict. Replace the standing close-out rather than posting a second one beside it.
316
-
317
- Resolve its numeric id. `gh pr view --json reviews` carries a GraphQL node id under `id`, which no REST route accepts, so read the id off the REST listing instead:
318
-
319
- ```bash
320
- gh api repos/{owner}/{repo}/pulls/<number>/reviews --jq '[.[] | select((.body // "") | split("\n")[0] | rtrimstr("\r") == "## Review closed")] | last | .id'
321
- ```
322
-
323
- Write the replacement body to `<body-file>`, the name Step 4 already derived at the top of this step, keeping the heading and the footer and naming what this pass covered on the scope line in place of what the old one covered. Derive that name the same way whichever path reached here, since the guard reads `<prior-heading>` alone and a repeated head resolves the third segment as usual. The folder then gains a record of every covered head rather than losing the one the standing comment named. Then replace the comment:
324
-
325
- ```bash
326
- gh api -X PUT repos/{owner}/{repo}/pulls/<number>/reviews/<review-id> -F body=@<body-file>
327
- ```
328
-
329
- `PUT` keeps the comment's timestamp and its position in the thread, so the verdict stays where a reader already found it and the thread gains no second entry. A submitted review cannot be deleted, which is why this rewrites the standing comment rather than posting a fresh one.
330
-
331
- The guard fires on `## Review closed` alone. Two open passes carry different findings and both are worth reading, so a repeated `## Review` posts normally. A pass carrying anything owed posts normally too, under `## Review`, which is what keeps a finding raised after a close-out from being swallowed by the guard that exists for a silent one.
332
-
333
- The rewrite used to cost the review's `commit.oid`, which `PUT` leaves pinned at the commit the standing close-out was first submitted against. Step 2's `<prior-oid>` and the prior commit `poll.sh` derives both read that field, so the next pass read a range wider than its delta, and the poll's `SEEN` branch, which fires on `prior` equalling the head, could never be reached at all: an out-of-band pass reported as `MOVED` for the rest of the pull request's life.
334
-
335
- The marker closes both, because `PUT` replaces the body and the marker is in it. The rewritten close-out carries the commit this pass read rather than the one the comment was first submitted against, and every reader now takes that in preference to the pinned field. The one thing `PUT` still cannot move is `submittedAt`, which stays at the original submission and is what the poll's age test reads, so a rewritten close-out ages from when it first landed rather than from when it was last rewritten. That is the correct reading for a thread waiting on a human, which is the question the age test asks.
106
+ Read `${CLAUDE_SKILL_DIR}/references/post.md` for the body file name, the body shape for each pass, the Testing, reviewer, and PR body blocks, the marker every body carries, and the scans ahead of the post. That file sends a pass repeating a standing close-out on to `${CLAUDE_SKILL_DIR}/references/close-out.md`, which rewrites the comment in place.
336
107
 
337
108
  ## Step 5: output
338
109
 
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: Rewrite a repeated close-out in place
3
+ description: The branch of review-pr Step 4 taken when the prior pass closed the review and this pass carries nothing owed, which replaces the standing close-out with a PUT rather than posting a second one
4
+ ---
5
+
6
+ # A close-out that repeats the standing one
7
+
8
+ Part of Step 4 of `review-pr`, reached only on the branch `post.md` names. Everything else that step does, including the body shape and the marker, still comes from `post.md`.
9
+
10
+ When `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed, the thread already holds this verdict. Replace the standing close-out rather than posting a second one beside it.
11
+
12
+ Resolve its numeric id. `gh pr view --json reviews` carries a GraphQL node id under `id`, which no REST route accepts, so read the id off the REST listing instead:
13
+
14
+ ```bash
15
+ gh api repos/{owner}/{repo}/pulls/<number>/reviews --jq '[.[] | select((.body // "") | split("\n")[0] | rtrimstr("\r") == "## Review closed")] | last | .id'
16
+ ```
17
+
18
+ Write the replacement body to `<body-file>`, the name `post.md` already derived at the top of Step 4, keeping the heading and the footer and naming what this pass covered on the scope line in place of what the old one covered. Derive that name the same way whichever path reached here, since the guard reads `<prior-heading>` alone and a repeated head resolves the third segment as usual. The folder then gains a record of every covered head rather than losing the one the standing comment named. Then replace the comment:
19
+
20
+ ```bash
21
+ gh api -X PUT repos/{owner}/{repo}/pulls/<number>/reviews/<review-id> -F body=@<body-file>
22
+ ```
23
+
24
+ `PUT` keeps the comment's timestamp and its position in the thread, so the verdict stays where a reader already found it and the thread gains no second entry. A submitted review cannot be deleted, which is why this rewrites the standing comment rather than posting a fresh one.
25
+
26
+ The guard fires on `## Review closed` alone. Two open passes carry different findings and both are worth reading, so a repeated `## Review` posts normally. A pass carrying anything owed posts normally too, under `## Review`, which is what keeps a finding raised after a close-out from being swallowed by the guard that exists for a silent one.
27
+
28
+ The rewrite used to cost the review's `commit.oid`, which `PUT` leaves pinned at the commit the standing close-out was first submitted against. Step 2's `<prior-oid>` and the prior commit `poll.sh` derives both read that field, so the next pass read a range wider than its delta, and the poll's `SEEN` branch, which fires on `prior` equalling the head, could never be reached at all: an out-of-band pass reported as `MOVED` for the rest of the pull request's life.
29
+
30
+ The marker closes both, because `PUT` replaces the body and the marker is in it. The rewritten close-out carries the commit this pass read rather than the one the comment was first submitted against, and every reader now takes that in preference to the pinned field. The one thing `PUT` still cannot move is `submittedAt`, which stays at the original submission and is what the poll's age test reads, so a rewritten close-out ages from when it first landed rather than from when it was last rewritten. That is the correct reading for a thread waiting on a human, which is the question the age test asks.
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: Post the review to the pull request
3
+ description: Step 4 of review-pr, which names the body file, gives the body shape for each pass, places the Testing, reviewer, and PR body blocks, carries the read-time marker, and rewrites a repeated close-out in place
4
+ ---
5
+
6
+ # Post to the PR
7
+
8
+ Step 4 of `review-pr`. The threshold that picks the heading and decides the dispatch sits in the skill body's opening, and this file applies it.
9
+
10
+ Write the comment to `.canon/tmp/pr/review/body-<number>-<short-sha>.md` at the main worktree root, not the current worktree, which the rest of this step calls `<body-file>`. Resolve that root and send the write as a heredoc, both the way `session-worktree` states. The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post, and the head commit stops a second pass overwriting the first one's body.
11
+
12
+ Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a name the folder already holds.
13
+
14
+ When `<prior-oid>` from Step 2 equals `headRefOid`, the head repeats and the folder already holds `body-<number>-<short-sha>.md`. Add a third segment taking the id of the reply Step 2 resolved, giving `body-<number>-<short-sha>-r<comment-id>.md`, which is `<body-file>` on that path. That satisfies both prohibitions above rather than carving an exception into either. Step 2 already stopped the pass when that resolution came back empty, so reaching this line means the comment id is in hand.
15
+
16
+ The comment is a rendered-for-human GitHub surface, so load the `write-human` skill for voice and follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for the banned words: cut editorializing, and keep every sentence load-bearing. Match this shape on a first pass:
17
+
18
+ ```markdown
19
+ ## Review
20
+
21
+ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
22
+
23
+ **`path/to/file.ext`**
24
+
25
+ - **should-fix**: what breaks and the fix, in two or three sentences.
26
+ - **minor**: finding.
27
+
28
+ **What is right**
29
+
30
+ - bounded confirmation.
31
+
32
+ 🤖 Reviewed by Claude Code
33
+
34
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
35
+ ```
36
+
37
+ A stale ticked box goes in a `**PR body**` block, in place of a `**`path/to/file.ext`**` block and ahead of every one of those, since it precedes the code the diff carries rather than sitting inside it.
38
+
39
+ Run `canon labels scan --title "<title>" --body "<body>" --head <headRefName>` against the PR under review, since Step 1 already holds all three and this pass is the last human-shaped gate before merge. A hit lands in the same `**PR body**` block, `should-fix`, naming each finding the scan returns: a token quoted from the title or body for a phase label, a board identifier, a session link, or an unspelled word, and the broken rule's name, `structure`, a casing issue, or `length`, for a title-format hit. This reads the pull request being reviewed, distinct from the comment this pass is about to post, which the scan later in this step still covers.
40
+
41
+ A later pass carrying findings keeps that shape and changes only the summary line:
42
+
43
+ ```markdown
44
+ ## Review
45
+
46
+ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-fix, Z minor.
47
+
48
+ **`path/to/file.ext`**
49
+
50
+ - **should-fix**: what breaks and the fix, in two or three sentences.
51
+
52
+ 🤖 Reviewed by Claude Code
53
+
54
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
55
+ ```
56
+
57
+ A Testing box the Step 3 check raised goes in a `**Testing**` block placed after the file blocks, one bullet per box, each quoting the box and naming what would drive it. It carries no severity and enters no count, and it is still something owed, so a pass carrying one takes `## Review` and the full body rather than either ✅ line. Say so on the summary line as `plus N testing question(s)`, since the three counts read as zero and would otherwise report the pass as silent.
58
+
59
+ ```markdown
60
+ **Testing**
61
+
62
+ - `- [ ] <the box as written>` names no capability the agent lacks. `scripts/sandbox/run.sh <arm>` drives it. Was there a reason to leave it?
63
+ ```
64
+
65
+ Keep it to the boxes the check raised. Restating a box whose stated requirement holds teaches the branch author to skip the block.
66
+
67
+ Every `## For the reviewer` bullet Step 3 read goes in a `**For the reviewer**` block placed after the Testing block, one bullet per request, each followed by its answer or, where the pass could not answer it, by what would settle it. It carries no severity and enters no count.
68
+
69
+ An unanswered bullet is owed the same way an unanswered Testing box is, so a pass carrying one takes `## Review` and the full body rather than either ✅ line. A bullet the pass answered is not owed, since the answer is discharged in the same comment that carries it. A pass still posting a numeric summary line, because a finding, a Testing question, or an unanswered bullet already forces one, says so there as `plus N reviewer request(s)`. The all-answered close-out below carries the block in place of that line and needs no addition to it.
70
+
71
+ ```markdown
72
+ **For the reviewer**
73
+
74
+ - Confirm the 401 and 403 split reads correctly for the public API. Confirmed — `AuthService.authenticate()` returns 401 for an expired token and 403 for a missing scope, and both paths are covered under `## Testing`.
75
+ ```
76
+
77
+ Keying either half of the opening's threshold on the grade was measured wrong: across 8 findings on one archived pass, 3 were posted as minor and 2 of those were defects a worker fixed rather than recorded, so a floor at should-fix loses real fixes to a grade that runs low. Splitting the two halves so the dispatch fired lower than the heading was the other candidate, and it left a thread reading closed while work was owed on it. The Testing question was first written to sit outside both, which is that same split reached from the other side, and it left the one party who could answer the question with no route to it.
78
+
79
+ The cost is that the merge decision no longer reads off the heading alone, since an open heading covers a minor as well as a critical. Take it from the counts on the summary line, which is where they already sit. No summary line reports the merge as unblocked under either heading, since a thread reading open cannot also report that nothing blocks it.
80
+
81
+ A minor the dispatched worker declines is what needs a surface that survives the merge, rather than every minor, since one that gets fixed on the branch needs no durable record. Write a declined minor into the `## Findings` section of the task the branch closes, which is where the queue-refill sweep already routes a finding that changes another task. A declined finding left on the thread alone is lost the moment the pull request merges.
82
+
83
+ Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises a finding of any grade.
84
+
85
+ Do not append the PR number to either heading, which GitHub already renders above the comment.
86
+
87
+ Name the scope in every summary line after the first pass, since a reader cannot otherwise tell a narrow read from a full one. When the fallback in Step 2 fired, replace the commit count with `Re-reviewed the full change, the prior pass's commit is no longer on the branch`.
88
+
89
+ Budget the body. State each finding as the failure and the fix in two or three sentences, not a paragraph of reasoning.
90
+
91
+ Omit files with no findings. Do not lecture on process. The integration, contract, and consumer lenses stay, but as findings, not asides.
92
+
93
+ The `What is right` section is optional, capped at three bullets, and included only when it changes the merge decision. Drop it otherwise and let the summary line carry the approval.
94
+
95
+ Close the body with `🤖 Reviewed by Claude Code` on its own line so the review reads as an independent machine pass, not a human sign-off.
96
+
97
+ ### The marker every body carries
98
+
99
+ End every body with this line, carrying `<headRefOid>` and `<read-at>` from Step 1 verbatim:
100
+
101
+ ```markdown
102
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
103
+ ```
104
+
105
+ Every body this step writes carries it, with no exception: the full body under either heading, both ✅ close-out lines, a withdrawal body, and the `PUT` rewrite in `close-out.md`. A body missing it reads as a pre-marker pass, so the next reader falls back to the stamps and the pass loses the coverage it actually had.
106
+
107
+ Last is load-bearing rather than tidy. `canon pr review-state` reads the last non-empty line and searches nowhere else, so a marker written above the footer is a marker the next pass does not see. That position is also what lets a finding quote the format safely, including inside a fenced block, since a quotation is never the line the reader takes.
108
+
109
+ It is what the review is scoped from. GitHub stamps `commit.oid` and `submittedAt` when a review is submitted, not when it was read, so a push landing in the compose window moves both onto a commit this pass never opened and the next pass reads that commit as covered. The marker is the read-time record those two fields are not, and `canon pr review-state` is the one place it is parsed, so Step 2 here and the orchestrator poll read one answer rather than each carrying a copy of the format.
110
+
111
+ Write it as a comment rather than as prose so a reader of the thread never meets it. HTML comments render as nothing on GitHub, which is why the fact travels here rather than in a footer line a person would have to be told to ignore.
112
+
113
+ It is inert data, so the `publish.md` scan below and `canon labels scan` have nothing to fire on. Confirm that against the posted body rather than assuming it, since a phase label or a board identifier appearing inside a commit sha is not a shape either scan was written against.
114
+
115
+ Before posting, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against the body. The hook skips `.canon/tmp/`, so this scan is the only gate ahead of the post. A finding phrased against an internal phase label is what the label half of the scan catches here. This repository reads the same text again once posted, on `phase-label-gate.yml`'s `pull_request_review` trigger, which is what closes the gap this scan leaves open for a review a person writes and posts by hand with no scripted step in front of it. That workflow now reaches every target on the `base` stack, seeded at `tooling/base/configs/.github/workflows/phase-label-gate.yml` invoking the published CLI rather than this checkout's own source tree, so a target's coverage extends past this pre-post scan to the same post-trigger re-read this repository gets.
116
+
117
+ Do not run the command below when `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed. That pass replaces the standing comment rather than adding one, so read `${CLAUDE_SKILL_DIR}/references/close-out.md` instead of posting. Posting first and reaching that section afterward leaves two close-outs both naming the new head, which is worse than the pair the guard exists against.
118
+
119
+ ```bash
120
+ gh pr review <number> --comment --body-file .canon/tmp/pr/review/body-<number>-<short-sha>.md
121
+ ```
122
+
123
+ A pass carrying nothing at all takes `## Review closed` and a short body, with the footer line included either way. On a first pass, post `✅ No findings. Reviewed against project docs and the board.` On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
124
+
125
+ A pass carrying only minors is an ordinary finding-carrying pass, so it takes the open heading and the full shape rather than either short line, since the minors have to be readable and neither line reports them. A pass carrying only Testing questions, or only an unanswered reviewer request, takes the same route for the same reason. Keep whichever scope sentence the pass owes on the summary line:
126
+
127
+ ```markdown
128
+ ## Review
129
+
130
+ 0 critical, 0 should-fix, Z minor. Reviewed against project docs and the board.
131
+
132
+ **`path/to/file.ext`**
133
+
134
+ - **minor**: finding, and the fix it wants.
135
+
136
+ 🤖 Reviewed by Claude Code
137
+
138
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
139
+ ```
140
+
141
+ A pass that closed by withdrawing a finding rather than by reading its fix takes neither ✅ line, per the withdrawal rule in Step 3. Both claim a fix landed, and the second names it, so posting either over a withdrawal credits work nobody did on the one comment a reader treats as the verdict. Write the withdrawal and the fact that settled it in place of the canned line, keeping the heading and the footer.
142
+
143
+ A pass whose only content is a `## For the reviewer` block with every bullet answered, and that owes nothing else, takes the same shape: `## Review closed`, the block in place of the canned line, and the footer. The heading reports what the branch author still owes rather than what the pass did, and an answer discharged in the same comment owes nothing back.
144
+
145
+ Post a close-out even when there is nothing to report. A review left with no closing comment reads as one nobody answered.