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