@erclx/canon 5.41.0 → 5.43.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 (220) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/references/adopted.md +34 -14
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +24 -4
  5. package/claude/skills/auto-ship/references/draft-mark.md +32 -6
  6. package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
  7. package/claude/skills/auto-ship/references/tier-failures.md +31 -10
  8. package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
  9. package/claude/skills/auto-ship/references/worktree-name.md +9 -3
  10. package/claude/skills/build-in-slices/references/adopted.md +35 -21
  11. package/claude/skills/canvas-board/references/canvas.md +17 -6
  12. package/claude/skills/canvas-board/references/design.md +26 -7
  13. package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
  14. package/claude/skills/ci-workflow/SKILL.md +12 -2
  15. package/claude/skills/ci-workflow/references/workflows.md +12 -9
  16. package/claude/skills/code-craft/references/adopted.md +58 -20
  17. package/claude/skills/codebase-layout/references/adopted.md +49 -16
  18. package/claude/skills/codebase-layout/references/frontend.md +11 -4
  19. package/claude/skills/codebase-layout/references/tests.md +4 -2
  20. package/claude/skills/context-fold/references/classify.md +10 -15
  21. package/claude/skills/context-fold/references/completion-report.md +11 -9
  22. package/claude/skills/context-fold/references/context-refresh.md +26 -9
  23. package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
  24. package/claude/skills/create-rule/REQUIREMENT.md +5 -0
  25. package/claude/skills/create-rule/SKILL.md +10 -1
  26. package/claude/skills/create-rule/references/checklist.md +26 -0
  27. package/claude/skills/create-rule/references/rule.md +98 -20
  28. package/claude/skills/create-skill/SKILL.md +1 -0
  29. package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
  30. package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
  31. package/claude/skills/create-skill/references/skill-paths.md +7 -4
  32. package/claude/skills/create-skill/references/skill-practice.md +6 -3
  33. package/claude/skills/create-skill/references/skill-requirement.md +4 -2
  34. package/claude/skills/create-skill/references/skill.md +67 -44
  35. package/claude/skills/decision-escalate/SKILL.md +1 -2
  36. package/claude/skills/deploy-app/references/vercel.md +2 -1
  37. package/claude/skills/deprecation-migration/references/adopted.md +44 -25
  38. package/claude/skills/design-taste/references/adopted.md +9 -1
  39. package/claude/skills/design-taste/references/craft.md +3 -1
  40. package/claude/skills/design-taste/references/kinds.md +8 -7
  41. package/claude/skills/design-taste/references/preflight.md +12 -0
  42. package/claude/skills/design-taste/references/systems.md +7 -6
  43. package/claude/skills/design-taste/references/tells.md +24 -25
  44. package/claude/skills/design-taste/references/vocabulary.md +7 -5
  45. package/claude/skills/document-health/references/axes.md +55 -30
  46. package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
  47. package/claude/skills/draft-doc/references/context.md +4 -3
  48. package/claude/skills/draft-doc/references/docs.md +6 -3
  49. package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
  50. package/claude/skills/draft-doc/references/readme/application.md +8 -2
  51. package/claude/skills/draft-doc/references/readme/cli.md +5 -1
  52. package/claude/skills/draft-doc/references/readme/library.md +4 -0
  53. package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
  54. package/claude/skills/draft-doc/references/readme.md +30 -13
  55. package/claude/skills/draft-doc/references/requirements.md +17 -4
  56. package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
  57. package/claude/skills/draft-doc/references/wiki.md +1 -1
  58. package/claude/skills/draft-figure/references/architecture-set.md +7 -3
  59. package/claude/skills/draft-figure/references/figures.md +8 -6
  60. package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
  61. package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
  62. package/claude/skills/draft-figure/references/mermaid.md +10 -5
  63. package/claude/skills/draft-ready/references/ready.md +22 -15
  64. package/claude/skills/git-branch/references/branch.md +11 -1
  65. package/claude/skills/git-branch/references/slug.md +17 -5
  66. package/claude/skills/git-commit/references/commit.md +9 -5
  67. package/claude/skills/git-commit/references/versioning.md +23 -17
  68. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  69. package/claude/skills/git-followup/SKILL.md +5 -6
  70. package/claude/skills/git-issue/references/issue.md +29 -16
  71. package/claude/skills/git-pr/REQUIREMENT.md +1 -5
  72. package/claude/skills/git-pr/SKILL.md +11 -22
  73. package/claude/skills/git-pr/references/final-command.md +21 -8
  74. package/claude/skills/git-pr/references/labels.md +45 -28
  75. package/claude/skills/git-pr/references/pr.md +29 -9
  76. package/claude/skills/git-pr/references/publish.md +42 -28
  77. package/claude/skills/git-pr/references/task-number.md +20 -8
  78. package/claude/skills/git-ship/SKILL.md +1 -1
  79. package/claude/skills/plan-feature/SKILL.md +1 -1
  80. package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
  81. package/claude/skills/plan-feature/references/plan.md +58 -21
  82. package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
  83. package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
  84. package/claude/skills/plan-intake/references/intake.md +29 -24
  85. package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
  86. package/claude/skills/pr-evidence/SKILL.md +53 -0
  87. package/claude/skills/pr-evidence/references/evidence.md +150 -0
  88. package/claude/skills/review-address/references/post-review-findings.md +10 -3
  89. package/claude/skills/review-address/references/rebase-conflicts.md +15 -7
  90. package/claude/skills/review-address/references/rest-reads.md +3 -1
  91. package/claude/skills/review-craft/references/adopted.md +35 -18
  92. package/claude/skills/review-craft/references/security.md +42 -7
  93. package/claude/skills/review-craft/references/ui.md +23 -8
  94. package/claude/skills/review-pr/SKILL.md +6 -6
  95. package/claude/skills/review-pr/references/body-variants.md +29 -9
  96. package/claude/skills/review-pr/references/close-out.md +20 -5
  97. package/claude/skills/review-pr/references/key-changes.md +27 -3
  98. package/claude/skills/review-pr/references/post.md +83 -31
  99. package/claude/skills/review-pr/references/scope.md +54 -19
  100. package/claude/skills/review-ui/references/close-out.md +7 -5
  101. package/claude/skills/review-ui/references/driving.md +24 -4
  102. package/claude/skills/review-ui/references/post.md +43 -15
  103. package/claude/skills/role-orchestrator/SKILL.md +2 -0
  104. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
  105. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +80 -40
  106. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
  107. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
  108. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
  109. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
  110. package/claude/skills/role-orchestrator/references/orchestrator-output.md +16 -4
  111. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
  112. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
  113. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
  114. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
  115. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
  116. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
  117. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
  118. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
  119. package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
  120. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  121. package/claude/skills/role-planner/SKILL.md +3 -3
  122. package/claude/skills/role-worker/REQUIREMENT.md +3 -0
  123. package/claude/skills/role-worker/SKILL.md +34 -2
  124. package/claude/skills/search-craft/REQUIREMENT.md +1 -1
  125. package/claude/skills/search-craft/SKILL.md +2 -2
  126. package/claude/skills/search-craft/references/adopted.md +18 -6
  127. package/claude/skills/session-map/references/session.md +26 -7
  128. package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
  129. package/claude/skills/systematic-debugging/references/adopted.md +30 -16
  130. package/claude/skills/target-check/references/domains.md +39 -11
  131. package/claude/skills/target-setup/references/detect.md +80 -65
  132. package/claude/skills/target-setup/references/indexes.md +8 -6
  133. package/claude/skills/task-board/references/board.md +86 -30
  134. package/claude/skills/task-board/references/decline.md +29 -13
  135. package/claude/skills/task-board/references/tasks-decline.md +12 -5
  136. package/claude/skills/task-board/references/tasks.md +65 -33
  137. package/claude/skills/task-complete/references/archive.md +41 -17
  138. package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
  139. package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
  140. package/claude/skills/teach-workspace/references/glossary.md +6 -2
  141. package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
  142. package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
  143. package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
  144. package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
  145. package/claude/skills/teach-workspace/references/promotion.md +31 -8
  146. package/claude/skills/teach-workspace/references/revisit.md +8 -2
  147. package/claude/skills/teach-workspace/references/teach.md +17 -9
  148. package/claude/skills/test-craft/references/adopted.md +48 -14
  149. package/claude/skills/test-craft/references/component.md +7 -4
  150. package/claude/skills/test-craft/references/e2e.md +25 -7
  151. package/claude/skills/test-craft/references/unit.md +3 -1
  152. package/claude/skills/test-first/references/adopted.md +12 -4
  153. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
  154. package/claude/skills/ui-checklist/SKILL.md +1 -1
  155. package/claude/skills/video-craft/references/adopted.md +62 -18
  156. package/claude/skills/write-human/references/adopted.md +21 -8
  157. package/docs/agents/audits.md +1 -0
  158. package/docs/agents/migrate.md +11 -8
  159. package/docs/target/migrations.md +11 -0
  160. package/docs/workflow/parallel-features.md +1 -1
  161. package/docs/workflow/skill-map.md +1 -0
  162. package/governance/detect.toml +4 -0
  163. package/governance/rules/canon/context.md +2 -1
  164. package/governance/rules/canon/plan.md +3 -5
  165. package/governance/rules/canon/scratch.md +3 -1
  166. package/governance/rules/canon/task-scope.md +2 -1
  167. package/governance/rules/canon/tasks.md +2 -1
  168. package/governance/rules/canon/worktrees.md +5 -2
  169. package/governance/rules/claude/output.md +7 -4
  170. package/governance/rules/claude/skill.md +6 -4
  171. package/governance/rules/tooling/ci-workflow.md +6 -39
  172. package/governance/rules/ui/design-reference.md +37 -0
  173. package/governance/rules/writing/markdown.md +2 -1
  174. package/package.json +6 -6
  175. package/src/audits/catalog.ts +42 -0
  176. package/src/claude/cases/workflow.ts +5 -0
  177. package/src/claude/settings.ts +8 -4
  178. package/src/commands/claude.ts +11 -1
  179. package/src/commands/gov.ts +198 -0
  180. package/src/commands/migrate.ts +121 -2
  181. package/src/commands/pr.ts +146 -105
  182. package/src/commands/tooling.ts +3 -3
  183. package/src/commands/worktrees.ts +49 -0
  184. package/src/gate/stages.ts +15 -0
  185. package/src/gov/audit.ts +440 -0
  186. package/src/listeners/belongs.ts +21 -0
  187. package/src/listeners/machine.ts +93 -0
  188. package/src/listeners/parse.ts +41 -0
  189. package/src/markdown/scope.ts +144 -0
  190. package/src/migrate/session-defaults.ts +138 -0
  191. package/src/pr/lift.ts +6 -3
  192. package/src/pr/local.ts +4 -69
  193. package/src/pr/review-state.ts +182 -0
  194. package/src/sync/check.ts +15 -0
  195. package/src/tooling/stamp.ts +56 -12
  196. package/src/worktrees/stop.ts +92 -0
  197. package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
  198. package/tooling/base/configs/commitlint.config.js +0 -20
  199. package/tooling/base/reference.md +1 -1
  200. package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
  201. package/tooling/claude/reference.md +13 -7
  202. package/tooling/claude/seeds/.claude/settings.json +4 -0
  203. package/tooling/claude/user/settings.template.json +0 -4
  204. package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
  205. package/tooling/public/configs/.github/dependabot.yml +12 -0
  206. package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
  207. package/tooling/public/manifest.toml +8 -0
  208. package/tooling/public/reference.md +53 -0
  209. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
  210. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
  211. package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
  212. package/tooling/public/seeds/.release-please-manifest.json +3 -0
  213. package/tooling/public/seeds/CONTRIBUTING.md +50 -0
  214. package/tooling/public/seeds/LICENSE +21 -0
  215. package/tooling/public/seeds/SECURITY.md +13 -0
  216. package/tooling/public/seeds/release-please-config.json +12 -0
  217. package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
  218. package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
  219. package/tooling/web/configs/.github/workflows/verify.yml +15 -12
  220. package/claude/skills/git-pr/references/evidence.md +0 -124
@@ -4,6 +4,7 @@ import type { ReachRefusal } from '@/claude/skills-reach'
4
4
  import type { ContextAuditRefusal } from '@/context/audit'
5
5
  import type { SweepVerdict } from '@/context/classify/patterns'
6
6
  import type { AuditRefusal } from '@/deps/audit'
7
+ import type { RuleAuditRefusal } from '@/gov/audit'
7
8
  import type { RestatedRefusal } from '@/gov/restated'
8
9
  import type { LabelAuditRefusal } from '@/labels/audit'
9
10
  import type { ValidateRefusal as RecordRefusal } from '@/records/validate'
@@ -412,6 +413,33 @@ function restatedCounts(record: unknown): Record<string, number> | undefined {
412
413
  })
413
414
  }
414
415
 
416
+ /**
417
+ * Counts rules rather than findings, since a rule past four limits is one
418
+ * rewrite, and keeps the two ledger findings apart because a missing ledger and
419
+ * a short one are fixed by different passes.
420
+ */
421
+ function ruleCounts(record: unknown): Record<string, number> | undefined {
422
+ const rules = asObject(record)?.rules
423
+ if (!Array.isArray(rules)) return undefined
424
+
425
+ let shapeRules = 0
426
+ let missingLedgers = 0
427
+ let ledgerMismatches = 0
428
+
429
+ for (const raw of rules) {
430
+ const rule = asObject(raw)
431
+ const findings = lengthOf(rule?.findings)
432
+ const ledger = asObject(rule?.ledger)
433
+ if (findings === undefined || ledger === undefined) return undefined
434
+
435
+ if (findings > 0) shapeRules += 1
436
+ if (ledger.status === 'missing') missingLedgers += 1
437
+ if ((lengthOf(ledger.mismatches) ?? 0) > 0) ledgerMismatches += 1
438
+ }
439
+
440
+ return { shapeRules, missingLedgers, ledgerMismatches }
441
+ }
442
+
415
443
  function findingsOnly(record: unknown): Record<string, number> | undefined {
416
444
  const root = asObject(record)
417
445
  if (root === undefined) return undefined
@@ -681,6 +709,20 @@ export const AUDITS: readonly AuditSpec[] = [
681
709
  ] satisfies RestatedRefusal[],
682
710
  counts: restatedCounts,
683
711
  },
712
+ {
713
+ id: 'rules',
714
+ label: 'Rule shape and ledgers',
715
+ argv: ['gov', 'audit', '--json'],
716
+ // Reports rather than gates. The corpus starts far from zero, and the
717
+ // `rule-shape` stage already fails a push on each folder once its rewrite
718
+ // lists it as gated, so gating the whole set here would fail every push.
719
+ gatingExits: [],
720
+ corpus: 'tracked',
721
+ // A target that authors no rule of its own has no corpus here, which is an
722
+ // absence rather than a break, on the same test the restated sweep takes.
723
+ absentReasons: ['no-rules'] satisfies RuleAuditRefusal[],
724
+ counts: ruleCounts,
725
+ },
684
726
  {
685
727
  id: 'counts',
686
728
  label: 'Self-stated catalog counts',
@@ -81,6 +81,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
81
81
  'Open the preview for that pull request and click through each box on its visual checklist, then post what happened.',
82
82
  expect: 'review-ui',
83
83
  },
84
+ {
85
+ prompt:
86
+ 'I pushed another commit to the open PR. Update the screenshot comparison on it so the reviewer sees the current head.',
87
+ expect: 'pr-evidence',
88
+ },
84
89
  {
85
90
  prompt:
86
91
  'The screencast draft is finished. Turn it into an actual video now.',
@@ -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 }
@@ -69,6 +69,7 @@ import {
69
69
  serializeSettings,
70
70
  writeSettings,
71
71
  } from '@/claude/settings'
72
+ import { readSessionDefaults } from '@/migrate/session-defaults'
72
73
  import { copyPreservingMode } from '@/sync/copy'
73
74
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
74
75
  import { recordDir } from '@/roots/record'
@@ -255,7 +256,7 @@ export function register(program: Command): void {
255
256
 
256
257
  claude
257
258
  .command('setup')
258
- .description('One-shot user-level config (statusline, attribution)')
259
+ .description('One-shot user-level config (statusline, permissions)')
259
260
  .argument('[dest]', 'User config directory', join(homedir(), '.claude'))
260
261
  .helpOption('-h, --help', 'Show this help message')
261
262
  .action(async (dest: string) => {
@@ -663,6 +664,15 @@ async function runSync(target: string): Promise<number> {
663
664
  else logWarn(`${name}/ missing. Run \`canon claude init\``)
664
665
  }
665
666
 
667
+ // Sync never rewrites an existing settings file, so a target seeded before
668
+ // the session defaults learns of them only here.
669
+ const defaults = await readSessionDefaults(resolved, PROJECT_ROOT)
670
+ if (defaults.kind === 'planned' && defaults.plan.predates) {
671
+ logWarn(
672
+ 'settings.json lacks the session defaults. Run `canon migrate session-defaults --write`',
673
+ )
674
+ }
675
+
666
676
  logStep('Scanning .gitignore')
667
677
  const chain = claudeChain(PROJECT_ROOT)
668
678
  const pruned = await pruneGitignore(chain, resolved)
@@ -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
  /**
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
- import { join } from 'node:path'
2
+ import { join, resolve } from 'node:path'
3
3
  import type { Command } from 'commander'
4
4
  import { listRepositoryFiles } from '@/git/files'
5
5
  import { indexSourceRules } from '@/gov/adapter'
@@ -25,6 +25,11 @@ import {
25
25
  type RuleLayoutPlan,
26
26
  walkFlatRules,
27
27
  } from '@/migrate/rule-layout'
28
+ import {
29
+ applySessionDefaults,
30
+ readSessionDefaults,
31
+ type SessionDefaultsRead,
32
+ } from '@/migrate/session-defaults'
28
33
  import {
29
34
  SKILL_CITATION_MAP,
30
35
  SKILL_NAME_MAP,
@@ -50,7 +55,7 @@ import {
50
55
  } from '@/migrate/scratch-evidence'
51
56
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
52
57
  import { readStamp, stampedHashes } from '@/sync/stamp'
53
- import { logError, logInfo, logStep, logWarn, plural } from '@/cli/ui'
58
+ import { logAdd, logError, logInfo, logStep, logWarn, plural } from '@/cli/ui'
54
59
 
55
60
  interface SweepOptions {
56
61
  readonly json?: boolean
@@ -959,6 +964,81 @@ function toRuleLayoutRecord(
959
964
  }
960
965
  }
961
966
 
967
+ interface SessionDefaultsOptions {
968
+ readonly json?: boolean
969
+ readonly write?: boolean
970
+ readonly root?: string
971
+ }
972
+
973
+ /**
974
+ * Adds the seeded session defaults to a target settings file that predates
975
+ * them. A missing file is nothing to do, since `canon claude init` seeds the
976
+ * whole file with the defaults already in it.
977
+ */
978
+ async function runSessionDefaults(
979
+ opts: SessionDefaultsOptions,
980
+ ): Promise<number> {
981
+ const root = resolve(opts.root ?? process.cwd())
982
+
983
+ const mismatch = checkoutMismatchWarning(process.cwd())
984
+ if (mismatch !== undefined) logWarn(mismatch)
985
+
986
+ const read = await readSessionDefaults(root, PROJECT_ROOT)
987
+
988
+ if (opts.json) {
989
+ process.stdout.write(
990
+ `${JSON.stringify(toSessionDefaultsRecord(read, opts.write))}\n`,
991
+ )
992
+ }
993
+
994
+ if (read.kind === 'refused') {
995
+ logError(read.reason)
996
+ return 1
997
+ }
998
+
999
+ if (read.kind === 'absent') {
1000
+ logInfo(`No ${read.path}, so there is nothing to migrate.`)
1001
+ return 0
1002
+ }
1003
+
1004
+ for (const key of read.plan.keys) {
1005
+ if (key.changed) logAdd(key.label)
1006
+ else logInfo(key.label)
1007
+ }
1008
+
1009
+ if (!read.plan.changed) {
1010
+ logInfo('Session defaults already present.')
1011
+ return 0
1012
+ }
1013
+
1014
+ if (!opts.write) {
1015
+ logWarn('Nothing was written. Pass --write to apply this plan.')
1016
+ return 2
1017
+ }
1018
+
1019
+ await applySessionDefaults(read)
1020
+ logStep(`Wrote ${read.path}.`)
1021
+ return 0
1022
+ }
1023
+
1024
+ function toSessionDefaultsRecord(
1025
+ read: SessionDefaultsRead,
1026
+ wrote: boolean | undefined,
1027
+ ): unknown {
1028
+ if (read.kind === 'refused') {
1029
+ return { ok: false, wrote: false, reason: read.reason }
1030
+ }
1031
+ if (read.kind === 'absent') {
1032
+ return { ok: true, wrote: false, path: read.path, keys: [] }
1033
+ }
1034
+ return {
1035
+ ok: true,
1036
+ wrote: wrote === true && read.plan.changed,
1037
+ path: read.path,
1038
+ keys: read.plan.keys,
1039
+ }
1040
+ }
1041
+
962
1042
  export function register(program: Command): void {
963
1043
  const migrate = program
964
1044
  .command('migrate')
@@ -1322,4 +1402,43 @@ export function register(program: Command): void {
1322
1402
  .action(async (opts: RuleLayoutOptions) => {
1323
1403
  process.exitCode = await runRuleLayout(opts)
1324
1404
  })
1405
+
1406
+ migrate
1407
+ .command('session-defaults')
1408
+ .description('Add the seeded session defaults to a target settings file')
1409
+ .helpOption('-h, --help', 'Show this help message')
1410
+ .option('--json', 'Add a machine-readable record on stdout')
1411
+ .option('--write', 'Apply the plan rather than reporting it')
1412
+ .option(
1413
+ '--root <path>',
1414
+ 'Project root, defaulting to the working directory',
1415
+ )
1416
+ .addHelpText(
1417
+ 'after',
1418
+ [
1419
+ '',
1420
+ 'Adds Agent and Task to permissions.deny in .claude/settings.json,',
1421
+ 'appending to the list without reordering it, and sets',
1422
+ 'autoMemoryEnabled to false only when the key is absent. A deny entry',
1423
+ 'is never removed and an existing autoMemoryEnabled is never changed.',
1424
+ '',
1425
+ 'A missing settings file is nothing to do. A file that is not valid',
1426
+ 'JSON, or whose permissions or deny has an unexpected shape, is',
1427
+ 'refused by name and never partly written.',
1428
+ '',
1429
+ 'Exit codes:',
1430
+ ' 0 nothing to add, or --write applied the plan',
1431
+ ' 1 refused: the toolkit root, or an unreadable settings file',
1432
+ ' 2 a plan exists and --write was not passed',
1433
+ '',
1434
+ 'Examples:',
1435
+ ' canon migrate session-defaults',
1436
+ ' canon migrate session-defaults --write',
1437
+ ' canon migrate session-defaults --json',
1438
+ '',
1439
+ ].join('\n'),
1440
+ )
1441
+ .action(async (opts: SessionDefaultsOptions) => {
1442
+ process.exitCode = await runSessionDefaults(opts)
1443
+ })
1325
1444
  }