@erclx/canon 5.26.0 → 5.28.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 (320) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/SKILL.md +2 -0
  4. package/claude/skills/auto-ship/SKILL.md +3 -11
  5. package/claude/skills/auto-ship/references/review-findings.md +20 -0
  6. package/claude/skills/backlog-triage/SKILL.md +2 -0
  7. package/claude/skills/bash-cli-script/SKILL.md +2 -0
  8. package/claude/skills/build-in-slices/SKILL.md +2 -0
  9. package/claude/skills/canon-cli/SKILL.md +2 -0
  10. package/claude/skills/canon-feedback/SKILL.md +2 -0
  11. package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
  12. package/claude/skills/canon-operator/SKILL.md +2 -0
  13. package/claude/skills/canon-rollout/SKILL.md +3 -11
  14. package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
  15. package/claude/skills/canvas/SKILL.md +13 -0
  16. package/claude/skills/ci-workflow/SKILL.md +2 -0
  17. package/claude/skills/code-craft/SKILL.md +2 -0
  18. package/claude/skills/codebase-layout/SKILL.md +2 -0
  19. package/claude/skills/context-fold/SKILL.md +3 -17
  20. package/claude/skills/context-fold/references/completion-report.md +26 -0
  21. package/claude/skills/create-rule/SKILL.md +2 -0
  22. package/claude/skills/create-skill/SKILL.md +2 -0
  23. package/claude/skills/create-standard/SKILL.md +2 -0
  24. package/claude/skills/decision-escalate/SKILL.md +2 -0
  25. package/claude/skills/deploy-app/SKILL.md +2 -0
  26. package/claude/skills/deprecation-migration/SKILL.md +2 -0
  27. package/claude/skills/design-extract/SKILL.md +2 -0
  28. package/claude/skills/design-taste/SKILL.md +2 -0
  29. package/claude/skills/docs-sync/SKILL.md +2 -0
  30. package/claude/skills/document-health/SKILL.md +2 -0
  31. package/claude/skills/draft-and-pick/SKILL.md +2 -0
  32. package/claude/skills/draft-doc/SKILL.md +2 -0
  33. package/claude/skills/draft-doc/references/wiki.md +1 -1
  34. package/claude/skills/draft-figure/SKILL.md +2 -0
  35. package/claude/skills/draft-identity/SKILL.md +2 -0
  36. package/claude/skills/draft-ready/SKILL.md +5 -1
  37. package/claude/skills/draft-screencast/SKILL.md +2 -0
  38. package/claude/skills/draft-slides/SKILL.md +2 -0
  39. package/claude/skills/git-branch/SKILL.md +2 -0
  40. package/claude/skills/git-commit/SKILL.md +2 -0
  41. package/claude/skills/git-followup/SKILL.md +2 -0
  42. package/claude/skills/git-issue/SKILL.md +2 -0
  43. package/claude/skills/git-pr/SKILL.md +2 -0
  44. package/claude/skills/git-pr/references/evidence.md +7 -7
  45. package/claude/skills/git-ship/SKILL.md +2 -0
  46. package/claude/skills/git-split/SKILL.md +2 -0
  47. package/claude/skills/git-stage/SKILL.md +2 -0
  48. package/claude/skills/git-worktree/SKILL.md +2 -0
  49. package/claude/skills/index-lookup/SKILL.md +2 -0
  50. package/claude/skills/markdown-propose/SKILL.md +2 -0
  51. package/claude/skills/memory-capture/SKILL.md +2 -0
  52. package/claude/skills/memory-review/SKILL.md +2 -0
  53. package/claude/skills/plan-feature/SKILL.md +2 -0
  54. package/claude/skills/plan-groundwork/SKILL.md +2 -0
  55. package/claude/skills/plan-intake/SKILL.md +2 -0
  56. package/claude/skills/plan-intake-answer/SKILL.md +2 -0
  57. package/claude/skills/project-commands/SKILL.md +2 -0
  58. package/claude/skills/read-frames/SKILL.md +2 -0
  59. package/claude/skills/record-screencast/SKILL.md +2 -0
  60. package/claude/skills/repo-metadata/SKILL.md +2 -0
  61. package/claude/skills/restate-plainly/SKILL.md +2 -0
  62. package/claude/skills/review-address/REQUIREMENT.md +4 -2
  63. package/claude/skills/review-address/SKILL.md +19 -14
  64. package/claude/skills/review-address/references/post-review-findings.md +16 -0
  65. package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
  66. package/claude/skills/review-branch/SKILL.md +2 -0
  67. package/claude/skills/review-craft/SKILL.md +2 -0
  68. package/claude/skills/review-craft/references/ui.md +6 -6
  69. package/claude/skills/review-pr/SKILL.md +4 -8
  70. package/claude/skills/review-pr/references/body-variants.md +52 -0
  71. package/claude/skills/review-pr/references/key-changes.md +16 -0
  72. package/claude/skills/review-pr/references/post.md +15 -43
  73. package/claude/skills/review-ui/REQUIREMENT.md +2 -2
  74. package/claude/skills/review-ui/SKILL.md +16 -12
  75. package/claude/skills/review-ui/references/driving.md +8 -8
  76. package/claude/skills/review-ui/references/post.md +13 -3
  77. package/claude/skills/review-ui/references/tick.md +22 -0
  78. package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
  79. package/claude/skills/role-orchestrator/SKILL.md +31 -42
  80. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
  81. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +13 -3
  82. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
  83. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +6 -34
  84. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
  85. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
  86. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +12 -23
  87. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
  88. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
  89. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
  90. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
  91. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
  92. package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
  93. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  94. package/claude/skills/role-planner/SKILL.md +4 -2
  95. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  96. package/claude/skills/role-reviewer/SKILL.md +3 -1
  97. package/claude/skills/role-worker/SKILL.md +5 -2
  98. package/claude/skills/search-craft/SKILL.md +2 -0
  99. package/claude/skills/seed-sync/SKILL.md +2 -0
  100. package/claude/skills/session-compact/SKILL.md +2 -0
  101. package/claude/skills/session-map/SKILL.md +2 -0
  102. package/claude/skills/session-relay/SKILL.md +2 -0
  103. package/claude/skills/session-resume/SKILL.md +2 -0
  104. package/claude/skills/session-worktree/SKILL.md +3 -11
  105. package/claude/skills/session-worktree/references/install-command.md +20 -0
  106. package/claude/skills/sketch-design/SKILL.md +2 -0
  107. package/claude/skills/systematic-debugging/SKILL.md +2 -0
  108. package/claude/skills/target-check/SKILL.md +2 -0
  109. package/claude/skills/target-setup/SKILL.md +2 -0
  110. package/claude/skills/task-board/SKILL.md +2 -0
  111. package/claude/skills/teach-workspace/SKILL.md +5 -15
  112. package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
  113. package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
  114. package/claude/skills/teach-workspace/references/revisit.md +22 -0
  115. package/claude/skills/test-craft/SKILL.md +2 -0
  116. package/claude/skills/test-first/SKILL.md +2 -0
  117. package/claude/skills/ui-checklist/SKILL.md +3 -1
  118. package/claude/skills/ux-audit/SKILL.md +2 -0
  119. package/claude/skills/ux-measure/SKILL.md +2 -0
  120. package/claude/skills/ux-walkthrough/SKILL.md +2 -0
  121. package/claude/skills/video-craft/SKILL.md +2 -0
  122. package/claude/skills/write-human/SKILL.md +2 -0
  123. package/claude/skills/youtube-transcripts/SKILL.md +2 -0
  124. package/docs/agents/canvas.md +24 -8
  125. package/docs/agents/capture.md +5 -5
  126. package/docs/agents/commands.md +2 -2
  127. package/docs/agents/gate.md +3 -1
  128. package/docs/agents/index.md +5 -4
  129. package/docs/agents/install-and-sync.md +7 -0
  130. package/docs/agents/key-changes.md +1 -1
  131. package/docs/agents/pr-evidence.md +47 -23
  132. package/docs/agents/pr-local.md +19 -13
  133. package/docs/agents/pr-preview.md +44 -21
  134. package/docs/agents/pr-tick.md +72 -0
  135. package/docs/agents/sandbox.md +30 -6
  136. package/docs/agents/scripting.md +8 -1
  137. package/docs/agents/skills-audit.md +2 -1
  138. package/docs/agents/tasks-plans.md +1 -1
  139. package/docs/agents/tasks-validate.md +18 -18
  140. package/docs/agents/tasks.md +1 -1
  141. package/docs/workflow/operating-model.md +15 -21
  142. package/docs/workflow/parallel-features.md +24 -3
  143. package/docs/workflow/skill-map.md +3 -1
  144. package/docs/workflow/zshrc-aliases.md +1 -1
  145. package/package.json +1 -1
  146. package/scripts/core/regen-hero.ts +1 -1
  147. package/scripts/lib/sandbox-path.sh +6 -162
  148. package/src/audits/baseline.ts +1 -1
  149. package/src/audits/run.ts +2 -2
  150. package/src/canvas/address.ts +12 -3
  151. package/src/canvas/capture.ts +1 -1
  152. package/src/canvas/client/inspector/align-grid.tsx +162 -0
  153. package/src/canvas/client/inspector/color-field.tsx +201 -171
  154. package/src/canvas/client/inspector/color-inputs.tsx +139 -0
  155. package/src/canvas/client/inspector/color-picker.tsx +668 -0
  156. package/src/canvas/client/inspector/color-space.ts +240 -0
  157. package/src/canvas/client/inspector/field.tsx +66 -3
  158. package/src/canvas/client/inspector/fields.css +473 -6
  159. package/src/canvas/client/inspector/size-field.tsx +69 -0
  160. package/src/canvas/client/inspector/token-select.tsx +109 -0
  161. package/src/canvas/client/inspector/values.ts +117 -3
  162. package/src/canvas/client/inspector.tsx +311 -15
  163. package/src/canvas/client/keys.ts +54 -0
  164. package/src/canvas/client/selection.tsx +199 -0
  165. package/src/canvas/client/shell.css +210 -12
  166. package/src/canvas/client/state.ts +165 -14
  167. package/src/canvas/client/surface.tsx +341 -39
  168. package/src/canvas/client/tools.tsx +64 -0
  169. package/src/canvas/content.ts +51 -2
  170. package/src/canvas/edit.ts +49 -6
  171. package/src/canvas/editing.ts +203 -0
  172. package/src/canvas/global.d.ts +15 -0
  173. package/src/canvas/server.ts +149 -12
  174. package/src/canvas/tokens.ts +60 -18
  175. package/src/census/count.ts +1 -1
  176. package/src/claude/seeds.ts +3 -3
  177. package/src/claude/skills-audit.ts +28 -0
  178. package/src/claude/skills-drift.ts +1 -1
  179. package/src/claude/skills-families.ts +36 -0
  180. package/src/claude/skills-list.ts +76 -15
  181. package/src/claude/skills-reach.ts +1 -1
  182. package/src/commands/audits.ts +2 -2
  183. package/src/commands/canvas.ts +180 -3
  184. package/src/commands/claude.ts +70 -12
  185. package/src/commands/design.ts +145 -5
  186. package/src/commands/docs.ts +1 -1
  187. package/src/commands/feedback.ts +2 -2
  188. package/src/commands/gate.ts +9 -2
  189. package/src/commands/gov.ts +3 -3
  190. package/src/commands/hooks.ts +1 -1
  191. package/src/commands/indexes.ts +1 -1
  192. package/src/commands/init.ts +2 -2
  193. package/src/commands/intake.ts +1 -1
  194. package/src/commands/migrate.ts +2 -2
  195. package/src/commands/pr.ts +288 -65
  196. package/src/commands/records.ts +1 -1
  197. package/src/commands/repo.ts +1 -1
  198. package/src/commands/sandbox.ts +64 -15
  199. package/src/commands/sessions.ts +1 -1
  200. package/src/commands/slides.ts +1 -1
  201. package/src/commands/sync.ts +2 -2
  202. package/src/commands/tasks.ts +21 -4
  203. package/src/commands/teach.ts +1 -1
  204. package/src/commands/tooling.ts +1 -1
  205. package/src/commands/transcripts.ts +2 -2
  206. package/src/commands/upgrade.ts +1 -1
  207. package/src/commands/wiki.ts +1 -1
  208. package/src/commands/worktrees.ts +1 -1
  209. package/src/comments/scan.ts +1 -1
  210. package/src/comments/trend.ts +1 -1
  211. package/src/context/architecture.ts +1 -1
  212. package/src/context/citations.ts +3 -3
  213. package/src/context/classify/extract.ts +3 -3
  214. package/src/context/folders.ts +2 -2
  215. package/src/context/requirements.ts +1 -1
  216. package/src/design/project-fonts.ts +392 -0
  217. package/src/docs/read.ts +1 -1
  218. package/src/exec.ts +1 -1
  219. package/src/feedback/github.ts +2 -2
  220. package/src/gate/measures.ts +48 -10
  221. package/src/gate/sequencer.ts +56 -4
  222. package/src/gate/stages.ts +17 -0
  223. package/src/gate/suite-lock.ts +214 -0
  224. package/src/{git-files.ts → git/files.ts} +2 -2
  225. package/src/{git-ignore.ts → git/ignore.ts} +1 -1
  226. package/src/{worktree.ts → git/worktree.ts} +1 -1
  227. package/src/gov/citations.ts +1 -1
  228. package/src/gov/install.ts +1 -1
  229. package/src/gov/superseded.ts +1 -1
  230. package/src/gov/test-order.ts +2 -2
  231. package/src/help.ts +1 -1
  232. package/src/hooks/post-merge.ts +1 -1
  233. package/src/indexes/walk.ts +1 -1
  234. package/src/intake/folder.ts +1 -1
  235. package/src/labels/audit.ts +1 -1
  236. package/src/labels/map.ts +1 -1
  237. package/src/labels/phase.ts +3 -3
  238. package/src/markdown/files.ts +1 -1
  239. package/src/migrate/apply.ts +1 -1
  240. package/src/migrate/record-layout.ts +1 -1
  241. package/src/migrate/records.ts +11 -11
  242. package/src/migrate/scratch-evidence.ts +1 -1
  243. package/src/migrate/surface-roots.ts +6 -6
  244. package/src/pr/bijection.ts +1 -1
  245. package/src/pr/evidence.ts +250 -59
  246. package/src/pr/local.ts +16 -10
  247. package/src/pr/preview.ts +78 -2
  248. package/src/pr/tick.ts +56 -0
  249. package/src/process/harness.ts +1 -1
  250. package/src/records/backup.ts +2 -2
  251. package/src/records/ordinal.ts +1 -1
  252. package/src/records/prune.ts +1 -1
  253. package/src/records/size.ts +1 -1
  254. package/src/records/stale-canonical.ts +2 -2
  255. package/src/records/stale.ts +1 -1
  256. package/src/records/validate.ts +2 -2
  257. package/src/{project-root.ts → roots/project.ts} +1 -1
  258. package/src/{record-root.ts → roots/record.ts} +1 -1
  259. package/src/sandbox/coverage.ts +11 -12
  260. package/src/sandbox/equivalence.ts +57 -26
  261. package/src/sandbox/headless/dispatch.ts +264 -0
  262. package/src/sandbox/headless/run.ts +653 -0
  263. package/src/sandbox/headless/watch.ts +145 -0
  264. package/src/sandbox/hooks.ts +1 -1
  265. package/src/sandbox/provision.ts +90 -29
  266. package/src/sandbox/scenario.ts +588 -0
  267. package/src/sandbox/tree.ts +4 -8
  268. package/src/secrets/scan.ts +1 -1
  269. package/src/sessions/claim.ts +1 -1
  270. package/src/sessions/live.ts +4 -1
  271. package/src/sessions/resolve.ts +1 -1
  272. package/src/sessions/transfer/bundle.ts +2 -2
  273. package/src/slides/decks.ts +1 -1
  274. package/src/slides/package/fonts.ts +6 -2
  275. package/src/standards/audit.ts +2 -2
  276. package/src/standards/read.ts +1 -1
  277. package/src/sync/check.ts +2 -2
  278. package/src/sync/engine.ts +3 -3
  279. package/src/sync/git.ts +1 -1
  280. package/src/sync/history.ts +1 -1
  281. package/src/sync/layout.ts +2 -2
  282. package/src/sync/reverse.ts +1 -1
  283. package/src/sync/stamp.ts +1 -1
  284. package/src/sync/target.ts +1 -1
  285. package/src/sync/workflow.ts +1 -1
  286. package/src/targets/pulls.ts +2 -2
  287. package/src/targets/sweep.ts +2 -2
  288. package/src/{target.ts → targets/validate.ts} +1 -1
  289. package/src/tasks/answers.ts +1 -1
  290. package/src/tasks/archive.ts +1 -1
  291. package/src/tasks/label.ts +1 -1
  292. package/src/tasks/reach.ts +2 -2
  293. package/src/tasks/trunk.ts +1 -1
  294. package/src/tasks/validate.ts +38 -14
  295. package/src/teach/nav.ts +1 -1
  296. package/src/teach/workspace.ts +1 -1
  297. package/src/tooling/inject.ts +2 -2
  298. package/src/tooling/read.ts +1 -1
  299. package/src/tooling/scan.ts +1 -1
  300. package/src/tooling/subfolder.ts +1 -1
  301. package/src/version/installed.ts +1 -1
  302. package/src/version/skew.ts +1 -1
  303. package/src/web/readme-citations.ts +1 -1
  304. package/src/worktrees/bare-flag.ts +1 -1
  305. package/src/worktrees/reclaim.ts +2 -2
  306. package/src/worktrees/remove.ts +1 -1
  307. package/standards/board.md +3 -3
  308. package/standards/canvas.md +2 -1
  309. package/standards/plan.md +13 -7
  310. package/standards/skill.md +1 -0
  311. package/standards/wiki.md +12 -3
  312. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
  313. package/tsconfig.json +1 -1
  314. package/scripts/lib/sandbox-dispatch.sh +0 -190
  315. /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
  316. /package/src/{git-base.ts → git/base.ts} +0 -0
  317. /package/src/{git-env.ts → git/env.ts} +0 -0
  318. /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
  319. /package/src/{surface-root.ts → roots/surface.ts} +0 -0
  320. /package/src/{copy.ts → sync/copy.ts} +0 -0
@@ -4,7 +4,7 @@ import { basename, join, resolve } from 'node:path'
4
4
  import { $ } from 'bun'
5
5
  import type { Command } from 'commander'
6
6
  import { execa } from 'execa'
7
- import { gitEnv } from '@/git-env'
7
+ import { gitEnv } from '@/git/env'
8
8
  import {
9
9
  listChangedFiles,
10
10
  listIgnoreAdditions,
@@ -13,7 +13,7 @@ import {
13
13
  parseIgnoreAdditions,
14
14
  type RenamePair,
15
15
  resolveBaseRef,
16
- } from '@/git-files'
16
+ } from '@/git/files'
17
17
  import {
18
18
  type Bijection,
19
19
  type BijectionRefusal,
@@ -29,12 +29,15 @@ import {
29
29
  findEvidenceCaseCount,
30
30
  findEvidenceChecklist,
31
31
  findEvidenceCommentId,
32
+ findEvidenceHead,
32
33
  findEvidenceLocal,
33
34
  findEvidencePreview,
34
35
  groupEvidence,
35
36
  hasMarkedEvidenceComment,
37
+ readChecklistBoxes,
36
38
  readOwed,
37
39
  renderEvidenceBody,
40
+ settleChecklist,
38
41
  } from '@/pr/evidence'
39
42
  import {
40
43
  dropFrames,
@@ -51,13 +54,13 @@ import {
51
54
  } from '@/pr/frames'
52
55
  import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
53
56
  import {
57
+ dropAddressLine,
54
58
  findLocalServer,
55
59
  type Listener,
56
60
  type LocalRefusal,
57
61
  type LocalRunner,
58
62
  parseLsofListeners,
59
63
  parseProcNetTcp,
60
- stripLocalLine,
61
64
  } from '@/pr/local'
62
65
  import { KEY_CHANGES } from '@/pr/paths'
63
66
  import {
@@ -68,8 +71,10 @@ import {
68
71
  type PreviewRefusal,
69
72
  type PreviewRunner,
70
73
  type RunRow,
74
+ servesChange,
71
75
  type WorkflowFile,
72
76
  } from '@/pr/preview'
77
+ import { type TickRefusal, tickBoxes } from '@/pr/tick'
73
78
  import {
74
79
  commentRowOf,
75
80
  identityOf,
@@ -110,17 +115,34 @@ interface EvidenceOptions extends ReadOptions {
110
115
 
111
116
  interface LocalOptions extends ReadOptions {
112
117
  readonly remove?: boolean
113
- readonly note?: string
118
+ }
119
+
120
+ interface TickOptions extends ReadOptions {
121
+ readonly boxes?: string
122
+ readonly head?: string
123
+ }
124
+
125
+ type TickReason = TickRefusal | 'stale-head' | 'bad-boxes' | 'no-comment'
126
+
127
+ const TICK_REFUSALS: Record<TickReason | 'gh-failed', string> = {
128
+ 'stale-head':
129
+ 'The head named is not the branch tip on the remote, so a tick would claim a pass on a commit that is no longer the pull request. Re-drive the checklist at the tip.',
130
+ 'bad-boxes':
131
+ '--boxes takes box numbers as a comma-separated list of positive whole numbers, such as 1,3, and --head takes the commit that was driven.',
132
+ 'no-comment': 'No comment on this pull request carries the evidence marker.',
133
+ 'no-checklist': 'The evidence comment carries no checklist to tick.',
134
+ 'no-box':
135
+ 'A named box is past the end of the checklist. Number boxes off `canon pr evidence --json`.',
136
+ 'taste-box':
137
+ 'A named box ends in (taste), which no driver passes. Nothing was written.',
138
+ 'gh-failed':
139
+ 'gh could not read or edit the marked comment on this pull request.',
114
140
  }
115
141
 
116
142
  /** Shared by every verb that resolves a pull request from the checkout's branch. */
117
143
  const AMBIGUOUS_PULL =
118
144
  'More than one pull request is open on this branch, each against another base. Name the pull request number.'
119
145
 
120
- /** What replaces the local address line once the pull request closes. */
121
- const LOCAL_REMOVED_NOTE =
122
- '_Local preview removed when the pull request closed._'
123
-
124
146
  /** How long the probe waits on a listener before reading it as not a server. */
125
147
  const LOCAL_PROBE_TIMEOUT_MS = 2_000
126
148
 
@@ -194,7 +216,7 @@ const PREVIEW_TIMEOUT_MINUTES = 15
194
216
  const PREVIEW_POLL_MS = 15_000
195
217
 
196
218
  const PREVIEW_REFUSALS: Record<
197
- PreviewRefusal | 'bad-timeout' | 'check-timeout',
219
+ PreviewRefusal | 'bad-timeout' | 'check-timeout' | 'unreadable-changes',
198
220
  string
199
221
  > = {
200
222
  'bad-timeout': '--timeout takes a positive number of minutes.',
@@ -202,6 +224,8 @@ const PREVIEW_REFUSALS: Record<
202
224
  '--check only reads the deploy runs and waits on nothing, so it takes no --timeout.',
203
225
  'no-deploy':
204
226
  'No workflow under .github/workflows/ runs pages deploy and carries a workflow_dispatch trigger, so there is nothing to dispatch.',
227
+ unserved:
228
+ "The deploy workflow's push path filter matches no path this pull request changed, so the site it publishes would not show the change. Nothing was dispatched.",
205
229
  // biome-ignore lint/suspicious/noTemplateCurlyInString: a workflow expression, not a template
206
230
  unfenced:
207
231
  'The deploy workflow passes no --branch=${{ github.ref_name }}, so a dispatch from this branch would publish it to production. Add the flag before asking for a preview.',
@@ -209,6 +233,8 @@ const PREVIEW_REFUSALS: Record<
209
233
  'The deploy never printed a canon-preview-alias line, so no address could be read. Add the alias step from the cloudflare stack reference to the workflow.',
210
234
  'gh-failed':
211
235
  'gh could not list or dispatch the deploy workflow for this branch.',
236
+ 'unreadable-changes':
237
+ 'GitHub could not list what this pull request changed, so whether the deploy serves it is unknown. Nothing was dispatched.',
212
238
  'run-failed':
213
239
  'The deploy run finished without succeeding, so no preview was published. Read the run log.',
214
240
  timeout:
@@ -508,7 +534,7 @@ export function register(program: Command): void {
508
534
  .option('--json', 'Add a machine-readable record on stdout')
509
535
  .option(
510
536
  '--preview <url>',
511
- 'Open the body with this preview address, from canon pr preview',
537
+ 'Put this preview address on the line under the heading, from canon pr preview',
512
538
  )
513
539
  .option(
514
540
  '--checklist <path>',
@@ -516,7 +542,7 @@ export function register(program: Command): void {
516
542
  )
517
543
  .option(
518
544
  '--local <url>',
519
- "Add this worktree's running server under any preview line, from canon pr local",
545
+ "Add this worktree's running server to the address line, from canon pr local",
520
546
  )
521
547
  .option(
522
548
  '--check',
@@ -544,32 +570,46 @@ export function register(program: Command): void {
544
570
  ' would-empty the render holds no cases and the marked comment holds',
545
571
  ' some, so no body is printed and a person edits the comment',
546
572
  '',
547
- "--preview puts the address on the body's first line. With no evidence",
548
- 'in the diff, the body is that line and the marker alone, reported as',
549
- 'ok, so the preview still lands in one comment a later call can edit.',
550
- 'Without --preview, an address the marked comment already opens with',
551
- 'is carried into the new body, so a re-render after a push keeps it.',
573
+ 'Every body opens with `## Evidence`, then one address line joining',
574
+ 'the hosted and local previews with " · ", carrying whichever apply,',
575
+ 'then the Base and Head line when there are screenshots.',
576
+ '',
577
+ '--preview puts the hosted address on that line. With no evidence in',
578
+ 'the diff, the body is the heading, that line, and the marker alone,',
579
+ 'reported as ok, so the preview still lands in one comment a later',
580
+ 'call can edit. Without --preview, an address the marked comment',
581
+ 'already carries is carried into the new body, so a re-render after a',
582
+ 'push keeps it, unless a deploy workflow resolves and its push path',
583
+ 'filter matches no path the pull request changed, in which case the',
584
+ 'carried hosted address is dropped. An explicit --preview is always',
585
+ 'kept. The address region above the comparison is read for each',
586
+ 'prefix wherever it sits, so a comment that opens on the addresses',
587
+ 'still carries them forward.',
552
588
  '',
553
589
  '--checklist closes the body with a visual checklist, below the',
554
590
  'comparison it annotates. A checklist the marked comment already',
555
591
  'carries is read back and carried forward the same way the preview',
556
- 'address is, which keeps a re-render from wiping ticked boxes. It',
557
- 'turns no-evidence into ok: a branch with a checklist and no evidence',
558
- 'image renders a marked body holding the checklist, so a later call',
559
- 'finds and edits that comment.',
592
+ 'address is. A tick survives only a render at the head it names: one',
593
+ 'stamped `passed at <sha>` for another head is cleared and unstamped,',
594
+ 'and so is one carrying no stamp when the comment described another',
595
+ 'head. A checklist, supplied or carried, turns no-evidence into ok: a',
596
+ 'branch with a checklist and no evidence image renders a marked body',
597
+ 'holding the checklist, so a later call finds and edits that comment.',
560
598
  '',
561
- '--local adds a **Local preview:** line under the hosted preview line,',
562
- 'or opens the body with it when there is none, and is carried forward',
563
- 'the same way. It does not by itself turn no-evidence into ok, so a',
599
+ '--local adds a **Local preview:** segment after any hosted one on the',
600
+ 'address line, or carries the line alone, and is carried forward the',
601
+ 'same way. It does not by itself turn no-evidence into ok, so a',
564
602
  'branch with a server running and nothing to show posts no link-only',
565
603
  'comment. Together with --checklist the link and the checklist land in',
566
604
  'one comment.',
567
605
  '',
568
606
  'Both reasons carry what the marked comment already posted, each field',
569
607
  'present only when that comment holds it:',
570
- ' preview the hosted **Preview:** address the comment opens with',
571
- ' local the **Local preview:** address under it',
608
+ ' preview the hosted **Preview:** address the comment carries',
609
+ ' local the **Local preview:** address beside it',
572
610
  ' checklist the checklist between its delimiters, ticks included',
611
+ ' boxes each checklist box with its number, tick, stamp, and taste',
612
+ ' mark, the numbering `canon pr tick` takes',
573
613
  'These come from the comment already posted, never from the flags this',
574
614
  'call passed. A checklist the caller posted raw after a refused render',
575
615
  'carries no marker, so the record reports none of the three for it. An',
@@ -582,8 +622,9 @@ export function register(program: Command): void {
582
622
  ' owed `owed` lists evidence, preview, or both, in that order',
583
623
  'evidence is owed when an evidence image changed and no comment carries',
584
624
  'the marker. preview is owed when either holds, a deploy workflow in',
585
- "this checkout resolves, and the marked comment's first line is no",
586
- '**Preview:** line. A failed deploy reads the same as a skipped one.',
625
+ 'this checkout resolves, its push path filter matches a path the pull',
626
+ 'request changed, and the marked comment carries no **Preview:**',
627
+ 'address. A failed deploy reads the same as a skipped one.',
587
628
  'The record carries `owed` and the same marked fields. --check refuses',
588
629
  'with --preview, --local, or --checklist as check-writes.',
589
630
  '',
@@ -636,9 +677,16 @@ export function register(program: Command): void {
636
677
  'A deploy without the --branch flag is refused before anything runs,',
637
678
  'because Pages publishes an unfenced deploy to production.',
638
679
  '',
680
+ "The pull request's changed paths are then matched against the",
681
+ "workflow's own on.push.paths or paths-ignore filter, since a dispatch",
682
+ 'ignores that filter and would publish a site showing none of the',
683
+ 'change. A workflow with neither filter serves every change.',
684
+ '',
639
685
  'Read `reason` on the JSON record:',
640
686
  ' ok the preview was published, with its address in `url`',
641
687
  ' no-deploy no dispatchable workflow runs pages deploy',
688
+ ' unserved the push filter matches no changed path, so nothing was',
689
+ ' dispatched, and --check answers the same',
642
690
  ' unfenced the deploy passes no --branch, so nothing was dispatched',
643
691
  ' no-alias the workflow prints no canon-preview-alias line',
644
692
  ' run-failed the deploy run finished without succeeding',
@@ -675,15 +723,7 @@ export function register(program: Command): void {
675
723
  .helpOption('-h, --help', 'Show this help message')
676
724
  .option('--root <path>', 'Worktree to read, defaulting to the cwd')
677
725
  .option('--json', 'Add a machine-readable record on stdout')
678
- .option(
679
- '--remove',
680
- "Replace the evidence comment's local preview line with a note",
681
- )
682
- .option(
683
- '--note <text>',
684
- 'What replaces the line on --remove',
685
- LOCAL_REMOVED_NOTE,
686
- )
726
+ .option('--remove', "Drop the evidence comment's preview address line")
687
727
  .addHelpText(
688
728
  'after',
689
729
  [
@@ -700,16 +740,19 @@ export function register(program: Command): void {
700
740
  'without it.',
701
741
  '',
702
742
  '--remove reads the pull request comment carrying the pr-evidence',
703
- 'marker and replaces its **Local preview:** line with --note, editing',
704
- 'the comment itself so a close workflow with no session can call it.',
743
+ 'marker and drops its address line, the **Preview:** and **Local',
744
+ 'preview:** segments together, leaving no note in its place. It edits',
745
+ 'the comment itself so a close workflow with no session can call it,',
746
+ 'and drops the hosted link too because the same close deletes the',
747
+ "branch's preview deployments.",
705
748
  '',
706
749
  'Read `reason` on the JSON record:',
707
750
  ' ok a page answered, with its address in `url`',
708
751
  ' no-server nothing inside this worktree served a page',
709
752
  ' no-listener-reader neither lsof nor /proc is available',
710
- ' removed --remove replaced the line',
753
+ ' removed --remove dropped the line',
711
754
  ' no-comment --remove found no marked comment, a no-op',
712
- ' no-line --remove found no local line, a no-op',
755
+ ' no-line --remove found no address line, a no-op',
713
756
  '',
714
757
  'Exit codes:',
715
758
  ' 0 a server was found, or --remove finished, including a no-op',
@@ -728,6 +771,51 @@ export function register(program: Command): void {
728
771
  : await runLocal(opts)
729
772
  })
730
773
 
774
+ pr.command('tick')
775
+ .description(
776
+ "Tick the checklist boxes a UI pass drove, stamping each with the commit it passed at, on the pull request's evidence comment",
777
+ )
778
+ .argument('[number]', 'Pull request to edit, defaulting to this branch')
779
+ .helpOption('-h, --help', 'Show this help message')
780
+ .option('--root <path>', 'Repository to read, defaulting to the cwd')
781
+ .option('--json', 'Add a machine-readable record on stdout')
782
+ .requiredOption(
783
+ '--boxes <list>',
784
+ 'Box numbers to tick, comma-separated, as `canon pr evidence --json` numbers them',
785
+ )
786
+ .requiredOption('--head <sha>', 'The commit the boxes were driven at')
787
+ .addHelpText(
788
+ 'after',
789
+ [
790
+ '',
791
+ 'Edits the marked evidence comment in place, so a skill never',
792
+ 'hand-edits a comment body. Each named box becomes',
793
+ '`- [x] <box> · passed at <short sha>`. Ticking a box again restamps',
794
+ 'it. A later render at another head clears a tick whose stamp names',
795
+ 'a different commit.',
796
+ '',
797
+ 'Read `reason` on the JSON record:',
798
+ ' ticked the boxes were ticked, with the tip in `head`',
799
+ ' stale-head --head is not the remote branch tip, nothing written',
800
+ ' taste-box a named box ends in (taste), nothing written',
801
+ ' no-box a named number is past the checklist, nothing written',
802
+ ' no-checklist the evidence comment carries no checklist',
803
+ ' no-comment no comment carries the evidence marker',
804
+ ' bad-boxes --boxes or --head is malformed',
805
+ '',
806
+ 'Exit codes:',
807
+ ' 0 the boxes were ticked',
808
+ ' 1 refused, with the reason on stderr or in the JSON record',
809
+ '',
810
+ 'Examples:',
811
+ ' canon pr tick 1341 --boxes 1,2,4 --head 1a2b3c4 --json',
812
+ '',
813
+ ].join('\n'),
814
+ )
815
+ .action(async (number: string | undefined, opts: TickOptions) => {
816
+ process.exitCode = await runTick(number, opts)
817
+ })
818
+
731
819
  pr.command('frames')
732
820
  .description(
733
821
  `Push a UI review frame to the never-merged ${FRAMES_BRANCH} branch and return its embed link, or drop frames from it`,
@@ -1597,18 +1685,24 @@ async function runReviewState(
1597
1685
  return 0
1598
1686
  }
1599
1687
 
1688
+ interface PullFiles {
1689
+ /** Each path that still exists at the head, tagged with whether the pull request added it. */
1690
+ readonly present: ReadonlyMap<string, boolean>
1691
+ /** Each path the pull request removed, which a deploy's push filter still counts. */
1692
+ readonly removed: readonly string[]
1693
+ }
1694
+
1600
1695
  /**
1601
- * The paths a pull request changed that still exist at its head, each tagged
1602
- * with whether the pull request added it, read through the paginated files
1603
- * endpoint so the set is the same from any checkout and never capped at the
1604
- * first view. `renamed` and `copied` count as added, since the new path has no
1696
+ * The paths a pull request changed, read through the paginated files endpoint
1697
+ * so the set is the same from any checkout and never capped at the first
1698
+ * view. `renamed` and `copied` count as added, since the new path has no
1605
1699
  * counterpart at the merge base. Returns undefined when the read fails, which
1606
1700
  * refuses rather than rendering a short set.
1607
1701
  */
1608
1702
  async function listPullFiles(
1609
1703
  cwd: string,
1610
1704
  number: number,
1611
- ): Promise<ReadonlyMap<string, boolean> | undefined> {
1705
+ ): Promise<PullFiles | undefined> {
1612
1706
  const stdout = await gh(cwd, [
1613
1707
  'api',
1614
1708
  '--paginate',
@@ -1617,15 +1711,19 @@ async function listPullFiles(
1617
1711
  '.[] | [.status, .filename] | @tsv',
1618
1712
  ])
1619
1713
  if (stdout === null) return undefined
1620
- const files = new Map<string, boolean>()
1714
+ const present = new Map<string, boolean>()
1715
+ const removed: string[] = []
1621
1716
  for (const line of stdout.split('\n').filter(Boolean)) {
1622
1717
  const [status, path] = line.split('\t')
1623
- if (status === undefined || path === undefined || status === 'removed') {
1624
- continue
1625
- }
1626
- files.set(path, status !== 'modified' && status !== 'changed')
1718
+ if (status === undefined || path === undefined) continue
1719
+ if (status === 'removed') removed.push(path)
1720
+ else present.set(path, status !== 'modified' && status !== 'changed')
1627
1721
  }
1628
- return files
1722
+ return { present, removed }
1723
+ }
1724
+
1725
+ function everyChangedPath(files: PullFiles): string[] {
1726
+ return [...files.present.keys(), ...files.removed]
1629
1727
  }
1630
1728
 
1631
1729
  /**
@@ -1729,8 +1827,8 @@ async function runEvidence(
1729
1827
  }
1730
1828
 
1731
1829
  const grouped = await groupEvidence(
1732
- [...pullFiles.keys()],
1733
- async (path) => pullFiles.get(path) === false,
1830
+ [...pullFiles.present.keys()],
1831
+ async (path) => pullFiles.present.get(path) === false,
1734
1832
  )
1735
1833
 
1736
1834
  let existingCases = 0
@@ -1763,16 +1861,26 @@ async function runEvidence(
1763
1861
  const carried = {
1764
1862
  ...(carriedPreview !== undefined && { preview: carriedPreview }),
1765
1863
  ...(carriedLocal !== undefined && { local: carriedLocal }),
1766
- ...(carriedChecklist !== undefined && { checklist: carriedChecklist }),
1864
+ ...(carriedChecklist !== undefined && {
1865
+ checklist: carriedChecklist,
1866
+ boxes: readChecklistBoxes(carriedChecklist),
1867
+ }),
1767
1868
  }
1768
1869
 
1870
+ // A deploy that resolves and does not build from any changed path makes a
1871
+ // carried hosted link point at a site that shows none of this change.
1872
+ const deploy = findDeployWorkflow(await readWorkflows(root))
1873
+ const deployServesChange =
1874
+ deploy.kind === 'found' &&
1875
+ servesChange(deploy.filter, everyChangedPath(pullFiles))
1876
+ const isUnserved = deploy.kind === 'found' && !deployServesChange
1877
+
1769
1878
  if (isCheck) {
1770
1879
  const owed = readOwed({
1771
1880
  hasEvidenceChange: grouped.kind === 'read',
1772
1881
  hasMarkedComment: hasMarkedEvidenceComment(comments),
1773
1882
  carriedPreview: carriedPreview !== undefined,
1774
- deployWorkflowFound:
1775
- findDeployWorkflow(await readWorkflows(root)).kind === 'found',
1883
+ deployServesChange,
1776
1884
  })
1777
1885
  logStep(owed.length === 0 ? 'Settled' : 'Owed')
1778
1886
  logInfo(
@@ -1804,7 +1912,8 @@ async function runEvidence(
1804
1912
  // comment a later call edits. A local address rides on a checklist or a
1805
1913
  // hosted preview and never opens one alone, so a docs-only branch with a
1806
1914
  // server up posts nothing.
1807
- const hasChecklist = suppliedChecklist !== undefined
1915
+ const hasChecklist =
1916
+ suppliedChecklist !== undefined || carriedChecklist !== undefined
1808
1917
  if (
1809
1918
  grouped.kind === 'refused' &&
1810
1919
  opts.preview === undefined &&
@@ -1838,9 +1947,13 @@ async function runEvidence(
1838
1947
  )
1839
1948
  }
1840
1949
 
1841
- const preview = opts.preview ?? carriedPreview
1950
+ const preview = opts.preview ?? (isUnserved ? undefined : carriedPreview)
1842
1951
  const local = opts.local ?? carriedLocal
1843
- const checklist = suppliedChecklist ?? carriedChecklist
1952
+ const unsettled = suppliedChecklist ?? carriedChecklist
1953
+ const checklist =
1954
+ unsettled === undefined
1955
+ ? undefined
1956
+ : settleChecklist(unsettled, identity.head, findEvidenceHead(comments))
1844
1957
  const states = grouped.kind === 'read' ? grouped.states : []
1845
1958
  const body = renderEvidenceBody(
1846
1959
  states,
@@ -2037,6 +2150,24 @@ async function runPreview(
2037
2150
  }
2038
2151
  const { identity } = read
2039
2152
 
2153
+ // A dispatch ignores the workflow's own push filter, so the filter is read
2154
+ // here, ahead of both the mint and the check, rather than left to GitHub.
2155
+ const changed =
2156
+ identity.number === undefined
2157
+ ? undefined
2158
+ : await listPullFiles(root, identity.number)
2159
+ if (changed === undefined) {
2160
+ return refuseWith(
2161
+ 'unreadable-changes',
2162
+ PREVIEW_REFUSALS['unreadable-changes'],
2163
+ emitJson,
2164
+ root,
2165
+ )
2166
+ }
2167
+ if (!servesChange(pick.filter, everyChangedPath(changed))) {
2168
+ return refuseWith('unserved', PREVIEW_REFUSALS.unserved, emitJson, root)
2169
+ }
2170
+
2040
2171
  if (opts.check === true) {
2041
2172
  return runPreviewCheck(root, emitJson, pick.path, identity)
2042
2173
  }
@@ -2280,7 +2411,7 @@ async function runLocal(opts: LocalOptions): Promise<number> {
2280
2411
  }
2281
2412
 
2282
2413
  /**
2283
- * Replaces the local line on the marked comment. The verb writes here rather
2414
+ * Drops the address line on the marked comment. The verb writes here rather
2284
2415
  * than handing a body back, because its caller is a close workflow with no
2285
2416
  * session to post one.
2286
2417
  */
@@ -2290,7 +2421,6 @@ async function runLocalRemove(
2290
2421
  ): Promise<number> {
2291
2422
  const root = resolve(opts.root ?? process.cwd())
2292
2423
  const emitJson = opts.json ?? false
2293
- const note = opts.note ?? LOCAL_REMOVED_NOTE
2294
2424
 
2295
2425
  intro('canon pr local --remove')
2296
2426
 
@@ -2333,9 +2463,9 @@ async function runLocalRemove(
2333
2463
  return finish('no-comment', 'No comment carries the evidence marker.')
2334
2464
  }
2335
2465
 
2336
- const stripped = stripLocalLine(marked.body, note)
2337
- if (stripped === undefined) {
2338
- return finish('no-line', 'The evidence comment carries no local line.')
2466
+ const dropped = dropAddressLine(marked.body)
2467
+ if (dropped === undefined) {
2468
+ return finish('no-line', 'The evidence comment carries no address line.')
2339
2469
  }
2340
2470
 
2341
2471
  const patched = await gh(root, [
@@ -2344,7 +2474,7 @@ async function runLocalRemove(
2344
2474
  'PATCH',
2345
2475
  `repos/{owner}/{repo}/issues/comments/${commentId}`,
2346
2476
  '-f',
2347
- `body=${stripped}`,
2477
+ `body=${dropped}`,
2348
2478
  ])
2349
2479
  if (patched === null) {
2350
2480
  return refuseWith('gh-failed', LOCAL_REFUSALS['gh-failed'], emitJson, root)
@@ -2355,6 +2485,99 @@ async function runLocalRemove(
2355
2485
 
2356
2486
  const POSITIVE_WHOLE = /^[1-9]\d*$/
2357
2487
 
2488
+ const MIN_TICK_HEAD_LENGTH = 7
2489
+
2490
+ /**
2491
+ * Ticks the named boxes on the marked comment. It refuses a head that is not
2492
+ * the remote tip and a taste box before writing, so both rules hold as checks
2493
+ * rather than as prose a driver can talk itself out of.
2494
+ */
2495
+ async function runTick(
2496
+ number: string | undefined,
2497
+ opts: TickOptions,
2498
+ ): Promise<number> {
2499
+ const root = resolve(opts.root ?? process.cwd())
2500
+ const emitJson = opts.json ?? false
2501
+
2502
+ intro('canon pr tick')
2503
+
2504
+ const refuseTick = (reason: TickReason | 'gh-failed'): number =>
2505
+ refuseWith(reason, TICK_REFUSALS[reason], emitJson, root)
2506
+
2507
+ const numbers = (opts.boxes ?? '').split(',').map((part) => part.trim())
2508
+ const head = (opts.head ?? '').trim()
2509
+ if (
2510
+ numbers.length === 0 ||
2511
+ !numbers.every((part) => POSITIVE_WHOLE.test(part)) ||
2512
+ !/^[0-9a-f]+$/.test(head) ||
2513
+ head.length < MIN_TICK_HEAD_LENGTH
2514
+ ) {
2515
+ return refuseTick('bad-boxes')
2516
+ }
2517
+
2518
+ const read = await readIdentity(root, number)
2519
+ if (read.kind === 'refused') {
2520
+ return refuseWith(read.reason, PULL_REFUSALS[read.reason], emitJson, root)
2521
+ }
2522
+ const { identity } = read
2523
+ if (identity.number === undefined) return refuseTick('gh-failed')
2524
+
2525
+ const reading = await resolveTip(identity.branch, refReader(root))
2526
+ if (reading.kind === 'refused') {
2527
+ return refuseWith(
2528
+ reading.reason,
2529
+ PULL_REFUSALS[reading.reason],
2530
+ emitJson,
2531
+ root,
2532
+ )
2533
+ }
2534
+ if (!reading.tip.startsWith(head)) return refuseTick('stale-head')
2535
+
2536
+ const comments = await listComments(root, String(identity.number))
2537
+ if (comments === undefined) return refuseTick('gh-failed')
2538
+ const commentId = findEvidenceCommentId(comments)
2539
+ const marked = comments.find((comment) =>
2540
+ comment.url?.endsWith(`#issuecomment-${commentId}`),
2541
+ )
2542
+ if (commentId === undefined || marked === undefined) {
2543
+ return refuseTick('no-comment')
2544
+ }
2545
+
2546
+ const result = tickBoxes(marked.body, numbers.map(Number), reading.tip)
2547
+ if (result.kind === 'refused') return refuseTick(result.reason)
2548
+
2549
+ if (result.body !== marked.body) {
2550
+ const patched = await gh(root, [
2551
+ 'api',
2552
+ '-X',
2553
+ 'PATCH',
2554
+ `repos/{owner}/{repo}/issues/comments/${commentId}`,
2555
+ '-f',
2556
+ `body=${result.body}`,
2557
+ ])
2558
+ if (patched === null) return refuseTick('gh-failed')
2559
+ }
2560
+
2561
+ logStep('Ticked')
2562
+ logInfo(
2563
+ `${plural(numbers.length, 'box')} at ${reading.tip.slice(0, 7)} on comment ${commentId}`,
2564
+ )
2565
+ outro()
2566
+ if (emitJson) {
2567
+ process.stdout.write(
2568
+ `${JSON.stringify({
2569
+ root,
2570
+ number: identity.number,
2571
+ reason: 'ticked',
2572
+ head: reading.tip,
2573
+ boxes: numbers.map(Number),
2574
+ commentId,
2575
+ })}\n`,
2576
+ )
2577
+ }
2578
+ return 0
2579
+ }
2580
+
2358
2581
  /** One `gh api` call, keeping the HTTP status of a failure so a refused write can be told from a missing ref. */
2359
2582
  type ApiCall =
2360
2583
  | { readonly ok: true; readonly data: unknown }
@@ -56,7 +56,7 @@ import {
56
56
  pipeOutput,
57
57
  plural,
58
58
  } from '@/ui'
59
- import { currentWorktreeRoot, mainWorktreeRoot } from '@/worktree'
59
+ import { currentWorktreeRoot, mainWorktreeRoot } from '@/git/worktree'
60
60
 
61
61
  /** Returned when a record carries a finding, which is the gating result. */
62
62
  const EXIT_FINDINGS = 2
@@ -1,7 +1,7 @@
1
1
  import { resolve } from 'node:path'
2
2
  import type { Command } from 'commander'
3
3
  import { execa } from 'execa'
4
- import { gitEnv } from '@/git-env'
4
+ import { gitEnv } from '@/git/env'
5
5
  import {
6
6
  compareMetadata,
7
7
  type CurrentMetadata,