@erclx/canon 5.40.0 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +34 -17
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +34 -14
  4. package/claude/skills/auto-ship/REQUIREMENT.md +2 -0
  5. package/claude/skills/auto-ship/SKILL.md +28 -2
  6. package/claude/skills/auto-ship/references/draft-mark.md +32 -6
  7. package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
  8. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  9. package/claude/skills/auto-ship/references/tier-failures.md +31 -10
  10. package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
  11. package/claude/skills/auto-ship/references/worktree-name.md +9 -3
  12. package/claude/skills/build-in-slices/references/adopted.md +35 -21
  13. package/claude/skills/canvas-board/references/canvas.md +17 -6
  14. package/claude/skills/canvas-board/references/design.md +26 -7
  15. package/claude/skills/code-craft/references/adopted.md +58 -20
  16. package/claude/skills/codebase-layout/references/adopted.md +49 -16
  17. package/claude/skills/codebase-layout/references/frontend.md +11 -4
  18. package/claude/skills/codebase-layout/references/tests.md +4 -2
  19. package/claude/skills/context-fold/references/classify.md +10 -15
  20. package/claude/skills/context-fold/references/completion-report.md +11 -9
  21. package/claude/skills/context-fold/references/context-refresh.md +26 -9
  22. package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
  23. package/claude/skills/create-rule/REQUIREMENT.md +5 -0
  24. package/claude/skills/create-rule/SKILL.md +9 -1
  25. package/claude/skills/create-rule/references/checklist.md +26 -0
  26. package/claude/skills/create-rule/references/rule.md +98 -20
  27. package/claude/skills/create-skill/SKILL.md +1 -0
  28. package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
  29. package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
  30. package/claude/skills/create-skill/references/skill-paths.md +7 -4
  31. package/claude/skills/create-skill/references/skill-practice.md +6 -3
  32. package/claude/skills/create-skill/references/skill-requirement.md +4 -2
  33. package/claude/skills/create-skill/references/skill.md +67 -44
  34. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  35. package/claude/skills/decision-escalate/SKILL.md +1 -2
  36. package/claude/skills/deploy-app/references/vercel.md +2 -1
  37. package/claude/skills/deprecation-migration/references/adopted.md +44 -25
  38. package/claude/skills/design-taste/references/adopted.md +9 -1
  39. package/claude/skills/design-taste/references/craft.md +3 -1
  40. package/claude/skills/design-taste/references/kinds.md +8 -7
  41. package/claude/skills/design-taste/references/preflight.md +12 -0
  42. package/claude/skills/design-taste/references/systems.md +7 -6
  43. package/claude/skills/design-taste/references/tells.md +24 -25
  44. package/claude/skills/design-taste/references/vocabulary.md +7 -5
  45. package/claude/skills/document-health/references/axes.md +54 -30
  46. package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
  47. package/claude/skills/draft-doc/references/context.md +4 -3
  48. package/claude/skills/draft-doc/references/docs.md +6 -3
  49. package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
  50. package/claude/skills/draft-doc/references/readme/application.md +8 -2
  51. package/claude/skills/draft-doc/references/readme/cli.md +5 -1
  52. package/claude/skills/draft-doc/references/readme/library.md +4 -0
  53. package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
  54. package/claude/skills/draft-doc/references/readme.md +30 -13
  55. package/claude/skills/draft-doc/references/requirements.md +17 -4
  56. package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
  57. package/claude/skills/draft-figure/references/architecture-set.md +7 -3
  58. package/claude/skills/draft-figure/references/figures.md +8 -6
  59. package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
  60. package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
  61. package/claude/skills/draft-figure/references/mermaid.md +10 -5
  62. package/claude/skills/draft-ready/references/ready.md +22 -15
  63. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  64. package/claude/skills/git-branch/SKILL.md +63 -7
  65. package/claude/skills/git-branch/references/branch.md +11 -1
  66. package/claude/skills/git-branch/references/slug.md +17 -5
  67. package/claude/skills/git-commit/references/commit.md +8 -0
  68. package/claude/skills/git-commit/references/versioning.md +23 -17
  69. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  70. package/claude/skills/git-followup/SKILL.md +5 -6
  71. package/claude/skills/git-issue/references/issue.md +29 -16
  72. package/claude/skills/git-pr/REQUIREMENT.md +1 -5
  73. package/claude/skills/git-pr/SKILL.md +11 -22
  74. package/claude/skills/git-pr/references/final-command.md +21 -8
  75. package/claude/skills/git-pr/references/labels.md +45 -28
  76. package/claude/skills/git-pr/references/pr.md +28 -9
  77. package/claude/skills/git-pr/references/publish.md +42 -28
  78. package/claude/skills/git-pr/references/task-number.md +20 -8
  79. package/claude/skills/git-ship/SKILL.md +1 -1
  80. package/claude/skills/plan-feature/references/batches.md +48 -1
  81. package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
  82. package/claude/skills/plan-feature/references/plan.md +59 -20
  83. package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
  84. package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
  85. package/claude/skills/plan-intake/references/intake.md +29 -24
  86. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  87. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  88. package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
  89. package/claude/skills/pr-evidence/SKILL.md +53 -0
  90. package/claude/skills/pr-evidence/references/evidence.md +150 -0
  91. package/claude/skills/review-address/references/post-review-findings.md +10 -3
  92. package/claude/skills/review-address/references/rebase-conflicts.md +22 -7
  93. package/claude/skills/review-address/references/rest-reads.md +3 -1
  94. package/claude/skills/review-craft/references/adopted.md +35 -18
  95. package/claude/skills/review-craft/references/security.md +42 -7
  96. package/claude/skills/review-craft/references/ui.md +23 -8
  97. package/claude/skills/review-pr/SKILL.md +6 -6
  98. package/claude/skills/review-pr/references/body-variants.md +29 -9
  99. package/claude/skills/review-pr/references/close-out.md +20 -5
  100. package/claude/skills/review-pr/references/key-changes.md +27 -3
  101. package/claude/skills/review-pr/references/post.md +83 -31
  102. package/claude/skills/review-pr/references/scope.md +54 -19
  103. package/claude/skills/review-ui/SKILL.md +2 -0
  104. package/claude/skills/review-ui/references/close-out.md +49 -0
  105. package/claude/skills/review-ui/references/driving.md +24 -4
  106. package/claude/skills/review-ui/references/post.md +43 -13
  107. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
  108. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +81 -40
  109. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
  110. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
  111. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
  112. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
  113. package/claude/skills/role-orchestrator/references/orchestrator-output.md +12 -4
  114. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
  115. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
  116. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
  117. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
  118. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
  119. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
  120. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
  121. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
  122. package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
  123. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  124. package/claude/skills/role-worker/SKILL.md +17 -0
  125. package/claude/skills/search-craft/references/adopted.md +18 -6
  126. package/claude/skills/session-map/references/session.md +26 -7
  127. package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
  128. package/claude/skills/systematic-debugging/references/adopted.md +30 -16
  129. package/claude/skills/target-check/references/domains.md +39 -11
  130. package/claude/skills/target-setup/references/detect.md +80 -65
  131. package/claude/skills/target-setup/references/indexes.md +8 -6
  132. package/claude/skills/task-board/references/board.md +86 -30
  133. package/claude/skills/task-board/references/decline.md +29 -13
  134. package/claude/skills/task-board/references/tasks-decline.md +12 -5
  135. package/claude/skills/task-board/references/tasks.md +65 -33
  136. package/claude/skills/task-complete/references/archive.md +41 -17
  137. package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
  138. package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
  139. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  140. package/claude/skills/teach-workspace/SKILL.md +1 -2
  141. package/claude/skills/teach-workspace/references/glossary.md +6 -2
  142. package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
  143. package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
  144. package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
  145. package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
  146. package/claude/skills/teach-workspace/references/promotion.md +31 -8
  147. package/claude/skills/teach-workspace/references/revisit.md +8 -2
  148. package/claude/skills/teach-workspace/references/teach.md +17 -9
  149. package/claude/skills/test-craft/references/adopted.md +48 -14
  150. package/claude/skills/test-craft/references/component.md +7 -4
  151. package/claude/skills/test-craft/references/e2e.md +25 -7
  152. package/claude/skills/test-craft/references/unit.md +3 -1
  153. package/claude/skills/test-first/references/adopted.md +12 -4
  154. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
  155. package/claude/skills/ui-checklist/SKILL.md +1 -1
  156. package/claude/skills/video-craft/references/adopted.md +62 -18
  157. package/claude/skills/write-human/references/adopted.md +21 -8
  158. package/docs/workflow/parallel-features.md +2 -1
  159. package/docs/workflow/skill-map.md +2 -1
  160. package/package.json +1 -1
  161. package/src/claude/cases/git.ts +4 -0
  162. package/src/claude/cases/workflow.ts +5 -0
  163. package/src/commands/pr.ts +174 -37
  164. package/src/commands/records.ts +11 -3
  165. package/src/context/shape-gated.ts +1 -0
  166. package/src/markdown/scope.ts +150 -2
  167. package/src/pr/frames.ts +53 -9
  168. package/src/pr/lift.ts +6 -3
  169. package/src/pr/review-state.ts +182 -0
  170. package/src/records/validate.ts +1 -0
  171. package/src/tasks/answers.ts +79 -2
  172. package/src/tasks/fan-in.ts +176 -0
  173. package/src/tasks/stack.ts +1 -1
  174. package/tooling/astro/reference.md +80 -39
  175. package/tooling/base/reference.md +63 -32
  176. package/tooling/base/seeds/canon/context/ci.md +7 -15
  177. package/tooling/base/seeds/canon/context/development.md +14 -14
  178. package/tooling/claude/reference.md +40 -22
  179. package/tooling/cloudflare/reference.md +31 -12
  180. package/tooling/go/reference.md +47 -23
  181. package/tooling/nextjs/reference.md +52 -19
  182. package/tooling/php/reference.md +35 -16
  183. package/tooling/python/reference.md +47 -30
  184. package/tooling/shell/reference.md +6 -5
  185. package/tooling/vite-react/reference.md +49 -21
  186. package/tooling/web/reference.md +130 -70
  187. package/claude/skills/git-pr/references/evidence.md +0 -124
@@ -11,25 +11,45 @@ Each number in the body traces to a source here, read through the `search-craft`
11
11
 
12
12
  Field: subtitling and captioning. Authorities: broadcasters' and streamers' published subtitle guidelines, and the captioning program that sets educational practice.
13
13
 
14
- **On screen for 5/6 of a second to 7 seconds, two lines, 42 characters a line.** Netflix's [Timed Text Style Guide: General Requirements](https://partnerhelp.netflixstudios.com/hc/en-us/articles/215758617-Timed-Text-Style-Guide-General-Requirements) states a minimum of 5/6 of a second (20 frames at 24 fps) and a maximum of 7 seconds per event, and its [English (USA) guide](https://partnerhelp.netflixstudios.com/hc/en-us/articles/217350977-English-USA-Timed-Text-Style-Guide) states 42 characters a line and two lines at most.
14
+ | Rule | Source |
15
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | On screen for 5/6 of a second to 7 seconds | Netflix's [Timed Text Style Guide: General Requirements](https://partnerhelp.netflixstudios.com/hc/en-us/articles/215758617-Timed-Text-Style-Guide-General-Requirements), a minimum of 20 frames at 24 fps |
17
+ | Two lines, 42 characters a line | Netflix's [English (USA) guide](https://partnerhelp.netflixstudios.com/hc/en-us/articles/217350977-English-USA-Timed-Text-Style-Guide) |
18
+ | 20 characters per second, 17 for children | The same English (USA) guide, adult and children's programs respectively |
19
+ | Never over what the frame is about | The DCMP [Captioning Tip Sheet](https://dcmp.org/learn/225-captioning-tip-sheet): "Placement must not interfere with existing visuals/graphics." |
15
20
 
16
- **20 characters per second, 17 for children.** The same English (USA) guide, adult and children's programs respectively.
21
+ ### Start on the first frame of the action
17
22
 
18
- **Start on the first frame of the action.** Netflix's [Subtitle Timing Guidelines](https://partnerhelp.netflixstudios.com/hc/en-us/articles/360051554394-Timed-Text-Style-Guide-Subtitle-Timing-Guidelines) put a subtitle's in-time on the first frame of its audio. A software video's caption names an action rather than a line of speech, so the body keys the in-time on the action's first frame.
23
+ Netflix's [Subtitle Timing Guidelines](https://partnerhelp.netflixstudios.com/hc/en-us/articles/360051554394-Timed-Text-Style-Guide-Subtitle-Timing-Guidelines) put a subtitle's in-time on the first frame of its audio. A software video's caption names an action rather than a line of speech, so the body keys the in-time on the action's first frame.
19
24
 
20
- **Never over what the frame is about.** The DCMP [Captioning Tip Sheet](https://dcmp.org/learn/225-captioning-tip-sheet): "Placement must not interfere with existing visuals/graphics."
25
+ ### Captions name what the frame shows
21
26
 
22
- **Captions name what the frame shows.** No source states this for a software video. It is the operator's read of an unguided render whose caption narrated the whole flow, kept because the timing rules above already assume a caption tied to one moment.
27
+ No source states this for a software video. It is the operator's read of an unguided render whose caption narrated the whole flow, kept because the timing rules above already assume a caption tied to one moment.
23
28
 
24
29
  ## Zoom
25
30
 
26
31
  Fields: interface motion design, and screen recording practice. Authorities: design systems that publish motion specs, and the reference documentation of a screen recording tool.
27
32
 
28
- **A push onto an action no faster than HyperFrames' floor, never past 1 second.** HyperFrames' `viewport-change` rule states a zoom of 1.0 to 2.0 seconds and gives its reason beside the value: "under 0.8s teleports, over 2.5s drags". The body keeps that floor by pointing at the rule rather than restating it. The 1 second ceiling has two grounds. Measured: an unguided render pushed in over 2.2 seconds and a second over 1.4 seconds, and the operator judged both slow, so the range's lower half already reads slow on a push onto a click. Published: Material Design 3's [easing and duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs) end their scale at 1000 ms, the top of the extra-long band they reserve for ambient transitions, so no input-driven transition in that system runs longer.
33
+ ### A push onto an action stays under 1 second
29
34
 
30
- **Zoom on a click.** Screen Studio's [auto zoom guide](https://screen.studio/guide/auto-zoom) keys its zoom on click positions and zooms nowhere a click did not occur.
35
+ The floor is HyperFrames'. Its `viewport-change` rule states a zoom of 1.0 to 2.0 seconds with the reason beside it, "under 0.8s teleports, over 2.5s drags", and the body points at the rule rather than restating it.
31
36
 
32
- **Conflict, and the pick.** HyperFrames' range and the measured renders disagree on the push onto an action, and the body takes the overlap: HyperFrames' teleport floor below, and 1 second above, which sits inside HyperFrames' range at its fast end. HyperFrames' full range stays with an ambient reveal, which matches Material's own split between input-driven and ambient motion. A tighter band was considered and declined. Material puts large expressive transitions at 450 to 600 ms, and Nielsen Norman Group's [animation duration article](https://www.nngroup.com/articles/animation-duration/) calls 400 ms "very slow", but both write for an interface a user waits on, which a video viewer is not, and that band sits below HyperFrames' stated teleport floor with nothing measured to show it reads right. Narrow the band once a composed demo is compared against the unguided renders.
37
+ - Measured: an unguided render pushed in over 2.2 seconds and a second over 1.4 seconds, and the operator judged both slow, so the range's lower half already reads slow on a push onto a click
38
+ - Published: Material Design 3's [easing and duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs) end at 1000 ms, the top of a band reserved for ambient transitions, so no input-driven transition there runs longer
39
+
40
+ ### Zoom on a click
41
+
42
+ Screen Studio's [auto zoom guide](https://screen.studio/guide/auto-zoom) keys its zoom on click positions and zooms nowhere a click did not occur.
43
+
44
+ ### Conflict, and the pick
45
+
46
+ HyperFrames' range and the measured renders disagree on the push onto an action. The body takes the overlap, HyperFrames' teleport floor below and 1 second above, which sits inside HyperFrames' range at its fast end.
47
+
48
+ - HyperFrames' full range stays with an ambient reveal, matching Material's own split between input-driven and ambient motion
49
+ - A tighter band was declined. Material puts large expressive transitions at 450 to 600 ms, and Nielsen Norman Group's [animation duration article](https://www.nngroup.com/articles/animation-duration/) calls 400 ms "very slow".
50
+ - Both write for an interface a user waits on, which a video viewer is not, and that band sits below HyperFrames' teleport floor with nothing measured to show it reads right
51
+
52
+ Narrow the band once a composed demo is compared against the unguided renders.
33
53
 
34
54
  ## Pointer
35
55
 
@@ -39,28 +59,52 @@ The two pointer rules in the body carry no number. One pointer on a frame and a
39
59
 
40
60
  Fields: web accessibility, and audio post-production. Authorities: the W3C's accessibility guidelines, and the audio engineering society's streaming recommendation.
41
61
 
42
- **Music at least 20 dB under the voice.** WCAG 2.1 [Success Criterion 1.4.7](https://www.w3.org/WAI/WCAG21/Understanding/low-or-no-background-audio.html): "The background sounds are at least 20 decibels lower than the foreground speech content, with the exception of occasional sounds that last for only one or two seconds."
62
+ ### Music at least 20 dB under the voice
63
+
64
+ WCAG 2.1 [Success Criterion 1.4.7](https://www.w3.org/WAI/WCAG21/Understanding/low-or-no-background-audio.html) is the source, reading "The background sounds are at least 20 decibels lower than the foreground speech content, with the exception of occasional sounds that last for only one or two seconds."
65
+
66
+ HyperFrames' `media-use` sets its narrated bed at a volume of 0.12, about 18 dB under a voice at full scale. The body takes WCAG's 20 dB, since it is the published floor and the bed default sits 2 dB short of it.
67
+
68
+ ### Overall loudness is delegated
43
69
 
44
- **Conflict, and the pick.** HyperFrames' `media-use` sets its narrated bed at a volume of 0.12, about 18 dB under a voice at full scale. The body takes WCAG's 20 dB, since it is the published floor and the bed default sits 2 dB short of it.
70
+ The AES [TD1008](https://aes.org/wp-content/uploads/2024/01/20210924_TD1008_v3.13.pdf) streaming recommendation normalizes speech to -18 LUFS with a true peak no higher than -1 dBTP. `media-use` ships -14 and -16 LUFS targets for social and podcast delivery.
45
71
 
46
- **Overall loudness is delegated.** The AES [TD1008](https://aes.org/wp-content/uploads/2024/01/20210924_TD1008_v3.13.pdf) streaming recommendation normalizes speech to -18 LUFS with a true peak no higher than -1 dBTP, while `media-use` ships -14 and -16 LUFS targets for social and podcast delivery. The body states no overall target, since the right one depends on where the video is published and `media-use` owns that operation.
72
+ The body states no overall target, since the right one depends on where the video is published and `media-use` owns that operation.
47
73
 
48
74
  ## Voiceover
49
75
 
50
76
  Field: captioning, for the reading ceiling a voiceover's captions inherit. Authority: the captioning program's presentation rate guidance.
51
77
 
52
- **At or under 160 words a minute when captioned.** The DCMP [Captioning Key: Presentation Rate](https://dcmp.org/learn/601-captioning-key---presentation-rate) caps upper-level captions at 160 words a minute. `hyperframes-creative`'s narration reference states 2.5 words a second, 150 a minute, which sits under the ceiling, so the two do not conflict. An open search of voiceover practitioners put explainers at 140 to 160 words a minute, which agrees.
78
+ ### At or under 160 words a minute when captioned
53
79
 
54
- Narration has no standards body for pace, and the scoped search found none, so the body sets only the ceiling captions impose and points at HyperFrames for the pace itself.
80
+ The DCMP [Captioning Key: Presentation Rate](https://dcmp.org/learn/601-captioning-key---presentation-rate) caps upper-level captions at 160 words a minute.
81
+
82
+ - `hyperframes-creative`'s narration reference states 2.5 words a second, 150 a minute, under the ceiling, so the two do not conflict
83
+ - An open search of voiceover practitioners put explainers at 140 to 160 words a minute, which agrees
84
+ - Narration has no standards body for pace, and the scoped search found none, so the body sets only the ceiling and points at HyperFrames for pace
55
85
 
56
86
  ## Declined
57
87
 
58
- **Restating HyperFrames' motion rules.** Ease variety, anchoring, click timing, the scale and dwell of a zoom, and title-safe margins are stated in HyperFrames' own skills, which update on their own cadence. A copy here goes stale on theirs.
88
+ Each item below stays out of the body for the reason given.
89
+
90
+ ### Restating HyperFrames' motion rules
91
+
92
+ Ease variety, anchoring, click timing, the scale and dwell of a zoom, and title-safe margins are stated in HyperFrames' own skills, which update on their own cadence. A copy here goes stale on theirs.
93
+
94
+ ### A loudness target in the body
95
+
96
+ See Audio above.
97
+
98
+ ### Pointer smoothing and hiding at rest
99
+
100
+ Screen Studio's [cursor guide](https://screen.studio/guide/cursor) smooths the pointer and hides it when idle, and its [guide to disabling smooth movement](https://screen.studio/guide/disable-smooth-mouse-movement) turns smoothing off for a dropdown menu.
101
+
102
+ Both are recorder settings. A take is composed with its pointer baked into the frames, so a session composing from it cannot change either, and they belong to whatever records the take.
59
103
 
60
- **A loudness target in the body.** See Audio above.
104
+ ### Zoom out before a cut, and a sound effect only on a meaningful click
61
105
 
62
- **Pointer smoothing and hiding at rest.** Screen Studio's [cursor guide](https://screen.studio/guide/cursor) smooths the pointer and hides it when idle, and its [guide to disabling smooth movement](https://screen.studio/guide/disable-smooth-mouse-movement) turns smoothing off for a dropdown menu. Declined because both are recorder settings. A take is composed with its pointer already baked into the frames, so a session composing from it cannot change either. They belong to whatever records the take.
106
+ Neither has a source, and no operator read stands behind either, so both stay out until one does.
63
107
 
64
- **Zoom out before a cut, and a sound effect only on a click that changes something.** Neither has a source, and no operator read stands behind either, so both stay out until one does.
108
+ ### Vertical 9:16 framing
65
109
 
66
- **Vertical 9:16 framing.** Every video measured when this skill was written was landscape. Add it when a project asks for vertical video, with its safe areas sourced from the platform's own creator documentation.
110
+ Every video measured when this skill was written was landscape. Add it when a project asks for vertical video, with its safe areas sourced from the platform's own creator documentation.
@@ -11,23 +11,34 @@ Neither was imported wholesale, and one measurement settles why. Read this befor
11
11
 
12
12
  ## What was adopted
13
13
 
14
- Roughly half the pattern catalog had no counterpart in this corpus, and six of those patterns are now in `machine-tells.md`: rule-of-three, synonym cycling, false ranges, inline-header lists, adverb propping, and passive voice with an unnamed actor. Each is a shape rather than a token, which is what kept it out of a ban list that matches words.
14
+ Roughly half the pattern catalog had no counterpart in this corpus. Six of those patterns are now in `machine-tells.md`, each a shape rather than a token, which kept it out of a ban list that matches words.
15
+
16
+ - Rule-of-three, synonym cycling, and false ranges
17
+ - Inline-header lists, adverb propping, and passive voice with an unnamed actor
18
+
19
+ ### The density layer
15
20
 
16
21
  The density layer had no counterpart at all and is now `density.md`. Its useful half is the split between what a compression pass may cut and what it may not, since a terse register already instructs a session to cut and names nothing that has to survive.
17
22
 
18
- The ambiguity rules travel with that layer for the same reason. Compression is what creates the ambiguity, so a rule about pronouns and modifiers belongs beside the rule that shortened the sentence.
23
+ The ambiguity rules travel with that layer. Compression creates the ambiguity, so a rule about pronouns and modifiers belongs beside the rule that shortened the sentence.
19
24
 
20
25
  ## What was declined
21
26
 
22
- - **The ban on abstract metaphor nouns.** The nouns it names are load-bearing terms in this corpus rather than decoration, and the count below is what decides it.
23
- - **Document mode as a selector.** The source has the writer pick a mode before drafting. In this corpus a document's shape is already fixed by the standard governing its type, so a mode selector adds a second authority over one question and the two will disagree.
24
- - **A voice register per surface.** One baseline covers every surface. A surface needing a warmer register states its own voice and claims the yield, which lifts voice alone and leaves the bans, the rhythm rules, and the tells in force.
27
+ - **The ban on abstract metaphor nouns.** The nouns it names are load-bearing terms in this corpus rather than decoration, and the count below decides it.
28
+ - **Document mode as a selector.** In this corpus the standard governing a document's type already fixes its shape, so a mode picked before drafting adds a second authority over one question.
29
+ - **A voice register per surface.** One baseline covers every surface. A surface needing a warmer register states its own voice and claims the yield, which lifts voice alone.
30
+
31
+ ### The count behind the first
25
32
 
26
- The first of those three rests on a count. The source names `surface` among the nouns it bans, and tracked markdown in the authoring repository used `surface` 593 times, `harness` 95, `scaffolding` 17, and `primitive` 4. Adopting the item either rewrites established vocabulary across the corpus or is ignored in silence, and a reader arriving later cannot tell which happened.
33
+ The source names `surface` among the nouns it bans. Tracked markdown in the authoring repository used `surface` 593 times, `harness` 95, `scaffolding` 17, and `primitive` 4.
34
+
35
+ Adopting the item either rewrites established vocabulary across the corpus or is ignored in silence, and a reader arriving later cannot tell which happened.
27
36
 
28
37
  ## What moved in from the markdown standard
29
38
 
30
- The marketing words and vague qualifiers under `## Voice` were a gated word set in the markdown standard until the audit narrowed to characters. A word ban catches the token and misses the habit behind it, so a writer swaps `robust` for a synonym and the claim stays unproven. The list now lives here as guidance a reader applies, and this body is its one copy, since no command reads it and a second copy would drift with nothing comparing the two.
39
+ The marketing words and vague qualifiers under `## Voice` were a gated word set in the markdown standard until the audit narrowed to characters. A word ban catches the token and misses the habit, so a writer swaps `robust` for a synonym and the claim stays unproven.
40
+
41
+ The list now lives here as guidance a reader applies. This body is its one copy, since no command reads it and a second copy would drift with nothing comparing the two.
31
42
 
32
43
  ## Where this diverges
33
44
 
@@ -37,4 +48,6 @@ The source material also prefers shorter sentences without bound. This skill mea
37
48
 
38
49
  ## The industry-guide record
39
50
 
40
- The cadence rules and the bans they pair with trace to three industry style guides, being Google's developer documentation style guide, Microsoft's writing style guide, and Apple's style guide. The authoring toolkit keeps the per-rule attributions and the dates each guide was read, and no install carries that record, so this file is the position a target reads.
51
+ The cadence rules and the bans they pair with trace to three industry style guides, being Google's developer documentation style guide, Microsoft's writing style guide, and Apple's style guide.
52
+
53
+ The authoring toolkit keeps the per-rule attributions and the dates each guide was read. No install carries that record, so this file is the position a target reads.
@@ -11,7 +11,7 @@ Run independent features in parallel, one git worktree and one Claude Code sessi
11
11
  ## One worktree per feature
12
12
 
13
13
  - Invoke `canon:plan-feature` in each session. Plans land at `.canon/plans/feature-<slug>.md` at the main root.
14
- - `ui-checklist` writes a per-branch handoff at `tmp/handoff/ui-checklist/<slug>.md`, which `git-pr` posts and removes. The slug is the branch name minus its leading type segment.
14
+ - `ui-checklist` writes a per-branch handoff at `tmp/handoff/ui-checklist/<slug>.md`, which `pr-evidence` posts and removes. The slug is the branch name minus its leading type segment.
15
15
  - Ship each worktree with `canon:git-ship`, or `canon:auto-ship` for a draft pull request
16
16
  - Only the operator or the controlling session readies a draft
17
17
 
@@ -41,6 +41,7 @@ Assert the orchestrator role in one warm session with `canon:role-orchestrator`.
41
41
 
42
42
  Merge order matters only when two branches touch the same file. Expect to rebase over `CLAUDE.md`, `tooling/**`, `canon/context/context-model.md`, `docs/agents/`, and any folder's `index.md`. Resolve an `index.md` conflict with `canon indexes regen` over the rebased tree.
43
43
 
44
+ - **Fan-in.** Batches of one change sharing edge files ship as one pull request, opened by an integrator that merges each member branch. See `claude/skills/plan-feature/references/batches.md`.
44
45
  - **Merge order.** Merge the smallest hotspot footprint first, and a branch touching `CLAUDE.md` or a context entry last.
45
46
  - **Rebase.** The branch merging second runs `/review-address`, which trial-merges against `origin/main` and rebases. Push, then merge.
46
47
  - **Clean up.** Run `/git-worktree cleanup` to remove merged worktrees. To start fresh, `ExitWorktree(action: "keep")`, then `/session-worktree <new-name>`.
@@ -67,7 +67,7 @@ Groups run in the order a project meets them. Every name `canon claude skills li
67
67
  | `canon:docs-sync` | A change left `README.md` or `docs/` stale |
68
68
  | `canon:git-stage` | Staged changes span several concerns |
69
69
  | `canon:git-commit` | Staged changes are one concern |
70
- | `canon:git-branch` | A branch name needs generating or renaming |
70
+ | `canon:git-branch` | A branch needs creating, naming, or renaming |
71
71
  | `canon:git-pr` | A pull request needs a title and body |
72
72
 
73
73
  ## After the pull request opens
@@ -78,6 +78,7 @@ Groups run in the order a project meets them. Every name `canon claude skills li
78
78
  | `canon:review-ui` | Beside code review, to drive the checklist |
79
79
  | `canon:review-address` | As the worker, to fix posted findings |
80
80
  | `canon:git-followup` | A small edit on a branch with an open PR |
81
+ | `canon:pr-evidence` | A new head needs its screenshots re-posted |
81
82
  | `canon:git-split` | A branch carries unrelated commits |
82
83
  | `canon:git-issue` | Something belongs on the tracker |
83
84
  | `canon:git-worktree` | After a merge, to reclaim worktrees |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.40.0",
4
+ "version": "5.42.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -6,6 +6,10 @@ export const GIT_CASES: readonly SkillCase[] = [
6
6
  prompt: 'Come up with a conventional name for this branch.',
7
7
  expect: 'git-branch',
8
8
  },
9
+ {
10
+ prompt: 'I am on main and about to start a feature, create its branch.',
11
+ expect: 'git-branch',
12
+ },
9
13
  {
10
14
  prompt: "Turn what's staged into one commit message.",
11
15
  expect: 'git-commit',
@@ -81,6 +81,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
81
81
  'Open the preview for that pull request and click through each box on its visual checklist, then post what happened.',
82
82
  expect: 'review-ui',
83
83
  },
84
+ {
85
+ prompt:
86
+ 'I pushed another commit to the open PR. Update the screenshot comparison on it so the reviewer sees the current head.',
87
+ expect: 'pr-evidence',
88
+ },
84
89
  {
85
90
  prompt:
86
91
  'The screencast draft is finished. Turn it into an actual video now.',
@@ -57,6 +57,7 @@ import {
57
57
  pruneFramesWithLegacy,
58
58
  pushFrame,
59
59
  readFrame,
60
+ referenceSlug,
60
61
  type TreeEntry,
61
62
  } from '@/pr/frames'
62
63
  import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
@@ -106,7 +107,12 @@ import {
106
107
  reviewRowOf,
107
108
  selectBranchPull,
108
109
  } from '@/pr/rest'
109
- import { resolveReviewScope } from '@/pr/review-scope'
110
+ import {
111
+ readReviewState,
112
+ type ReviewStateApi,
113
+ type ReviewStateRecord,
114
+ waitForReviews,
115
+ } from '@/pr/review-state'
110
116
  import { PROJECT_ROOT } from '@/roots/project'
111
117
  import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
112
118
 
@@ -139,6 +145,10 @@ interface LiftCommandOptions extends ReadOptions {
139
145
  readonly wait?: string
140
146
  }
141
147
 
148
+ interface ReviewStateOptions extends ReadOptions {
149
+ readonly wait?: string
150
+ }
151
+
142
152
  interface LocalOptions extends ReadOptions {
143
153
  readonly remove?: boolean
144
154
  }
@@ -184,6 +194,7 @@ const LOCAL_REFUSALS: Record<LocalRefusal | 'gh-failed', string> = {
184
194
  interface FramesOptions extends ReadOptions {
185
195
  readonly add?: string
186
196
  readonly box?: string
197
+ readonly reference?: string
187
198
  readonly head?: string
188
199
  readonly pass?: string
189
200
  readonly theme?: string
@@ -196,6 +207,7 @@ type FramesCommandRefusal =
196
207
  | 'bad-mode'
197
208
  | 'no-number'
198
209
  | 'bad-box'
210
+ | 'bad-reference'
199
211
  | 'bad-head'
200
212
  | 'bad-pass'
201
213
  | 'bad-theme'
@@ -212,13 +224,15 @@ const FRAMES_REFUSALS: Record<FramesCommandRefusal, string> = {
212
224
  'no-number':
213
225
  '--add and --drop write for one pull request, so name its number.',
214
226
  'bad-box':
215
- '--add takes --box <n>, a positive whole number naming the checklist box.',
227
+ '--add takes --box <n>, a positive whole number naming the checklist box, or --reference <page>/<frame>, never both.',
228
+ 'bad-reference':
229
+ '--reference takes <page>/<frame>, each half opening on a letter or digit and holding only letters, digits, dots, underscores, and dashes, with no `--` in either, so the capture files under a name that reads back as the frame.',
216
230
  'bad-head':
217
231
  '--head takes a commit sha, 7 to 40 lowercase hex characters. A branch name or a path would file the frame under a folder that names no commit.',
218
232
  'bad-pass':
219
233
  '--add takes --pass <stamp>, the instant the pass started in compact UTC such as 20261002T154450Z, so a second pass never replaces an earlier one.',
220
234
  'bad-theme':
221
- '--theme takes dark or light and goes with --add, so a second shot of one box never replaces the first.',
235
+ '--theme takes dark or light and goes with --add --box, so a second shot of one box never replaces the first.',
222
236
  'bad-days': '--prune takes a positive whole number of days.',
223
237
  'unreadable-frame':
224
238
  'The file named by --add is missing, empty, or not a PNG, so it would embed as a broken image. Nothing is pushed.',
@@ -556,11 +570,17 @@ export function register(program: Command): void {
556
570
  })
557
571
 
558
572
  pr.command('review-state')
559
- .description('Report the commit and instant the last review pass covered')
573
+ .description(
574
+ 'Report what the last code review and UI review passes covered at the tip',
575
+ )
560
576
  .argument('[number]', 'Pull request to read, defaulting to this branch')
561
577
  .helpOption('-h, --help', 'Show this help message')
562
578
  .option('--root <path>', 'Repository to read, defaulting to the cwd')
563
579
  .option('--json', 'Add a machine-readable record on stdout')
580
+ .option(
581
+ '--wait <seconds>',
582
+ `Hold until a pass covers the tip in every bound family, default ${DEFAULT_WAIT_SECONDS}`,
583
+ )
564
584
  .addHelpText(
565
585
  'after',
566
586
  [
@@ -582,6 +602,31 @@ export function register(program: Command): void {
582
602
  ' fallback a pass posted before the marker shipped, off GitHub stamps',
583
603
  ' none the thread carries no pass, so the next one is a first pass',
584
604
  '',
605
+ 'Those code review fields stay at the top level of the record. Beside',
606
+ 'them, `head` is the branch tip read from the remote, and `review` and',
607
+ '`ui` carry each family at that tip:',
608
+ ' reason the refusal `canon pr lift` gives the family, absent when',
609
+ ' its gate passes, read through the same gate function',
610
+ ' isCovered a pass covers the tip: the gate passes, or the newest pass,',
611
+ ' open or unmarked, names the tip',
612
+ '',
613
+ '`ui` also carries the newest `## UI review` pass (`heading`, `state`,',
614
+ '`commit`, `source`), `isBound`, which is whether the evidence comment',
615
+ 'carries a checklist, and `carriedFrom` or `paintedPaths` when the lift',
616
+ 'carried a close forward or refused to. An unbound family reads covered.',
617
+ '',
618
+ 'Both families post as pull request reviews, so this reads',
619
+ '`pulls/<n>/reviews` alone, as the lift does. Issue comments are not',
620
+ 'read: neither review skill posts there, and a heading found only in a',
621
+ 'comment would let a worker move on a pass the lift then refuses.',
622
+ '',
623
+ '`--wait <seconds>` holds until `isCovered` reads true in both families',
624
+ 'or the deadline passes, re-reading the tip every 30 seconds so a push',
625
+ 'moves what a pass must cover. The evidence is read once at the start.',
626
+ 'The record then adds `reason`, `covered` or `timeout`, and',
627
+ '`waitedSeconds`. A wait past 600 seconds outlasts a foreground shell',
628
+ 'call, so run it in the background.',
629
+ '',
585
630
  'Refusals, read off `reason`:',
586
631
  ' ambiguous-pull two open pull requests share the branch against',
587
632
  ' different bases, so name a number',
@@ -594,18 +639,22 @@ export function register(program: Command): void {
594
639
  ' gh-missing `gh` is not on the path, so no pull request resolved',
595
640
  ' gh-failed `gh` could not answer for this branch, so name a number',
596
641
  ' no-object-head the pull request object reported no head commit',
642
+ ' bad-wait --wait takes a whole number of seconds',
597
643
  '',
598
644
  'Exit codes:',
599
- ' 0 the thread was read, whether it carries a pass or not',
600
- ' 1 refused, with the reason on stderr or in the JSON record',
645
+ ' 0 the thread was read, whether it carries a pass or not, or the',
646
+ ' wait ended covered',
647
+ ' 1 refused, with the reason on stderr or in the JSON record, or the',
648
+ ' wait reached its deadline as `timeout`',
601
649
  '',
602
650
  'Examples:',
603
651
  ' canon pr review-state',
604
652
  ' canon pr review-state 1341 --json',
653
+ ' canon pr review-state 1341 --wait 1200 --json',
605
654
  '',
606
655
  ].join('\n'),
607
656
  )
608
- .action(async (number: string | undefined, opts: ReadOptions) => {
657
+ .action(async (number: string | undefined, opts: ReviewStateOptions) => {
609
658
  process.exitCode = await runReviewState(number, opts)
610
659
  })
611
660
 
@@ -1011,6 +1060,10 @@ export function register(program: Command): void {
1011
1060
  .option('--json', 'Add a machine-readable record on stdout')
1012
1061
  .option('--add <file>', 'Push this PNG as a frame')
1013
1062
  .option('--box <n>', 'The checklist box the frame shows, with --add')
1063
+ .option(
1064
+ '--reference <address>',
1065
+ 'The design reference the capture shows, as <page>/<frame>, with --add in place of --box',
1066
+ )
1014
1067
  .option(
1015
1068
  '--head <sha>',
1016
1069
  "The head the frame was driven at, with --add, defaulting to the pull request's",
@@ -1035,7 +1088,8 @@ export function register(program: Command): void {
1035
1088
  `An add writes only refs/heads/${FRAMES_BRANCH}, through the GitHub git data API,`,
1036
1089
  'so nothing is checked out and no other branch is touched. A frame lands',
1037
1090
  'at pr-<number>/<short-head>/<pass>/box-<n>.png, or box-<n>-<theme>.png',
1038
- 'with --theme, and `link` names that',
1091
+ 'with --theme, or ref-<page>--<frame>.png for a design reference capture',
1092
+ 'passed as --reference <page>/<frame> in place of --box, and `link` names that',
1039
1093
  `path on ${FRAMES_BRANCH}, so it survives a rewrite that drops other pull`,
1040
1094
  'requests and resolves until its own pull request is dropped. The pass',
1041
1095
  'segment keeps a second pass at one head and box from replacing an image',
@@ -1056,10 +1110,11 @@ export function register(program: Command): void {
1056
1110
  ' ok an add pushed, or a drop or prune finished',
1057
1111
  ' bad-mode not exactly one of --add, --drop, --prune',
1058
1112
  ' no-number --add or --drop named no pull request',
1059
- ' bad-box --add without a positive --box',
1113
+ ' bad-box --add with both --box and --reference, or neither',
1114
+ ' bad-reference --reference is not <page>/<frame> in the safe name form',
1060
1115
  ' bad-head --head is not 7 to 40 lowercase hex characters',
1061
1116
  ' bad-pass --add without a compact UTC --pass stamp',
1062
- ' bad-theme --theme is not dark or light, or no --add',
1117
+ ' bad-theme --theme is not dark or light, or no --add, or rides a --reference',
1063
1118
  ' bad-days --prune without a positive number of days',
1064
1119
  ' unreadable-frame the --add file is missing, empty, or not a PNG',
1065
1120
  ' read-only GitHub refused the write, as on a fork token',
@@ -1807,13 +1862,21 @@ async function runChecks(
1807
1862
 
1808
1863
  async function runReviewState(
1809
1864
  number: string | undefined,
1810
- opts: ReadOptions,
1865
+ opts: ReviewStateOptions,
1811
1866
  ): Promise<number> {
1812
1867
  const root = resolve(opts.root ?? process.cwd())
1813
1868
  const emitJson = opts.json ?? false
1814
1869
 
1815
1870
  intro('canon pr review-state')
1816
1871
 
1872
+ let waitSeconds: number | undefined
1873
+ if (opts.wait !== undefined) {
1874
+ if (!/^\d+$/.test(opts.wait)) {
1875
+ return refuseWith('bad-wait', LIFT_REFUSALS['bad-wait'], emitJson, root)
1876
+ }
1877
+ waitSeconds = Number(opts.wait)
1878
+ }
1879
+
1817
1880
  if (Bun.which('gh') === null) {
1818
1881
  return refuseWith('gh-missing', PULL_REFUSALS['gh-missing'], emitJson, root)
1819
1882
  }
@@ -1830,20 +1893,84 @@ async function runReviewState(
1830
1893
  )
1831
1894
  }
1832
1895
 
1833
- const reviews = await listReviews(root, resolved.number)
1834
- if (reviews.kind === 'refused') {
1835
- const reason =
1836
- reviews.reason === 'gh-failed' ? 'gh-failed' : 'reviews-unreadable'
1837
- return refuseWith(reason, PULL_REFUSALS[reason], emitJson, root)
1896
+ const read = await readIdentity(root, resolved.number)
1897
+ if (read.kind === 'refused') {
1898
+ return refuseWith(read.reason, PULL_REFUSALS[read.reason], emitJson, root)
1838
1899
  }
1839
1900
 
1840
- const listing = { number: Number(resolved.number) }
1841
- const scope = resolveReviewScope({
1842
- reviews: reviews.rows.map(reviewRowOf),
1843
- })
1901
+ // The module refuses every failed read as `gh-failed`. The api keeps each
1902
+ // read's own reason, so a caller still tells an unreadable thread from an
1903
+ // unreadable remote.
1904
+ let failure: PullRefusal | HeadRefusal = 'gh-failed'
1905
+ const git = gitRunner(root)
1906
+ const api: ReviewStateApi = {
1907
+ readTip: async () => {
1908
+ const tip = await resolveTip(read.identity.branch, refReader(root))
1909
+ if (tip.kind === 'read') return tip.tip
1910
+ failure = tip.reason
1911
+ return undefined
1912
+ },
1913
+ readReviews: async () => {
1914
+ const reviews = await listReviews(root, resolved.number)
1915
+ if (reviews.kind === 'read') return reviews.rows.map(reviewRowOf)
1916
+ failure =
1917
+ reviews.reason === 'gh-failed' ? 'gh-failed' : 'reviews-unreadable'
1918
+ return undefined
1919
+ },
1920
+ readEvidence: () => readEvidenceGate(root, resolved.number),
1921
+ readChanged: (from, to) => readChangedPaths(git, from, to),
1922
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
1923
+ now: () => Date.now(),
1924
+ }
1925
+
1926
+ const painted = loadPaintedPatterns(root)
1927
+ const wait =
1928
+ waitSeconds === undefined
1929
+ ? undefined
1930
+ : await waitForReviews(api, { waitSeconds, painted })
1931
+ const state = wait ?? (await readReviewState(api, painted))
1932
+ if (state.kind === 'refused') {
1933
+ return refuseWith(failure, PULL_REFUSALS[failure], emitJson, root)
1934
+ }
1935
+
1936
+ const { record } = state
1937
+ const pull = Number(resolved.number)
1938
+ logReviewState(pull, record)
1939
+ const waited =
1940
+ wait?.kind === 'read'
1941
+ ? { reason: wait.reason, waitedSeconds: wait.waitedSeconds }
1942
+ : undefined
1943
+ if (waited !== undefined) {
1944
+ logStep(waited.reason === 'covered' ? 'Covered' : 'Timeout')
1945
+ logInfo(
1946
+ waited.reason === 'covered'
1947
+ ? `a pass covers ${record.head.slice(0, 8)} in every bound family after ${waited.waitedSeconds}s`
1948
+ : `no pass covered ${record.head.slice(0, 8)} in every bound family within ${waited.waitedSeconds}s`,
1949
+ )
1950
+ }
1951
+
1952
+ outro()
1953
+
1954
+ if (emitJson) {
1955
+ const { scope, ...families } = record
1956
+ process.stdout.write(
1957
+ `${JSON.stringify({
1958
+ root,
1959
+ number: pull,
1960
+ ...scope,
1961
+ ...families,
1962
+ ...waited,
1963
+ })}\n`,
1964
+ )
1965
+ }
1844
1966
 
1967
+ return waited?.reason === 'timeout' ? 1 : 0
1968
+ }
1969
+
1970
+ function logReviewState(number: number, record: ReviewStateRecord): void {
1971
+ const { scope } = record
1845
1972
  logStep('Scope')
1846
- logInfo(`#${listing.number}`)
1973
+ logInfo(`#${number} at ${record.head.slice(0, 8)}`)
1847
1974
 
1848
1975
  if (scope.source === 'none') {
1849
1976
  logStep('First pass')
@@ -1863,19 +1990,17 @@ async function runReviewState(
1863
1990
  )
1864
1991
  }
1865
1992
 
1866
- outro()
1867
-
1868
- if (emitJson) {
1869
- process.stdout.write(
1870
- `${JSON.stringify({
1871
- root,
1872
- number: listing.number,
1873
- ...scope,
1874
- })}\n`,
1875
- )
1993
+ for (const [name, family] of [
1994
+ ['review', record.review],
1995
+ ['ui', record.ui],
1996
+ ] as const) {
1997
+ const line = `${name}: ${family.reason ?? 'clear'}, ${family.isCovered ? 'a pass covers the tip' : 'no pass covers the tip'}`
1998
+ if (family.isCovered) logInfo(line)
1999
+ else logWarn(line)
2000
+ }
2001
+ if (!record.ui.isBound) {
2002
+ logInfo('ui: the evidence carries no checklist, so the family is unbound')
1876
2003
  }
1877
-
1878
- return 0
1879
2004
  }
1880
2005
 
1881
2006
  interface PullFiles {
@@ -3084,10 +3209,18 @@ function framesArgumentRefusal(
3084
3209
  if (number === undefined || !POSITIVE_WHOLE.test(number)) return 'no-number'
3085
3210
  if (
3086
3211
  opts.add !== undefined &&
3087
- (opts.box === undefined || !POSITIVE_WHOLE.test(opts.box))
3212
+ (opts.box === undefined) === (opts.reference === undefined)
3088
3213
  ) {
3089
3214
  return 'bad-box'
3090
3215
  }
3216
+ if (opts.add !== undefined && opts.box !== undefined) {
3217
+ if (!POSITIVE_WHOLE.test(opts.box)) return 'bad-box'
3218
+ }
3219
+ if (opts.reference !== undefined) {
3220
+ if (opts.add === undefined || referenceSlug(opts.reference) === undefined) {
3221
+ return 'bad-reference'
3222
+ }
3223
+ }
3091
3224
  if (opts.head !== undefined && !isFrameHead(opts.head)) return 'bad-head'
3092
3225
  if (
3093
3226
  opts.add !== undefined &&
@@ -3097,7 +3230,9 @@ function framesArgumentRefusal(
3097
3230
  }
3098
3231
  if (
3099
3232
  opts.theme !== undefined &&
3100
- (opts.add === undefined || !isFrameTheme(opts.theme))
3233
+ (opts.add === undefined ||
3234
+ opts.reference !== undefined ||
3235
+ !isFrameTheme(opts.theme))
3101
3236
  ) {
3102
3237
  return 'bad-theme'
3103
3238
  }
@@ -3199,7 +3334,8 @@ async function runFrames(
3199
3334
  if (head === undefined || head === '') return refuseFrames('no-object-head')
3200
3335
  if (!isFrameHead(head)) return refuseFrames('bad-head')
3201
3336
 
3202
- const box = Number(opts.box)
3337
+ const box = opts.box === undefined ? undefined : Number(opts.box)
3338
+ const reference = opts.reference
3203
3339
  const pass = opts.pass ?? ''
3204
3340
  const theme = opts.theme as FrameTheme | undefined
3205
3341
  const result = await pushFrame(api, {
@@ -3207,6 +3343,7 @@ async function runFrames(
3207
3343
  head,
3208
3344
  pass,
3209
3345
  box,
3346
+ reference,
3210
3347
  theme,
3211
3348
  bytes: bytes ?? new Uint8Array(),
3212
3349
  })
@@ -3215,7 +3352,7 @@ async function runFrames(
3215
3352
  return finish(
3216
3353
  {
3217
3354
  number: pull,
3218
- box,
3355
+ ...(box === undefined ? { reference } : { box }),
3219
3356
  ...(theme === undefined ? {} : { theme }),
3220
3357
  head,
3221
3358
  pass,