@erclx/canon 5.40.0 → 5.42.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 (187) hide show
  1. package/README.md +34 -17
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +34 -14
  4. package/claude/skills/auto-ship/REQUIREMENT.md +2 -0
  5. package/claude/skills/auto-ship/SKILL.md +28 -2
  6. package/claude/skills/auto-ship/references/draft-mark.md +32 -6
  7. package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
  8. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  9. package/claude/skills/auto-ship/references/tier-failures.md +31 -10
  10. package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
  11. package/claude/skills/auto-ship/references/worktree-name.md +9 -3
  12. package/claude/skills/build-in-slices/references/adopted.md +35 -21
  13. package/claude/skills/canvas-board/references/canvas.md +17 -6
  14. package/claude/skills/canvas-board/references/design.md +26 -7
  15. package/claude/skills/code-craft/references/adopted.md +58 -20
  16. package/claude/skills/codebase-layout/references/adopted.md +49 -16
  17. package/claude/skills/codebase-layout/references/frontend.md +11 -4
  18. package/claude/skills/codebase-layout/references/tests.md +4 -2
  19. package/claude/skills/context-fold/references/classify.md +10 -15
  20. package/claude/skills/context-fold/references/completion-report.md +11 -9
  21. package/claude/skills/context-fold/references/context-refresh.md +26 -9
  22. package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
  23. package/claude/skills/create-rule/REQUIREMENT.md +5 -0
  24. package/claude/skills/create-rule/SKILL.md +9 -1
  25. package/claude/skills/create-rule/references/checklist.md +26 -0
  26. package/claude/skills/create-rule/references/rule.md +98 -20
  27. package/claude/skills/create-skill/SKILL.md +1 -0
  28. package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
  29. package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
  30. package/claude/skills/create-skill/references/skill-paths.md +7 -4
  31. package/claude/skills/create-skill/references/skill-practice.md +6 -3
  32. package/claude/skills/create-skill/references/skill-requirement.md +4 -2
  33. package/claude/skills/create-skill/references/skill.md +67 -44
  34. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  35. package/claude/skills/decision-escalate/SKILL.md +1 -2
  36. package/claude/skills/deploy-app/references/vercel.md +2 -1
  37. package/claude/skills/deprecation-migration/references/adopted.md +44 -25
  38. package/claude/skills/design-taste/references/adopted.md +9 -1
  39. package/claude/skills/design-taste/references/craft.md +3 -1
  40. package/claude/skills/design-taste/references/kinds.md +8 -7
  41. package/claude/skills/design-taste/references/preflight.md +12 -0
  42. package/claude/skills/design-taste/references/systems.md +7 -6
  43. package/claude/skills/design-taste/references/tells.md +24 -25
  44. package/claude/skills/design-taste/references/vocabulary.md +7 -5
  45. package/claude/skills/document-health/references/axes.md +54 -30
  46. package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
  47. package/claude/skills/draft-doc/references/context.md +4 -3
  48. package/claude/skills/draft-doc/references/docs.md +6 -3
  49. package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
  50. package/claude/skills/draft-doc/references/readme/application.md +8 -2
  51. package/claude/skills/draft-doc/references/readme/cli.md +5 -1
  52. package/claude/skills/draft-doc/references/readme/library.md +4 -0
  53. package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
  54. package/claude/skills/draft-doc/references/readme.md +30 -13
  55. package/claude/skills/draft-doc/references/requirements.md +17 -4
  56. package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
  57. package/claude/skills/draft-figure/references/architecture-set.md +7 -3
  58. package/claude/skills/draft-figure/references/figures.md +8 -6
  59. package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
  60. package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
  61. package/claude/skills/draft-figure/references/mermaid.md +10 -5
  62. package/claude/skills/draft-ready/references/ready.md +22 -15
  63. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  64. package/claude/skills/git-branch/SKILL.md +63 -7
  65. package/claude/skills/git-branch/references/branch.md +11 -1
  66. package/claude/skills/git-branch/references/slug.md +17 -5
  67. package/claude/skills/git-commit/references/commit.md +8 -0
  68. package/claude/skills/git-commit/references/versioning.md +23 -17
  69. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  70. package/claude/skills/git-followup/SKILL.md +5 -6
  71. package/claude/skills/git-issue/references/issue.md +29 -16
  72. package/claude/skills/git-pr/REQUIREMENT.md +1 -5
  73. package/claude/skills/git-pr/SKILL.md +11 -22
  74. package/claude/skills/git-pr/references/final-command.md +21 -8
  75. package/claude/skills/git-pr/references/labels.md +45 -28
  76. package/claude/skills/git-pr/references/pr.md +28 -9
  77. package/claude/skills/git-pr/references/publish.md +42 -28
  78. package/claude/skills/git-pr/references/task-number.md +20 -8
  79. package/claude/skills/git-ship/SKILL.md +1 -1
  80. package/claude/skills/plan-feature/references/batches.md +48 -1
  81. package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
  82. package/claude/skills/plan-feature/references/plan.md +59 -20
  83. package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
  84. package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
  85. package/claude/skills/plan-intake/references/intake.md +29 -24
  86. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  87. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  88. package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
  89. package/claude/skills/pr-evidence/SKILL.md +53 -0
  90. package/claude/skills/pr-evidence/references/evidence.md +150 -0
  91. package/claude/skills/review-address/references/post-review-findings.md +10 -3
  92. package/claude/skills/review-address/references/rebase-conflicts.md +22 -7
  93. package/claude/skills/review-address/references/rest-reads.md +3 -1
  94. package/claude/skills/review-craft/references/adopted.md +35 -18
  95. package/claude/skills/review-craft/references/security.md +42 -7
  96. package/claude/skills/review-craft/references/ui.md +23 -8
  97. package/claude/skills/review-pr/SKILL.md +6 -6
  98. package/claude/skills/review-pr/references/body-variants.md +29 -9
  99. package/claude/skills/review-pr/references/close-out.md +20 -5
  100. package/claude/skills/review-pr/references/key-changes.md +27 -3
  101. package/claude/skills/review-pr/references/post.md +83 -31
  102. package/claude/skills/review-pr/references/scope.md +54 -19
  103. package/claude/skills/review-ui/SKILL.md +2 -0
  104. package/claude/skills/review-ui/references/close-out.md +49 -0
  105. package/claude/skills/review-ui/references/driving.md +24 -4
  106. package/claude/skills/review-ui/references/post.md +43 -13
  107. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
  108. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +81 -40
  109. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
  110. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
  111. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
  112. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
  113. package/claude/skills/role-orchestrator/references/orchestrator-output.md +12 -4
  114. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
  115. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
  116. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
  117. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
  118. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
  119. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
  120. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
  121. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
  122. package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
  123. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  124. package/claude/skills/role-worker/SKILL.md +17 -0
  125. package/claude/skills/search-craft/references/adopted.md +18 -6
  126. package/claude/skills/session-map/references/session.md +26 -7
  127. package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
  128. package/claude/skills/systematic-debugging/references/adopted.md +30 -16
  129. package/claude/skills/target-check/references/domains.md +39 -11
  130. package/claude/skills/target-setup/references/detect.md +80 -65
  131. package/claude/skills/target-setup/references/indexes.md +8 -6
  132. package/claude/skills/task-board/references/board.md +86 -30
  133. package/claude/skills/task-board/references/decline.md +29 -13
  134. package/claude/skills/task-board/references/tasks-decline.md +12 -5
  135. package/claude/skills/task-board/references/tasks.md +65 -33
  136. package/claude/skills/task-complete/references/archive.md +41 -17
  137. package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
  138. package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
  139. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  140. package/claude/skills/teach-workspace/SKILL.md +1 -2
  141. package/claude/skills/teach-workspace/references/glossary.md +6 -2
  142. package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
  143. package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
  144. package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
  145. package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
  146. package/claude/skills/teach-workspace/references/promotion.md +31 -8
  147. package/claude/skills/teach-workspace/references/revisit.md +8 -2
  148. package/claude/skills/teach-workspace/references/teach.md +17 -9
  149. package/claude/skills/test-craft/references/adopted.md +48 -14
  150. package/claude/skills/test-craft/references/component.md +7 -4
  151. package/claude/skills/test-craft/references/e2e.md +25 -7
  152. package/claude/skills/test-craft/references/unit.md +3 -1
  153. package/claude/skills/test-first/references/adopted.md +12 -4
  154. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
  155. package/claude/skills/ui-checklist/SKILL.md +1 -1
  156. package/claude/skills/video-craft/references/adopted.md +62 -18
  157. package/claude/skills/write-human/references/adopted.md +21 -8
  158. package/docs/workflow/parallel-features.md +2 -1
  159. package/docs/workflow/skill-map.md +2 -1
  160. package/package.json +1 -1
  161. package/src/claude/cases/git.ts +4 -0
  162. package/src/claude/cases/workflow.ts +5 -0
  163. package/src/commands/pr.ts +174 -37
  164. package/src/commands/records.ts +11 -3
  165. package/src/context/shape-gated.ts +1 -0
  166. package/src/markdown/scope.ts +150 -2
  167. package/src/pr/frames.ts +53 -9
  168. package/src/pr/lift.ts +6 -3
  169. package/src/pr/review-state.ts +182 -0
  170. package/src/records/validate.ts +1 -0
  171. package/src/tasks/answers.ts +79 -2
  172. package/src/tasks/fan-in.ts +176 -0
  173. package/src/tasks/stack.ts +1 -1
  174. package/tooling/astro/reference.md +80 -39
  175. package/tooling/base/reference.md +63 -32
  176. package/tooling/base/seeds/canon/context/ci.md +7 -15
  177. package/tooling/base/seeds/canon/context/development.md +14 -14
  178. package/tooling/claude/reference.md +40 -22
  179. package/tooling/cloudflare/reference.md +31 -12
  180. package/tooling/go/reference.md +47 -23
  181. package/tooling/nextjs/reference.md +52 -19
  182. package/tooling/php/reference.md +35 -16
  183. package/tooling/python/reference.md +47 -30
  184. package/tooling/shell/reference.md +6 -5
  185. package/tooling/vite-react/reference.md +49 -21
  186. package/tooling/web/reference.md +130 -70
  187. package/claude/skills/git-pr/references/evidence.md +0 -124
@@ -15,11 +15,19 @@ Step 4 of `review-pr`. The threshold that picks the heading and decides the disp
15
15
 
16
16
  ## The body file and finding links
17
17
 
18
- 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.
18
+ 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.
19
19
 
20
- Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a name the folder already holds.
20
+ - The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post
21
+ - The head commit stops a second pass overwriting the first one's body
22
+ - Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a name the folder already holds.
21
23
 
22
- 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.
24
+ ### A repeated head
25
+
26
+ When `<prior-oid>` from Step 2 equals `headRefOid`, 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` as `<body-file>`.
27
+
28
+ 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.
29
+
30
+ ### Finding links
23
31
 
24
32
  A finding bound to a line links to it, so a reader skips the hunt:
25
33
 
@@ -27,13 +35,23 @@ A finding bound to a line links to it, so a reader skips the hunt:
27
35
  - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/<path>#L12)): what breaks and the fix
28
36
  ```
29
37
 
30
- A finding that spans lines anchors to the span as `#L12-L14`, with no spaces and no en dash, and a single-line finding anchors as `#L12`. Take the line number from the file read at `<headRefOid>`, `git show <headRefOid>:<path>`, never from another checkout. The URL carries the full 40-character `<headRefOid>`, never `<short-sha>`, `HEAD`, or the branch name, so the link survives a push. A later pass restating an open finding links it again at its own `<headRefOid>` rather than copying the old bullet.
38
+ - A span anchors as `#L12-L14`, with no spaces and no en dash, and a single line as `#L12`
39
+ - Take the line number from the file read at `<headRefOid>`, `git show <headRefOid>:<path>`, never from another checkout
40
+ - The URL carries the full 40-character `<headRefOid>`, never `<short-sha>`, `HEAD`, or the branch name, so the link survives a push
41
+ - A later pass restating an open finding links it again at its own `<headRefOid>` rather than copying the old bullet
42
+
43
+ ### Findings with no link
44
+
45
+ Four findings take the plain bullet with no link, since the head holds no line number for them.
31
46
 
32
- Four findings take the plain bullet with no link: a `**PR body**` entry, a finding on a file the head deleted, a finding on a line the diff removed from a file that survives, since the head holds no number for it, and a finding about a whole file with no one line. A path holding a character a URL needs encoded, such as a space or `#`, is percent-encoded in the link or keeps the plain bullet. Keep the file block header a bare backticked path, so the section-marker reading in `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md` does not change.
47
+ - A `**PR body**` entry, and a finding about a whole file with no one line
48
+ - A finding on a file the head deleted, or on a line the diff removed from a file that survives
49
+ - A path holding a character a URL needs encoded, such as a space or `#`, is percent-encoded in the link or keeps the plain bullet
50
+ - Keep the file block header a bare backticked path, so the section-marker reading in `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md` holds
33
51
 
34
52
  ## The body shapes
35
53
 
36
- The comment is a rendered-for-human GitHub surface, so load the `write-human` skill for voice and word choice and follow `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md` for punctuation: cut editorializing, and keep every sentence load-bearing. Match this shape on a first pass:
54
+ The comment is a rendered-for-human GitHub surface, so load the `write-human` skill for voice and word choice and follow `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md` for punctuation. Cut editorializing, and keep every sentence load-bearing. Match this shape on a first pass:
37
55
 
38
56
  ```markdown
39
57
  ## Review
@@ -54,10 +72,6 @@ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
54
72
  <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
55
73
  ```
56
74
 
57
- 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.
58
-
59
- 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, or a session link, and the broken rule's name, `structure`, a casing issue, or `length`, for a title-format hit. A heading in `sections.outside`, a heading in `sections.missing`, or a character in `bannedCharacters` is a hit of the same weight, naming the heading or the character. 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.
60
-
61
75
  A later pass carrying findings keeps that shape and changes only the summary line:
62
76
 
63
77
  ```markdown
@@ -76,25 +90,43 @@ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-
76
90
 
77
91
  A Testing question or a `## For the reviewer` bullet Step 3 read takes its own block after the file blocks, per `${CLAUDE_SKILL_DIR}/references/body-variants.md`.
78
92
 
79
- 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.
93
+ ### The PR body block
80
94
 
81
- 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.
95
+ A stale ticked box goes in a `**PR body**` block, ahead of every file block, since it precedes the code the diff carries rather than sitting inside it.
82
96
 
83
- 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, as a line opening with the bold source-and-date label from `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md`, 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.
97
+ 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 block as a `should-fix`.
84
98
 
85
- 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.
99
+ - Name the token quoted from the title or body for a phase label, a board identifier, or a session link
100
+ - Name the broken rule, being `structure`, a casing issue, or `length`, for a title-format hit
101
+ - A heading in `sections.outside` or `sections.missing`, or a character in `bannedCharacters`, is a hit of the same weight, naming the heading or the character
102
+ - This reads the pull request under review, distinct from the comment this pass posts, which the scan later in this step covers
86
103
 
87
- Do not append the PR number to either heading, which GitHub already renders above the comment.
104
+ ### Why the threshold ignores the grade
88
105
 
89
- 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`.
106
+ 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, so a floor at should-fix loses real fixes to a grade that runs low.
107
+
108
+ - Firing the dispatch lower than the heading left a thread reading closed while work was owed on it
109
+ - A Testing question sitting outside both is the same split, and it left the one party who could answer it with no route
110
+ - The cost is that the merge decision reads off the summary line's counts rather than the heading, since an open heading covers a minor and a critical alike
111
+ - No summary line reports the merge as unblocked under either heading, since a thread reading open cannot also report that nothing blocks it
112
+
113
+ ### Declined minors and reopening
90
114
 
91
- Budget the body. State each finding as the failure and the fix in two or three sentences, not a paragraph of reasoning.
115
+ A minor the dispatched worker declines needs a surface that survives the merge, since one fixed on the branch needs no record and one left on the thread is lost at merge. Write it into the `## Findings` section of the task the branch closes.
92
116
 
93
- Omit files with no findings. Do not lecture on process. The integration, contract, and consumer lenses stay, but as findings, not asides.
117
+ - Open the line with the bold source-and-date label from `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md`, where the queue-refill sweep already routes such a finding
118
+ - Read the state off the most recent review comment rather than off the presence of a closed one
119
+ - A close-out does not close the pull request, so a later commit gets its own pass, which reopens under `## Review` on a finding of any grade
94
120
 
95
- 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.
121
+ ### Summary line and budget
96
122
 
97
- 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.
123
+ 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`.
124
+
125
+ - Do not append the PR number to either heading, which GitHub already renders above the comment
126
+ - State each finding as the failure and the fix in two or three sentences, not a paragraph of reasoning
127
+ - Omit files with no findings and do not lecture on process. The integration, contract, and consumer lenses stay as findings, not asides.
128
+ - `What is right` is optional, capped at three bullets, and included only when it changes the merge decision
129
+ - Close with `🤖 Reviewed by Claude Code` on its own line, so the review reads as an independent machine pass, not a human sign-off
98
130
 
99
131
  ## The marker, the scans, and the post
100
132
 
@@ -106,26 +138,46 @@ End every body with this line, carrying `<headRefOid>` and `<read-at>` from Step
106
138
  <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
107
139
  ```
108
140
 
109
- 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.
141
+ Every body this step writes carries it, being 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 its coverage.
142
+
143
+ - Last is load-bearing, since `canon pr review-state` reads the last non-empty line and searches nowhere else
144
+ - That position also lets a finding quote the format safely, even in a fence, since a quotation is never the line the reader takes
145
+ - Write it as an HTML comment, which GitHub renders as nothing, so a reader of the thread never meets it
146
+
147
+ ### Why the marker exists
148
+
149
+ GitHub stamps `commit.oid` and `submittedAt` at submission, not at read. A push landing in the compose window moves both onto a commit this pass never opened, and the next pass reads it as covered.
110
150
 
111
- 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.
151
+ The marker is the read-time record those fields are not.
112
152
 
113
- 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.
153
+ - `canon pr review-state` is the one place it is parsed, so Step 2 and the orchestrator poll read one answer rather than each copying the format
154
+ - It is inert data, so `canon labels scan` has nothing to fire on
155
+ - Confirm that against the posted body, since a phase label or board identifier inside a commit sha is not a shape either scan was written against
114
156
 
115
- 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.
157
+ ### The scans
116
158
 
117
- 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.
159
+ Before posting, run `canon labels scan --body-file .canon/tmp/pr/review/body-<number>-<short-sha>.md`, which applies the rule in `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md`. Fix the body and scan again on a finding.
118
160
 
119
- Before posting, run `canon labels scan --body-file .canon/tmp/pr/review/body-<number>-<short-sha>.md` against the body, which applies the rule in `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md`. Fix the body and scan again on a finding. A finding phrased against an internal phase label is what the label half of the scan catches here. The call carries no `--title`, so the scan reads the text as a review.
161
+ - A finding phrased against an internal phase label is what the label half of the scan catches here
162
+ - The call carries no `--title`, so the scan reads the text as a review
120
163
 
121
- A `PreToolUse` hook on `Bash` runs the same scan against the `-F body=@<file>` of the post below and denies the call on a hit, so the check holds whether or not this step ran it. The hook reads the file before the command runs, so write the body in an earlier call than the post, and pass it as a file rather than inline. 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 for a review a person writes and posts by hand with no hook 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 the hook to the same post-trigger re-read this repository gets.
164
+ A `PreToolUse` hook on `Bash` runs the same scan against the `-F body=@<file>` of the post and denies the call on a hit, so the check holds whether or not this step ran. The hook reads the file before the command runs, so write the body in an earlier call and pass it as a file rather than inline.
122
165
 
123
- 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.
166
+ ### The post-trigger re-read
167
+
168
+ `phase-label-gate.yml` reads the same text again once posted, on its `pull_request_review` trigger, which covers a review a person posts by hand with no hook in front of it.
169
+
170
+ - That workflow reaches every target on the `base` stack, seeded at `tooling/base/configs/.github/workflows/phase-label-gate.yml`
171
+ - It invokes the published CLI rather than a checkout's own source tree, so a target gets the same re-read past the hook
172
+
173
+ ### The post
174
+
175
+ Do not run the command below when `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed. Read `${CLAUDE_SKILL_DIR}/references/close-out.md` instead, since posting first leaves two close-outs both naming the new head.
124
176
 
125
177
  ```bash
126
178
  gh api -X POST 'repos/{owner}/{repo}/pulls/<number>/reviews' -f event=COMMENT -F body=@.canon/tmp/pr/review/body-<number>-<short-sha>.md --jq .html_url
127
179
  ```
128
180
 
129
- The post goes through the REST reviews endpoint rather than the `gh pr` review subcommand, which runs on GraphQL and fails where a cloud session's GitHub proxy refuses it. `event=COMMENT` submits the review as a comment, the same state the old `--comment` flag produced.
130
-
131
- A pass carrying nothing at all, a withdrawal, or only answered reviewer requests takes a close-out body, and a pass carrying only minors keeps the full shape, both per `${CLAUDE_SKILL_DIR}/references/body-variants.md`.
181
+ - The post uses the REST reviews endpoint, since the `gh pr` review subcommand runs on GraphQL and fails where a cloud session's proxy refuses it
182
+ - `event=COMMENT` submits the review as a comment, the same state the old `--comment` flag produced
183
+ - A pass carrying nothing, a withdrawal, or only answered requests takes a close-out body, and only minors keep the full shape, per `${CLAUDE_SKILL_DIR}/references/body-variants.md`
@@ -7,27 +7,42 @@ description: Step 2 of review-pr, which finds the commit and verdict the prior p
7
7
 
8
8
  Step 2 of `review-pr`. The session reads this file on reaching that step, ahead of Step 3, so the unchanged-head stop below still fires before any review runs.
9
9
 
10
+ ## Find the prior pass
11
+
10
12
  Find the commit the last pass covered and the verdict it posted:
11
13
 
12
14
  ```bash
13
15
  canon pr review-state <number> --json
14
16
  ```
15
17
 
16
- 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.
18
+ Take `<prior-oid>` off `commit`, `<prior-heading>` off `heading`, and `<prior-at>` off `readAt // submittedAt`. Branch on the record rather than on the exit code, which a shell function wrapping `canon` can flatten to zero.
19
+
20
+ - 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
21
+ - One call answers all three rather than three reads of the same review
22
+
23
+ ### Which stamp answered
24
+
25
+ `source` says which stamp answered, and `marker` is the authority. Never read `commit_id` or `submitted_at` off the reviews listing here, since both are stamped at submission and a push between a pass's read and its post moves them onto a commit that pass never saw.
17
26
 
18
- `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.
27
+ - `marker`: the pass's own read-time record
28
+ - `fallback`: a pass posted before the marker shipped, whose commit is the head when GitHub recorded the review, so a push inside its compose window is invisible
29
+ - `none`: a thread carrying no pass at all
19
30
 
20
- Do not read `commit_id` or `submitted_at` off the reviews listing 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.
31
+ ### When the verb is missing
21
32
 
22
- A target whose CLI predates the verb meets a missing subcommand rather than a record. Fall back there to the REST read 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:
33
+ A target whose CLI predates the verb meets a missing subcommand rather than a record. Fall back to this REST read, 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:
23
34
 
24
35
  ```bash
25
36
  gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review" or . == "## Review closed") | ((.commit_id // "") + "\t" + (.body | split("\n")[0] | rtrimstr("\r")) + "\t" + (.submitted_at // ""))' | tail -n 1
26
37
  ```
27
38
 
28
- The three fields are `<prior-oid>`, `<prior-heading>`, and `<prior-at>`. The filter emits one line per matching review and `tail` keeps the newest, since `--paginate` runs the filter once per page and a `last` inside it would answer for each page rather than for the thread.
39
+ The three fields are `<prior-oid>`, `<prior-heading>`, and `<prior-at>`.
29
40
 
30
- 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.
41
+ - The filter emits one line per matching review and `tail` keeps the newest, since `--paginate` runs the filter once per page
42
+ - Match the first line for equality, since a prefix test also matches `## Review response` and scopes the pass to that comment's commit
43
+ - The `\r` trim covers a body composed in the GitHub web editor, which stores CRLF
44
+
45
+ ## A first pass
31
46
 
32
47
  A `source` of `none`, or an empty result from the fallback, is a first pass. Read the whole change:
33
48
 
@@ -39,7 +54,9 @@ gh api 'repos/{owner}/{repo}/pulls/<number>' -H 'Accept: application/vnd.github.
39
54
  gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/files?per_page=100' --jq '.[].filename'
40
55
  ```
41
56
 
42
- Both reads go through REST, since the `gh pr` diff subcommand runs on GraphQL and a cloud session's GitHub proxy refuses it. The diff media type returns the same unified diff, and the paginated files listing returns every path rather than the first page.
57
+ Both reads go through REST, since the `gh pr` diff subcommand runs on GraphQL and a cloud session's GitHub proxy refuses it. The diff media type returns the same unified diff, and the paginated listing returns every path rather than the first page.
58
+
59
+ ## A later pass
43
60
 
44
61
  A commit is a later pass. Fetch the pull request head so both commits are local:
45
62
 
@@ -47,7 +64,7 @@ A commit is a later pass. Fetch the pull request head so both commits are local:
47
64
  git fetch -q origin pull/<number>/head
48
65
  ```
49
66
 
50
- 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.
67
+ A failed fetch stops the skill: `❌ Could not fetch the PR head. Retry once the remote is reachable.` Never fall through to the full pass, since a fetch failure and a rebase both leave the prior commit unreachable, and the fallback states a rebase as fact on the pull request.
51
68
 
52
69
  Then test that the prior commit still reaches the head:
53
70
 
@@ -55,30 +72,48 @@ Then test that the prior commit still reaches the head:
55
72
  git merge-base --is-ancestor <prior-oid> <headRefOid>
56
73
  ```
57
74
 
58
- 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.
75
+ - On exit zero, `git diff` and `git log --oneline` over `<prior-oid>..<headRefOid>` are the whole read, since the first pass covered everything behind it
76
+ - A non-zero exit means a rebase or force-push, so the delta is undefined rather than empty. Fall back to the full pass and say so in the body.
77
+
78
+ ## An unchanged head
59
79
 
60
- 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:
80
+ A commit is its own ancestor, so an unchanged head passes that test with an empty range. When `<prior-oid>` equals `<headRefOid>`, decide whether this pass has anything to add before reading anything else:
61
81
 
62
82
  ```bash
63
83
  gh api --paginate 'repos/{owner}/{repo}/issues/<number>/comments?per_page=100' --jq '.[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Rebase" or . == "## Post-review findings") | select(.created_at > "<prior-at>") | .id' | tail -n 1
64
84
  ```
65
85
 
66
- `<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.
86
+ `<prior-at>` is the prior pass's `readAt` where it wrote one. Reading `submittedAt` here is the submission-time defect on the time axis, since a reply posted inside that pass's compose window would read as already answered.
87
+
88
+ - Scope the replies to those newer than the prior pass, since an unscoped read hands a re-run after a close-out the filename its prior pass already wrote
89
+ - The REST `id` is the number the thread displays in each comment's anchor
90
+ - Keep `tail` outside the filter for the same per-page reason as the reviews read
91
+
92
+ ### No reply since the prior pass
93
+
94
+ An empty result means 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.`
95
+
96
+ This is the earliest point every path crosses. A stop inside Step 4's filename derivation would sit on a step reached only when composing a body, which a shortcut path can route around.
97
+
98
+ ### A response or rebase reply
99
+
100
+ A non-empty result carries the comment id Step 4 needs for the third filename segment. A `## Review response` answers a finding already argued, and a `## Rebase` reports a stale branch resolved without one.
67
101
 
68
- 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.
102
+ - Read it for what the worker changed or accepted, and treat an accepted finding as closed rather than restating it
103
+ - Skip the diff and file reads below. This is the entire read on a repeated head, since the empty range cannot say whether a prior finding landed.
69
104
 
70
- The REST `id` is the number the thread displays in each comment's anchor. Keep `tail` outside the filter for the same per-page reason as the reviews read above.
105
+ ### A post-review findings reply
71
106
 
72
- 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.
107
+ A `## Post-review findings` reply asserts a new defect rather than answering one, and this pass is its first independent reader. Restating it unread repeats the worker's claim.
73
108
 
74
- 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.
109
+ Read the file it names at `<headRefOid>`, the same `git show <headRefOid>:<path>` read Step 3 runs to confirm a ticked box, and confirm the defect before it becomes this pass's own finding.
75
110
 
76
- 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.
111
+ ## A closed verdict on a moved head
77
112
 
78
- 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.
113
+ A moved head has its own way to add nothing, which the ancestor test cannot see. When `<prior-heading>` reads `## Review closed`, a pass over the new commits that raises nothing would repeat the standing verdict. It arises when a prior pass leaves a change to the author's call, the author makes it, and the delta has nothing left to say.
79
114
 
80
- 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.
115
+ Carry `<prior-heading>` into Step 4 and run Step 3 unchanged. This guard turns on what the pass carries, which is Step 3's output, so deciding ahead of the review would swallow the pass that does find something, the costlier error.
81
116
 
82
- 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.
117
+ ## The read
83
118
 
84
119
  Read each changed file in scope. Skip deleted files. Run reads in parallel.
@@ -148,6 +148,8 @@ A console error or an overflow is a finding. A probe result is a reading to conf
148
148
 
149
149
  Read `${CLAUDE_SKILL_DIR}/references/post.md` for the heading rule, the body shape, the marker every body ends on, and the post command. Each box lands in the section its verdict names, and the `link` its Step 5 push returned goes as given into that box's own entry.
150
150
 
151
+ That file sends a pass closing under a prior `## UI review closed` at an older head on to `${CLAUDE_SKILL_DIR}/references/close-out.md`, which rewrites the standing close in place.
152
+
151
153
  ## Step 8: tick the passed boxes
152
154
 
153
155
  Read `${CLAUDE_SKILL_DIR}/references/tick.md` once the verdict has posted, for how every box that passed is ticked through `canon pr tick` at the head this pass drove, and what a refusal reports.
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: Rewrite a repeated UI close in place
3
+ description: The branch of review-ui Step 7 taken when the prior verdict closed the review and this pass owes nothing, which replaces the standing close with a PUT rather than posting a second one
4
+ ---
5
+
6
+ # A UI close that repeats the standing one
7
+
8
+ Part of Step 7 of `review-ui`, reached only on the branch `post.md` names. The body shape, the scan, and the marker still come from `post.md`.
9
+
10
+ When the newest UI verdict from Step 1 reads `## UI review closed` at an older head and this pass closes too, the thread already holds this verdict. Replace the standing close rather than posting a second one beside it.
11
+
12
+ - Only a closed prior is replaced, so after a prior `## UI review` this pass's close posts normally
13
+ - A pass that owes anything posts under `## UI review` whatever the prior read, since a rewrite would hide a failed box behind a closed heading
14
+
15
+ Resolve the standing close's numeric id off the REST listing, keeping the newest match across every page:
16
+
17
+ ```bash
18
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | select((.body // "") | split("\n")[0] | rtrimstr("\r") == "## UI review closed") | .id' | tail -n 1
19
+ ```
20
+
21
+ ## Write the replacement
22
+
23
+ Write the full body for this pass to the file `post.md` names, with the heading, the count line, and the Passed table regenerated from what this pass drove. Patching only the marker would leave a count line and an address naming the old head.
24
+
25
+ Add one line under the count line, reading the prior head off the standing verdict's marker and the commit count off the compare:
26
+
27
+ ```bash
28
+ gh api repos/{owner}/{repo}/compare/<prior-head>...<head> --jq .total_commits
29
+ ```
30
+
31
+ ```plaintext
32
+ Re-checked at `<short-sha>`, <N> commits since the prior pass.
33
+ ```
34
+
35
+ Omit the line when the standing verdict carried no marker, since there is no prior head to count from. Run the scan `post.md` states against the file. Then record the instant just before the call, in UTC, as `<posted-at>`, since Step 9 keeps only the review-event runs created after it and the `PUT` fires one on `edited`. Replace the comment:
36
+
37
+ ```bash
38
+ date -u +%Y-%m-%dT%H:%M:%SZ
39
+ gh api -X PUT repos/{owner}/{repo}/pulls/<number>/reviews/<review-id> -F body=@<body-file>
40
+ ```
41
+
42
+ ## The marker
43
+
44
+ The body ends on `<!-- review-ui: head=<head> -->` at the full head this pass drove, with no line below it. `PUT` leaves the review's `commit.oid` pinned at the head the close was first posted against, so `resolveUiScope`, the lift gate, and the orchestrator poll read the marker in its place.
45
+
46
+ - A body missing it reads as the pinned stamp, so the lift refuses `ui-behind` and the poll reports the close as behind
47
+ - `PUT` keeps the comment's timestamp and place, so the poll's age test reads a rewritten close as old, the reading the code review accepts
48
+ - A submitted review cannot be deleted, which is why this rewrites the standing close rather than posting a fresh one
49
+ - The standing verdict's frames sit under their own pass stamp on the frames branch, so the new body links this pass's frames and the old stay reachable by path
@@ -32,7 +32,16 @@ A box written before that format may carry no route, no width, and a verb such a
32
32
  - Text, presence, and attributes: `pw --raw eval "<expression>"`, quoting the value back.
33
33
  - Computed style: `getComputedStyle(<element>).<property>`.
34
34
  - Geometry and overflow: `getBoundingClientRect()`, and `scrollWidth` against `clientWidth` on `document.documentElement`.
35
- - A frame: `pw screenshot --filename=frames/<box-number>.png`, then open it with the file-reading tool before saying what it shows. A taste box takes the shot twice, once per theme, naming the theme in the file as `frames/<box-number>-<theme>.png`. Reach each theme through the page's own control when the box names one, and through `pw set-color-scheme <dark|light>` otherwise, then read the page's background before the shot to confirm it changed. A page that paints the same under both has no second theme to show, so the box is not driven, with the reason naming that. `pw clear-color-scheme` restores the default.
35
+ - A frame: `pw screenshot --filename=frames/<box-number>.png`, then open it with the file-reading tool before saying what it shows.
36
+
37
+ ### Taste frames
38
+
39
+ A taste box takes the shot twice, once per theme, naming the theme in the file as `frames/<box-number>-<theme>.png`.
40
+
41
+ - Reach each theme through the page's own control when the box names one, and through `pw set-color-scheme <dark|light>` otherwise
42
+ - Read the page's background before the shot to confirm the theme changed
43
+ - A page painting the same under both has no second theme, so the box is not driven, with the reason naming that
44
+ - `pw clear-color-scheme` restores the default
36
45
 
37
46
  ## The four verdicts
38
47
 
@@ -45,8 +54,19 @@ Each box takes exactly one.
45
54
  | needs eyes | The box ends in `(taste)` | The dark and the light shot as a pair, each the matching evidence stem or a pushed frame's link, and what the frames showed, with nothing about whether it looks right |
46
55
  | not driven | The state could not be reached | The reason, such as an unreachable address, a `(local only)` box on a hosted preview, or a target the snapshot does not hold |
47
56
 
48
- A pass or fail resting on a frame carries one image at most, and never one the worker's evidence already shows. A needs-eyes box carries the pair, dark and light, since a taste call is made by comparing them. Match each theme separately: when a state and stem in the evidence record match the box's route, width, and that theme, the evidence names that stem and nothing is pushed for it, so a view with the dark shot in evidence pushes the light one alone. Otherwise the frame goes up through `canon pr frames` with `--theme`, and the `link` the record returned, which names the frame's path on the frames branch, goes in the box's own section of the body and never inside a folded block. Either way the words stay, describing the value or region judged, such as the element, its position, and the colors or text read, so a reader whose image fails to load can still check it.
57
+ A fail names what was expected beside what was read. A not driven never reads as a fail, since it says the pass could not look rather than that the page is wrong. Its reason says whether the cause sits on the page or off it, since only a cause on the page opens the heading Step 7 picks.
49
58
 
50
- A push that refuses leaves the words alone, with the reason named. Never cite the local frame path. A path under `.canon/` is a board identifier the label scan rejects, and it resolves only on this machine.
59
+ ### Which image a verdict carries
51
60
 
52
- A fail names what was expected beside what was read. A not driven never reads as a fail, since it says the pass could not look rather than that the page is wrong. Its reason says whether the cause sits on the page or off it, since only a cause on the page opens the heading Step 7 picks.
61
+ A pass or fail resting on a frame carries one image at most, and never one the worker's evidence already shows. A needs-eyes box carries the pair, dark and light, since a taste call is made by comparing them.
62
+
63
+ - Match each theme separately. An evidence stem matching the box's route, width, and theme is named and nothing is pushed for it.
64
+ - A view with the dark shot in evidence therefore pushes the light one alone
65
+ - Otherwise push the frame through `canon pr frames` with `--theme`, and put the returned `link` in the box's own section, never inside a folded block
66
+
67
+ ### The words beside an image
68
+
69
+ Either way the words stay, describing the value or region judged, such as the element, its position, and the colors or text read, so a reader whose image fails to load can still check it.
70
+
71
+ - A push that refuses leaves the words alone, with the reason named
72
+ - Never cite the local frame path, since a path under `.canon/` is a board identifier the label scan rejects and resolves only on this machine
@@ -14,7 +14,11 @@ The heading reports whether the branch owes anything, the way the code review's
14
14
  - `## UI review` when a box failed, the sweep found a console error or an overflow, or a box was not driven for a cause on the page, such as a target the snapshot does not hold.
15
15
  - `## UI review closed` otherwise.
16
16
 
17
- A needs-eyes box leaves the review closed, and so does a box not driven for a cause off the page: an address that did not answer, or a `(local only)` box on a hosted preview. Each is owed to the person who merges rather than to the branch, and no commit on the branch can settle it, so opening the heading would hold the draft mark at every head. Name each such box in the count line's not-driven count and in its Not driven entry. Do not append the pull request number, which GitHub already renders.
17
+ A needs-eyes box leaves the review closed, and so does a box not driven for a cause off the page, such as an address that did not answer or a `(local only)` box on a hosted preview.
18
+
19
+ - Each is owed to the person who merges, and no commit can settle it, so opening the heading would hold the draft mark at every head
20
+ - Name each such box in the count line's not-driven count and in its Not driven entry
21
+ - Do not append the pull request number, which GitHub already renders
18
22
 
19
23
  ## The body
20
24
 
@@ -74,15 +78,35 @@ Driven at `<short-sha>` on <address>, rendered by <renderer>.
74
78
 
75
79
  The body opens on its count line, in that order, so the operator reads what they owe before anything else. The second line carries the address, the head, and the renderer, and says the address was driven unverified when it came from the local rung or the launch.
76
80
 
77
- Each section is dropped when it holds no box, along with its heading. Order is fixed: Failed, Needs your eyes, Not driven, then the folded blocks. A box takes exactly one section, chosen by its verdict, so a box that fails and also ends in `(taste)` appears once, under Failed.
81
+ - Drop each section holding no box, along with its heading. Order is fixed: Failed, Needs your eyes, Not driven, then the folded blocks.
82
+ - A box takes exactly one section by its verdict, so a box that fails and also ends in `(taste)` appears once, under Failed
83
+ - Quote page text in backticks as content, never as the pass's own words
84
+
85
+ ### A failed box
86
+
87
+ A box in a section opens on its number and the box as written, in bold, with the reading beneath it in words. A failed box that rests on a read has no image.
88
+
89
+ - A failed box resting on a frame shows it inline under its heading, the `![box <n>](link)` embed with the link exactly as `canon pr frames` returned it
90
+ - That link names the frame's path on the frames branch, which stays put while other pull requests' frames are dropped and carries this pass's stamp
91
+ - A later pass therefore never changes the image
92
+
93
+ ### A needs-eyes box
78
94
 
79
- A box in a section opens on its number and the box as written, in bold, with the reading beneath it in words. A failed box that rests on a frame shows that frame inline under its heading, the `![box <n>](link)` embed with the link exactly as `canon pr frames` returned it. It names the frame's path on the frames branch, which stays put while other pull requests' frames are dropped and carries this pass's stamp, so a later pass never changes the image. A failed box that rests on a read has no image.
95
+ A needs-eyes box shows its dark and light shots in a two-cell table ahead of anything else it carries, since the operator decides from them without opening the app. A box whose evidence holds one theme still renders both cells.
80
96
 
81
- A needs-eyes box shows its dark and light shots in a two-cell table ahead of anything else it carries, since the operator decides from them without opening the app. A cell is either the `link` Step 5 pushed or, for a theme the worker's evidence already shows, the evidence image at `https://github.com/<owner>/<repo>/blob/<head>/<path>?raw=true`, with `<path>` the matching state item's `path` off the evidence record. A box whose evidence holds one theme still renders both cells, one evidence link and one pushed link. A push that refused leaves its cell holding the words `not shown, <reason>`, so the table keeps its shape, and the box's line names what the frame showed. Never cite the local frame path, since a path under `.canon/` is a board identifier the label scan rejects.
97
+ - A cell is the `link` Step 5 pushed, or for a theme the worker's evidence shows, `https://github.com/<owner>/<repo>/blob/<head>/<path>?raw=true`
98
+ - `<path>` there is the matching state item's `path` off the evidence record
99
+ - A refused push leaves its cell reading `not shown, <reason>`, so the table keeps its shape, and the box's line names what the frame showed
100
+ - Never cite the local frame path, since a path under `.canon/` is a board identifier the label scan rejects
82
101
 
83
- The Passed block holds one row per passed box, with each row's measurement and the evidence stem it matched in backticks, so every reading stays one click away. A pass that cites a stem links it as the evidence image at the form above when the record holds the item. Omit the block when nothing passed.
102
+ ### The folded blocks
84
103
 
85
- Omit the Sweep block when it found nothing, and the Guessed wording block when every box named its route and width, and drop the folded block when both are empty. Quote page text in backticks as content, never as the pass's own words.
104
+ The Passed block holds one row per passed box, with its measurement and the evidence stem it matched in backticks, so every reading stays one click away. A row citing a stem links it as the evidence image in the form above when the record holds the item.
105
+
106
+ - Omit the Passed block when nothing passed, and the Sweep block when it found nothing
107
+ - Omit the Guessed wording block when every box named its route and width, and drop the folded block when both are empty
108
+
109
+ ### An all-pass body
86
110
 
87
111
  A pass where every box passed is the heading, the count line, the folded Passed block, and the marker, with no empty headings:
88
112
 
@@ -104,21 +128,27 @@ The count line and the sections beneath it hold the same totals, and Step 10's p
104
128
 
105
129
  ## The marker
106
130
 
107
- End every body on `<!-- review-ui: head=<head> -->`, carrying the full sha Step 1 resolved. The orchestrator's poll reads it off the last non-empty line to decide whether the verdict covers the current head, and the submission stamp cannot answer that, since GitHub stamps it with whatever the head is at the moment of posting.
131
+ End every body on `<!-- review-ui: head=<head> -->`, carrying the full sha Step 1 resolved. A body that replaces a standing close by `PUT` carries it too, at the head this pass drove.
132
+
133
+ The orchestrator's poll reads it off the last non-empty line to decide whether the verdict covers the current head. The submission stamp cannot answer that, since GitHub stamps it with whatever the head is at the moment of posting.
108
134
 
109
135
  ## Posting
110
136
 
111
- Run `canon labels scan --body-file .canon/tmp/pr/review-ui/body-<number>-<short-sha>.md` against the body first, which applies the rule in `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md`, since nothing gates a file under `.canon/tmp/` before the review-event run does. Fix the body and scan again on a finding. Then post it as a review rather than an issue comment, since the poll reads the UI family off reviews:
137
+ Run `canon labels scan --body-file .canon/tmp/pr/review-ui/body-<number>-<short-sha>.md` against the body first, which applies the rule in `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md`. Nothing gates a file under `.canon/tmp/` before the review-event run does. Fix the body and scan again on a finding.
112
138
 
113
- Record the instant just before the post, in UTC, as `<posted-at>`. Step 9 keeps only review-event runs created after it, since the code review posts its own review on the same head and fires a run of the same gate.
139
+ - Post it as a review rather than an issue comment, since the poll reads the UI family off reviews
140
+ - Skip the post when Step 1's newest verdict reads `## UI review closed` at an older head and this pass closes too, and read `${CLAUDE_SKILL_DIR}/references/close-out.md` instead
141
+ - Posting first in that case leaves two closes, both naming a head
142
+ - Record the instant just before the post, in UTC, as `<posted-at>`, since Step 9 keeps only review-event runs created after it
143
+ - The code review posts its own review on the same head and fires a run of the same gate, which is why that cutoff matters
114
144
 
115
145
  ```bash
116
146
  date -u +%Y-%m-%dT%H:%M:%SZ
117
147
  gh api -X POST 'repos/{owner}/{repo}/pulls/<number>/reviews' -f event=COMMENT -F body=@.canon/tmp/pr/review-ui/body-<number>-<short-sha>.md --jq .html_url
118
148
  ```
119
149
 
120
- The post goes through the REST reviews endpoint rather than the `gh pr` review subcommand, which runs on GraphQL and fails where a cloud session's GitHub proxy refuses it. `-F` reads the file into the body, where `-f` would post the path itself.
121
-
122
- The ticks come after the post, in Step 8, so the verdict and the ticks report the same `<head>`. The post never ticks, and a pass that stops before posting ticks nothing.
150
+ The post goes through the REST reviews endpoint, since the `gh pr` review subcommand runs on GraphQL and fails where a cloud session's GitHub proxy refuses it.
123
151
 
124
- Never pass `event=APPROVE` or `event=REQUEST_CHANGES`, which carry a state the poll does not read and a weight this pass has not earned.
152
+ - `-F` reads the file into the body, where `-f` would post the path itself
153
+ - The ticks come after the post, in Step 8, so the verdict and the ticks report the same `<head>`. A pass that stops before posting ticks nothing.
154
+ - Never pass `event=APPROVE` or `event=REQUEST_CHANGES`, which carry a state the poll does not read and a weight this pass has not earned