@erclx/canon 5.46.0 → 5.48.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 (55) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/git-followup/SKILL.md +1 -0
  3. package/claude/skills/git-pr/REQUIREMENT.md +2 -2
  4. package/claude/skills/git-pr/SKILL.md +2 -1
  5. package/claude/skills/git-pr/references/closing-issues.md +2 -0
  6. package/claude/skills/git-pr/references/pr.md +58 -7
  7. package/claude/skills/review-address/SKILL.md +18 -6
  8. package/claude/skills/review-address/references/post-review-findings.md +4 -1
  9. package/claude/skills/review-address/references/rebase-only.md +14 -2
  10. package/claude/skills/review-pr/SKILL.md +9 -2
  11. package/claude/skills/review-pr/references/body-variants.md +29 -8
  12. package/claude/skills/review-pr/references/post.md +29 -10
  13. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  14. package/claude/skills/review-ui/SKILL.md +4 -3
  15. package/claude/skills/review-ui/references/post.md +16 -2
  16. package/claude/skills/review-ui/references/tick.md +7 -1
  17. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +4 -2
  19. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  20. package/claude/skills/role-reviewer/SKILL.md +4 -1
  21. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  22. package/claude/skills/role-worker/SKILL.md +1 -0
  23. package/claude/skills/task-board/references/decline.md +2 -2
  24. package/claude/skills/task-board/references/tasks.md +4 -2
  25. package/claude/skills/task-complete/SKILL.md +1 -0
  26. package/claude/skills/task-complete/references/archive.md +5 -3
  27. package/claude/skills/ui-checklist/SKILL.md +3 -3
  28. package/docs/agents/migrate.md +2 -2
  29. package/docs/target/migrations.md +4 -2
  30. package/docs/workflow/operating-model.md +2 -2
  31. package/docs/workflow/zshrc-aliases.md +3 -0
  32. package/package.json +1 -1
  33. package/scripts/core/bootstrap.sh +1 -0
  34. package/src/commands/claude.ts +4 -0
  35. package/src/commands/labels.ts +66 -1
  36. package/src/commands/migrate.ts +5 -4
  37. package/src/commands/pr.ts +405 -30
  38. package/src/commands/sessions.ts +25 -11
  39. package/src/commands/tasks.ts +11 -1
  40. package/src/labels/density.ts +363 -0
  41. package/src/migrate/session-defaults.ts +20 -5
  42. package/src/pr/evidence.ts +1 -1
  43. package/src/pr/lift.ts +19 -0
  44. package/src/pr/merge.ts +175 -0
  45. package/src/pr/rest.ts +3 -0
  46. package/src/pr/review-scope.ts +40 -1
  47. package/src/pr/tick.ts +109 -0
  48. package/src/sessions/live.ts +26 -0
  49. package/src/sessions/resolve.ts +62 -11
  50. package/src/tasks/archive.ts +32 -8
  51. package/src/tasks/closes.ts +9 -5
  52. package/src/tasks/validate.ts +4 -2
  53. package/tooling/base/seeds/.github/pull_request_template.md +1 -1
  54. package/tooling/claude/reference.md +3 -1
  55. package/tooling/claude/seeds/.claude/settings.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
4
- "version": "5.46.0",
4
+ "version": "5.48.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -58,6 +58,7 @@ A missing tracking ref is not a guard, since an open pull request proves the bra
58
58
  - Read the live title and body with `gh api repos/{owner}/{repo}/pulls/<number> --jq '{html_url, title, body}'`. Update the body with `gh api -X PATCH repos/{owner}/{repo}/pulls/<number> -F body=@<file> --silent` when the new commit changes scope, writing the new body to `<file>` first, and the title with `gh api -X PATCH repos/{owner}/{repo}/pulls/<number> -f title="<title>" --silent` when the scope shifted enough to make it inaccurate.
59
59
  - A fix commit answering a review changes what shipped exactly as much as an ordinary followup does, so the sync cannot wait on the invocation or the comment count below.
60
60
  - A body a person edited by hand between rounds gets no special handling: judge it against the tree the same way regardless of who wrote it last, since a hand-edit the fix commit has made stale is the exact drift this sync exists to close.
61
+ - Carry a `UI pass` line marked `<!-- pr-ui-pass -->` into any rewritten body unchanged, since `canon pr tick` owns it and a dropped marker defeats the reset on a new head.
61
62
  8. Route on the invocation and the comment count for the reply alone.
62
63
  - When invoked with `reply-owned`, skip this step: the caller posts its own reply.
63
64
  - Otherwise, if the count is above zero, the followup addresses review feedback: write a one-line summary of the fix to `.canon/tmp/pr/followup/summary-<number>.md` in its own call, run `canon labels scan --body-file .canon/tmp/pr/followup/summary-<number>.md` against it, which applies the rule in `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md`, then post it with `gh api -X POST repos/{owner}/{repo}/issues/<number>/comments -F body=@.canon/tmp/pr/followup/summary-<number>.md --silent`. A hook on `Bash` runs the same scan on that body file and denies the post on a hit, and it refuses an inline `-f body=` value, so the body goes through a file. The `pull_request` check the git-pr surface carries triggers on a push or an open rather than a plain edit, so neither this comment nor the title and body step 7 synced reaches it.
@@ -7,13 +7,13 @@ description: What pull request generation is for, the gaps it closes, and what i
7
7
 
8
8
  ## Gap
9
9
 
10
- Without this skill, a pull request body is written from memory rather than the diff, so it describes the intent and omits what the work became. Testing boxes get ticked from intent, a second push errors or opens a duplicate, and banned characters survive into a body the hook never sees.
10
+ Without this skill, a pull request body is written from memory rather than the diff, so it describes the intent and omits what the work became. Testing boxes get ticked from intent, a second push errors or opens a duplicate, and banned characters survive into a body the hook never sees. The body grows into a wall nothing measures.
11
11
 
12
12
  Every pull request lands unlabelled, so a merged list cannot be filtered by surface. A head-branch lookup resolves a reused name to the merged pull request and rewrites it. A pull request changing a rendered surface reaches review with no screenshots, and a hand-written `Closes` line closes an issue on a middle slice or misses one the task names.
13
13
 
14
14
  ## Must
15
15
 
16
- - The title and body come from the commits and the diff against main with lockfiles excluded, scanned for banned characters and phase labels, and every ticked box records a check this run executed
16
+ - The title and body come from the commits and the lockfile-free diff against main, scanned for banned characters, phase labels, and density, and every ticked box records a check this run executed
17
17
  - A follow-up run edits the one open pull request on the current head and default base, resolved once, and every write first proves that pull request's head and open state
18
18
  - Closing lines come from `canon tasks closes`, one `Closes #NNN` per issue on the task's last slice only, with the scan failing a body that disagrees
19
19
  - Labels come from the paths the branch changed against a map the project declares, applied after the pull request exists, with a refused label reported
@@ -105,7 +105,7 @@ Drop `--closes` when the closes step skipped a refusal, since no list was decide
105
105
 
106
106
  ### Reading the scan
107
107
 
108
- Branch on the JSON record rather than the exit code, since a shell function wrapping `canon` can flatten a non-zero exit to 0. Stop and fix the title or body on any key below, and do not proceed to `### Final command` until a re-run comes back clean on all eight.
108
+ Branch on the JSON record rather than the exit code, since a shell function wrapping `canon` can flatten a non-zero exit to 0. Stop and fix the title or body on any key below, and do not proceed to `### Final command` until a re-run comes back clean on all nine.
109
109
 
110
110
  | Key | Fix |
111
111
  | ------------------- | ------------------------------------------------------------------------------------ |
@@ -117,6 +117,7 @@ Branch on the JSON record rather than the exit code, since a shell function wrap
117
117
  | `sections` | Rename or drop each heading in `outside`, and add each required heading in `missing` |
118
118
  | `missingClosures` | Add a `Closes #NNN` line for each number listed |
119
119
  | `strayClosures` | Delete the closing keyword for each number listed |
120
+ | `density` | Regroup or split the named section per `## Formatting` in `pr.md`, keeping each path |
120
121
 
121
122
  Leave `cutsRelease` and `semverTags` alone, since a release-please pull request legitimately carries version references its own fixed shape explains.
122
123
 
@@ -24,6 +24,8 @@ The verb reads the current branch itself and counts the names it carried before
24
24
  | `ok: false`, `no-board`, `no-match`, or `ambiguous` | Write nothing and run the scan without `--closes` |
25
25
  | Any other refusal | Report it rather than swallowing it |
26
26
 
27
+ The record also carries `operatorChecks`, the unticked `Operator check:` outcomes. They hold nothing open, so a task with only those left still reads `closing: true`. An operator outcome nobody marked reads as `open-outcomes`, so the mark has to land before `git-pr` runs.
28
+
27
29
  The three skipped refusals are the ones `task-number.md` skips, and for the same reason. A guessed task would close the wrong issue.
28
30
 
29
31
  ## The line
@@ -52,8 +52,17 @@ A body describes the new behavior and nothing the diff or the commits already sa
52
52
 
53
53
  ## Sections
54
54
 
55
- - `## Summary`: 1-2 sentences following `<Action Verb> <Direct Object> to <Result>`, expand for clarity if needed
56
- - `## Key changes`: name actual files, functions, or modules (e.g., `AuthService.verify()` not "auth handler"). Always use bullet points, never prose.
55
+ - `## Summary`: one paragraph following `<Action Verb> <Direct Object> to <Result>`, then the closing lines
56
+ - `## Key changes`: bullets only, one change each, naming actual files, functions, or modules (e.g., `AuthService.verify()` not "auth handler")
57
+ - Name every changed path in a Key changes bullet. A trim may cut a clause and never a path, since `canon pr key-changes` reports a path no bullet claims.
58
+
59
+ ### Key changes groups
60
+
61
+ Past 5 bullets, group Key changes under bold labels, 5 bullets a group at most.
62
+
63
+ - A label sits on its own line, names the area a reviewer opens together, and ends with no period
64
+ - Write a label in plain words, since a code span inside bold renders unbolded
65
+ - Open each group on a bullet, never on prose
57
66
 
58
67
  ### Closing lines
59
68
 
@@ -61,10 +70,12 @@ The closing lines end `## Summary`, one `Closes #NNN` per issue and no trailing
61
70
 
62
71
  ### Context section
63
72
 
64
- `## Technical context` is optional and carries 1-2 lines of architectural reasoning explaining why, not what.
73
+ `## Technical context` is optional and says why, naming the decisions the code can't show and any shortcoming of the approach.
65
74
 
66
75
  - Omit Technical context for docs, config, or trivial changes
67
- - Use bullet points for multiple reasons, one sentence for a single reason
76
+ - Take one form per section, never bullets and paragraphs mixed
77
+ - Use bullets when every reason fits one sentence under 200 characters
78
+ - Otherwise write one paragraph per decision, opening on a bold lead-in that ends with a period and runs into the paragraph
68
79
 
69
80
  ### Testing section
70
81
 
@@ -72,6 +83,7 @@ The closing lines end `## Summary`, one `Closes #NNN` per issue and no trailing
72
83
 
73
84
  - Omit Testing for docs, config, or trivial sync changes
74
85
  - Use checkboxes, never prose. See Testing discipline for which box gets ticked.
86
+ - Write one box per command, pointing at a test file rather than listing its cases
75
87
 
76
88
  ### Reviewer section and visuals
77
89
 
@@ -90,6 +102,8 @@ The closing lines end `## Summary`, one `Closes #NNN` per issue and no trailing
90
102
  - A ticked box describes the tree as it stands. A later commit removing or replacing what it names makes the box false.
91
103
  - The branch that made that commit corrects the box in the same pass, as with any other stale claim in the body
92
104
 
105
+ Leave the `UI pass` line to `canon pr tick`, and carry it on a rewrite.
106
+
93
107
  ### Human-only boxes
94
108
 
95
109
  Leave a box unchecked only when a human is required, and name which human and why on the same line. The agent runs everything else.
@@ -110,6 +124,17 @@ A live agent session is never the human a box waits on.
110
124
  ## Formatting
111
125
 
112
126
  - End every bullet point with a period
127
+ - Carry no H3 or H4 anywhere in a body. GitHub draws an H3 one step under an H2, so stacked H3s compete with the sections.
128
+ - Group with a bold label or lead-in instead, in the forms the Sections rules above state
129
+
130
+ ### Density
131
+
132
+ `canon labels scan` reports each section's density under `density`, naming the section, the kind, and the line in the whole body. Its help lists every limit.
133
+
134
+ - Every section but Testing holds bullets to 200 characters and 5 in a row, and paragraphs to 350 characters and 2 in a row
135
+ - Any section reports a `subheading`, and Technical context reports a `mixed-form`
136
+ - A bold label alone on a line breaks a Key changes run, and a bold lead-in starts a new Technical context run
137
+ - Fix a finding by regrouping or splitting, never by cutting a path
113
138
 
114
139
  ## Examples
115
140
 
@@ -122,19 +147,29 @@ Every section a body may carry, in order.
122
147
 
123
148
  <Action Verb> <Direct Object> to <Result>.
124
149
 
150
+ Closes #<number>
151
+
125
152
  ## Key changes
126
153
 
154
+ **<Area a reviewer opens together>**
155
+
127
156
  - <Verb> <specific component/file/function> (<reason if non-obvious>)
128
157
  - <Verb> <specific component/file/function>
129
158
 
159
+ **<Next area>**
160
+
161
+ - <Verb> <specific component/file/function>
162
+
130
163
  ## Technical context
131
164
 
132
- - <Architectural reasoning explaining why, not what>
165
+ **<Decision in a few words>.** <Why this approach, what the code can't show, and any shortcoming>
166
+
167
+ **<Next decision>.** <Why>
133
168
 
134
169
  ## Testing
135
170
 
136
171
  - [x] <Command run> <observed result>
137
- - [x] <Edge case verified> <what was observed>
172
+ - [x] <Test file run> <observed result>
138
173
  - [ ] <Human-only check> (<which human, why>)
139
174
 
140
175
  ## For the reviewer
@@ -153,12 +188,20 @@ Update auth middleware to enforce jwt expiration checks. # imperative + direct o
153
188
 
154
189
  ## Key changes
155
190
 
191
+ **Validation** # bold label on its own line, no period
192
+
156
193
  - Add `verifyExpiration()` to `src/auth/validators.ts`. # specific function + file path
194
+ - Add the clock skew window to `src/auth/config.ts`. # one change per bullet
195
+
196
+ **Session handling** # one label per area a reviewer opens together
197
+
157
198
  - Refactor `AuthService.authenticate()` to handle 401 codes. # named component + clear change
158
199
 
159
200
  ## Technical context
160
201
 
161
- - Migration to stateless session management for horizontal scalability. # why, not what
202
+ **Stateless sessions.** Expiry lives in the token, so any instance can reject it without a shared store. # bold lead-in + why
203
+
204
+ **Clock skew.** A 30-second window absorbs drift between instances, so a token can outlive its expiry by that much. # decision + shortcoming
162
205
 
163
206
  ## Testing
164
207
 
@@ -182,9 +225,17 @@ This PR updates the authentication system to be more robust. # "This PR" opener
182
225
 
183
226
  ## Key changes
184
227
 
228
+ ### Middleware # H3 inside a body
229
+
185
230
  - Updated auth middleware files # vague, no specific component, no period
186
231
  - The old system used to check tokens differently # describes historical behavior
187
232
 
233
+ ## Technical context
234
+
235
+ - Tokens are stateless now. # a bullet
236
+
237
+ The skew window trades precision for no shared store. # mixed with a paragraph in one section
238
+
188
239
  ## Testing
189
240
 
190
241
  - Tested manually # no specific command or case
@@ -88,33 +88,45 @@ Once the fixes are commits, re-run step 5's staleness test against the new head.
88
88
 
89
89
  ### Write the reply
90
90
 
91
- Write a summary reply to `.canon/tmp/pr/reply/reply-<number>.md`, keyed on the PR number so two sessions addressing different pull requests never overwrite each other. Load `write-human` for voice, follow `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md`, and keep each mapping to a line or two.
91
+ Write a summary reply to `.canon/tmp/pr/reply/reply-<number>.md`, keyed on the PR number so two sessions addressing different pull requests never overwrite each other. Load `write-human` for voice, follow `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md`, and keep each mapping to one line.
92
92
 
93
93
  | Part | Content |
94
94
  | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
95
95
  | Heading | `## Review response`, distinct from human threads and subordinate to the review's own `## Review` |
96
96
  | Summary | One sentence. After a step 5 rebase, say so and name files resolved by hand and those the regen rebuilt, since the next review is a full pass. |
97
- | Findings | One bullet per finding, opening with the bolded identifier, naming what changed or a one-line reason for a conscious-accept |
97
+ | Findings | One line per finding under 200 characters, naming its outcome and the commit that carries it, in the shape below |
98
98
  | Docs | Any `.claude/` docs refreshed as a result of the fixes |
99
99
  | Close | `🤖 Addressed by Claude Code` on its own line, so the reply reads as a machine pass rather than a human sign-off |
100
100
 
101
+ ```markdown
102
+ - **<severity>**, `<file>` line <n>: fixed in `<sha>`. <What now holds, in one sentence.>
103
+ - **<severity>**, `<file>` line <n>: declined. <The fact that settled it.>
104
+ ```
105
+
106
+ The commit carries the fix and its test, so the line names neither.
107
+
101
108
  A run the guard sent straight to step 5 has no findings to map, so it takes the `## Rebase` body in `${CLAUDE_SKILL_DIR}/references/rebase-only.md` instead. A run with findings skips that file.
102
109
 
103
110
  ### What a declined finding carries
104
111
 
105
- A finding this run declined carries the fact that settled it inside its own bullet, such as the plan question that answered it, a constraint the diff does not show, or a measurement the reviewer lacked. Left in the channel, that fact is lost when both sessions end.
112
+ A finding this run declined carries the fact that settled it inside its own line, which may take a second sentence for it, such as the plan question that answered it, a constraint the diff does not show, or a measurement the reviewer lacked. Left in the channel, that fact is lost when both sessions end.
106
113
 
107
114
  The rule reaches a finding and nothing else. A correction to what the reviewing session believes about the world changes no finding here, so answer it wherever the dispatch reached this session and keep it off the comment.
108
115
 
109
116
  ### Scan and post
110
117
 
111
- Nothing tests the reply for either rule above. Follow `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md` and run its scan, which reads for banned characters and phase labels alone:
118
+ Nothing tests the reply for either rule above. Follow `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md` and run its scan, which reads for banned characters and phase labels, and reports the reply's density:
112
119
 
113
120
  ```bash
114
- canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md
121
+ canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md --json
115
122
  ```
116
123
 
117
- The hook skips `.canon/tmp/`, and the `pull_request` check reads only a pull request's own title and body, so this scan is the only gate on the reply. Fix any hit, then post and keep the `id` the POST answers with, since Step 7 edits this exact comment:
124
+ The hook skips `.canon/tmp/`, and the `pull_request` check reads only a pull request's own title and body, so this scan is the only gate on the reply.
125
+
126
+ - Fix any hit, and stop on a `density` entry too, shortening the line it names
127
+ - Read the record rather than the exit, since density never sets the exit code
128
+
129
+ Then post and keep the `id` the POST answers with, since Step 7 edits this exact comment:
118
130
 
119
131
  ```bash
120
132
  gh api -X POST 'repos/{owner}/{repo}/issues/<number>/comments' -F body=@.canon/tmp/pr/reply/reply-<number>.md --jq .id > .canon/tmp/pr/reply/reply-<number>.id
@@ -12,10 +12,13 @@ Outside the numbered flow of `review-address`. The session reads this file when
12
12
  Write the body the way Step 6 writes a reply.
13
13
 
14
14
  - Load `write-human` for voice and word choice, and follow `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md` for punctuation
15
- - Run the `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md` scan before posting, with `canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md`
15
+ - Run the `${CLAUDE_SKILL_DIR}/../git-pr/references/publish.md` scan before posting, with `canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md --json`
16
+ - Stop on a `density` entry as on any hit, since density never sets the exit code
16
17
  - Open with `## Post-review findings` rather than `## Review response`, since nothing on the thread is being answered
17
18
  - Close the body with `🤖 Addressed by Claude Code` on its own line, matching the reply's footer
18
19
 
20
+ Give each finding one line under 200 characters in the response's shape, naming its severity, its file and line, and what holds now or what was filed.
21
+
19
22
  `review-pr` states the full heading set this belongs to and routes it the same as a response, so `role-orchestrator`'s poll picks it up and sends the reviewing session back for a pass.
20
23
 
21
24
  ```bash
@@ -11,10 +11,22 @@ Read from Steps 6, 7, and 8 of `review-address` when the second guard sent the r
11
11
 
12
12
  A rebase-only run has no findings to map, so it takes a different body rather than an empty list. Open it with `## Rebase`, since nothing on the pull request is being responded to and `## Review response` claims a review this run never read.
13
13
 
14
- - State which half of the staleness test fired, being a branch that stopped merging or one `main` changed files under
15
- - Name what landed on `main`, then the files resolved by hand and those the regen rebuilt
14
+ - Open on one sentence naming the commit rebased onto and what landed on `main`, and which half of the staleness test fired, being a branch that stopped merging or one `main` changed files under
15
+ - Give one short line each to the files resolved by hand, those the regen rebuilt, and those replayed with no conflict, each under 200 characters
16
16
  - Close with `🤖 Addressed by Claude Code` on its own line, the same as a review response
17
17
 
18
+ ```markdown
19
+ ## Rebase
20
+
21
+ Rebased onto main at `<short-sha>`, because <why>.
22
+
23
+ - Resolved by hand: `<path>`. <What the resolution kept, in one sentence.>
24
+ - Rebuilt by the regen: `<path>`, `<path>`
25
+ - Replayed with no conflict: `<path>`
26
+
27
+ 🤖 Addressed by Claude Code
28
+ ```
29
+
18
30
  The heading stays outside the `## Review` family, so the close-out's equality test on the first line never matches it.
19
31
 
20
32
  ## Closing confirmation
@@ -123,11 +123,15 @@ Whether a human is genuinely required is a reading the author may hold a reason
123
123
 
124
124
  Read `## For the reviewer` the same way, bounded to the bullets under that heading. The Summary and Technical context carry the author's argument, and reading them while judging is what an independent pass exists to avoid.
125
125
 
126
- Answer each bullet in the body with no severity and no count. An unanswered request stays owed under the heading and dispatch, as an unanswered Testing box does, and an answered one is discharged in the same comment.
126
+ Answer each bullet in the body with no severity and no count.
127
+
128
+ - An unanswered request stays owed under the heading and dispatch, as an unanswered Testing box does
129
+ - An answered one is discharged in the same comment
130
+ - A request the diff answers against the author becomes a graded finding under the file that should have handled it, and enters the count, as an unconfirmed Review focus item does
127
131
 
128
132
  ### Review focus
129
133
 
130
- On a first pass, confirm each `**Review focus:**` item the plan names against the diff, since only the planning session's list names the inputs that would break the change. Report them in a `**Review focus**` block after `**For the reviewer**`, one bullet each naming what confirmed it.
134
+ On a first pass, confirm each `**Review focus:**` item the plan names against the diff, since only the planning session's list names the inputs that would break the change. Report them folded under `<details>` after the reviewer block, with the count in its summary as `Review focus, N checked`, one bullet each naming the case and what confirmed it.
131
135
 
132
136
  - The block carries no severity and enters no count
133
137
  - An item the pass cannot confirm becomes a `should-fix` finding under the file that should have handled it, so a checked item reads apart from a skipped one
@@ -156,6 +160,9 @@ Read `${CLAUDE_SKILL_DIR}/references/post.md` for the body file name, the body s
156
160
 
157
161
  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.
158
162
 
163
+ - Stop on a `density` entry in the body scan's record as on any other finding, and regroup or split the item it names before posting
164
+ - Read the record rather than the exit, since the scan exits 0 on density alone
165
+
159
166
  ## Step 5: output
160
167
 
161
168
  ```plaintext
@@ -23,17 +23,21 @@ A Testing box the Step 3 check raised goes in a `**Testing**` block placed after
23
23
 
24
24
  ## The reviewer block
25
25
 
26
- 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 is followed by its answer, or by what would settle it where the pass could not answer. It carries no severity and enters no count.
26
+ Every `## For the reviewer` bullet Step 3 read gets one bullet in the reviewer block, placed after the Testing block. It carries no severity and enters no count, and a request the pass answered against the author becomes a finding under its file block instead.
27
27
 
28
- - An unanswered bullet is owed like an unanswered Testing box, so the pass takes `## Review` and the full body
29
28
  - An answered bullet is not owed, since the answer is discharged in the same comment that carries it
29
+ - It folds under `<details>` with the count in its summary, as the request shortened and the read or test that settled it
30
+ - An unanswered bullet is owed like an unanswered Testing box, so it stays out of the fold in a `**For the reviewer**` block naming what would settle it, and the pass takes `## Review` and the full body
30
31
  - A pass already posting a numeric summary line adds `plus N reviewer request(s)` to it
31
- - The all-answered close-out below carries the block in place of that line and needs no addition
32
+ - The all-answered close-out below carries the folded block in place of that line and needs no addition
32
33
 
33
34
  ```markdown
34
- **For the reviewer**
35
+ <details>
36
+ <summary>For the reviewer, N confirmed</summary>
35
37
 
36
- - 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`.
38
+ - The 401 and 403 split: `AuthService.authenticate()` returns 401 for an expired token and 403 for a missing scope
39
+
40
+ </details>
37
41
  ```
38
42
 
39
43
  ## Close-out bodies
@@ -41,7 +45,23 @@ Every `## For the reviewer` bullet Step 3 read goes in a `**For the reviewer**`
41
45
  A pass carrying nothing at all takes `## Review closed` and a short body, with the footer line included either way. Post a close-out even with nothing to report, since a review left with no closing comment reads as one nobody answered.
42
46
 
43
47
  - On a first pass, post `✅ No findings. Reviewed against project docs and the board.`
44
- - On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
48
+ - On a later pass, post the count line, then one line per prior finding, minors included, so a closed review always says what it closed
49
+
50
+ ```markdown
51
+ ## Review closed
52
+
53
+ ✅ N prior findings addressed at `<short-sha>`. Re-reviewed the N commits since the prior pass.
54
+
55
+ - **<severity>**, `<path>`: what now holds, in one line
56
+
57
+ 🤖 Reviewed by Claude Code
58
+
59
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
60
+ ```
61
+
62
+ - Each line names what holds now rather than the commit that fixed it, and stays under 200 characters
63
+ - A prior finding this pass withdrew leaves the count, and its line names the fact that settled it rather than a fix, per the withdrawal rule below
64
+ - A closing remark that fits no prior finding, such as a ticked Testing box, takes one line of its own after them
45
65
 
46
66
  ### Only minors
47
67
 
@@ -54,7 +74,8 @@ A pass carrying only minors is an ordinary finding-carrying pass, so it takes th
54
74
 
55
75
  **`path/to/file.ext`**
56
76
 
57
- - **minor** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): finding, and the fix it wants.
77
+ - **minor** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks, in one sentence.
78
+ - Fix: the change
58
79
 
59
80
  🤖 Reviewed by Claude Code
60
81
 
@@ -69,4 +90,4 @@ Write the withdrawal and the fact that settled it in place of the canned line, k
69
90
 
70
91
  ### Only answered reviewer requests
71
92
 
72
- A pass whose only content is a `## For the reviewer` block with every bullet answered, owing nothing else, takes `## Review closed`, the block in place of the canned line, and the footer. The heading reports what the author still owes, and an answer discharged in the same comment owes nothing back.
93
+ A pass whose only content is a `## For the reviewer` block with every bullet answered, owing nothing else, takes `## Review closed`, the folded block in place of the canned line, and the footer. The heading reports what the author still owes, and an answer discharged in the same comment owes nothing back.
@@ -32,7 +32,7 @@ That satisfies both prohibitions above rather than carving an exception into eit
32
32
  A finding bound to a line links to it, so a reader skips the hunt:
33
33
 
34
34
  ```markdown
35
- - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/<path>#L12)): what breaks and the fix
35
+ - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/<path>#L12)): what breaks, in one sentence
36
36
  ```
37
37
 
38
38
  - A span anchors as `#L12-L14`, with no spaces and no en dash, and a single line as `#L12`
@@ -60,12 +60,18 @@ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
60
60
 
61
61
  **`path/to/file.ext`**
62
62
 
63
- - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks and the fix, in two or three sentences.
64
- - **minor** ([lines 30-34](<repo-url>/blob/<headRefOid>/path/to/file.ext#L30-L34)): finding.
63
+ - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks, in one sentence.
64
+ - one fact of evidence, with its link
65
+ - Fix: the change
66
+ - **minor** ([lines 30-34](<repo-url>/blob/<headRefOid>/path/to/file.ext#L30-L34)): what breaks, in one sentence.
67
+ - Fix: the change
65
68
 
66
- **What is right**
69
+ <details>
70
+ <summary>Review focus, N checked</summary>
67
71
 
68
- - bounded confirmation.
72
+ - the case: the test or read that covers it
73
+
74
+ </details>
69
75
 
70
76
  🤖 Reviewed by Claude Code
71
77
 
@@ -81,14 +87,24 @@ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-
81
87
 
82
88
  **`path/to/file.ext`**
83
89
 
84
- - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks and the fix, in two or three sentences.
90
+ - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks, in one sentence.
91
+ - Fix: the change
85
92
 
86
93
  🤖 Reviewed by Claude Code
87
94
 
88
95
  <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
89
96
  ```
90
97
 
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`.
98
+ ### The finding shape
99
+
100
+ A reader decides from the first line of a finding whether to open the rest, so the subject carries the failure alone.
101
+
102
+ - The subject is one sentence under 200 characters, naming what breaks rather than how the pass found it
103
+ - Each nested bullet carries one fact of evidence with its link, and the last one opens on `Fix:`
104
+ - A nested bullet stays under 200 characters too, since `canon labels scan` measures it as a bullet of its own
105
+ - A finding with nothing to add past its subject takes the `Fix:` bullet alone
106
+
107
+ 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`. A request or a focus case that fails leaves `<details>` and becomes a finding.
92
108
 
93
109
  ### The PR body block
94
110
 
@@ -123,9 +139,9 @@ A minor the dispatched worker declines needs a surface that survives the merge,
123
139
  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
140
 
125
141
  - 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
142
+ - State each finding in the shape above, never as a paragraph of reasoning
127
143
  - 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
144
+ - Leave `What is right` out unless the author asked a direct question it answers, then cap it at three bullets
129
145
  - Close with `🤖 Reviewed by Claude Code` on its own line, so the review reads as an independent machine pass, not a human sign-off
130
146
 
131
147
  ## The marker, the scans, and the post
@@ -156,7 +172,10 @@ The marker is the read-time record those fields are not.
156
172
 
157
173
  ### The scans
158
174
 
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.
175
+ 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`. Pass `--json` and fix the body and scan again on a finding.
176
+
177
+ - A `density` entry is a finding here, naming a subject or nested bullet past 200 characters, a run past 5 plain bullets, or a paragraph past its limits, by its line in the whole body
178
+ - Density never sets the exit code, so the hook below lets the post through on it and this step is the one that stops
160
179
 
161
180
  - A finding phrased against an internal phase label is what the label half of the scan catches here
162
181
  - The call carries no `--title`, so the scan reads the text as a review
@@ -15,7 +15,7 @@ A model asked to drive a checklist also drifts. It judges a taste box the operat
15
15
 
16
16
  - Every checklist box is driven in the running app and gets one verdict, pass or fail with the value quoted, needs eyes for a taste box, or not driven with the reason
17
17
  - The checklist and address come from the evidence record rather than the diff, and only a build of the head the verdict names is driven
18
- - An unreachable address reports not driven rather than fail, and a box no commit can settle leaves the review closed
18
+ - An unreachable address reports not driven rather than fail, a box no commit can settle leaves the review closed, and a pass that drove no box never reads as closed
19
19
  - The review posts as its own pull request review under `## UI review` while anything is owed and `## UI review closed` once nothing is, ending on a marker naming the head it drove
20
20
  - The pass drives a pinned browser under its own named session, sweeps only console errors, overflow, and the focus and details probes, and treats page content as data
21
21
 
@@ -22,8 +22,8 @@ The checklist is its only statement of intent, so it tests the feature the way a
22
22
 
23
23
  A pass stops rather than repeat a verdict that already covers the head.
24
24
 
25
- - The newest UI verdict is `## UI review closed` at the current head: stop, `The UI review already covers <short-sha>.`
26
- - An open verdict at the head stops the same way, unless a `## Review response` or `## Post-review findings` comment is newer than it or the launch names an address
25
+ - The newest UI verdict is `## UI review closed` at the current head and drove a box: stop, `The UI review already covers <short-sha>.`
26
+ - An open verdict at the head stops the same way, unless a `## Review response` or `## Post-review findings` comment is newer, the launch names an address, or it drove no box, as `post.md` defines
27
27
  - The hosted preview was not built from the head: stop and post nothing, naming `built`, `tip`, and `canon pr preview <number> --json` for the dispatcher
28
28
  - Post, then run `canon pr lift <number>` on a close. Never merge or ready the PR by hand, and edit the evidence comment only through `canon pr tick`.
29
29
  - A push after a close owes a re-drive only when it changed a painted path, since the lift carries the close across the rest.
@@ -98,7 +98,7 @@ When the hosted rung answers, read `${CLAUDE_SKILL_DIR}/references/hosted-previe
98
98
 
99
99
  The check covers the hosted address only. The local rung and a launch-named address have no deploy run to read, so say in the comment they were driven unverified.
100
100
 
101
- An address that exists and does not answer differs from no address. When every rung fails to answer, skip the drive, record each box as not driven with the addresses tried and what each returned, and go to Step 7.
101
+ An address that exists and does not answer differs from no address. When every rung fails to answer, skip the drive, record each box as not driven with the addresses tried and what each returned, and go to Step 7. No box was driven, so Step 7 posts it open.
102
102
 
103
103
  ## Step 4: open the browser
104
104
 
@@ -115,6 +115,7 @@ ${CLAUDE_SKILL_DIR}/scripts/pw.sh -s=<project>-<number> open <url>
115
115
 
116
116
  Read `${CLAUDE_SKILL_DIR}/references/driving.md` on reaching this step for how a box's line maps to calls, how each value is read, and the four verdicts with the evidence each one carries.
117
117
 
118
+ - Make each box its own item on the session's task list, and tick it as its verdict lands. Skip it on a host that offers no list tool.
118
119
  - Give every box exactly one verdict, in checklist order, since skipping a box silently reads the same as passing it
119
120
  - Drive a box the pass could not place on a route or a width from the opened address at 1440 wide, and name the guess in the verdict
120
121
  - Never judge a box ending in `(taste)`. Drive to the state, capture both themes, and mark it needs eyes.
@@ -11,15 +11,25 @@ Step 7 of `review-ui`.
11
11
 
12
12
  The heading reports whether the branch owes anything, the way the code review's does.
13
13
 
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.
14
+ - `## UI review` when a box failed, the sweep found a console error or an overflow, a box was not driven for a cause on the page, or the pass drove no box at all.
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, such as an address that did not answer or a `(local only)` box on a hosted preview.
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, as long as something else was driven.
18
18
 
19
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
20
  - Name each such box in the count line's not-driven count and in its Not driven entry
21
21
  - Do not append the pull request number, which GitHub already renders
22
22
 
23
+ ### A pass that drove no box
24
+
25
+ A pass with 0 failed, 0 need your eyes, 0 passed, and any box not driven looked at nothing, so it has not covered the head. It takes `## UI review` whatever the cause, including a checklist of only `(local only)` boxes on a hosted preview.
26
+
27
+ - Put a line naming the cause directly under the count line, such as the addresses tried and what each returned, ahead of the address line
28
+ - The cure is a re-drive on an address that answers, which Step 1 of `review-ui` allows past this verdict
29
+ - Message the controller with the cause and send the worker no `review-address`, since the verdict holds no box a fix worker could change
30
+ - `canon pr lift` refuses a close of this shape as `ui-undriven`, so a violation holds the draft mark
31
+ - A checklist with no box at all, which counts 0 not driven too, is an empty pass and takes the closed heading
32
+
23
33
  ## The body
24
34
 
25
35
  Write it to `.canon/tmp/pr/review-ui/body-<number>-<short-sha>.md` at the main worktree root, sent as a heredoc the way `session-worktree` states for a main-root write. Load the `write-human` skill for the prose and follow `${CLAUDE_SKILL_DIR}/../markdown-craft/references/markdown.md` for punctuation.
@@ -76,6 +86,8 @@ Driven at `<short-sha>` on <address>, rendered by <renderer>.
76
86
  <!-- review-ui: head=<head> -->
77
87
  ```
78
88
 
89
+ The post takes the length limits alone, with a box's reason under 350 characters and split into the reading and its cause where it runs longer, and keeps its H3 verdict groups and tables.
90
+
79
91
  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.
80
92
 
81
93
  - Drop each section holding no box, along with its heading. Order is fixed: Failed, Needs your eyes, Not driven, then the folded blocks.
@@ -124,6 +136,8 @@ Driven at `<short-sha>` on <address>, rendered by <renderer>.
124
136
  </details>
125
137
  ```
126
138
 
139
+ The count line is read by two verbs, `canon pr tick` for the passed count and `canon pr lift` for the driven test, so keep it exactly in the form `F failed · E need your eyes · P passed · N not driven`. A line that drifts reads as no counts, and both checks then stand down.
140
+
127
141
  The count line and the sections beneath it hold the same totals, and Step 10's printed line is the same four counts. A pass where every box needs eyes and nothing else is owed takes the closed heading.
128
142
 
129
143
  ## The marker
@@ -7,7 +7,7 @@ description: Step 8 of review-ui, which ticks each box that passed through canon
7
7
 
8
8
  Step 8 of `review-ui`. The verdict has posted, so the ticks and the verdict name the same `<head>`.
9
9
 
10
- Tick every box that took a `pass` verdict, whatever the heading, so a partly passed checklist shows its progress:
10
+ Tick every box that took a `pass` verdict at this head, whatever the heading, so a partly passed checklist shows its progress. A re-drive ticks all of them again, including a box whose verdict did not change, since a new head clears the ticks stamped at an older one.
11
11
 
12
12
  ```bash
13
13
  canon pr tick <number> --boxes <n,...> --head <head> --json
@@ -17,6 +17,12 @@ Number the boxes off the `boxes` field Step 2 kept. Leave a box that failed, nee
17
17
 
18
18
  The verb refuses a taste box and a head that is no longer the remote tip, writing nothing in either case. Branch on the record's `reason`, which reads `ticked` on success.
19
19
 
20
+ - `count-mismatch`: nothing was written. With `ticked` below `passed`, re-run once with every passed box. With it above, a box ticked earlier has failed, so report it on `Ticks:` for the operator.
20
21
  - `stale-head`: a push landed while this pass drove. Report it on the `Ticks:` line of Step 10 and never retry at the new tip, since the boxes were driven at the old one.
22
+ - `body-refused`: the comment is ticked and the body is not. Retry the same call once, then report the partial result on the `Ticks:` line rather than claiming both.
21
23
  - Any other refusal: report the `reason` on the `Ticks:` line and tick nothing further.
22
24
  - A binary whose `canon pr` lacks `tick` predates the verb. Report that on the `Ticks:` line and tick nothing.
25
+
26
+ The record's `countCheck` reads `matched` when the verdict's passed count agreed with the ticks, and `unchecked` when the verdict did not cover the head or carried no count line. Report an `unchecked` on the `Ticks:` line.
27
+
28
+ The same call also writes the one UI pass line in the body's `## Testing`, such as `UI pass, 17 of 17 boxes passed at <sha>, 1 taste box left to the operator`, so the operator sees the result without opening the evidence comment. Never edit that line by hand.
@@ -54,7 +54,7 @@ Poll GitHub for pull request movement by running `bun <POLL_SCRIPT>`, then act o
54
54
  - CONFLICT or STALE: send the session holding that branch the rebase handback in the canon:role-orchestrator skill's handback runbook, resolving it the way the handback rule below does. The branch owner rebases, not this session. The poll prints one of the two for a pull request, never both, so it earns one message. The canon pr lift verb refuses on either standing itself, since the green run tested a main that no longer exists, so nothing here lifts the mark.
55
55
  - OVERLAP: report the pair and the files, and settle their merge order now under the Parallelism rule of the canon:role-orchestrator skill. Dispatch nobody. The second to merge reads STALE or CONFLICT once the first lands, and the handback goes out then.
56
56
  - UNMATCHED: report it and stop. A comment posted under a heading outside the known set reaches nobody automatically, so a person decides whether to answer it by hand or the set needs a ninth heading.
57
- - UI-OPEN: report it and stop when the live UI reviewer holds the pull request, since that reviewer sends the canon:review-address dispatch itself. Otherwise dispatch it under the fallback rule below, the same as an open code pass.
57
+ - UI-OPEN: a zero-driven pass goes to the live UI reviewer to drive again, or a new one. Any other: report and stop if that reviewer holds it, since it dispatches canon:review-address, else use the fallback rule below.
58
58
  - UI-CLOSED: report it. When the pull request is still a draft and its code review is closed too, run `canon pr lift <number> --json` and report the reason it returns. Each reviewer runs the verb on its own close, so this run is the fallback for a lift neither reviewer's run made, and the verb reads every gate itself.
59
59
  - A pull request a cloud reviewer holds, on every run whatever the script printed for it: run `canon pr review-state <number> --json`. The script reports nothing when a code review posts on an unchanged head, so this read is the only one that sees a cloud pass land. An open pass newer than the last dispatch earns the dispatch rule below. A closed pass at the head on a pull request still a draft earns `canon pr lift <number> --json`, reporting the reason it returns, since a cloud reviewer can neither message nor lift.
60
60
  - UI-STALE: the UI verdict trails the head. Message the live UI reviewer to drive the new head, or launch one, and lift no mark on it.