@erclx/canon 5.32.0 → 5.34.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 (250) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/REQUIREMENT.md +1 -2
  3. package/claude/skills/api-design/SKILL.md +2 -3
  4. package/claude/skills/api-design/references/adopted.md +1 -1
  5. package/claude/skills/auto-ship/SKILL.md +1 -1
  6. package/claude/skills/auto-ship/references/worktree-name.md +1 -1
  7. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  8. package/claude/skills/backlog-triage/SKILL.md +1 -1
  9. package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
  10. package/claude/skills/canon-cli/SKILL.md +75 -32
  11. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
  12. package/claude/skills/canon-feedback/SKILL.md +11 -29
  13. package/claude/skills/canon-feedback/references/template.md +53 -0
  14. package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
  15. package/claude/skills/{canvas → canvas-board}/SKILL.md +6 -3
  16. package/claude/skills/{canvas → canvas-board}/references/canvas.md +26 -3
  17. package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
  18. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  19. package/claude/skills/code-craft/SKILL.md +0 -1
  20. package/claude/skills/context-fold/REQUIREMENT.md +25 -33
  21. package/claude/skills/context-fold/SKILL.md +25 -70
  22. package/claude/skills/context-fold/references/classify.md +3 -3
  23. package/claude/skills/context-fold/references/completion-report.md +4 -4
  24. package/claude/skills/context-fold/references/context-refresh.md +5 -5
  25. package/{standards → claude/skills/context-fold/references}/context.md +16 -5
  26. package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
  27. package/claude/skills/create-rule/SKILL.md +3 -3
  28. package/{standards → claude/skills/create-rule/references}/rule.md +15 -6
  29. package/claude/skills/create-skill/SKILL.md +2 -2
  30. package/{standards → claude/skills/create-skill/references}/skill.md +18 -15
  31. package/claude/skills/design-taste/REQUIREMENT.md +3 -3
  32. package/claude/skills/design-taste/SKILL.md +1 -1
  33. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  34. package/claude/skills/docs-sync/SKILL.md +3 -3
  35. package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
  36. package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
  37. package/claude/skills/draft-and-pick/SKILL.md +5 -5
  38. package/claude/skills/draft-doc/SKILL.md +6 -6
  39. package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
  40. package/claude/skills/draft-figure/SKILL.md +2 -2
  41. package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
  42. package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
  43. package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
  44. package/claude/skills/draft-identity/SKILL.md +2 -2
  45. package/claude/skills/draft-ready/SKILL.md +1 -1
  46. package/claude/skills/git-branch/SKILL.md +1 -1
  47. package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
  48. package/claude/skills/git-commit/SKILL.md +2 -2
  49. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  50. package/claude/skills/git-followup/SKILL.md +1 -1
  51. package/claude/skills/git-issue/SKILL.md +1 -1
  52. package/claude/skills/git-issue/references/issue.md +4 -1
  53. package/claude/skills/git-pr/SKILL.md +13 -13
  54. package/claude/skills/git-pr/references/evidence.md +2 -2
  55. package/claude/skills/git-pr/references/final-command.md +38 -14
  56. package/claude/skills/git-pr/references/labels.md +5 -1
  57. package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
  58. package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
  59. package/claude/skills/git-pr/references/task-number.md +1 -1
  60. package/claude/skills/git-ship/SKILL.md +15 -14
  61. package/claude/skills/git-split/SKILL.md +3 -3
  62. package/claude/skills/git-stage/SKILL.md +1 -1
  63. package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
  64. package/claude/skills/review-address/SKILL.md +1 -1
  65. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  66. package/claude/skills/review-craft/references/ui.md +1 -1
  67. package/claude/skills/review-pr/REQUIREMENT.md +1 -1
  68. package/claude/skills/review-pr/SKILL.md +3 -3
  69. package/claude/skills/review-pr/references/body-variants.md +2 -2
  70. package/claude/skills/review-pr/references/key-changes.md +3 -3
  71. package/claude/skills/review-pr/references/post.md +4 -2
  72. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  73. package/claude/skills/review-ui/SKILL.md +8 -6
  74. package/claude/skills/review-ui/references/driving.md +8 -8
  75. package/claude/skills/review-ui/references/post.md +53 -17
  76. package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
  77. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  78. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
  79. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
  80. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +32 -1
  81. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
  82. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
  83. package/claude/skills/role-planner/REQUIREMENT.md +2 -1
  84. package/claude/skills/role-planner/SKILL.md +13 -2
  85. package/claude/skills/role-reviewer/SKILL.md +1 -1
  86. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  87. package/claude/skills/role-worker/SKILL.md +9 -1
  88. package/claude/skills/session-map/SKILL.md +1 -1
  89. package/claude/skills/session-resume/REQUIREMENT.md +2 -2
  90. package/claude/skills/session-worktree/SKILL.md +5 -5
  91. package/claude/skills/target-setup/SKILL.md +10 -4
  92. package/claude/skills/target-setup/references/detect.md +20 -3
  93. package/claude/skills/task-board/REQUIREMENT.md +7 -12
  94. package/claude/skills/task-board/SKILL.md +5 -17
  95. package/claude/skills/task-board/references/tasks.md +2 -2
  96. package/claude/skills/task-complete/REQUIREMENT.md +47 -0
  97. package/claude/skills/task-complete/SKILL.md +86 -0
  98. package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
  99. package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
  100. package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
  101. package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
  102. package/claude/skills/teach-workspace/SKILL.md +8 -4
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  104. package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
  105. package/claude/skills/teach-workspace/references/promotion.md +3 -3
  106. package/claude/skills/test-craft/REQUIREMENT.md +2 -3
  107. package/claude/skills/test-craft/SKILL.md +15 -3
  108. package/claude/skills/ui-checklist/SKILL.md +1 -1
  109. package/claude/skills/ux-audit/SKILL.md +1 -1
  110. package/claude/skills/ux-measure/SKILL.md +1 -1
  111. package/docs/agents/canvas.md +6 -2
  112. package/docs/agents/commands.md +4 -3
  113. package/docs/agents/context-audit-checks.md +4 -4
  114. package/docs/agents/gov-detect.md +47 -0
  115. package/docs/agents/index.md +7 -4
  116. package/docs/agents/indexes.md +1 -1
  117. package/docs/agents/install-and-sync.md +10 -11
  118. package/docs/agents/key-changes.md +5 -5
  119. package/docs/agents/label-coverage.md +11 -0
  120. package/docs/agents/migrate.md +1 -1
  121. package/docs/agents/pr-evidence-layout.md +70 -0
  122. package/docs/agents/pr-evidence.md +8 -9
  123. package/docs/agents/pr-frames.md +7 -2
  124. package/docs/agents/pr-lift.md +132 -0
  125. package/docs/agents/records-push.md +2 -2
  126. package/docs/agents/records-stale.md +12 -47
  127. package/docs/agents/records-validate.md +3 -7
  128. package/docs/agents/records.md +5 -27
  129. package/docs/agents/rule-citations.md +1 -1
  130. package/docs/agents/sandbox.md +4 -4
  131. package/docs/agents/scripting.md +2 -1
  132. package/docs/agents/skills-audit.md +1 -1
  133. package/docs/agents/tasks-plans.md +2 -2
  134. package/docs/agents/tasks.md +2 -2
  135. package/docs/target/migrations.md +10 -2
  136. package/docs/target/projects.md +4 -4
  137. package/docs/target/sync.md +1 -1
  138. package/docs/workflow/operating-model.md +2 -2
  139. package/docs/workflow/parallel-features.md +7 -7
  140. package/docs/workflow/skill-map-on-request.md +9 -9
  141. package/docs/workflow/skill-map.md +12 -11
  142. package/docs/workflow/visual-design-workflow.md +2 -2
  143. package/governance/detect.toml +201 -0
  144. package/governance/renames.toml +0 -11
  145. package/governance/rules/canon/context.md +13 -0
  146. package/governance/rules/claude/skill.md +17 -0
  147. package/governance/rules/code/000-code.md +1 -13
  148. package/governance/rules/code/010-testing.md +2 -30
  149. package/governance/rules/ui/400-ui.md +1 -1
  150. package/governance/rules/ui/480-canvas.md +13 -0
  151. package/governance/stacks/base.toml +2 -4
  152. package/package.json +4 -4
  153. package/scripts/core/install-check.sh +3 -3
  154. package/scripts/core/regen-hero.ts +0 -6
  155. package/scripts/lib/ui.sh +0 -168
  156. package/src/audits/catalog.ts +0 -1
  157. package/src/canvas/arrange.ts +66 -0
  158. package/src/canvas/capture.ts +20 -6
  159. package/src/canvas/content.ts +39 -0
  160. package/src/canvas/fit.ts +238 -0
  161. package/src/canvas/tokens.ts +28 -10
  162. package/src/capture/frame-values.ts +2 -32
  163. package/src/claude/cases/misc.ts +1 -1
  164. package/src/claude/cases/setup.ts +4 -0
  165. package/src/claude/cases/workflow.ts +6 -1
  166. package/src/claude/seeds.ts +1 -1
  167. package/src/claude/skills-audit.ts +2 -2
  168. package/src/cli/help.ts +3 -1
  169. package/src/cli/ui.ts +2 -3
  170. package/src/commands/canvas.ts +179 -1
  171. package/src/commands/claude.ts +1 -1
  172. package/src/commands/gov.ts +146 -16
  173. package/src/commands/labels.ts +326 -7
  174. package/src/commands/migrate.ts +6 -1
  175. package/src/commands/pr.ts +377 -2
  176. package/src/commands/records.ts +13 -399
  177. package/src/commands/sandbox.ts +3 -6
  178. package/src/commands/tasks.ts +3 -3
  179. package/src/context/audit.ts +5 -5
  180. package/src/context/folders.ts +1 -1
  181. package/src/design/budget.ts +1 -1
  182. package/src/design/components.ts +1 -1
  183. package/src/design/document.ts +1 -1
  184. package/src/design/regen.ts +1 -1
  185. package/src/design/render.ts +1 -1
  186. package/src/feedback/body.ts +26 -5
  187. package/src/gate/measures.ts +59 -45
  188. package/src/gate/sequencer.ts +56 -1
  189. package/src/gate/stages.ts +4 -11
  190. package/src/gov/detect.ts +301 -0
  191. package/src/gov/install.ts +2 -3
  192. package/src/gov/list.ts +23 -2
  193. package/src/gov/stacks.ts +10 -1
  194. package/src/hooks/post-merge.ts +1 -1
  195. package/src/labels/apply.ts +39 -0
  196. package/src/labels/format.ts +3 -3
  197. package/src/labels/phase.ts +3 -3
  198. package/src/labels/sections.ts +43 -0
  199. package/src/migrate/skill-names.ts +20 -1
  200. package/src/pr/evidence.ts +218 -45
  201. package/src/pr/frames.ts +23 -3
  202. package/src/pr/lift.ts +418 -0
  203. package/src/pr/paths.ts +4 -4
  204. package/src/pr/rest.ts +3 -0
  205. package/src/pr/review-scope.ts +76 -0
  206. package/src/records/backup.ts +16 -6
  207. package/src/records/stale-canonical.ts +31 -1
  208. package/src/records/validate.ts +2 -188
  209. package/src/sandbox/census.ts +1 -1
  210. package/src/sandbox/coverage.ts +2 -6
  211. package/src/sandbox/equivalence.ts +19 -51
  212. package/src/sandbox/expect.ts +4 -4
  213. package/src/sandbox/provision.ts +13 -36
  214. package/src/sandbox/scenario.ts +26 -48
  215. package/src/sandbox/tree.ts +2 -6
  216. package/src/standards/read.ts +14 -1
  217. package/src/sync/git.ts +1 -1
  218. package/src/tasks/archive.ts +5 -5
  219. package/src/tasks/branch.ts +3 -3
  220. package/src/tasks/label.ts +1 -1
  221. package/src/tasks/record.ts +2 -2
  222. package/src/tasks/validate.ts +1 -1
  223. package/standards/index.md +0 -13
  224. package/standards/standard.md +1 -7
  225. package/tooling/base/reference.md +1 -1
  226. package/tooling/base/seeds/.github/pull_request_template.md +2 -2
  227. package/tooling/claude/reference.md +1 -1
  228. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
  229. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
  230. package/tooling/claude/seeds/.claude/settings.json +9 -0
  231. package/governance/rules/standards/context.md +0 -14
  232. package/governance/rules/standards/design.md +0 -12
  233. package/governance/rules/standards/readme.md +0 -12
  234. package/governance/rules/standards/rule.md +0 -13
  235. package/governance/rules/standards/skill.md +0 -21
  236. package/governance/rules/standards/standard.md +0 -13
  237. package/scripts/config.sh +0 -11
  238. package/scripts/lib/gov.sh +0 -14
  239. package/scripts/lib/sandbox-fixtures.sh +0 -191
  240. package/scripts/lib/sandbox-git.sh +0 -125
  241. package/scripts/lib/sandbox-path.sh +0 -50
  242. package/scripts/sandbox-hook.sh +0 -99
  243. package/src/gov/standard-rules.ts +0 -179
  244. package/src/records/migrate.ts +0 -78
  245. package/src/records/stale.ts +0 -243
  246. package/src/sandbox/hooks.ts +0 -128
  247. /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
  248. /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
  249. /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
  250. /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
@@ -1,11 +1,18 @@
1
1
  import { resolve } from 'node:path'
2
+ import { $ } from 'bun'
2
3
  import type { Command } from 'commander'
4
+ import { execa } from 'execa'
5
+ import { gitEnv } from '@/git/env'
6
+ import { diffLabels } from '@/labels/apply'
3
7
  import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
4
8
  import { resolveScanInput } from '@/labels/event'
5
9
  import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
6
10
  import { MAP_REL } from '@/labels/map'
7
- import { scanPhaseLabels } from '@/labels/phase'
11
+ import { isReleasePullRequest, scanPhaseLabels } from '@/labels/phase'
12
+ import { checkSections } from '@/labels/sections'
8
13
  import { scanTitleSpelling } from '@/labels/spelling'
14
+ import { BAN_SETS } from '@/markdown/bans'
15
+ import { bodyLines, scanBans } from '@/markdown/scan'
9
16
  import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
10
17
 
11
18
  interface AuditOptions {
@@ -14,6 +21,14 @@ interface AuditOptions {
14
21
  readonly json?: boolean
15
22
  }
16
23
 
24
+ interface ApplyOptions extends AuditOptions {
25
+ readonly check?: boolean
26
+ }
27
+
28
+ type ApplyRefusal = 'bad-number' | 'no-branch' | 'gh-failed' | 'head-mismatch'
29
+
30
+ const GH_TIMEOUT_MS = 30_000
31
+
17
32
  interface ScanOptions {
18
33
  readonly event?: string
19
34
  readonly title?: string
@@ -47,6 +62,16 @@ const REFUSALS: Record<LabelAuditRefusal, string> = {
47
62
  'git could not list what this branch changed, so the set is unknown.',
48
63
  }
49
64
 
65
+ const APPLY_REFUSALS: Record<ApplyRefusal, string> = {
66
+ 'bad-number': 'The pull request number must be a positive whole number.',
67
+ 'no-branch':
68
+ 'This checkout is on no branch, so there is no head to bind the pull request to.',
69
+ 'gh-failed':
70
+ 'gh could not read the pull request or the labels, so nothing was written.',
71
+ 'head-mismatch':
72
+ 'That pull request is not open on the current branch. Nothing was written to it.',
73
+ }
74
+
50
75
  export function register(program: Command): void {
51
76
  const labels = program
52
77
  .command('labels')
@@ -99,6 +124,48 @@ export function register(program: Command): void {
99
124
  process.exitCode = await runAudit(paths, opts)
100
125
  })
101
126
 
127
+ labels
128
+ .command('apply')
129
+ .description(
130
+ 'Add the labels this branch earns to its open pull request, adding what is missing and never removing',
131
+ )
132
+ .argument('<number>', "This branch's open pull request")
133
+ .helpOption('-h, --help', 'Show this help message')
134
+ .option('--base <ref>', 'Far side of the range, defaulting to the trunk')
135
+ .option('--root <path>', 'Repository to read, defaulting to the cwd')
136
+ .option('--check', 'Report the labels owed and write nothing')
137
+ .option('--json', 'Add a machine-readable record on stdout')
138
+ .addHelpText(
139
+ 'after',
140
+ [
141
+ '',
142
+ 'Resolves the earned set the way `canon labels audit` does, binds to the',
143
+ 'pull request only when its head is the current branch and it is open,',
144
+ 'and adds each earned label the pull request lacks over REST.',
145
+ '',
146
+ 'It adds and never removes, so a label a person applied by hand stays.',
147
+ 'A label the remote does not carry is reported under missingOnRemote and',
148
+ 'skipped, never created. A project declaring no map earns nothing and',
149
+ 'exits 0.',
150
+ '',
151
+ 'Record: number, earned, present, added, owed, missingOnRemote, uncovered',
152
+ '',
153
+ 'Exit codes:',
154
+ ' 0 nothing is owed after the run',
155
+ ' 1 refused, with the reason on stderr or in the JSON record',
156
+ ' 2 labels are still owed, being any under --check, or a label the',
157
+ ' remote lacks or a failed add otherwise',
158
+ '',
159
+ 'Examples:',
160
+ ' canon labels apply 123 --json',
161
+ ' canon labels apply 123 --check',
162
+ '',
163
+ ].join('\n'),
164
+ )
165
+ .action(async (number: string, opts: ApplyOptions) => {
166
+ process.exitCode = await runApply(number, opts)
167
+ })
168
+
102
169
  labels
103
170
  .command('scan')
104
171
  .description(
@@ -152,19 +219,27 @@ export function register(program: Command): void {
152
219
  'a target project carries none, rather than reaching the network or',
153
220
  'forcing a new dependency.',
154
221
  '',
155
- "It also grades the title alone against standards/pr.md's ## Title",
222
+ "It also grades the title alone against claude/skills/git-pr/references/pr.md's ## Title",
156
223
  'section: the `<type>(<scope>)[!]: <subject>` structure, lowercase casing',
157
224
  'for the type, the scope, and the first subject word, and a 72-',
158
225
  'character length cap. A scan given no title, whether from a review',
159
226
  'or from a bare --body or --body-file invocation, skips this check',
160
227
  'rather than grading an empty string.',
161
228
  '',
229
+ 'It grades the body of an ordinary pull request against the closed',
230
+ 'section list: a level-two heading outside it, or a missing',
231
+ '## Summary or ## Key changes, fails. A review, a release-please body,',
232
+ 'and a scan carrying no title skip this check. It also fails a banned',
233
+ 'character, an em dash or a semicolon, in the title or body of a pull',
234
+ 'request or a review, outside a code span or a link.',
235
+ '',
162
236
  'Exit codes:',
163
- ' 0 none of the five found',
237
+ ' 0 none of the seven found',
164
238
  ' 1 refused, with the reason on stderr or in the JSON record',
165
239
  ' 2 the title or body carries a phase label, a board identifier, a',
166
- ' session link, a title word no dictionary holds, or a title',
167
- " breaking standards/pr.md's format, casing, or length",
240
+ ' session link, a banned character, a heading outside the closed',
241
+ ' list or a required one missing, a title word no dictionary holds, or a title',
242
+ " breaking claude/skills/git-pr/references/pr.md's format, casing, or length",
168
243
  '',
169
244
  'Examples:',
170
245
  ' canon labels scan --event "$GITHUB_EVENT_PATH"',
@@ -265,6 +340,177 @@ async function runAudit(paths: string[], opts: AuditOptions): Promise<number> {
265
340
  return coverage.uncovered.length === 0 ? 0 : 2
266
341
  }
267
342
 
343
+ /** Runs one `gh` call under the stripped environment, null when it failed. */
344
+ async function gh(
345
+ cwd: string,
346
+ args: readonly string[],
347
+ ): Promise<string | null> {
348
+ try {
349
+ const result = await execa('gh', [...args], {
350
+ cwd,
351
+ timeout: GH_TIMEOUT_MS,
352
+ env: gitEnv(),
353
+ extendEnv: false,
354
+ })
355
+ return result.stdout
356
+ } catch {
357
+ return null
358
+ }
359
+ }
360
+
361
+ function nonEmptyLines(text: string): string[] {
362
+ return text.split('\n').filter((line) => line !== '')
363
+ }
364
+
365
+ async function runApply(number: string, opts: ApplyOptions): Promise<number> {
366
+ const root = resolve(opts.root ?? process.cwd())
367
+ const emitJson = opts.json ?? false
368
+ const isCheck = opts.check ?? false
369
+
370
+ intro('canon labels apply')
371
+
372
+ const refuse = (reason: ApplyRefusal | LabelAuditRefusal): number => {
373
+ const message =
374
+ reason in APPLY_REFUSALS
375
+ ? APPLY_REFUSALS[reason as ApplyRefusal]
376
+ : REFUSALS[reason as LabelAuditRefusal]
377
+ logStep('Refused')
378
+ logWarn(message)
379
+ outro()
380
+ if (emitJson) {
381
+ process.stdout.write(`${JSON.stringify({ root, reason, message })}\n`)
382
+ }
383
+ return 1
384
+ }
385
+
386
+ if (!/^[1-9][0-9]*$/.test(number)) return refuse('bad-number')
387
+
388
+ const branch = await $`git -C ${root} symbolic-ref --short -q HEAD`
389
+ .env(gitEnv())
390
+ .quiet()
391
+ .nothrow()
392
+ const head = branch.exitCode === 0 ? branch.text().trim() : ''
393
+ if (head === '') return refuse('no-branch')
394
+
395
+ // Binding runs ahead of the audit, every label read, and every write, under
396
+ // --check and a project with no map as well, so a number this branch does
397
+ // not own costs the run and nothing else.
398
+ const target = await gh(root, [
399
+ 'api',
400
+ `repos/{owner}/{repo}/pulls/${number}`,
401
+ '--jq',
402
+ '"\\(.head.ref) \\(.state)"',
403
+ ])
404
+ if (target === null) return refuse('gh-failed')
405
+ if (target.trim() !== `${head} open`) return refuse('head-mismatch')
406
+
407
+ const report = await auditLabels(root, { base: opts.base })
408
+
409
+ // A project declaring no map earns nothing and is labelled silently, which
410
+ // is an answer and exits clean rather than reading as a break.
411
+ if (report.kind === 'refused' && report.reason === 'no-map') {
412
+ logStep('Labels')
413
+ logInfo(REFUSALS['no-map'])
414
+ outro()
415
+ if (emitJson) {
416
+ process.stdout.write(
417
+ `${JSON.stringify({
418
+ number: Number(number),
419
+ reason: 'no-map',
420
+ earned: [],
421
+ present: [],
422
+ added: [],
423
+ owed: [],
424
+ missingOnRemote: [],
425
+ uncovered: [],
426
+ })}\n`,
427
+ )
428
+ }
429
+ return 0
430
+ }
431
+ if (report.kind === 'refused') return refuse(report.reason)
432
+
433
+ const presentRaw = await gh(root, [
434
+ 'api',
435
+ '--paginate',
436
+ `repos/{owner}/{repo}/issues/${number}/labels`,
437
+ '--jq',
438
+ '.[].name',
439
+ ])
440
+ const remoteRaw = await gh(root, [
441
+ 'api',
442
+ '--paginate',
443
+ 'repos/{owner}/{repo}/labels',
444
+ '--jq',
445
+ '.[].name',
446
+ ])
447
+ if (presentRaw === null || remoteRaw === null) return refuse('gh-failed')
448
+
449
+ const earned = report.coverage.labels
450
+ const present = nonEmptyLines(presentRaw)
451
+ const diff = diffLabels(earned, present, nonEmptyLines(remoteRaw))
452
+
453
+ let added: string[] = []
454
+ let failed: string[] = []
455
+
456
+ if (!isCheck && diff.toAdd.length > 0) {
457
+ const wrote = await gh(root, [
458
+ 'api',
459
+ '-X',
460
+ 'POST',
461
+ `repos/{owner}/{repo}/issues/${number}/labels`,
462
+ ...diff.toAdd.flatMap((label) => ['-f', `labels[]=${label}`]),
463
+ '--silent',
464
+ ])
465
+ if (wrote === null) failed = [...diff.toAdd]
466
+ else added = [...diff.toAdd]
467
+ }
468
+
469
+ logStep('Labels')
470
+ logInfo(earned.length === 0 ? 'this set earns no label' : earned.join(', '))
471
+
472
+ if (isCheck) {
473
+ logStep(diff.owed.length === 0 ? 'Nothing owed' : 'Owed')
474
+ for (const label of diff.owed) logWarn(label)
475
+ } else {
476
+ logStep('Added')
477
+ logInfo(added.length === 0 ? 'no label was added' : added.join(', '))
478
+ }
479
+
480
+ for (const label of diff.missingOnRemote) {
481
+ logWarn(
482
+ `${label} is not on the remote. Create it with: gh label create ${label}`,
483
+ )
484
+ }
485
+ for (const label of failed) logWarn(`Label apply failed: ${label}`)
486
+
487
+ if (report.coverage.uncovered.length > 0) {
488
+ logWarn(
489
+ `${plural(report.coverage.uncovered.length, 'path')} reached by no row of ${MAP_REL}.`,
490
+ )
491
+ }
492
+
493
+ outro()
494
+
495
+ const owed = diff.owed.filter((label) => !added.includes(label))
496
+
497
+ if (emitJson) {
498
+ process.stdout.write(
499
+ `${JSON.stringify({
500
+ number: Number(number),
501
+ earned,
502
+ present,
503
+ added,
504
+ owed,
505
+ missingOnRemote: diff.missingOnRemote,
506
+ uncovered: report.coverage.uncovered,
507
+ })}\n`,
508
+ )
509
+ }
510
+
511
+ return owed.length === 0 ? 0 : 2
512
+ }
513
+
268
514
  async function runScan(opts: ScanOptions): Promise<number> {
269
515
  const emitJson = opts.json ?? false
270
516
 
@@ -363,6 +609,74 @@ async function runScan(opts: ScanOptions): Promise<number> {
363
609
 
364
610
  const titleFormatIssues = titleFormat?.issues ?? []
365
611
 
612
+ // A review carries its own headings, a release-please body opens on a
613
+ // version link, and a scan with no title is a reply or a review body rather
614
+ // than a pull request description, so none of the three has the closed list
615
+ // to grade against.
616
+ const sectionsChecked =
617
+ resolved.source === 'pull-request' &&
618
+ resolved.title !== '' &&
619
+ !isReleasePullRequest(resolved)
620
+ const sections = sectionsChecked
621
+ ? checkSections(resolved.body)
622
+ : { outside: [], missing: [] }
623
+
624
+ logStep(
625
+ !sectionsChecked
626
+ ? 'Sections not checked'
627
+ : sections.outside.length === 0 && sections.missing.length === 0
628
+ ? 'Clean'
629
+ : 'Section issue found',
630
+ )
631
+ if (!sectionsChecked) {
632
+ logInfo(
633
+ 'only an ordinary pull request body is graded against the closed section list',
634
+ )
635
+ } else if (sections.outside.length === 0 && sections.missing.length === 0) {
636
+ logInfo(
637
+ 'every heading is in the closed list and the required pair is there',
638
+ )
639
+ } else {
640
+ if (sections.outside.length > 0) {
641
+ logWarn(
642
+ `${plural(sections.outside.length, 'heading')} outside the closed list in the pull request standard. Rename or drop each.`,
643
+ )
644
+ for (const heading of sections.outside) logWarn(heading)
645
+ }
646
+ if (sections.missing.length > 0) {
647
+ logWarn(
648
+ `${plural(sections.missing.length, 'required heading')} missing from the body.`,
649
+ )
650
+ for (const heading of sections.missing) logWarn(heading)
651
+ }
652
+ }
653
+
654
+ const banFindings = [
655
+ ...scanBans(
656
+ [{ number: 1, text: resolved.title, fenced: false }],
657
+ BAN_SETS,
658
+ ).map((finding) => ({ ...finding, where: 'title' })),
659
+ ...scanBans(bodyLines(resolved.body), BAN_SETS).map((finding) => ({
660
+ ...finding,
661
+ where: 'body',
662
+ })),
663
+ ]
664
+ const bannedCharacters = [
665
+ ...new Set(banFindings.map((finding) => finding.term)),
666
+ ]
667
+
668
+ logStep(banFindings.length === 0 ? 'Clean' : 'Banned character found')
669
+ if (banFindings.length === 0) {
670
+ logInfo('no banned character outside a code span or a link')
671
+ } else {
672
+ logWarn(
673
+ `${plural(banFindings.length, 'banned character')} in the title or body. Rewrite the sentence rather than swapping the character.`,
674
+ )
675
+ for (const finding of banFindings) {
676
+ logWarn(`${finding.where} line ${finding.line}: ${finding.term}`)
677
+ }
678
+ }
679
+
366
680
  logStep(
367
681
  titleFormat === undefined
368
682
  ? 'Title format not checked'
@@ -378,7 +692,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
378
692
  )
379
693
  } else {
380
694
  logWarn(
381
- `${plural(titleFormatIssues.length, 'title format issue')} against standards/pr.md's ## Title section.`,
695
+ `${plural(titleFormatIssues.length, 'title format issue')} against claude/skills/git-pr/references/pr.md's ## Title section.`,
382
696
  )
383
697
  for (const issue of titleFormatIssues) logWarn(TITLE_FORMAT_MESSAGES[issue])
384
698
  }
@@ -396,6 +710,8 @@ async function runScan(opts: ScanOptions): Promise<number> {
396
710
  unspelledWords,
397
711
  spellingChecked,
398
712
  titleFormatIssues,
713
+ sections: { checked: sectionsChecked, ...sections },
714
+ bannedCharacters,
399
715
  })}\n`,
400
716
  )
401
717
  }
@@ -404,7 +720,10 @@ async function runScan(opts: ScanOptions): Promise<number> {
404
720
  result.boardReferences.length === 0 &&
405
721
  result.sessionLinks.length === 0 &&
406
722
  unspelledWords.length === 0 &&
407
- titleFormatIssues.length === 0
723
+ titleFormatIssues.length === 0 &&
724
+ sections.outside.length === 0 &&
725
+ sections.missing.length === 0 &&
726
+ banFindings.length === 0
408
727
  ? 0
409
728
  : 2
410
729
  }
@@ -25,7 +25,11 @@ import {
25
25
  type RuleLayoutPlan,
26
26
  walkFlatRules,
27
27
  } from '@/migrate/rule-layout'
28
- import { SKILL_NAME_MAP, SKILL_NAME_RULES } from '@/migrate/skill-names'
28
+ import {
29
+ SKILL_CITATION_MAP,
30
+ SKILL_NAME_MAP,
31
+ SKILL_NAME_RULES,
32
+ } from '@/migrate/skill-names'
29
33
  import {
30
34
  planSurfaceRootsMove,
31
35
  type SurfaceRootsPlan,
@@ -1250,6 +1254,7 @@ export function register(program: Command): void {
1250
1254
  [
1251
1255
  '',
1252
1256
  `Rewrites ${Object.keys(SKILL_NAME_MAP).length} skill names and moves the folders that carry them.`,
1257
+ `It also carries ${Object.keys(SKILL_CITATION_MAP).length} namespaced-only rows for a skill renamed from a single word.`,
1253
1258
  'It takes no scope. The skill folders it moves are authored in the',
1254
1259
  'toolkit and no target holds a copy of that catalog, so in a target it',
1255
1260
  'rewrites citations of a renamed skill and moves nothing.',