@erclx/canon 5.42.0 → 5.44.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 (195) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +3 -3
  3. package/claude/skills/backlog-triage/references/apply.md +6 -6
  4. package/claude/skills/canon-cli/SKILL.md +4 -4
  5. package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
  8. package/claude/skills/ci-workflow/SKILL.md +12 -2
  9. package/claude/skills/ci-workflow/references/workflows.md +12 -9
  10. package/claude/skills/context-fold/references/context.md +1 -1
  11. package/claude/skills/create-rule/SKILL.md +1 -0
  12. package/claude/skills/create-skill/REQUIREMENT.md +1 -1
  13. package/claude/skills/create-skill/SKILL.md +2 -2
  14. package/claude/skills/create-skill/references/skill-requirement.md +68 -9
  15. package/claude/skills/create-skill/references/skill.md +1 -1
  16. package/claude/skills/decision-escalate/SKILL.md +1 -2
  17. package/claude/skills/design-identity/REQUIREMENT.md +40 -0
  18. package/claude/skills/design-identity/SKILL.md +138 -0
  19. package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
  20. package/claude/skills/design-taste/REQUIREMENT.md +4 -4
  21. package/claude/skills/design-taste/SKILL.md +2 -2
  22. package/claude/skills/design-taste/references/kinds.md +1 -1
  23. package/claude/skills/design-taste/references/preflight.md +1 -1
  24. package/claude/skills/document-health/references/axes.md +2 -1
  25. package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
  26. package/claude/skills/draft-and-pick/SKILL.md +3 -153
  27. package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
  28. package/claude/skills/draft-doc/SKILL.md +4 -76
  29. package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
  30. package/claude/skills/draft-figure/SKILL.md +3 -77
  31. package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
  32. package/claude/skills/draft-identity/SKILL.md +3 -130
  33. package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
  34. package/claude/skills/draft-ready/SKILL.md +3 -128
  35. package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
  36. package/claude/skills/draft-screencast/SKILL.md +3 -62
  37. package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
  38. package/claude/skills/draft-slides/SKILL.md +3 -72
  39. package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
  40. package/claude/skills/draw-figure/SKILL.md +85 -0
  41. package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
  42. package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
  43. package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
  44. package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
  45. package/claude/skills/git-commit/references/commit.md +1 -5
  46. package/claude/skills/git-pr/references/pr.md +1 -0
  47. package/claude/skills/make-deck/REQUIREMENT.md +44 -0
  48. package/claude/skills/make-deck/SKILL.md +80 -0
  49. package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
  50. package/claude/skills/markdown-craft/SKILL.md +2 -2
  51. package/claude/skills/pick-by-eye/REQUIREMENT.md +86 -0
  52. package/claude/skills/pick-by-eye/SKILL.md +161 -0
  53. package/claude/skills/plan-feature/SKILL.md +3 -2
  54. package/claude/skills/plan-feature/references/plan.md +25 -11
  55. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  56. package/claude/skills/plan-intake/SKILL.md +1 -1
  57. package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
  58. package/claude/skills/plan-ready/SKILL.md +136 -0
  59. package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
  60. package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
  61. package/claude/skills/pr-evidence/references/evidence.md +1 -1
  62. package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
  63. package/claude/skills/record-screencast/SKILL.md +2 -2
  64. package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
  65. package/claude/skills/role-orchestrator/SKILL.md +2 -0
  66. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
  67. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -0
  68. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  69. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
  70. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  71. package/claude/skills/role-planner/SKILL.md +5 -4
  72. package/claude/skills/role-worker/REQUIREMENT.md +2 -0
  73. package/claude/skills/role-worker/SKILL.md +35 -2
  74. package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
  75. package/claude/skills/script-screencast/SKILL.md +70 -0
  76. package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
  77. package/claude/skills/search-craft/REQUIREMENT.md +4 -4
  78. package/claude/skills/search-craft/SKILL.md +2 -2
  79. package/claude/skills/target-setup/SKILL.md +1 -0
  80. package/claude/skills/task-board/SKILL.md +11 -9
  81. package/claude/skills/task-board/references/board.md +15 -12
  82. package/claude/skills/teach-workspace/SKILL.md +1 -1
  83. package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
  84. package/claude/skills/ui-checklist/SKILL.md +2 -0
  85. package/claude/skills/write-doc/REQUIREMENT.md +46 -0
  86. package/claude/skills/write-doc/SKILL.md +83 -0
  87. package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
  88. package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
  89. package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
  90. package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +4 -4
  91. package/claude/skills/{draft-doc → write-doc}/references/wiki.md +1 -1
  92. package/docs/agents/audits.md +1 -0
  93. package/docs/agents/markdown-audit.md +4 -2
  94. package/docs/agents/migrate.md +13 -10
  95. package/docs/target/migrations.md +28 -4
  96. package/docs/target/projects.md +1 -1
  97. package/docs/workflow/parallel-features.md +3 -2
  98. package/docs/workflow/skill-map-on-request.md +18 -12
  99. package/docs/workflow/skill-map.md +13 -12
  100. package/docs/workflow/visual-design-workflow.md +2 -2
  101. package/governance/detect.toml +4 -0
  102. package/governance/ledger/framework/astro.md +73 -0
  103. package/governance/ledger/framework/fastapi.md +76 -0
  104. package/governance/ledger/framework/nextjs.md +86 -0
  105. package/governance/ledger/framework/react.md +83 -0
  106. package/governance/ledger/framework/shadcn.md +60 -0
  107. package/governance/ledger/framework/tailwind.md +55 -0
  108. package/governance/rules/canon/context.md +2 -1
  109. package/governance/rules/canon/plan.md +3 -5
  110. package/governance/rules/canon/scratch.md +3 -1
  111. package/governance/rules/canon/task-scope.md +2 -1
  112. package/governance/rules/canon/tasks.md +2 -1
  113. package/governance/rules/canon/worktrees.md +5 -2
  114. package/governance/rules/claude/output.md +7 -4
  115. package/governance/rules/claude/skill.md +6 -4
  116. package/governance/rules/framework/astro.md +12 -14
  117. package/governance/rules/framework/fastapi.md +20 -24
  118. package/governance/rules/framework/nextjs.md +19 -27
  119. package/governance/rules/framework/react.md +22 -33
  120. package/governance/rules/framework/shadcn.md +11 -15
  121. package/governance/rules/framework/tailwind.md +12 -14
  122. package/governance/rules/tooling/ci-workflow.md +6 -39
  123. package/governance/rules/ui/design-reference.md +37 -0
  124. package/governance/rules/writing/markdown.md +3 -2
  125. package/package.json +6 -6
  126. package/src/audits/catalog.ts +42 -0
  127. package/src/claude/cases/authoring.ts +15 -3
  128. package/src/claude/cases/misc.ts +12 -0
  129. package/src/claude/cases/workflow.ts +19 -1
  130. package/src/claude/settings.ts +8 -4
  131. package/src/claude/skills-audit.ts +113 -1
  132. package/src/commands/claude.ts +45 -12
  133. package/src/commands/demo.ts +1 -1
  134. package/src/commands/gov.ts +198 -0
  135. package/src/commands/markdown.ts +1 -1
  136. package/src/commands/migrate.ts +125 -2
  137. package/src/commands/pr.ts +34 -83
  138. package/src/commands/tasks.ts +383 -2
  139. package/src/commands/tooling.ts +3 -3
  140. package/src/commands/worktrees.ts +49 -0
  141. package/src/context/audit.ts +1 -1
  142. package/src/demo/beats.ts +1 -1
  143. package/src/gate/stages.ts +15 -0
  144. package/src/gov/audit.ts +441 -0
  145. package/src/listeners/belongs.ts +21 -0
  146. package/src/listeners/machine.ts +93 -0
  147. package/src/listeners/parse.ts +41 -0
  148. package/src/markdown/scope.ts +45 -35
  149. package/src/migrate/record-layout.ts +1 -1
  150. package/src/migrate/session-defaults.ts +138 -0
  151. package/src/migrate/skill-names.ts +68 -21
  152. package/src/pr/frames.ts +14 -3
  153. package/src/pr/local.ts +4 -69
  154. package/src/records/validate.ts +31 -0
  155. package/src/sync/check.ts +15 -0
  156. package/src/tasks/board.ts +854 -0
  157. package/src/tasks/record.ts +20 -1
  158. package/src/tasks/validate.ts +206 -9
  159. package/src/tooling/stamp.ts +56 -12
  160. package/src/worktrees/stop.ts +92 -0
  161. package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
  162. package/tooling/astro/reference.md +2 -2
  163. package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
  164. package/tooling/base/configs/commitlint.config.js +0 -20
  165. package/tooling/base/reference.md +1 -1
  166. package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
  167. package/tooling/claude/reference.md +13 -7
  168. package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
  169. package/tooling/claude/seeds/.claude/settings.json +8 -0
  170. package/tooling/claude/user/settings.template.json +0 -4
  171. package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
  172. package/tooling/public/configs/.github/dependabot.yml +12 -0
  173. package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
  174. package/tooling/public/manifest.toml +8 -0
  175. package/tooling/public/reference.md +53 -0
  176. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
  177. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
  178. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
  179. package/tooling/public/seeds/.release-please-manifest.json +3 -0
  180. package/tooling/public/seeds/CONTRIBUTING.md +50 -0
  181. package/tooling/public/seeds/LICENSE +21 -0
  182. package/tooling/public/seeds/SECURITY.md +13 -0
  183. package/tooling/public/seeds/release-please-config.json +12 -0
  184. package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
  185. package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
  186. package/tooling/web/configs/.github/workflows/verify.yml +15 -12
  187. /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
  188. /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
  189. /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
  190. /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
  191. /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
  192. /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
  193. /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
  194. /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
  195. /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
@@ -22,12 +22,18 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
22
22
  },
23
23
  {
24
24
  prompt: 'Draw me a diagram of how the pieces of this system connect.',
25
+ expect: 'draw-figure',
26
+ },
27
+ {
28
+ prompt:
29
+ 'I typed draft-figure and it said the figure skill was renamed to draw-figure.', // canon-keep-retired
30
+ // The pointer answers under its old name on purpose. canon-keep-retired
25
31
  expect: 'draft-figure',
26
32
  },
27
33
  {
28
34
  prompt:
29
35
  'This onboarding doc needs a hand-drawn picture of the signup-to-active path, sketch it in and make sure it actually renders correctly.',
30
- expect: 'draft-figure',
36
+ expect: 'draw-figure',
31
37
  },
32
38
  {
33
39
  prompt:
@@ -108,10 +114,22 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
108
114
  },
109
115
  {
110
116
  prompt: 'Draft me a script with beats for a screencast recording.',
117
+ expect: 'script-screencast',
118
+ },
119
+ {
120
+ prompt:
121
+ 'I typed draft-screencast and it said the screencast skill was renamed to script-screencast.', // canon-keep-retired
122
+ // The pointer answers under its old name on purpose. canon-keep-retired
111
123
  expect: 'draft-screencast',
112
124
  },
113
125
  {
114
126
  prompt: 'Turn this topic into a slide deck I can render.',
127
+ expect: 'make-deck',
128
+ },
129
+ {
130
+ prompt:
131
+ 'I typed draft-slides and it said the slides skill was renamed to make-deck.', // canon-keep-retired
132
+ // The pointer answers under its old name on purpose. canon-keep-retired
115
133
  expect: 'draft-slides',
116
134
  },
117
135
  // Rot across documents nobody changed is what this reads, rather than a
@@ -5,6 +5,10 @@ import { dirname } from 'node:path'
5
5
  const DEFAULT_INDENT = ' '
6
6
  const INDENT = /^([ \t]+)\S/m
7
7
 
8
+ // The pair setup once wrote to suppress Claude's attribution. Only this exact
9
+ // value is retracted, since anything else is a person's own choice.
10
+ const RETIRED_ATTRIBUTION = { commit: '', pr: '' }
11
+
8
12
  export type SettingsRecord = Record<string, unknown>
9
13
 
10
14
  export interface SettingsKey {
@@ -107,10 +111,10 @@ export function planSettings(
107
111
  }
108
112
  keys.push({ label: 'statusLine', changed: statusLineChanged })
109
113
 
110
- const attribution = template.attribution
111
- const attributionChanged = !equals(next.attribution, attribution)
112
- if (attributionChanged) next.attribution = attribution
113
- keys.push({ label: 'attribution', changed: attributionChanged })
114
+ if (equals(next.attribution, RETIRED_ATTRIBUTION)) {
115
+ delete next.attribution
116
+ keys.push({ label: 'attribution', changed: true })
117
+ }
114
118
 
115
119
  const permissions = asRecord(next.permissions)
116
120
  const merged: SettingsRecord = { ...permissions }
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { isFamilyKey } from '@/claude/skills-families'
4
4
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
5
- import { bodyLines, maskDisplayed } from '@/markdown/scan'
5
+ import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
6
6
 
7
7
  /** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
8
8
  export const EXIT_MISSING_REQUIREMENT = 2
@@ -13,6 +13,19 @@ export const DESCRIPTION_LIMIT = 1024
13
13
  /** The two headings the requirement template declares, matched at any level. */
14
14
  export const REQUIREMENT_SECTIONS: readonly string[] = ['Gap', 'Must']
15
15
 
16
+ /**
17
+ * The caps `claude/skills/create-skill/references/skill-requirement.md` states.
18
+ * Words count body tokens carrying a letter or digit, so a heading or bullet
19
+ * marker costs nothing and frontmatter is left out.
20
+ */
21
+ export const REQUIREMENT_LIMITS = { bullets: 5, words: 400 } as const
22
+
23
+ /** The section the standard retired into Must not, matched at any level. */
24
+ export const RETIRED_REQUIREMENT_SECTION = 'Guards'
25
+
26
+ /** The section that states today's failures, where history is reported. */
27
+ const GAP_SECTION = 'Gap'
28
+
16
29
  /** The one tree that ships to a target, and the only one the practice list names. */
17
30
  export const SHIPPED_CORPUS = join('claude', 'skills')
18
31
 
@@ -100,6 +113,13 @@ const SECTION = /^##\s+(.+?)\s*$/
100
113
  */
101
114
  const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
102
115
 
116
+ /** An issue or pull request number, which a heading's `#` never matches. */
117
+ const ISSUE_NUMBER = /(?<![\w&/#])#\d+\b/g
118
+
119
+ const TOP_LEVEL_BULLET = /^(?:[-*+]|\d+[.)])\s/
120
+
121
+ const WORD = /\S*[\p{L}\p{N}]\S*/gu
122
+
103
123
  export interface SkillFinding {
104
124
  readonly rel: string
105
125
  readonly detail: string
@@ -126,6 +146,7 @@ export interface SkillsAudit {
126
146
  readonly nameMismatch: readonly SkillFinding[]
127
147
  readonly longDescription: readonly SkillFinding[]
128
148
  readonly requirementSections: readonly SkillFinding[]
149
+ readonly requirementLimits: readonly SkillFinding[]
129
150
  readonly datedProvenance: readonly SkillFinding[]
130
151
  readonly practiceShape: readonly SkillFinding[]
131
152
  readonly referenceContents: readonly SkillFinding[]
@@ -142,6 +163,7 @@ interface SkillSource {
142
163
  readonly family: string | undefined
143
164
  /** Undefined when the folder carries no `REQUIREMENT.md` at all. */
144
165
  readonly requirementHeadings: readonly string[] | undefined
166
+ readonly requirement: string | undefined
145
167
  /** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
146
168
  readonly sections: readonly string[]
147
169
  readonly hasLedger: boolean
@@ -198,6 +220,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
198
220
  nameMismatch: sources.flatMap(nameFindings),
199
221
  longDescription: sources.flatMap(lengthFindings),
200
222
  requirementSections: sources.flatMap(sectionFindings),
223
+ requirementLimits: sources.flatMap(limitFindings),
201
224
  datedProvenance: sources.flatMap((source) =>
202
225
  source.bodies.flatMap(dateFindings),
203
226
  ),
@@ -269,6 +292,7 @@ async function readSkill(
269
292
  family: declaredFamily(fields?.fields.metadata),
270
293
  requirementHeadings:
271
294
  requirement === undefined ? undefined : headings(requirement),
295
+ requirement,
272
296
  sections: sectionsOf(body),
273
297
  hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
274
298
  bodies: [
@@ -349,6 +373,94 @@ function sectionFindings(source: SkillSource): SkillFinding[] {
349
373
  ]
350
374
  }
351
375
 
376
+ /**
377
+ * Reports every cap the standard states, each as its own finding so a file
378
+ * breaching two reads as two edits. Silent on an absent requirement for the
379
+ * reason `sectionFindings` is.
380
+ */
381
+ function limitFindings(source: SkillSource): SkillFinding[] {
382
+ const { requirement, requirementHeadings } = source
383
+ if (requirement === undefined || requirementHeadings === undefined) return []
384
+
385
+ const rel = join(source.rel, 'REQUIREMENT.md')
386
+ const lines = bodyLines(requirement)
387
+ const details = [
388
+ ...bulletDetails(lines),
389
+ ...wordDetails(lines),
390
+ ...gapHistoryDetails(lines),
391
+ ...(requirementHeadings.includes(RETIRED_REQUIREMENT_SECTION)
392
+ ? [`## ${RETIRED_REQUIREMENT_SECTION}: retired, fold it into Must not`]
393
+ : []),
394
+ ]
395
+
396
+ return details.map((detail) => ({ rel, detail }))
397
+ }
398
+
399
+ /**
400
+ * Counts top-level bullets per H2, since the template holds one list per
401
+ * section. A nested bullet elaborates its parent rather than adding to it.
402
+ */
403
+ function bulletDetails(lines: readonly BodyLine[]): string[] {
404
+ const counts = new Map<string, number>()
405
+ let section: string | undefined
406
+
407
+ for (const line of lines) {
408
+ if (line.fenced) continue
409
+ const heading = SECTION.exec(line.text)?.[1]
410
+ if (heading !== undefined) {
411
+ section = heading
412
+ continue
413
+ }
414
+ if (section === undefined || !TOP_LEVEL_BULLET.test(line.text)) continue
415
+ counts.set(section, (counts.get(section) ?? 0) + 1)
416
+ }
417
+
418
+ return [...counts]
419
+ .filter(([, count]) => count > REQUIREMENT_LIMITS.bullets)
420
+ .map(
421
+ ([name, count]) =>
422
+ `## ${name}: ${count} bullets, cap ${REQUIREMENT_LIMITS.bullets}`,
423
+ )
424
+ }
425
+
426
+ function wordDetails(lines: readonly BodyLine[]): string[] {
427
+ const words = lines.reduce(
428
+ (total, line) => total + (line.text.match(WORD)?.length ?? 0),
429
+ 0,
430
+ )
431
+ if (words <= REQUIREMENT_LIMITS.words) return []
432
+ return [`${words} words, cap ${REQUIREMENT_LIMITS.words}`]
433
+ }
434
+
435
+ /**
436
+ * Gap states what fails today, so a date or a number naming the incident that
437
+ * earned a line is history git already holds. A code span is quoted data, such
438
+ * as a sample filename, and is masked before matching.
439
+ */
440
+ function gapHistoryDetails(lines: readonly BodyLine[]): string[] {
441
+ const details: string[] = []
442
+ let inGap = false
443
+
444
+ for (const line of lines) {
445
+ if (line.fenced) continue
446
+ const heading = HEADING.exec(line.text)?.[1]
447
+ if (heading !== undefined) {
448
+ inGap = heading === GAP_SECTION
449
+ continue
450
+ }
451
+ if (!inGap) continue
452
+ const masked = maskDisplayed(line.text)
453
+ for (const match of [
454
+ ...masked.matchAll(ISO_DATE),
455
+ ...masked.matchAll(ISSUE_NUMBER),
456
+ ]) {
457
+ details.push(`line ${line.number}: ${match[0]} in ${GAP_SECTION}`)
458
+ }
459
+ }
460
+
461
+ return details
462
+ }
463
+
352
464
  /**
353
465
  * A listed folder the shipped corpus does not hold, such as a renamed skill or
354
466
  * a misspelled append, would otherwise read as a pass. Silent where the
@@ -32,7 +32,9 @@ import {
32
32
  PRACTICE_LEDGER,
33
33
  PRACTICE_SECTIONS,
34
34
  PRACTICE_SKILLS,
35
+ REQUIREMENT_LIMITS,
35
36
  REQUIREMENT_SECTIONS,
37
+ RETIRED_REQUIREMENT_SECTION,
36
38
  SHIPPED_CORPUS,
37
39
  type SkillFinding,
38
40
  type SkillsAudit,
@@ -69,6 +71,7 @@ import {
69
71
  serializeSettings,
70
72
  writeSettings,
71
73
  } from '@/claude/settings'
74
+ import { readSessionDefaults } from '@/migrate/session-defaults'
72
75
  import { copyPreservingMode } from '@/sync/copy'
73
76
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
74
77
  import { recordDir } from '@/roots/record'
@@ -255,7 +258,7 @@ export function register(program: Command): void {
255
258
 
256
259
  claude
257
260
  .command('setup')
258
- .description('One-shot user-level config (statusline, attribution)')
261
+ .description('One-shot user-level config (statusline, permissions)')
259
262
  .argument('[dest]', 'User config directory', join(homedir(), '.claude'))
260
263
  .helpOption('-h, --help', 'Show this help message')
261
264
  .action(async (dest: string) => {
@@ -440,7 +443,7 @@ export function register(program: Command): void {
440
443
  'Refuses when neither claude/skills/ nor .claude/skills/ resolves.',
441
444
  '',
442
445
  'Only a missing REQUIREMENT.md sets a failing exit code. Name, description,',
443
- 'folder, and requirement-section findings are advisory.',
446
+ 'folder, and requirement shape and limit findings are advisory.',
444
447
  '',
445
448
  'Examples:',
446
449
  ' canon claude skills audit',
@@ -663,6 +666,15 @@ async function runSync(target: string): Promise<number> {
663
666
  else logWarn(`${name}/ missing. Run \`canon claude init\``)
664
667
  }
665
668
 
669
+ // Sync never rewrites an existing settings file, so a target seeded before
670
+ // the session defaults learns of them only here.
671
+ const defaults = await readSessionDefaults(resolved, PROJECT_ROOT)
672
+ if (defaults.kind === 'planned' && defaults.plan.predates) {
673
+ logWarn(
674
+ 'settings.json lacks the session defaults. Run `canon migrate session-defaults --write`',
675
+ )
676
+ }
677
+
666
678
  logStep('Scanning .gitignore')
667
679
  const chain = claudeChain(PROJECT_ROOT)
668
680
  const pruned = await pruneGitignore(chain, resolved)
@@ -1493,6 +1505,7 @@ async function runSkillsAudit(
1493
1505
  readme: report.readme,
1494
1506
  folderName: report.folderName,
1495
1507
  requirementSections: report.requirementSections,
1508
+ requirementLimits: report.requirementLimits,
1496
1509
  datedProvenance: report.datedProvenance,
1497
1510
  practiceShape: report.practiceShape,
1498
1511
  referenceContents: report.referenceContents,
@@ -1501,6 +1514,7 @@ async function runSkillsAudit(
1501
1514
  checkpoints: {
1502
1515
  descriptionLimit: DESCRIPTION_LIMIT,
1503
1516
  requirementSections: REQUIREMENT_SECTIONS,
1517
+ requirementLimits: REQUIREMENT_LIMITS,
1504
1518
  corpora: CORPORA,
1505
1519
  practiceSkills: PRACTICE_SKILLS,
1506
1520
  ledgerSkills: LEDGER_SKILLS,
@@ -1661,21 +1675,40 @@ function reportRequirementShape(report: SkillsAudit): void {
1661
1675
  'A folder carrying no requirement is reported above rather than counted twice here.',
1662
1676
  )
1663
1677
 
1664
- if (report.requirementSections.length === 0) {
1665
- logInfo('Every requirement declares both.')
1678
+ logInfo(
1679
+ `Each list holds at most ${REQUIREMENT_LIMITS.bullets} top-level bullets, the body at most ${REQUIREMENT_LIMITS.words} words, and Gap no date or issue number outside a code span.`,
1680
+ )
1681
+ logInfo(
1682
+ `A ## ${RETIRED_REQUIREMENT_SECTION} section is retired into Must not. These limits report and never fail the run.`,
1683
+ )
1684
+
1685
+ if (
1686
+ report.requirementSections.length === 0 &&
1687
+ report.requirementLimits.length === 0
1688
+ ) {
1689
+ logInfo('Every requirement declares both and holds to the limits.')
1666
1690
  return
1667
1691
  }
1668
1692
 
1669
- logWarn(
1670
- `${plural(report.requirementSections.length, 'requirement')} short a declared section`,
1671
- )
1672
- reportFindings(report.requirementSections)
1693
+ if (report.requirementSections.length > 0) {
1694
+ logWarn(
1695
+ `${plural(report.requirementSections.length, 'requirement')} short a declared section`,
1696
+ )
1697
+ reportFindings(report.requirementSections)
1698
+ }
1699
+
1700
+ if (report.requirementLimits.length > 0) {
1701
+ logWarn(
1702
+ `${plural(report.requirementLimits.length, 'finding')} past a requirement limit`,
1703
+ )
1704
+ reportFindings(report.requirementLimits)
1705
+ }
1673
1706
  }
1674
1707
 
1675
1708
  /**
1676
- * Names where the date belongs, since the finding is a date to move rather
1677
- * than one to delete, and reports without failing the run for the same reason
1678
- * the frontmatter measures do.
1709
+ * Names where the date belongs, since a reader deleting it should know git
1710
+ * still holds it, and reports without failing the run for the same reason the
1711
+ * frontmatter measures do.
1679
1712
  */
1680
1713
  function reportProvenance(report: SkillsAudit): void {
1681
1714
  logStep('Provenance')
@@ -1683,7 +1716,7 @@ function reportProvenance(report: SkillsAudit): void {
1683
1716
  'A SKILL.md and its references state the current rule, with no ISO date outside a fence or code span.',
1684
1717
  )
1685
1718
  logInfo(
1686
- 'The incident and date that earned a rule go in REQUIREMENT.md under Gap, or stay in git.',
1719
+ 'The incident and date that earned a rule stay in git, where the log of the file finds them.',
1687
1720
  )
1688
1721
 
1689
1722
  if (report.datedProvenance.length === 0) {
@@ -62,7 +62,7 @@ export function register(program: Command): void {
62
62
  demo
63
63
  .command('compile')
64
64
  .description('Turn a screencast draft into a plan a run can drive')
65
- .argument('<draft>', 'Screencast draft written by draft-screencast')
65
+ .argument('<draft>', 'Screencast draft written by script-screencast')
66
66
  .helpOption('-h, --help', 'Show this help message')
67
67
  .option('-o, --out <dir>', 'Directory the plan is written to', DEFAULT_OUT)
68
68
  .option('-s, --slug <slug>', 'Plan name, defaulting to the draft filename')
@@ -32,6 +32,16 @@ import {
32
32
  SEED_REL,
33
33
  SHIPPED_SKILLS_REL,
34
34
  } from '@/gov/restated'
35
+ import {
36
+ auditExit,
37
+ hasFinding,
38
+ PRACTICE_FOLDERS,
39
+ readRuleAudit,
40
+ RULE_GATED_FOLDERS,
41
+ RULE_LIMITS,
42
+ type RuleAuditReport,
43
+ type RuleReading,
44
+ } from '@/gov/audit'
35
45
  import {
36
46
  CITATION_MARKER,
37
47
  type CitationReport,
@@ -123,6 +133,12 @@ interface CountsOptions {
123
133
  readonly json?: boolean
124
134
  }
125
135
 
136
+ interface AuditOptions {
137
+ readonly root?: string
138
+ readonly json?: boolean
139
+ readonly gate?: boolean
140
+ }
141
+
126
142
  /** What a reader does about each way the sweep produced no reading. */
127
143
  const RESTATED_REFUSALS: Record<RestatedRefusal, string> = {
128
144
  'no-instructions': `No ${INSTRUCTIONS_REL} here, or it carries no bullet, so there is no instruction corpus to sweep.`,
@@ -573,6 +589,188 @@ export function register(program: Command): void {
573
589
  .action((opts: RestatedOptions) => {
574
590
  process.exitCode = runRestated(opts)
575
591
  })
592
+
593
+ gov
594
+ .command('audit')
595
+ .description(
596
+ 'Report every rule past a shape limit, and in this repository every outside-practice rule missing a source ledger',
597
+ )
598
+ .helpOption('-h, --help', 'Show this help message')
599
+ .argument(
600
+ '[paths...]',
601
+ 'Rule files or folders of rules to read, defaulting to the corpus',
602
+ )
603
+ .option('--root <path>', 'Tree to read, defaulting to the cwd')
604
+ .option('--json', 'Add a machine-readable record on stdout')
605
+ .option(
606
+ '--gate',
607
+ 'Set 2 only for a finding in a gated folder, which the push stage reads',
608
+ )
609
+ .addHelpText(
610
+ 'after',
611
+ [
612
+ '',
613
+ 'Limits, each a finding one past its number:',
614
+ ` words ${RULE_LIMITS.words} words in the body`,
615
+ ` bullets ${RULE_LIMITS.bullets} bullets in the file`,
616
+ ` h2 ${RULE_LIMITS.h2} H2 headings`,
617
+ ` perH2 ${RULE_LIMITS.perH2} bullets under one H2, the text above the first H2 a group`,
618
+ ` chars ${RULE_LIMITS.chars} characters in one bullet, continuation lines folded in`,
619
+ ` sentences ${RULE_LIMITS.sentences} sentences in one bullet`,
620
+ ' emphasis any CRITICAL, IMPORTANT, MUST, ALWAYS, NEVER, or "you must"',
621
+ ' outside a code span',
622
+ '',
623
+ 'Corpus:',
624
+ ' A tree holding governance/rules/ reads it and internal/rules/, and',
625
+ ` asks a ledger of each rule under ${PRACTICE_FOLDERS.join(', ')}`,
626
+ ' at governance/ledger/<folder>/<slug>.md, one table row per bullet',
627
+ ' under a heading of the same name.',
628
+ ' Any other tree reads .claude/rules/project/ alone and asks no ledger.',
629
+ ' .claude/rules/canon/ is fixed at the toolkit source and never read',
630
+ ' unless named. A named path resolves against --root and must sit',
631
+ ' inside it.',
632
+ '',
633
+ `Gated folders: ${RULE_GATED_FOLDERS.length === 0 ? 'none yet' : RULE_GATED_FOLDERS.join(', ')}`,
634
+ '',
635
+ 'Exit codes:',
636
+ ' 0 no finding, or under --gate no finding in a gated folder',
637
+ ' 1 refused, with the reason on stderr or in the JSON record',
638
+ ' 2 at least one finding, or under --gate one in a gated folder',
639
+ '',
640
+ 'Reasons:',
641
+ ' no-rules the tree holds no rule corpus to read, or a named folder holds no rule',
642
+ ' no-path a named path does not exist',
643
+ ' outside-root a named path sits outside --root',
644
+ '',
645
+ 'Examples:',
646
+ ' canon gov audit',
647
+ ' canon gov audit .claude/rules/project/api.md',
648
+ ' canon gov audit --json',
649
+ '',
650
+ ].join('\n'),
651
+ )
652
+ .action(async (paths: string[], opts: AuditOptions) => {
653
+ process.exitCode = await runAudit(paths, opts)
654
+ })
655
+ }
656
+
657
+ /**
658
+ * Reports by default and gates only under `--gate`, and there only for the
659
+ * folders `RULE_GATED_FOLDERS` lists. The corpus starts far from zero, so the
660
+ * push stage fails on a folder once its rewrite has brought it there.
661
+ */
662
+ async function runAudit(
663
+ paths: readonly string[],
664
+ opts: AuditOptions,
665
+ ): Promise<number> {
666
+ const root = resolve(opts.root ?? process.cwd())
667
+ const report = await readRuleAudit(root, paths)
668
+ const emitJson = opts.json ?? false
669
+
670
+ if (report.kind === 'unreadable') {
671
+ intro('canon gov audit')
672
+ logStep('Refused')
673
+ logError(report.message)
674
+ outro()
675
+
676
+ if (emitJson) {
677
+ process.stdout.write(
678
+ `${JSON.stringify({ ok: false, reason: report.reason, message: report.message })}\n`,
679
+ )
680
+ }
681
+
682
+ return 1
683
+ }
684
+
685
+ reportAudit(report, root, paths.length > 0)
686
+
687
+ if (emitJson) {
688
+ process.stdout.write(
689
+ `${JSON.stringify({
690
+ ok: true,
691
+ root,
692
+ limits: RULE_LIMITS,
693
+ gated: RULE_GATED_FOLDERS,
694
+ ...report,
695
+ })}\n`,
696
+ )
697
+ }
698
+
699
+ return auditExit(report, {
700
+ gate: opts.gate ?? false,
701
+ gated: RULE_GATED_FOLDERS,
702
+ })
703
+ }
704
+
705
+ function describeLedger(reading: RuleReading): string[] {
706
+ const { ledger } = reading
707
+ if (ledger.status === 'missing') {
708
+ return [`${reading.file}: no ledger at ${ledger.path}`]
709
+ }
710
+ if (ledger.status !== 'present') return []
711
+ return ledger.mismatches.map(
712
+ (mismatch) =>
713
+ `${ledger.path}: ${mismatch.heading ?? '(above the first H2)'} has ${mismatch.rows} row(s) for ${mismatch.bullets} bullet(s)`,
714
+ )
715
+ }
716
+
717
+ function reportAudit(
718
+ report: Extract<RuleAuditReport, { kind: 'measured' }>,
719
+ root: string,
720
+ named: boolean,
721
+ ): void {
722
+ intro('canon gov audit')
723
+
724
+ const shaped = report.rules.filter((reading) => reading.findings.length > 0)
725
+ const ledgers = report.rules.flatMap(describeLedger)
726
+
727
+ logStep('Corpus')
728
+ logInfo(
729
+ named
730
+ ? `${report.rules.length} named rule(s) in ${root}`
731
+ : `${report.rules.length} rule(s) under ${report.corpus.join(' and ')} in ${root}`,
732
+ )
733
+
734
+ logStep('Shape')
735
+ if (shaped.length === 0) {
736
+ logInfo('every rule is within the six limits and carries no emphasis word')
737
+ } else {
738
+ for (const reading of shaped) {
739
+ for (const finding of reading.findings) {
740
+ const at =
741
+ finding.line === undefined
742
+ ? reading.file
743
+ : `${reading.file}:${finding.line}`
744
+ logWarn(
745
+ finding.limit === 'emphasis'
746
+ ? `${at}: emphasis word`
747
+ : `${at}: ${finding.limit} ${finding.count} past ${finding.max}`,
748
+ )
749
+ }
750
+ }
751
+ }
752
+
753
+ logStep('Ledgers')
754
+ if (report.scope === 'target') {
755
+ logInfo('not read, since ledgers live in the toolkit and never ship')
756
+ } else if (ledgers.length === 0) {
757
+ logInfo('every outside-practice rule has a ledger row per bullet')
758
+ } else {
759
+ for (const line of ledgers) logWarn(line)
760
+ }
761
+
762
+ const failing = report.rules.filter(hasFinding).length
763
+ logStep('Summary')
764
+ logInfo(
765
+ `${failing} of ${report.rules.length} rule(s) carry a finding: ${shaped.length} with a shape finding, ${ledgers.length} ledger finding(s)`,
766
+ )
767
+ logInfo(
768
+ RULE_GATED_FOLDERS.length === 0
769
+ ? 'no folder is gated yet, so --gate passes every finding'
770
+ : `gated: ${RULE_GATED_FOLDERS.join(', ')}`,
771
+ )
772
+
773
+ outro()
576
774
  }
577
775
 
578
776
  /**
@@ -486,7 +486,7 @@ function reportDensity(reports: readonly FileReport[], gates: boolean): void {
486
486
  `Paragraphs past ${CHECKPOINTS.paragraph} characters, more than 2 paragraphs in a row, bullets past ${CHECKPOINTS.bullet} characters, more than 5 plain bullets in a row, a heading past H3, and an H3 opening with neither a paragraph nor a table.`,
487
487
  )
488
488
  logInfo(
489
- 'Measured on context entries, docs, skill bodies and references, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. Every other path takes no density finding.',
489
+ 'Measured on context entries, docs, skill bodies, references, and requirements, tooling seeds and stack references, the wiki, README.md, and CONTRIBUTING.md. Every other path takes no density finding.',
490
490
  )
491
491
  logInfo(
492
492
  gates