@erclx/canon 4.69.0 → 4.70.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 (37) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/draft-and-pick/REQUIREMENT.md +1 -1
  4. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  5. package/claude/skills/{canon-screencast → draft-screencast}/REQUIREMENT.md +4 -4
  6. package/claude/skills/{canon-screencast → draft-screencast}/SKILL.md +4 -4
  7. package/claude/skills/{canon-slides-draft → draft-slides}/REQUIREMENT.md +3 -3
  8. package/claude/skills/{canon-slides-draft → draft-slides}/SKILL.md +2 -2
  9. package/claude/skills/{canon-frames-read → read-frames}/REQUIREMENT.md +2 -2
  10. package/claude/skills/{canon-frames-read → read-frames}/SKILL.md +3 -3
  11. package/claude/skills/{canon-record → record-screencast}/REQUIREMENT.md +5 -5
  12. package/claude/skills/{canon-record → record-screencast}/SKILL.md +4 -4
  13. package/claude/skills/review-pr/SKILL.md +55 -7
  14. package/claude/skills/role-orchestrator/SKILL.md +2 -1
  15. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +7 -3
  16. package/claude/skills/role-orchestrator/scripts/poll.sh +79 -34
  17. package/claude/skills/role-worker/SKILL.md +2 -1
  18. package/docs/agents/commands.md +2 -0
  19. package/docs/agents/demo.md +3 -3
  20. package/docs/agents/index.md +1 -1
  21. package/docs/agents/pr-reads.md +47 -12
  22. package/docs/agents/tasks.md +46 -3
  23. package/docs/workflow/ai-workflow.md +19 -19
  24. package/package.json +3 -2
  25. package/scripts/core/regen-web-previews.ts +94 -0
  26. package/src/claude/cases/workflow.ts +4 -4
  27. package/src/commands/demo.ts +1 -1
  28. package/src/commands/pr.ts +130 -1
  29. package/src/commands/tasks.ts +178 -1
  30. package/src/demo/beats.ts +1 -1
  31. package/src/migrate/skill-names.ts +15 -1
  32. package/src/pr/review-scope.ts +177 -0
  33. package/src/tasks/archive.ts +206 -3
  34. package/src/tasks/label.ts +14 -6
  35. package/src/tasks/validate.ts +22 -0
  36. package/standards/plan.md +1 -1
  37. package/standards/tasks.md +15 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.69.0",
4
+ "version": "4.70.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -199,7 +199,7 @@ Name the number `git-ship`'s pull request step returned on both calls rather tha
199
199
 
200
200
  Report what the read returned rather than what the command printed, since the exit says the call ran and says nothing about the state. A `true` reports a draft. A `false` reports the pull request as opened ready and unsupervised, and the chain stops there. Never re-issue the undo on a disagreeing read, which fights whoever readied it instead of guarding anything.
201
201
 
202
- Placement is why the call sits ahead of the watch rather than after it. Marking afterwards leaves the pull request unmarked for the whole CI run, which is the stretch an unattended worker's branch is least supervised. What the mark buys is a reader learning the pull request has had no review yet. It buys no bound on that stretch: readying a pull request to merge lifts the mark, GitHub requires it before a merge, and it is the operator's act to take.
202
+ Placement is why the call sits ahead of the watch rather than after it. Marking afterwards leaves the pull request unmarked for the whole CI run, which is the stretch an unattended worker's branch is least supervised. What the mark buys is a reader learning the pull request has had no review yet. It buys no bound on that stretch: readying a pull request to merge lifts the mark, GitHub requires it before a merge, and the act belongs to the operator or to the controlling session that closed the review, whichever it is, taken directly on the pull request rather than delegated to a worker. `role-worker` states the mirroring refusal.
203
203
 
204
204
  `git-ship` verifies again at its own gate, which repeats this chain's Step 3 on the run where nothing stopped. That cost is deliberate: four of the stop points in the table below hand the run straight back to that body, and a gate the chain skips for being redundant is a gate no resumed run ever meets.
205
205
 
@@ -49,7 +49,7 @@ The refusal strings sit in the body, since the runtime loads that file and ignor
49
49
  - `ux-audit` reads source to find roughness and reports it. This takes its input from the operator and changes nothing until they pick.
50
50
  - `ux-measure` measures what a running interface costs to paint. This measures whatever a visual claim depends on, which is usually geometry or contrast rather than cost.
51
51
  - `ui-test` writes tests for a change already made. This runs before there is a change to test.
52
- - `canon-screencast` scripts a recording of something already built. This has nothing built yet.
52
+ - `draft-screencast` scripts a recording of something already built. This has nothing built yet.
53
53
  - `canon capture`, `canon serve`, and `canon drive` own the render, the address, and the probes, and are invoked rather than reimplemented.
54
54
 
55
55
  ### What did not travel
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: draft-and-pick
3
- description: Drafts several candidates for a decision judged by looking, renders them side by side on one page, hands the operator the addresses, takes the pick through the structured question surface, and loops on the pick until they stop. Use when asked to "draft some options", "show me a few versions", "try a few variations", "mock up alternatives", "give me candidates for X", or when a choice is taste rather than correctness. Do NOT use when the request already names the answer and asks for it to be built, which is `plan-feature`. Do NOT use to read source for roughness, which is `ux-audit`, to measure what a running interface costs to paint, which is `ux-measure`, to write tests for a change already made, which is `ui-test`, or to script a recording, which is `canon-screencast`.
3
+ description: Drafts several candidates for a decision judged by looking, renders them side by side on one page, hands the operator the addresses, takes the pick through the structured question surface, and loops on the pick until they stop. Use when asked to "draft some options", "show me a few versions", "try a few variations", "mock up alternatives", "give me candidates for X", or when a choice is taste rather than correctness. Do NOT use when the request already names the answer and asks for it to be built, which is `plan-feature`. Do NOT use to read source for roughness, which is `ux-audit`, to measure what a running interface costs to paint, which is `ux-measure`, to write tests for a change already made, which is `ui-test`, or to script a recording, which is `draft-screencast`.
4
4
  ---
5
5
 
6
6
  # Draft and pick
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: canon-screencast
2
+ name: draft-screencast
3
3
  description: Why a recording script ships pre-seeded rather than blank, what the four discovery questions buy, and why the draft stays stack-agnostic
4
4
  ---
5
5
 
6
- # Canon screencast requirement
6
+ # Draft screencast requirement
7
7
 
8
8
  ## Gap
9
9
 
@@ -33,7 +33,7 @@ A draft that names the recording software, the editing software, or the window m
33
33
 
34
34
  ## Out of scope
35
35
 
36
- - Producing the recording, which `canon-record` owns
36
+ - Producing the recording, which `record-screencast` owns
37
37
  - Refining an existing draft, which is a direct edit of the file
38
- - Slide decks, which `canon-slides-draft` owns
38
+ - Slide decks, which `draft-slides` owns
39
39
  - Where the recording ships, which the draft lists and the user decides
@@ -1,14 +1,14 @@
1
1
  ---
2
- name: canon-screencast
2
+ name: draft-screencast
3
3
  description: Drafts a screencast script with pre-seeded beats, defaults, and decisions to `.canon/tmp/screencast/<slug>.md`. Reads project context, asks four discovery questions with proposed defaults, then writes a shippable draft. Use when asked to "draft a screencast", "write a recording script", "plan a demo video", or "scaffold a screencast for X". Do NOT re-invoke to refine an existing draft. Re-running overwrites. Edit the draft file directly.
4
4
  ---
5
5
 
6
- # Canon screencast
6
+ # Draft screencast
7
7
 
8
8
  ## Guards
9
9
 
10
10
  - If no topic is provided, stop: `❌ No screencast topic. Describe what you are recording.`
11
- - Draft, then hand off. Do not edit video or generate captions, and do not drive the application. A recording is another skill's job rather than something forbidden: write the draft, name `canon-record` as the next step, and stop.
11
+ - Draft, then hand off. Do not edit video or generate captions, and do not drive the application. A recording is another skill's job rather than something forbidden: write the draft, name `record-screencast` as the next step, and stop.
12
12
  - Stack-agnostic in the draft. Never name a recording tool, an editing tool, a font, or a window manager. Keep selectors, URLs, wait conditions, and timings out of the beats too, since those four are exactly what the compiler adds in a plan of its own. A beat carrying them stops being a document a person can read and edit down.
13
13
 
14
14
  ## Step 1: read the project context
@@ -159,7 +159,7 @@ Print the file path on its own line and a one-line summary. Do not paraphrase th
159
159
  Draft has 5 beats and pre-seeded defaults. Edit the beats and the resolved decisions.
160
160
 
161
161
  To record it rather than shoot it by hand:
162
- canon-record .canon/tmp/screencast/<slug>.md
162
+ record-screencast .canon/tmp/screencast/<slug>.md
163
163
  ```
164
164
 
165
165
  Name the skill and stop there. Do not compile the draft, do not run it, and do not generate captions. The operator edits the beats first, and the compiler reports which selectors and URLs they still owe it.
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: canon-slides-draft
2
+ name: draft-slides
3
3
  description: Why the skill owns deck content and the CLI owns layout, and why one render is read back before the deck is called done
4
4
  ---
5
5
 
6
- # Canon slides draft requirement
6
+ # Draft slides requirement
7
7
 
8
8
  ## Gap
9
9
 
@@ -36,4 +36,4 @@ Reimplementing spacing and palette logic is the failure that survives the sessio
36
36
  - Rendering, which the CLI owns end to end
37
37
  - The palette and the type scale, which live in the CLI rather than in the source
38
38
  - Writing the content the deck is about, which the caller brings
39
- - Recording a demo, which `canon-screencast` owns
39
+ - Recording a demo, which `draft-screencast` owns
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: canon-slides-draft
2
+ name: draft-slides
3
3
  description: Drafts a `.claude/SLIDES.md` source from a topic, picks a layout per slide, then renders it to PowerPoint via `canon slides render`. Use when asked to "draft slides", "make a deck", "build a presentation", "turn this into slides", or "render a SLIDES.md". Holds the deck design rules. Do NOT reimplement render logic. The CLI owns layout and styling. Assumes the `canon` CLI is on PATH.
4
4
  ---
5
5
 
6
- # Slides draft
6
+ # Draft slides
7
7
 
8
8
  Author a `SLIDES.md` source, then shell out to `canon slides render`. The CLI owns all layout and styling. This skill owns content and the design choices encoded in the source.
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: canon-frames-read
2
+ name: read-frames
3
3
  description: Why a recording gets read back through numbered frames rather than left for a person to open, and why the report stops at description and never reaches a verdict
4
4
  ---
5
5
 
6
- # Canon frames read requirement
6
+ # Read frames requirement
7
7
 
8
8
  ## Gap
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: canon-frames-read
3
- description: Pulls numbered still frames from a recorded video through `canon demo frames`, reads each one with the Read tool, and reports one plain description per frame. Never judges the recording, since a frame read is evidence rather than a verdict. Use when asked to "check the recording", "read the demo frames", "see what the video shows", or right after `canon demo run` writes a video and nobody has opened it yet. Do NOT use to record the video, which is `canon-record`, or to state whether the recording looks correct or broken, which is out of scope for every surface in the toolkit today.
2
+ name: read-frames
3
+ description: Pulls numbered still frames from a recorded video through `canon demo frames`, reads each one with the Read tool, and reports one plain description per frame. Never judges the recording, since a frame read is evidence rather than a verdict. Use when asked to "check the recording", "read the demo frames", "see what the video shows", or right after `canon demo run` writes a video and nobody has opened it yet. Do NOT use to record the video, which is `record-screencast`, or to state whether the recording looks correct or broken, which is out of scope for every surface in the toolkit today.
4
4
  ---
5
5
 
6
- # Canon frames read
6
+ # Read frames
7
7
 
8
8
  ## Guards
9
9
 
@@ -1,13 +1,13 @@
1
1
  ---
2
- name: canon-record
2
+ name: record-screencast
3
3
  description: Why compiling and running a screencast draft needs a routed skill rather than an operator typing two verbs by hand, and why the unresolved-field refusal cannot be a suggestion
4
4
  ---
5
5
 
6
- # Canon record requirement
6
+ # Record screencast requirement
7
7
 
8
8
  ## Gap
9
9
 
10
- Without this skill, `canon-screencast` writes a draft and names `canon demo compile` as the next step, and nothing after that routes a session there or to `canon demo run`. An operator has to know both verbs, type them in order, and read the compile record for unresolved fields by hand. A session that fills one in on its own reproduces the exact failure `canon demo run`'s `plan-unresolved` reason exists to catch, one layer up where nothing enforces it.
10
+ Without this skill, `draft-screencast` writes a draft and names `canon demo compile` as the next step, and nothing after that routes a session there or to `canon demo run`. An operator has to know both verbs, type them in order, and read the compile record for unresolved fields by hand. A session that fills one in on its own reproduces the exact failure `canon demo run`'s `plan-unresolved` reason exists to catch, one layer up where nothing enforces it.
11
11
 
12
12
  ## Must
13
13
 
@@ -21,7 +21,7 @@ Without this skill, `canon-screencast` writes a draft and names `canon demo comp
21
21
  - Pass `--force` to compile
22
22
  - Guess or fill a target, a URL, or any other unresolved field
23
23
  - Drive the application through anything other than `canon demo run`
24
- - Assume this skill's own invocation frequency needs no check. `canon-screencast`'s closing block names it by hand, but whether anything else reaches for it beyond that pointer or an operator typing its name has no answer at creation time, so a review pass some months in should read that back rather than take it on faith.
24
+ - Assume this skill's own invocation frequency needs no check. `draft-screencast`'s closing block names it by hand, but whether anything else reaches for it beyond that pointer or an operator typing its name has no answer at creation time, so a review pass some months in should read that back rather than take it on faith.
25
25
 
26
26
  ## Guards
27
27
 
@@ -29,6 +29,6 @@ Without this skill, `canon-screencast` writes a draft and names `canon demo comp
29
29
 
30
30
  ## Out of scope
31
31
 
32
- - Drafting the beats, which `canon-screencast` owns
32
+ - Drafting the beats, which `draft-screencast` owns
33
33
  - Filling an existing plan's target or URL, which is the operator's own edit
34
34
  - Verifying a recording beyond its own caption, which nothing in the toolkit does yet
@@ -1,13 +1,13 @@
1
1
  ---
2
- name: canon-record
3
- description: Drives a screencast draft through to a recording. Compiles it with `canon demo compile` when no plan exists yet at the default path, skipping compile when one is already there, then runs `canon demo run` once nothing is unresolved. Reports every unresolved field from the compile or run record and stops rather than guessing one. Use when asked to "record the screencast", "run the demo", "compile and record this draft", or right after `canon-screencast` prints its next-step line. Do NOT use to draft the beats, which is `canon-screencast`, or to fill in a plan's target or URL, which is the operator's own edit.
2
+ name: record-screencast
3
+ description: Drives a screencast draft through to a recording. Compiles it with `canon demo compile` when no plan exists yet at the default path, skipping compile when one is already there, then runs `canon demo run` once nothing is unresolved. Reports every unresolved field from the compile or run record and stops rather than guessing one. Use when asked to "record the screencast", "run the demo", "compile and record this draft", or right after `draft-screencast` prints its next-step line. Do NOT use to draft the beats, which is `draft-screencast`, or to fill in a plan's target or URL, which is the operator's own edit.
4
4
  ---
5
5
 
6
- # Canon record
6
+ # Record screencast
7
7
 
8
8
  ## Guards
9
9
 
10
- - If no draft path is given, stop: `❌ No draft path. Pass the path canon-screencast printed.`
10
+ - If no draft path is given, stop: `❌ No draft path. Pass the path draft-screencast printed.`
11
11
  - Never guess or fill an unresolved field, a target, a URL, or anything else the compile or run record names. Report it and stop. Filling one in reproduces the failure `canon demo run`'s `plan-unresolved` reason exists to catch, one layer up where nothing enforces it.
12
12
  - Never pass `--force` to compile. A plan already at the default output path may carry timing tuned by hand, and the draft cannot reproduce that, so leave it untouched.
13
13
  - Drive the application through `canon demo run` alone. Never open a browser, click through the app, or write to the output paths some other way.
@@ -29,7 +29,17 @@ reader scanning the thread finds the current verdict where the last one sat.
29
29
 
30
30
  ## Step 1: resolve the PR and read context
31
31
 
32
- Resolve the PR: `gh pr view --json number,headRefName,headRefOid,title,body` for the current branch, or use a PR number the user names. Take `<headRefOid>` from `canon pr head <number> --json`, off that record's `tip`, and fall back to the `headRefOid` field above when no record comes back, which is a target whose CLI predates the verb. The first seven characters are `<short-sha>`, which names the body file in Step 4.
32
+ Capture `<read-at>` first, ahead of every read this pass makes:
33
+
34
+ ```bash
35
+ date -u +%Y-%m-%dT%H:%M:%SZ
36
+ ```
37
+
38
+ Everything from that line to the post is the compose window, and a commit pushed inside it is one this pass never saw. Stamping the body with the instant the window opened is what leaves that commit outside the covered range, so the next pass reads it rather than assuming it covered. Taking the stamp later, at the head resolution below or at Step 4 where the body is composed, claims a stretch this pass had already stopped reading through.
39
+
40
+ Then resolve the PR: `gh pr view --json number,headRefName,headRefOid,title,body` for the current branch, or use a PR number the user names. Take `<headRefOid>` from `canon pr head <number> --json`, off that record's `tip`, and fall back to the `headRefOid` field above when no record comes back, which is a target whose CLI predates the verb. The first seven characters are `<short-sha>`, which names the body file in Step 4.
41
+
42
+ `<headRefOid>` and `<read-at>` travel together into Step 4's marker, and neither is re-derived after this point. Re-reading the head later in the pass would name a commit this pass did not review, which is the defect the marker exists against, reached from the inside.
33
43
 
34
44
  Read these in parallel from the project root, skipping any that do not exist:
35
45
 
@@ -46,14 +56,26 @@ Coding standards from `.claude/rules/` are auto-loaded by Claude Code.
46
56
  Find the commit the last pass covered and the verdict it posted:
47
57
 
48
58
  ```bash
49
- 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")))'
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. That fired for real on a pull request in this toolkit on 2026-09-07, and what it skipped was a genuine fix.
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 // ""))'
50
72
  ```
51
73
 
52
- The two fields are `<prior-oid>` and `<prior-heading>`. The commit scopes the read below and the heading feeds the repeat guard at the end of this step, so one query answers both rather than two reads of the same review. 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.
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.
53
75
 
54
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.
55
77
 
56
- An empty result is a first pass. Read the whole change:
78
+ A `source` of `none`, or an empty result from the fallback, is a first pass. Read the whole change:
57
79
 
58
80
  ```bash
59
81
  gh pr diff <number>
@@ -82,9 +104,11 @@ On exit zero, review `<prior-oid>..<headRefOid>` and nothing else. `git diff` an
82
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:
83
105
 
84
106
  ```bash
85
- gh pr view <number> --json reviews,comments --jq '([.reviews[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review" or . == "## Review closed")] | last | .submittedAt) as $prior | [.comments[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Rebase" or . == "## Post-review findings") | select(.createdAt > $prior)] | last | .url // empty | split("-") | last'
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'
86
108
  ```
87
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
+
88
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.
89
113
 
90
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.
@@ -164,6 +188,8 @@ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
164
188
  - bounded confirmation.
165
189
 
166
190
  🤖 Reviewed by Claude Code
191
+
192
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
167
193
  ```
168
194
 
169
195
  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.
@@ -182,6 +208,8 @@ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-
182
208
  - **should-fix**: what breaks and the fix, in two or three sentences.
183
209
 
184
210
  🤖 Reviewed by Claude Code
211
+
212
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
185
213
  ```
186
214
 
187
215
  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.
@@ -228,6 +256,24 @@ The `What is right` section is optional, capped at three bullets, and included o
228
256
 
229
257
  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.
230
258
 
259
+ ### The marker every body carries
260
+
261
+ End every body with this line, carrying `<headRefOid>` and `<read-at>` from Step 1 verbatim:
262
+
263
+ ```markdown
264
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
265
+ ```
266
+
267
+ 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.
268
+
269
+ 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.
270
+
271
+ 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.
272
+
273
+ 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.
274
+
275
+ 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.
276
+
231
277
  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.
232
278
 
233
279
  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.
@@ -250,6 +296,8 @@ A pass carrying only minors is an ordinary finding-carrying pass, so it takes th
250
296
  - **minor**: finding, and the fix it wants.
251
297
 
252
298
  🤖 Reviewed by Claude Code
299
+
300
+ <!-- review-pr: commit=<headRefOid> read-at=<read-at> -->
253
301
  ```
254
302
 
255
303
  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.
@@ -278,9 +326,9 @@ gh api -X PUT repos/{owner}/{repo}/pulls/<number>/reviews/<review-id> -F body=@<
278
326
 
279
327
  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.
280
328
 
281
- What the rewrite costs is the review's `commit.oid`, which `PUT` leaves 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 reads a range wider than its delta and its commit count spans back to the pinned commit rather than covering the delta.
329
+ 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.
282
330
 
283
- The poll's `SEEN` branch is the sharper half. It fires on `prior` equalling the head, which a pinned `commit.oid` never reaches, so an out-of-band pass over this pull request reports as `MOVED` for the rest of its life and never as already covered. That is one wasted dispatch per head move rather than a repeating one, since the poll gates its report on the head it wrote to its own baseline and not on `prior`. Each cost errs toward more reading, so neither is repaired here.
331
+ 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.
284
332
 
285
333
  ## Step 5: output
286
334
 
@@ -114,7 +114,7 @@ Write no shape for a correction. A correction is a sentence, and a format for ad
114
114
  4. Hand off. Read `${CLAUDE_SKILL_DIR}/references/orchestrator-dispatch.md` and follow it: check the branch is unclaimed, check the row's file set against every track in flight, then dispatch a background worker with `claude --bg`. Fall back to the human-launch line it replaces when the check refuses, the sets overlap, or a stated reason serializes the row behind something already out.
115
115
  5. Review the PR. When a worker opens a PR, run `review-pr` to post findings to it. This is the deep, independent pass. The worker's autoship self-review was only the green gate.
116
116
  - Learning that a PR moved is the mechanical half, so read `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` and run the poll under the condition it states rather than checking the board by hand. That runbook holds the routing and the trigger, and a summary of it here is a second source that drifts from it.
117
- 6. Dispatch the handback. A pass posting anything owed, a finding at any severity or a testing question, tells the session holding that branch to run `review-address`, rather than waiting for a person to relay it. Re-review when the worker's own message says the address pass finished, per the channel `role-worker` states, rather than polling for an answer nothing else marks as landed. Then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
117
+ 6. Dispatch the handback. A pass posting anything owed, a finding at any severity or a testing question, tells the session holding that branch to run `review-address`, rather than waiting for a person to relay it. Re-review when the worker's own message says the address pass finished, per the channel `role-worker` states, rather than polling for an answer nothing else marks as landed. Once a pass posts `## Review closed`, lift the pull request's draft mark yourself, per Boundaries below. Then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
118
118
  - Read the threshold off `review-pr`, which states it once and governs the heading with it, so an open heading and an owed dispatch answer the same question and either one is enough to send
119
119
  - Resolve the target at the moment of sending with `canon sessions list --branch`, never from a mapping written down earlier, since names rotate as sessions end and one recorded earlier in a session has failed inside the hour. The runbook read at step 5 routes on the count and the confidence it answers with
120
120
  - Open the message with the worktree and branch the sender believes the reader holds, asking to be corrected, whenever that mapping is inferred rather than confirmed
@@ -140,6 +140,7 @@ Stamp the block with the commit this session read the tree at, which the same se
140
140
  - Run one orchestrator at a time. The board is gitignored, so a second session sees none of this one's writes: two task files land minutes apart under different labels for the same work, one session archives a task mid-sweep in the other, and each archives a plan the other had retargeted. An Owner column does not fix this, since neither session can read the other's rows.
141
141
  - Do not implement features in this session. Hand the plan to a worker.
142
142
  - Do not merge. Recommend merge or changes. The human merges.
143
+ - Lift a pull request's draft mark once this session's own review of it closes, acting directly on the pull request rather than dispatching a worker to do it. `role-worker` states the mirroring refusal: a worker cannot verify who is asking or whether review actually closed, so the act stays with whoever closed the review.
143
144
  - Do not spawn a worker with the Agent tool. An in-process subagent shares this session's context and cannot be steered or reached independently, which breaks the property this boundary protects rather than the mechanism it names. The dispatch in `orchestrator-dispatch.md` is a separate `claude --bg` process with its own worktree and its own PR, so it preserves that property instead.
144
145
  - Dispatch a background worker only once the collision check in `orchestrator-dispatch.md` clears and the row's file set is disjoint from every track in flight. Colliding with an existing worktree or session is what the check exists to catch rather than a judgment call this session makes case by case. No fixed count binds how many tracks run at once, and Parallelism below states what does.
145
146
  - Do not edit tracked files from this session, at any size. The boundary offers no proportionality exception and nothing enforces it.
@@ -56,7 +56,7 @@ The script exits non-zero and classifies nothing when the open pull request list
56
56
 
57
57
  The baseline lives at `.canon/tmp/pr-poll/baseline.txt` under the main worktree root and is per-machine. A first run against a board already in flight reports each open pull request once before it settles.
58
58
 
59
- The five review headings the script matches are written by `review-pr` and `review-address`, and the whole set is stated once in the first. A project that posts its reviews under different headings edits the jq filters in the script to match, or every pull request reads as never reviewed.
59
+ The five review headings the script matches are written by `review-pr` and `review-address`, and the whole set is stated once in the first. The reply family is matched by jq filters in the script, so a project posting its replies under different headings edits those to match. The review family is matched inside `canon pr review-state` instead, which the script and `review-pr` both read through, so a project renaming either review heading changes the verb rather than the script. Either way, a heading nothing matches reads as a pull request nobody has reviewed.
60
60
 
61
61
  `UNMATCHED` is what a heading outside the five reaches, carried the same way `RESPONSE` is: a rising count against the baseline is what is new to this script, and the message names the heading so a person can tell whether to answer it by hand or add it to the set. It fires on a tracked pull request only, since a first sighting reports `SEEN` or `OPENED` and takes whatever count already sits on the thread as its starting baseline rather than flagging history the poll never watched.
62
62
 
@@ -70,9 +70,13 @@ The state reaches every stalled dispatch, since one threshold governs the headin
70
70
 
71
71
  The report is also where the count in `## Parallelism` is legible. That threshold trips on open pull requests awaiting a first pass, which is what `OPENED` and a pull request with no prior pass name here and what `SEEN` excludes, so read the count off these lines rather than off `gh pr list`, which counts a branch closed out and waiting on a merge the same as one nobody has read. It is a separate condition from the poll-start fallback above, which decides when this loop runs rather than where a review runs.
72
72
 
73
- The count reads low, and it errs in the direction that breaks the trigger. A review's `commit.oid` is stamped with the head at submission rather than with the commit the reviewer read, so an author pushing between the diff read and the post leaves the pass recorded against a commit it never saw, and `SEEN` then fires on a head still awaiting its first look at that delta. That has fired for real, and the delta it skipped once was a genuine fix. So a `SEEN` on a head you do not recognize is worth one `gh pr view --json reviews` before it is believed, and a wave past three is likelier to trip the fallback late than early.
73
+ The count used to read low, and it erred in the direction that breaks the trigger. A review's `commit.oid` is stamped with the head at submission rather than with the commit the reviewer read, so an author pushing between the diff read and the post left the pass recorded against a commit it never saw, and `SEEN` then fired on a head still awaiting its first look at that delta. That fired for real on a pull request in this toolkit on 2026-09-07, and the delta it skipped was a genuine fix.
74
74
 
75
- The other side of that comparison used to lag as well, which made the two errors compound rather than cancel. `gh pr view --json headRefOid` answers from the pull request object and that object trails the branch ref by up to a minute after a push, reporting nothing about the trail, so a pass stamped ahead of the commit it read was compared against a head stamped behind the commit that exists. Two sessions were wrong off that field on 2026-09-01: a reviewing session posted a finding calling a pushed commit unpushed, and a worker fired a green claim it retracted on its own thread minutes later. The head now comes from `canon pr head`, which resolves the tip through `git ls-remote` and reports which commit each source names, and the object's head stays behind it as the fallback for a target whose CLI predates the verb. The review stamp is unchanged, so the paragraph above still holds on its own half.
75
+ The pass now carries its own record instead. `review-pr` writes the commit it read and the instant it read it as a marker on the last line of every body it posts, and both this script and that skill resolve the covered state through `canon pr review-state`, which is the one place the marker is parsed. A commit pushed inside a pass's compose window falls outside the marked range, so it reads as `MOVED` rather than as `SEEN`. The manual double-check that used to hang off this paragraph is retired with the defect: a `SEEN` is now a claim about what a session read rather than about what GitHub stamped, so it wants no second read to be believed.
76
+
77
+ Read `source` on the record before trusting a `SEEN` on a thread whose newest pass is old. `marker` is the read-time record. `fallback` is a pass posted before the mechanism shipped, or a target whose CLI predates the verb, and it carries the defect above unchanged, so a `SEEN` under it is worth one `gh pr view --json reviews` before it is believed. `none` is a thread carrying no pass at all.
78
+
79
+ The other side of that comparison used to lag as well, which made the two errors compound rather than cancel. `gh pr view --json headRefOid` answers from the pull request object and that object trails the branch ref by up to a minute after a push, reporting nothing about the trail, so a pass stamped ahead of the commit it read was compared against a head stamped behind the commit that exists. Two sessions were wrong off that field on 2026-09-01: a reviewing session posted a finding calling a pushed commit unpushed, and a worker fired a green claim it retracted on its own thread minutes later. The head now comes from `canon pr head`, which resolves the tip through `git ls-remote` and reports which commit each source names, and the object's head stays behind it as the fallback for a target whose CLI predates the verb. Both halves of the comparison now answer about a commit somebody named rather than one GitHub stamped, which is what lets a `SEEN` be read as covered.
76
80
 
77
81
  ## The watch beside it
78
82
 
@@ -45,12 +45,10 @@ BASE_BRANCH="${BASE_REF#origin/}"
45
45
  # `## Review response` because neither answers a comment already on the thread,
46
46
  # which is why both were kept outside the `## Review` family rather than folded
47
47
  # into it.
48
- JQ_LAST_REVIEWED_HEAD='
49
- [ .reviews[]
50
- | select((.body // "") | split("\n")[0] | rtrimstr("\r")
51
- | . == "## Review" or . == "## Review closed")
52
- ] | last | .commit.oid // empty
53
- '
48
+ #
49
+ # The review family reaches this file through `canon pr review-state` rather
50
+ # than through a filter of its own, so the two headings are pinned here only in
51
+ # the fallback that answers for a target whose CLI predates that verb.
54
52
  # The count alone answers whether a reply is new to this script, which is not
55
53
  # the same question as whether it is newer than the pass it answers. The stamp
56
54
  # of the newest reply comes out of the same selection so the recency test costs
@@ -90,38 +88,72 @@ JQ_UNMATCHED_STATE='
90
88
  + (($unclassified | last) // "none")
91
89
  '
92
90
 
93
- # `review-pr` states the threshold and posts `## Review` exactly when a
94
- # pass carries a finding, so the heading of the last review is what says whether
95
- # any work is owed on it. Taking it as well as the commit is what separates a
96
- # thread waiting on a worker from one nothing is owed on. A project editing the
97
- # two filters above for its own headings edits this one with them.
91
+ # Four fields describe the last review pass: the commit it covered, whether it
92
+ # left anything owed, how long it has sat, and the instant it read. All four come
93
+ # out of `canon pr review-state`, which is the one place the read-time marker
94
+ # `review-pr` writes into its own body is parsed. Reading `commit.oid` and
95
+ # `submittedAt` off the thread here instead is what this replaces: GitHub stamps
96
+ # both when a review is submitted, so a push landing between a pass's read and
97
+ # its post moves them onto a commit that pass never opened, and `SEEN` below then
98
+ # reports it covered. That is the failure that loses work silently, measured on
99
+ # a pull request in this toolkit on 2026-09-07.
98
100
  #
99
- # The age of that review comes out of the same selection, because the heading
100
- # alone cannot separate the two. Under the rule above an open heading means a
101
- # dispatch was owed and made, so the ordinary healthy thread is a worker still
102
- # working and every one of them would be reported minutes after the pass posted.
103
- # jq computes the elapsed seconds itself, since `date -d` is GNU-only and this
104
- # script runs wherever the plugin is installed. A review carrying no stamp reads
105
- # as age zero and classifies nothing, which is the same answer the carry-forward
106
- # path gives a pull request this run could not read.
101
+ # `review-pr` posts `## Review` exactly when a dispatch is owed, per the
102
+ # threshold that skill states, so the heading is what separates a thread waiting
103
+ # on a worker from one nothing is owed on. A project editing the two comment
104
+ # filters above for its own headings has nothing to edit here, since the verb
105
+ # owns the review family now.
107
106
  #
108
- # The stamp itself is emitted as a third field beside the age it was derived
109
- # from, because the reply test below compares against the instant rather than
110
- # against the elapsed seconds. A missing stamp reads as zero there too, where it
111
- # sends a reply to be reported rather than suppressed. The two readers therefore
112
- # fail in opposite directions on the same absent field, since zero silences the
113
- # age test above and zero is the value the reply test reports on.
114
- JQ_LAST_REVIEW_STATE='
107
+ # The age is derived from `submittedAt` alone and the pass instant from
108
+ # `readAt // submittedAt`, which is deliberate rather than an oversight. The age
109
+ # measures how long a posted comment has waited on a human, a question about the
110
+ # submission, and the pass instant bounds what that pass had read, a question
111
+ # about the read. jq computes the elapsed seconds itself, since `date -d` is
112
+ # GNU-only and this script runs wherever the plugin is installed. A review
113
+ # carrying no stamp reads as age zero and classifies nothing, which is the same
114
+ # answer the carry-forward path gives a pull request this run could not read, and
115
+ # reads as pass instant zero, where it sends a reply to be reported rather than
116
+ # suppressed. The two readers fail in opposite directions on the same absent
117
+ # field on purpose.
118
+ #
119
+ # `source` is the field this branches on rather than the exit status, since an
120
+ # operator shell profile can wrap canon in a function that flattens every
121
+ # refusal to zero. A refusal record carries `reason` and no `source`, and it
122
+ # yields nothing here, which is what sends the read to the fallback below. A
123
+ # record read as a bare answer instead would report every refusal as a pull
124
+ # request nobody has reviewed.
125
+ JQ_SCOPE_FROM_VERB='
126
+ if (.source // "") == "" then empty else
127
+ (.commit // "none") as $prior
128
+ | (.state // "none") as $heading
129
+ | (if .submittedAt == null then 0
130
+ else (.submittedAt | fromdateiso8601 | floor) end) as $at
131
+ | (if .readAt == null then $at
132
+ else (.readAt | fromdateiso8601 | floor) end) as $read
133
+ | $prior + " " + $heading
134
+ + " " + (if $at == 0 then "0" else (((now | floor) - $at) | tostring) end)
135
+ + " " + ($read | tostring)
136
+ end
137
+ '
138
+ # The fallback for a target whose CLI predates the verb, mirroring the `canon pr
139
+ # head` fallback below. It reads the submission stamps and therefore carries the
140
+ # defect the verb closes, which is the behavior this poll already had. It does
141
+ # not parse the marker: a second reader of that format here is the drift the
142
+ # verb exists to prevent, and one that lags the format silently reports a
143
+ # reviewed commit as unreviewed.
144
+ JQ_SCOPE_FALLBACK='
115
145
  [ .reviews[]
116
146
  | select((.body // "") | split("\n")[0] | rtrimstr("\r")
117
147
  | . == "## Review" or . == "## Review closed")
118
148
  ] | last
119
- | if . == null then "none 0 0"
149
+ | if . == null then "none none 0 0"
120
150
  else (if .submittedAt == null then 0
121
151
  else (.submittedAt | fromdateiso8601 | floor)
122
152
  end) as $at
123
- | ((.body | split("\n")[0] | rtrimstr("\r")
124
- | if . == "## Review" then "open" else "closed" end)
153
+ | ((.commit.oid // "none")
154
+ + " "
155
+ + (.body | split("\n")[0] | rtrimstr("\r")
156
+ | if . == "## Review" then "open" else "closed" end)
125
157
  + " "
126
158
  + (if $at == 0 then "0" else (((now | floor) - $at) | tostring) end)
127
159
  + " "
@@ -164,7 +196,7 @@ carry_forward() {
164
196
  }
165
197
 
166
198
  snapshot() {
167
- local numbers n payload head prior resp merges review_state
199
+ local numbers n payload head prior resp merges scope review_scope review_state
168
200
  git fetch -q origin "$BASE_BRANCH" 2>/dev/null || true
169
201
 
170
202
  # A failed list reaches the caller as no open pull requests, and that reports
@@ -211,16 +243,29 @@ snapshot() {
211
243
  continue
212
244
  fi
213
245
 
214
- prior=$(jq -r "$JQ_LAST_REVIEWED_HEAD" <<<"$payload")
215
246
  # Split here rather than carried whole, because the count keeps the fourth
216
247
  # column every baseline written so far already reads, and the stamp goes to
217
248
  # the end of the line beside the pass stamp it is compared against.
218
249
  reply_state=$(jq -r "$JQ_REPLY_STATE" <<<"$payload")
219
250
  resp=${reply_state%% *}
220
251
  reply_at=${reply_state##* }
221
- # Three space-separated fields, so the line below carries them as its own
222
- # sixth, seventh, and eighth rather than needing a split.
223
- review_state=$(jq -r "$JQ_LAST_REVIEW_STATE" <<<"$payload")
252
+
253
+ # The marker `review-pr` writes is the authority for what a pass covered,
254
+ # and the verb is the only reader of it. The trailing assignment is
255
+ # load-bearing under `set -e` and `set -o pipefail`: every refusal exits 1,
256
+ # and a refusal here is ordinary rather than exceptional, so an unguarded
257
+ # pipeline would end the whole poll on the first pull request it could not
258
+ # answer for.
259
+ scope=$(canon pr review-state "$n" --json 2>/dev/null) || scope=""
260
+ review_scope=$(jq -r "$JQ_SCOPE_FROM_VERB" <<<"$scope" 2>/dev/null) || review_scope=""
261
+ if [ -z "$review_scope" ]; then
262
+ review_scope=$(jq -r "$JQ_SCOPE_FALLBACK" <<<"$payload")
263
+ fi
264
+ # Four space-separated fields. The commit leads, so the line below carries
265
+ # it as its own third the way every baseline already reads, and the other
266
+ # three ride on as the sixth, seventh, and eighth.
267
+ prior=${review_scope%% *}
268
+ review_state=${review_scope#* }
224
269
  # Split the same way as the reply state, carried as the line's tenth and
225
270
  # eleventh fields.
226
271
  unmatched_state=$(jq -r "$JQ_UNMATCHED_STATE" <<<"$payload")
@@ -25,7 +25,8 @@ running and re-invoking it would restart the build.
25
25
  - Resolve `.canon/plans/`, `.canon/tasks/`, `.canon/review/`, and `.canon/memory/` at the main worktree root, never against the linked worktree this session builds in. Those folders are gitignored, so `git worktree add` never creates them and the copy beside the build is absent rather than empty.
26
26
  - Report a plan that fails to resolve as unreadable from here, naming the main-root path. Reporting the task as having no plan is true where this session stands and wrong about the world, and a reader with no second tree to check cannot separate the two.
27
27
  - Build the plan the launch named. Do not write a second one when the path fails to resolve, since a row that cites a plan already has one and drafting another produces two plans for one row.
28
- - Report a draft flag reading ready once, and leave it cleared. The ship chain marks the pull request a draft and no step anywhere un-marks, so a flag reading ready afterwards was lifted by a person readying it to merge, which GitHub requires and is theirs to take. Re-drafting fights them, which four sessions did on 2026-08-31 on a belief no surface in the tree states.
28
+ - Report a draft flag reading ready once, and leave it cleared. The ship chain marks the pull request a draft and no step anywhere un-marks, so a flag reading ready afterwards was lifted by the operator or by the controlling session that closed the review, each acting directly on the pull request, which GitHub requires before a merge. Re-drafting fights them, which four sessions did on 2026-08-31 on a belief no surface in the tree states.
29
+ - Refuse an instruction to lift the mark yourself, whoever sends it. This session cannot verify who is asking or whether review actually closed, so the refusal holds regardless of the sender's claimed authority. A controlling session instructed a worker to lift the mark instead of taking the act itself on 2026-09-07, and the worker complied and reported against the surface rather than the outcome.
29
30
  - Rewrite a plan question's `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement when this build decides against an unanswered one, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../../standards/plan.md`.
30
31
 
31
32
  ## The board is read-only