@erclx/canon 4.0.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 (643) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/claude/.claude-plugin/plugin.json +19 -0
  4. package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
  5. package/claude/skills/bash-cli-script/SKILL.md +48 -0
  6. package/claude/skills/bash-cli-script/references/template.md +43 -0
  7. package/claude/skills/bash-script/REQUIREMENT.md +36 -0
  8. package/claude/skills/bash-script/SKILL.md +100 -0
  9. package/claude/skills/bash-script/references/patterns.md +349 -0
  10. package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
  11. package/claude/skills/canon-cli/SKILL.md +103 -0
  12. package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
  13. package/claude/skills/canon-feedback-file/SKILL.md +80 -0
  14. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
  15. package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
  16. package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
  17. package/claude/skills/canon-operator/SKILL.md +108 -0
  18. package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
  19. package/claude/skills/canon-rollout/SKILL.md +147 -0
  20. package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
  21. package/claude/skills/canon-screencast/SKILL.md +167 -0
  22. package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
  23. package/claude/skills/canon-slides-draft/SKILL.md +62 -0
  24. package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
  25. package/claude/skills/ci-workflow/SKILL.md +65 -0
  26. package/claude/skills/ci-workflow/references/workflows.md +98 -0
  27. package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
  28. package/claude/skills/claude-address-review/SKILL.md +212 -0
  29. package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
  30. package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
  31. package/claude/skills/claude-autoship/SKILL.md +207 -0
  32. package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
  33. package/claude/skills/claude-design-extract/SKILL.md +102 -0
  34. package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
  35. package/claude/skills/claude-diagram/SKILL.md +177 -0
  36. package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
  37. package/claude/skills/claude-docs/SKILL.md +287 -0
  38. package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
  39. package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
  40. package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
  41. package/claude/skills/claude-feature/SKILL.md +115 -0
  42. package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
  43. package/claude/skills/claude-groundwork/SKILL.md +142 -0
  44. package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
  45. package/claude/skills/claude-intake/SKILL.md +114 -0
  46. package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
  47. package/claude/skills/claude-intake-answer/SKILL.md +90 -0
  48. package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
  49. package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
  50. package/claude/skills/claude-markdown-propose/references/format.md +107 -0
  51. package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
  52. package/claude/skills/claude-memory-capture/SKILL.md +101 -0
  53. package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
  54. package/claude/skills/claude-memory-review/SKILL.md +210 -0
  55. package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
  56. package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
  57. package/claude/skills/claude-orchestrate/SKILL.md +241 -0
  58. package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
  59. package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
  60. package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
  61. package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
  62. package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
  63. package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
  64. package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
  65. package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
  66. package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
  67. package/claude/skills/claude-pr-review/SKILL.md +295 -0
  68. package/claude/skills/claude-review/REQUIREMENT.md +39 -0
  69. package/claude/skills/claude-review/SKILL.md +142 -0
  70. package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
  71. package/claude/skills/claude-seed-sync/SKILL.md +156 -0
  72. package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
  73. package/claude/skills/claude-standards-audit/SKILL.md +99 -0
  74. package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
  75. package/claude/skills/claude-tasks/SKILL.md +161 -0
  76. package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
  77. package/claude/skills/claude-teach/SKILL.md +196 -0
  78. package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
  79. package/claude/skills/claude-teach/references/pedagogy.md +67 -0
  80. package/claude/skills/claude-teach/references/promotion.md +54 -0
  81. package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
  82. package/claude/skills/claude-ui-test/SKILL.md +77 -0
  83. package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
  84. package/claude/skills/claude-ux-audit/SKILL.md +79 -0
  85. package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
  86. package/claude/skills/claude-ux-measure/SKILL.md +122 -0
  87. package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
  88. package/claude/skills/claude-worker/SKILL.md +96 -0
  89. package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
  90. package/claude/skills/claude-worktree/SKILL.md +136 -0
  91. package/claude/skills/create-rule/REQUIREMENT.md +45 -0
  92. package/claude/skills/create-rule/SKILL.md +68 -0
  93. package/claude/skills/create-skill/REQUIREMENT.md +38 -0
  94. package/claude/skills/create-skill/SKILL.md +32 -0
  95. package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
  96. package/claude/skills/create-snippet/SKILL.md +30 -0
  97. package/claude/skills/create-standard/REQUIREMENT.md +35 -0
  98. package/claude/skills/create-standard/SKILL.md +29 -0
  99. package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
  100. package/claude/skills/decision-escalate/SKILL.md +79 -0
  101. package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
  102. package/claude/skills/docs-sync/SKILL.md +95 -0
  103. package/claude/skills/git-branch/REQUIREMENT.md +38 -0
  104. package/claude/skills/git-branch/SKILL.md +60 -0
  105. package/claude/skills/git-commit/REQUIREMENT.md +36 -0
  106. package/claude/skills/git-commit/SKILL.md +49 -0
  107. package/claude/skills/git-followup/REQUIREMENT.md +43 -0
  108. package/claude/skills/git-followup/SKILL.md +48 -0
  109. package/claude/skills/git-issue/REQUIREMENT.md +38 -0
  110. package/claude/skills/git-issue/SKILL.md +65 -0
  111. package/claude/skills/git-pr/REQUIREMENT.md +50 -0
  112. package/claude/skills/git-pr/SKILL.md +164 -0
  113. package/claude/skills/git-pr/references/labels.md +95 -0
  114. package/claude/skills/git-ship/REQUIREMENT.md +42 -0
  115. package/claude/skills/git-ship/SKILL.md +55 -0
  116. package/claude/skills/git-split/REQUIREMENT.md +39 -0
  117. package/claude/skills/git-split/SKILL.md +162 -0
  118. package/claude/skills/git-stage/REQUIREMENT.md +39 -0
  119. package/claude/skills/git-stage/SKILL.md +73 -0
  120. package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
  121. package/claude/skills/git-worktree/SKILL.md +130 -0
  122. package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
  123. package/claude/skills/migration-claude-md/SKILL.md +76 -0
  124. package/claude/skills/migration-context/REQUIREMENT.md +36 -0
  125. package/claude/skills/migration-context/SKILL.md +95 -0
  126. package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
  127. package/claude/skills/migration-standards-drop/SKILL.md +113 -0
  128. package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
  129. package/claude/skills/migration-superseded/SKILL.md +115 -0
  130. package/claude/skills/project-commands/REQUIREMENT.md +42 -0
  131. package/claude/skills/project-commands/SKILL.md +85 -0
  132. package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
  133. package/claude/skills/restate-plainly/SKILL.md +39 -0
  134. package/claude/skills/session-map/REQUIREMENT.md +57 -0
  135. package/claude/skills/session-map/SKILL.md +70 -0
  136. package/claude/skills/session-resume/REQUIREMENT.md +49 -0
  137. package/claude/skills/session-resume/SKILL.md +51 -0
  138. package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
  139. package/claude/skills/setup-gov/SKILL.md +77 -0
  140. package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
  141. package/claude/skills/setup-indexes/SKILL.md +153 -0
  142. package/claude/skills/setup-init/REQUIREMENT.md +45 -0
  143. package/claude/skills/setup-init/SKILL.md +127 -0
  144. package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
  145. package/claude/skills/setup-plugins/SKILL.md +81 -0
  146. package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
  147. package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
  148. package/claude/skills/setup-verify/SKILL.md +51 -0
  149. package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
  150. package/claude/skills/systematic-debugging/SKILL.md +70 -0
  151. package/claude/skills/write-human/REQUIREMENT.md +46 -0
  152. package/claude/skills/write-human/SKILL.md +68 -0
  153. package/claude/skills/write-human/references/density.md +38 -0
  154. package/claude/skills/write-human/references/machine-tells.md +107 -0
  155. package/claude/skills/write-human/references/source-material.md +37 -0
  156. package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
  157. package/claude/skills/youtube-transcripts/SKILL.md +34 -0
  158. package/docs/agents/audits.md +98 -0
  159. package/docs/agents/capture.md +37 -0
  160. package/docs/agents/census.md +23 -0
  161. package/docs/agents/commands.md +146 -0
  162. package/docs/agents/comments.md +34 -0
  163. package/docs/agents/context-audit-checks.md +120 -0
  164. package/docs/agents/context-audit.md +83 -0
  165. package/docs/agents/counts.md +76 -0
  166. package/docs/agents/demo.md +86 -0
  167. package/docs/agents/docs.md +17 -0
  168. package/docs/agents/gate.md +84 -0
  169. package/docs/agents/index.md +47 -0
  170. package/docs/agents/indexes.md +35 -0
  171. package/docs/agents/install-and-sync.md +385 -0
  172. package/docs/agents/intake.md +81 -0
  173. package/docs/agents/key-changes.md +103 -0
  174. package/docs/agents/label-coverage.md +73 -0
  175. package/docs/agents/markdown-audit.md +197 -0
  176. package/docs/agents/output-shape.md +70 -0
  177. package/docs/agents/overview.md +26 -0
  178. package/docs/agents/records.md +170 -0
  179. package/docs/agents/restated.md +81 -0
  180. package/docs/agents/review-classification.md +77 -0
  181. package/docs/agents/routing.md +61 -0
  182. package/docs/agents/rule-citations.md +98 -0
  183. package/docs/agents/sandbox.md +71 -0
  184. package/docs/agents/scripting.md +149 -0
  185. package/docs/agents/sessions.md +120 -0
  186. package/docs/agents/skills-audit.md +94 -0
  187. package/docs/agents/skills-reach.md +64 -0
  188. package/docs/agents/standards-audit.md +38 -0
  189. package/docs/agents/state-scoped-risk.md +105 -0
  190. package/docs/agents/superseded.md +85 -0
  191. package/docs/agents/targets.md +83 -0
  192. package/docs/agents/tasks.md +200 -0
  193. package/docs/agents/teach.md +158 -0
  194. package/docs/agents/test-order.md +56 -0
  195. package/docs/agents/worktrees.md +62 -0
  196. package/docs/ai-workflow.md +317 -0
  197. package/docs/index.md +23 -0
  198. package/docs/operating-model.md +223 -0
  199. package/docs/target-projects.md +258 -0
  200. package/docs/visual-design-workflow.md +151 -0
  201. package/docs/zshrc-aliases.md +65 -0
  202. package/governance/rules/ci/700-ci-workflow.md +44 -0
  203. package/governance/rules/claude/500-prose.md +15 -0
  204. package/governance/rules/claude/501-markdown.md +14 -0
  205. package/governance/rules/claude/510-context.md +28 -0
  206. package/governance/rules/claude/511-indexes.md +14 -0
  207. package/governance/rules/claude/520-wireframes.md +12 -0
  208. package/governance/rules/claude/530-requirements.md +11 -0
  209. package/governance/rules/claude/540-architecture.md +11 -0
  210. package/governance/rules/claude/550-design.md +11 -0
  211. package/governance/rules/claude/555-tasks.md +12 -0
  212. package/governance/rules/claude/556-groundwork.md +11 -0
  213. package/governance/rules/claude/557-intake.md +11 -0
  214. package/governance/rules/claude/558-plan.md +22 -0
  215. package/governance/rules/claude/559-memory.md +11 -0
  216. package/governance/rules/claude/560-diagrams.md +18 -0
  217. package/governance/rules/claude/561-teach.md +13 -0
  218. package/governance/rules/claude/562-session.md +15 -0
  219. package/governance/rules/claude/570-skill.md +24 -0
  220. package/governance/rules/claude/575-hooks.md +17 -0
  221. package/governance/rules/claude/576-settings.md +14 -0
  222. package/governance/rules/claude/580-readme.md +11 -0
  223. package/governance/rules/claude/590-rule-authoring.md +12 -0
  224. package/governance/rules/claude/591-standard-authoring.md +12 -0
  225. package/governance/rules/claude/592-claude-md.md +19 -0
  226. package/governance/rules/core/000-constitution.md +30 -0
  227. package/governance/rules/core/005-behavior.md +27 -0
  228. package/governance/rules/core/010-testing.md +35 -0
  229. package/governance/rules/core/015-output.md +20 -0
  230. package/governance/rules/core/020-concurrency.md +22 -0
  231. package/governance/rules/core/025-indexes.md +9 -0
  232. package/governance/rules/core/030-error-handling.md +31 -0
  233. package/governance/rules/core/035-tasks.md +13 -0
  234. package/governance/rules/core/040-performance.md +20 -0
  235. package/governance/rules/core/045-memory.md +12 -0
  236. package/governance/rules/core/050-logging.md +20 -0
  237. package/governance/rules/core/055-scratch.md +9 -0
  238. package/governance/rules/core/060-naming.md +19 -0
  239. package/governance/rules/core/065-spelling.md +19 -0
  240. package/governance/rules/core/070-planning.md +18 -0
  241. package/governance/rules/core/075-dependencies.md +25 -0
  242. package/governance/rules/core/080-config-comments.md +22 -0
  243. package/governance/rules/core/085-worktrees.md +17 -0
  244. package/governance/rules/core/087-git.md +11 -0
  245. package/governance/rules/core/090-code-comments.md +39 -0
  246. package/governance/rules/framework/200-react.md +51 -0
  247. package/governance/rules/framework/210-astro.md +41 -0
  248. package/governance/rules/framework/220-fastapi.md +43 -0
  249. package/governance/rules/framework/230-nextjs.md +48 -0
  250. package/governance/rules/framework/250-tailwind.md +32 -0
  251. package/governance/rules/framework/260-shadcn.md +34 -0
  252. package/governance/rules/lang/100-typescript.md +40 -0
  253. package/governance/rules/lang/110-python.md +42 -0
  254. package/governance/rules/lang/120-bash.md +19 -0
  255. package/governance/rules/lib/300-testing-ts.md +39 -0
  256. package/governance/rules/lib/305-e2e-reliability.md +34 -0
  257. package/governance/rules/lib/306-test-scope.md +25 -0
  258. package/governance/rules/lib/310-zod.md +25 -0
  259. package/governance/rules/lib/320-tanstack-query.md +32 -0
  260. package/governance/rules/lib/330-testing-py.md +44 -0
  261. package/governance/rules/lib/340-pydantic.md +38 -0
  262. package/governance/rules/lib/350-security-web.md +32 -0
  263. package/governance/rules/lib/360-security-server.md +39 -0
  264. package/governance/rules/lib/370-database.md +35 -0
  265. package/governance/rules/snippets/505-at-references.md +9 -0
  266. package/governance/rules/ui/400-ui.md +36 -0
  267. package/governance/rules/ui/410-a11y.md +48 -0
  268. package/governance/rules/ui/420-forms.md +36 -0
  269. package/governance/rules/ui/430-ux-completeness.md +65 -0
  270. package/governance/rules/ui/440-surface-capture.md +34 -0
  271. package/governance/rules/ui/450-link-behavior.md +19 -0
  272. package/governance/stacks/astro.toml +2 -0
  273. package/governance/stacks/base.toml +8 -0
  274. package/governance/stacks/node-server.toml +2 -0
  275. package/governance/stacks/node.toml +2 -0
  276. package/governance/stacks/python-fastapi.toml +2 -0
  277. package/governance/stacks/python.toml +2 -0
  278. package/governance/stacks/react.toml +2 -0
  279. package/package.json +69 -0
  280. package/scripts/config.sh +11 -0
  281. package/scripts/core/bootstrap.sh +81 -0
  282. package/scripts/core/check-color-source.sh +41 -0
  283. package/scripts/core/check-ignore-parity.sh +162 -0
  284. package/scripts/core/check-plugin-boundary.sh +45 -0
  285. package/scripts/core/check-seed-independence.sh +59 -0
  286. package/scripts/core/check-skill-paths.sh +24 -0
  287. package/scripts/core/clean.sh +36 -0
  288. package/scripts/core/install-check.sh +101 -0
  289. package/scripts/core/list-seed-roots.sh +18 -0
  290. package/scripts/core/regen-claude-copies.sh +10 -0
  291. package/scripts/core/regen-hero.sh +217 -0
  292. package/scripts/core/regen-indexes.sh +10 -0
  293. package/scripts/core/regen-tooling-paths.sh +61 -0
  294. package/scripts/core/repair-bare-flag.sh +19 -0
  295. package/scripts/core/snapshot.sh +134 -0
  296. package/scripts/core/update.sh +35 -0
  297. package/scripts/docs/list.sh +165 -0
  298. package/scripts/lib/frontmatter.sh +30 -0
  299. package/scripts/lib/gov.sh +14 -0
  300. package/scripts/lib/sandbox-fixtures.sh +191 -0
  301. package/scripts/lib/sandbox-git.sh +125 -0
  302. package/scripts/lib/sandbox-path.sh +206 -0
  303. package/scripts/lib/tooling.sh +35 -0
  304. package/scripts/lib/ui.sh +266 -0
  305. package/scripts/lib/worktree.sh +20 -0
  306. package/scripts/manage-sandbox.sh +466 -0
  307. package/scripts/snippets/create.sh +156 -0
  308. package/scripts/standards/list.sh +115 -0
  309. package/scripts/tooling/create.sh +109 -0
  310. package/scripts/tooling/verify.sh +179 -0
  311. package/snippets/align.md +12 -0
  312. package/snippets/claude/decision-memo.md +39 -0
  313. package/snippets/claude/feature-recap.md +19 -0
  314. package/snippets/claude/figma-steps.md +24 -0
  315. package/snippets/compact-summary.md +5 -0
  316. package/snippets/decision-help.md +6 -0
  317. package/snippets/meta-prompt.md +14 -0
  318. package/snippets/research-prompt.md +7 -0
  319. package/snippets/session-notes.md +11 -0
  320. package/snippets/snippets.toml +5 -0
  321. package/snippets/step-by-step.md +10 -0
  322. package/snippets/web-research.md +21 -0
  323. package/src/audits/baseline.ts +201 -0
  324. package/src/audits/catalog.ts +876 -0
  325. package/src/audits/run.ts +204 -0
  326. package/src/autoship/classify.ts +75 -0
  327. package/src/autoship/paths.ts +51 -0
  328. package/src/binary.ts +16 -0
  329. package/src/browser/engine.ts +40 -0
  330. package/src/census/count.ts +113 -0
  331. package/src/claude/cases/all.ts +24 -0
  332. package/src/claude/cases/authoring.ts +53 -0
  333. package/src/claude/cases/claude-workflow.ts +158 -0
  334. package/src/claude/cases/git.ts +44 -0
  335. package/src/claude/cases/misc.ts +27 -0
  336. package/src/claude/cases/setup.ts +94 -0
  337. package/src/claude/gitignore.ts +51 -0
  338. package/src/claude/routing.ts +283 -0
  339. package/src/claude/seeds-list.ts +47 -0
  340. package/src/claude/seeds.ts +150 -0
  341. package/src/claude/settings.ts +151 -0
  342. package/src/claude/skills-audit.ts +228 -0
  343. package/src/claude/skills-drift.ts +156 -0
  344. package/src/claude/skills-list.ts +99 -0
  345. package/src/claude/skills-rank.ts +320 -0
  346. package/src/claude/skills-reach.ts +227 -0
  347. package/src/cli-run.ts +43 -0
  348. package/src/cli.ts +200 -0
  349. package/src/commands/audits.ts +350 -0
  350. package/src/commands/autoship.ts +129 -0
  351. package/src/commands/capture.ts +133 -0
  352. package/src/commands/census.ts +105 -0
  353. package/src/commands/claude.ts +1286 -0
  354. package/src/commands/comments.ts +240 -0
  355. package/src/commands/context.ts +857 -0
  356. package/src/commands/demo.ts +389 -0
  357. package/src/commands/deps.ts +173 -0
  358. package/src/commands/design.ts +36 -0
  359. package/src/commands/docs.ts +60 -0
  360. package/src/commands/feedback-format.ts +23 -0
  361. package/src/commands/feedback.ts +112 -0
  362. package/src/commands/gate.ts +189 -0
  363. package/src/commands/gov.ts +1265 -0
  364. package/src/commands/indexes.ts +184 -0
  365. package/src/commands/init.ts +113 -0
  366. package/src/commands/intake.ts +406 -0
  367. package/src/commands/inventory.ts +256 -0
  368. package/src/commands/labels.ts +361 -0
  369. package/src/commands/markdown.ts +544 -0
  370. package/src/commands/migrate.ts +175 -0
  371. package/src/commands/pass-through.ts +39 -0
  372. package/src/commands/pr.ts +411 -0
  373. package/src/commands/records.ts +728 -0
  374. package/src/commands/sandbox.ts +468 -0
  375. package/src/commands/secrets.ts +132 -0
  376. package/src/commands/serve.ts +159 -0
  377. package/src/commands/sessions.ts +408 -0
  378. package/src/commands/slides.ts +126 -0
  379. package/src/commands/snippets.ts +84 -0
  380. package/src/commands/standards.ts +247 -0
  381. package/src/commands/sync.ts +428 -0
  382. package/src/commands/targets.ts +319 -0
  383. package/src/commands/tasks.ts +743 -0
  384. package/src/commands/teach.ts +786 -0
  385. package/src/commands/tooling.ts +573 -0
  386. package/src/commands/transcripts.ts +44 -0
  387. package/src/commands/upgrade.ts +231 -0
  388. package/src/commands/wiki.ts +100 -0
  389. package/src/commands/worktrees.ts +191 -0
  390. package/src/comments/scan.ts +338 -0
  391. package/src/comments/trend.ts +207 -0
  392. package/src/comments/vocabulary.ts +85 -0
  393. package/src/context/architecture.ts +364 -0
  394. package/src/context/audit.ts +790 -0
  395. package/src/context/citations.ts +196 -0
  396. package/src/context/folders.ts +186 -0
  397. package/src/context/gate.ts +57 -0
  398. package/src/context/index-drift.ts +64 -0
  399. package/src/context/narration.ts +99 -0
  400. package/src/copy.ts +30 -0
  401. package/src/counts/catalogs.ts +96 -0
  402. package/src/counts/numbers.ts +79 -0
  403. package/src/counts/scan.ts +314 -0
  404. package/src/demo/beats.ts +135 -0
  405. package/src/demo/compile.ts +326 -0
  406. package/src/demo/container.ts +63 -0
  407. package/src/demo/cursors.ts +55 -0
  408. package/src/demo/drive.ts +357 -0
  409. package/src/demo/pointer.ts +178 -0
  410. package/src/demo/theme.ts +112 -0
  411. package/src/deps/audit.ts +153 -0
  412. package/src/design/parse.ts +116 -0
  413. package/src/design/render.ts +249 -0
  414. package/src/docs/read.ts +77 -0
  415. package/src/exec.ts +16 -0
  416. package/src/exempt-marker.ts +43 -0
  417. package/src/frontmatter.ts +13 -0
  418. package/src/gate/measures.ts +682 -0
  419. package/src/gate/sequencer.ts +386 -0
  420. package/src/gate/stages.ts +412 -0
  421. package/src/git-env.ts +36 -0
  422. package/src/git-files.ts +105 -0
  423. package/src/git-ignore.ts +46 -0
  424. package/src/github-format.ts +13 -0
  425. package/src/github.ts +24 -0
  426. package/src/gov/adapter.ts +103 -0
  427. package/src/gov/citations.ts +514 -0
  428. package/src/gov/consumed.ts +129 -0
  429. package/src/gov/install.ts +132 -0
  430. package/src/gov/list.ts +106 -0
  431. package/src/gov/payload.ts +39 -0
  432. package/src/gov/restated.ts +814 -0
  433. package/src/gov/stacks.ts +205 -0
  434. package/src/gov/superseded.ts +415 -0
  435. package/src/gov/test-order.ts +407 -0
  436. package/src/indexes/frontmatter.ts +46 -0
  437. package/src/indexes/regen.ts +84 -0
  438. package/src/indexes/render.ts +201 -0
  439. package/src/indexes/walk.ts +83 -0
  440. package/src/init/flags.ts +60 -0
  441. package/src/init/plan.ts +114 -0
  442. package/src/init/run.ts +46 -0
  443. package/src/init/steps.ts +77 -0
  444. package/src/intake/folder.ts +320 -0
  445. package/src/intake/items.ts +174 -0
  446. package/src/inventory/config.ts +117 -0
  447. package/src/inventory/group.ts +76 -0
  448. package/src/inventory/subjects.ts +114 -0
  449. package/src/inventory/walk.ts +129 -0
  450. package/src/labels/audit.ts +82 -0
  451. package/src/labels/coverage.ts +79 -0
  452. package/src/labels/map.ts +101 -0
  453. package/src/labels/phase.ts +95 -0
  454. package/src/markdown/bans.ts +94 -0
  455. package/src/markdown/files.ts +102 -0
  456. package/src/markdown/gate.ts +28 -0
  457. package/src/markdown/scan.ts +295 -0
  458. package/src/markdown/structure.ts +730 -0
  459. package/src/migrate/apply.ts +115 -0
  460. package/src/migrate/plan.ts +103 -0
  461. package/src/migrate/rename.ts +183 -0
  462. package/src/pr/bijection.ts +145 -0
  463. package/src/pr/paths.ts +335 -0
  464. package/src/process/harness.ts +167 -0
  465. package/src/project-root.ts +19 -0
  466. package/src/records/backup.ts +455 -0
  467. package/src/records/migrate.ts +78 -0
  468. package/src/records/size.ts +260 -0
  469. package/src/records/validate.ts +1162 -0
  470. package/src/sandbox/census.ts +228 -0
  471. package/src/sandbox/coverage.ts +115 -0
  472. package/src/sandbox/expect.ts +629 -0
  473. package/src/sandbox/tree.ts +47 -0
  474. package/src/secrets/marker.ts +30 -0
  475. package/src/secrets/patterns.ts +142 -0
  476. package/src/secrets/scan.ts +123 -0
  477. package/src/secrets/shipped.ts +111 -0
  478. package/src/seed-marker.ts +74 -0
  479. package/src/serve/static.ts +322 -0
  480. package/src/sessions/claim.ts +85 -0
  481. package/src/sessions/live.ts +79 -0
  482. package/src/sessions/registry.ts +137 -0
  483. package/src/sessions/resolve.ts +333 -0
  484. package/src/slides/layouts.ts +391 -0
  485. package/src/slides/open.ts +18 -0
  486. package/src/slides/parse.ts +84 -0
  487. package/src/slides/render.ts +88 -0
  488. package/src/slides/styles.ts +44 -0
  489. package/src/snippets/categories.ts +66 -0
  490. package/src/snippets/list.ts +32 -0
  491. package/src/snippets/presets.ts +50 -0
  492. package/src/standards/audit.ts +132 -0
  493. package/src/standards/read.ts +100 -0
  494. package/src/sync/check.ts +692 -0
  495. package/src/sync/engine.ts +576 -0
  496. package/src/sync/git.ts +225 -0
  497. package/src/sync/history.ts +123 -0
  498. package/src/sync/layout.ts +140 -0
  499. package/src/sync/reverse.ts +268 -0
  500. package/src/sync/seeds-report.ts +126 -0
  501. package/src/sync/stamp.ts +358 -0
  502. package/src/sync/target.ts +70 -0
  503. package/src/sync/workflow.ts +200 -0
  504. package/src/target.ts +43 -0
  505. package/src/targets/pulls.ts +250 -0
  506. package/src/targets/registry.ts +203 -0
  507. package/src/targets/resolve.ts +145 -0
  508. package/src/targets/sweep.ts +246 -0
  509. package/src/tasks/archive.ts +506 -0
  510. package/src/tasks/record.ts +311 -0
  511. package/src/tasks/trunk.ts +89 -0
  512. package/src/tasks/validate.ts +1001 -0
  513. package/src/teach/lesson.ts +180 -0
  514. package/src/teach/workspace.ts +842 -0
  515. package/src/tooling/gitignore.ts +122 -0
  516. package/src/tooling/inject.ts +193 -0
  517. package/src/tooling/list.ts +39 -0
  518. package/src/tooling/manifest.ts +176 -0
  519. package/src/tooling/package.ts +166 -0
  520. package/src/tooling/read.ts +65 -0
  521. package/src/tooling/scan.ts +147 -0
  522. package/src/tooling/stamp.ts +44 -0
  523. package/src/transcripts/fetch.ts +156 -0
  524. package/src/transcripts/metadata.ts +54 -0
  525. package/src/transcripts/vtt.ts +114 -0
  526. package/src/ui.ts +266 -0
  527. package/src/version/compare.ts +53 -0
  528. package/src/version/installed.ts +40 -0
  529. package/src/version/manager.ts +67 -0
  530. package/src/version/skew.ts +192 -0
  531. package/src/wiki/init.ts +85 -0
  532. package/src/worktree.ts +143 -0
  533. package/src/worktrees/reclaim.ts +306 -0
  534. package/standards/architecture.md +72 -0
  535. package/standards/branch.md +59 -0
  536. package/standards/commit.md +72 -0
  537. package/standards/context.md +151 -0
  538. package/standards/design.md +93 -0
  539. package/standards/diagrams.md +152 -0
  540. package/standards/glossary.md +75 -0
  541. package/standards/groundwork.md +211 -0
  542. package/standards/index.md +36 -0
  543. package/standards/intake.md +192 -0
  544. package/standards/issue.md +94 -0
  545. package/standards/markdown.md +137 -0
  546. package/standards/memory.md +144 -0
  547. package/standards/plan.md +172 -0
  548. package/standards/pr.md +139 -0
  549. package/standards/publish.md +51 -0
  550. package/standards/readme.md +208 -0
  551. package/standards/requirements.md +70 -0
  552. package/standards/rule.md +118 -0
  553. package/standards/session.md +109 -0
  554. package/standards/skill.md +300 -0
  555. package/standards/slug.md +39 -0
  556. package/standards/snippets.md +76 -0
  557. package/standards/standard.md +170 -0
  558. package/standards/tasks.md +254 -0
  559. package/standards/teach.md +153 -0
  560. package/standards/versioning.md +71 -0
  561. package/standards/wireframes.md +113 -0
  562. package/tooling/astro/configs/astro.config.mjs +31 -0
  563. package/tooling/astro/configs/eslint.config.js +79 -0
  564. package/tooling/astro/configs/playwright.config.ts +26 -0
  565. package/tooling/astro/configs/tsconfig.json +12 -0
  566. package/tooling/astro/configs/vitest.config.ts +22 -0
  567. package/tooling/astro/manifest.toml +34 -0
  568. package/tooling/astro/reference.md +60 -0
  569. package/tooling/base/configs/.editorconfig +5 -0
  570. package/tooling/base/configs/.github/pull_request_template.md +18 -0
  571. package/tooling/base/configs/.github/workflows/verify.yml +35 -0
  572. package/tooling/base/configs/.husky/commit-msg +1 -0
  573. package/tooling/base/configs/.husky/post-merge +61 -0
  574. package/tooling/base/configs/.husky/post-rewrite +21 -0
  575. package/tooling/base/configs/.husky/pre-commit +1 -0
  576. package/tooling/base/configs/.husky/pre-push +1 -0
  577. package/tooling/base/configs/.prettierrc +12 -0
  578. package/tooling/base/configs/.shellcheckrc +1 -0
  579. package/tooling/base/configs/.vscode/extensions.json +9 -0
  580. package/tooling/base/configs/.vscode/settings.json +3 -0
  581. package/tooling/base/configs/commitlint.config.js +11 -0
  582. package/tooling/base/configs/scripts/verify.sh +64 -0
  583. package/tooling/base/manifest.toml +30 -0
  584. package/tooling/base/reference.md +98 -0
  585. package/tooling/base/seeds/.claude/context/ci.md +33 -0
  586. package/tooling/base/seeds/.claude/context/development.md +37 -0
  587. package/tooling/base/seeds/.claude/context/index.md +11 -0
  588. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  589. package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
  590. package/tooling/base/seeds/.lintstagedrc +8 -0
  591. package/tooling/base/seeds/.prettierignore +0 -0
  592. package/tooling/base/seeds/cspell.json +20 -0
  593. package/tooling/claude/manifest.toml +14 -0
  594. package/tooling/claude/reference.md +79 -0
  595. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
  596. package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
  597. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
  598. package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
  599. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
  600. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
  601. package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
  602. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
  603. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
  604. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
  605. package/tooling/claude/seeds/.claude/memory/index.md +8 -0
  606. package/tooling/claude/seeds/.claude/settings.json +47 -0
  607. package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
  608. package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
  609. package/tooling/claude/seeds/CLAUDE.md +29 -0
  610. package/tooling/claude/user/settings.template.json +10 -0
  611. package/tooling/claude/user/statusline-command.sh +53 -0
  612. package/tooling/python/configs/.coveragerc +14 -0
  613. package/tooling/python/configs/.python-version +1 -0
  614. package/tooling/python/configs/mypy.ini +6 -0
  615. package/tooling/python/configs/pytest.ini +4 -0
  616. package/tooling/python/configs/ruff.toml +15 -0
  617. package/tooling/python/configs/scripts/verify.sh +77 -0
  618. package/tooling/python/manifest.toml +16 -0
  619. package/tooling/python/reference.md +66 -0
  620. package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
  621. package/tooling/python/seeds/tests/test_smoke.py +2 -0
  622. package/tooling/vite-react/configs/playwright.config.ts +26 -0
  623. package/tooling/vite-react/configs/tsconfig.json +35 -0
  624. package/tooling/vite-react/configs/vite.config.ts +24 -0
  625. package/tooling/vite-react/configs/vitest.config.ts +26 -0
  626. package/tooling/vite-react/manifest.toml +23 -0
  627. package/tooling/vite-react/reference.md +55 -0
  628. package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
  629. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
  630. package/tooling/web/configs/.github/workflows/verify.yml +134 -0
  631. package/tooling/web/configs/.vscode/extensions.json +13 -0
  632. package/tooling/web/configs/.vscode/settings.json +10 -0
  633. package/tooling/web/configs/e2e/home.spec.ts +6 -0
  634. package/tooling/web/configs/e2e/screenshot.ts +53 -0
  635. package/tooling/web/configs/eslint.config.js +82 -0
  636. package/tooling/web/configs/scripts/screenshot.sh +28 -0
  637. package/tooling/web/configs/scripts/verify.sh +80 -0
  638. package/tooling/web/configs/scripts/worktree-port.sh +74 -0
  639. package/tooling/web/configs/src/test/setup.ts +8 -0
  640. package/tooling/web/manifest.toml +58 -0
  641. package/tooling/web/reference.md +115 -0
  642. package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
  643. package/tsconfig.json +14 -0
@@ -0,0 +1,1286 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { chmod, readFile } from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+ import type { Command } from 'commander'
6
+ import { claudeChain, pendingEntries, planGitignore } from '@/claude/gitignore'
7
+ import {
8
+ applySeeds,
9
+ countByScope,
10
+ pendingSeeds,
11
+ planSeeds,
12
+ type Seed,
13
+ } from '@/claude/seeds'
14
+ import { listSeeds, readSeedContents } from '@/claude/seeds-list'
15
+ import {
16
+ auditExitCode,
17
+ auditSkills,
18
+ CORPORA,
19
+ DESCRIPTION_LIMIT,
20
+ REQUIREMENT_SECTIONS,
21
+ type SkillFinding,
22
+ type SkillsAudit,
23
+ type SkillsAuditRefusal,
24
+ } from '@/claude/skills-audit'
25
+ import {
26
+ type RoutingRefusal,
27
+ type RoutingReport,
28
+ scanRouting,
29
+ } from '@/claude/routing'
30
+ import { type DriftReport, readDrift } from '@/claude/skills-drift'
31
+ import { listSkills } from '@/claude/skills-list'
32
+ import {
33
+ type ReachRefusal,
34
+ type ReachReport,
35
+ scanReach,
36
+ } from '@/claude/skills-reach'
37
+ import { SKILL_CASES } from '@/claude/cases/all'
38
+ import {
39
+ loadCaseCorpus,
40
+ type RankRefusal,
41
+ type RankReport,
42
+ scanRank,
43
+ type SkillCase,
44
+ } from '@/claude/skills-rank'
45
+ import {
46
+ planSettings,
47
+ readSettings,
48
+ serializeSettings,
49
+ writeSettings,
50
+ } from '@/claude/settings'
51
+ import { copyPreservingMode } from '@/copy'
52
+ import { execScript } from '@/exec'
53
+ import { PROJECT_ROOT } from '@/project-root'
54
+ import { isDirectory, resolveTarget } from '@/target'
55
+ import { injectGitignore, pruneGitignore } from '@/tooling/inject'
56
+ import {
57
+ frameError,
58
+ intro,
59
+ isNonInteractive,
60
+ logAdd,
61
+ logError,
62
+ logInfo,
63
+ logStep,
64
+ logWarn,
65
+ outro,
66
+ palette,
67
+ pipeOutput,
68
+ plural,
69
+ select,
70
+ } from '@/ui'
71
+ import { describeSkew, readSkew, type SkewReport } from '@/version/skew'
72
+
73
+ interface SeedsListOptions {
74
+ readonly json?: boolean
75
+ readonly names?: boolean
76
+ }
77
+
78
+ interface SkillsListOptions {
79
+ readonly json?: boolean
80
+ readonly names?: boolean
81
+ }
82
+
83
+ interface SkillsAuditOptions {
84
+ readonly json?: boolean
85
+ readonly requirementsOnly?: boolean
86
+ }
87
+
88
+ interface SkillsDriftOptions {
89
+ readonly json?: boolean
90
+ }
91
+
92
+ interface SkillsReachOptions {
93
+ readonly json?: boolean
94
+ }
95
+
96
+ interface SkillsRankOptions {
97
+ readonly json?: boolean
98
+ readonly cases?: string
99
+ }
100
+
101
+ interface RoutingOptions {
102
+ readonly json?: boolean
103
+ }
104
+
105
+ const SEEDED_FILES: readonly string[] = [
106
+ 'ARCHITECTURE.md',
107
+ 'REQUIREMENTS.md',
108
+ 'DESIGN.md',
109
+ ]
110
+ const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
111
+ const USER_DIR = join('tooling', 'claude', 'user')
112
+ const STATUSLINE = 'statusline-command.sh'
113
+
114
+ export function register(program: Command): void {
115
+ const claude = program
116
+ .command('claude')
117
+ .description('Claude workflow (init, seeds, sync, setup, routing)')
118
+ .helpOption('-h, --help', 'Show this help message')
119
+ .addHelpText(
120
+ 'after',
121
+ [
122
+ '',
123
+ 'Examples:',
124
+ ' canon claude init',
125
+ ' canon claude seeds list --json',
126
+ ' canon claude sync ../my-app',
127
+ '',
128
+ ].join('\n'),
129
+ )
130
+
131
+ claude
132
+ .command('init')
133
+ .description('Seed .claude/ workflow docs into a project')
134
+ .argument('[target]', 'Target directory', '.')
135
+ .helpOption('-h, --help', 'Show this help message')
136
+ .action(async (target: string) => {
137
+ process.exitCode = await runInit(target)
138
+ })
139
+
140
+ claude
141
+ .command('sync')
142
+ .description('Reconcile .gitignore against the claude manifest')
143
+ .argument('[target]', 'Target directory', '.')
144
+ .helpOption('-h, --help', 'Show this help message')
145
+ .action(async (target: string) => {
146
+ process.exitCode = await runSync(target)
147
+ })
148
+
149
+ claude
150
+ .command('setup')
151
+ .description('One-shot user-level config (statusline, attribution)')
152
+ .argument('[dest]', 'User config directory', join(homedir(), '.claude'))
153
+ .helpOption('-h, --help', 'Show this help message')
154
+ .action(async (dest: string) => {
155
+ process.exitCode = await runSetup(dest)
156
+ })
157
+
158
+ const seeds = claude
159
+ .command('seeds')
160
+ .description('Seed doc sources (list)')
161
+ .argument('[subcommand]', "Only 'list' is supported")
162
+ .helpOption('-h, --help', 'Show this help message')
163
+ .action((subcommand: string | undefined) => {
164
+ intro('canon claude')
165
+ logError(
166
+ subcommand === undefined
167
+ ? "Missing subcommand. Use 'list'."
168
+ : `Unknown subcommand: ${subcommand}. Use 'list'.`,
169
+ )
170
+ outro()
171
+ process.exitCode = 1
172
+ })
173
+
174
+ seeds
175
+ .command('list')
176
+ .description('List toolkit seed docs as installed by canon claude init')
177
+ .helpOption('-h, --help', 'Show this help message')
178
+ .option('--json', 'Emit JSON with name, source, target, content')
179
+ .option('--names', 'Only list target paths, one per line')
180
+ .addHelpText(
181
+ 'after',
182
+ [
183
+ '',
184
+ 'Notes:',
185
+ ' JSON is intended for skills that audit drift in target projects.',
186
+ '',
187
+ ].join('\n'),
188
+ )
189
+ .action(async (opts: SeedsListOptions) => {
190
+ process.exitCode = await runSeedsList(opts)
191
+ })
192
+
193
+ claude
194
+ .command('routing')
195
+ .description('Report per CLAUDE.md section how many bullets name a path')
196
+ .argument('[path]', 'Repository root, defaulting to the current directory')
197
+ .helpOption('-h, --help', 'Show this help message')
198
+ .option('--json', 'Add a machine-readable record on stdout')
199
+ .addHelpText(
200
+ 'after',
201
+ [
202
+ '',
203
+ 'Scope:',
204
+ ' Every H2 and H3 in CLAUDE.md that owns at least one top-level',
205
+ ' bullet, counted against the path-scoped rules under .claude/rules/.',
206
+ ' A bullet is path-scoped here when it names a path, which is not the',
207
+ " same as firing only on one. A rule's glob covers a named folder",
208
+ ' only when the glob is anchored to a location rather than to a file',
209
+ ' type, so **/*.md covers README.md and no folder at all.',
210
+ '',
211
+ 'Exit codes:',
212
+ ' 0 the file was read',
213
+ ' 1 refused, with the reason on stderr',
214
+ '',
215
+ 'Reports rather than gates. Whether a bullet belongs in a rule is the',
216
+ 'judgment 592-claude-md states, and naming a path is evidence for it',
217
+ 'rather than the answer.',
218
+ '',
219
+ 'Examples:',
220
+ ' canon claude routing',
221
+ ' canon claude routing --json',
222
+ '',
223
+ ].join('\n'),
224
+ )
225
+ .action((path: string | undefined, opts: RoutingOptions) => {
226
+ process.exitCode = runRouting(path, opts)
227
+ })
228
+
229
+ const skills = claude
230
+ .command('skills')
231
+ .description('Plugin skill catalog (list, audit, drift, reach, rank)')
232
+ .argument(
233
+ '[subcommand]',
234
+ "One of 'list', 'audit', 'drift', 'reach', or 'rank'",
235
+ )
236
+ .helpOption('-h, --help', 'Show this help message')
237
+ .action((subcommand: string | undefined) => {
238
+ intro('canon claude')
239
+ logError(
240
+ subcommand === undefined
241
+ ? "Missing subcommand. Use 'list', 'audit', 'drift', 'reach', or 'rank'."
242
+ : `Unknown subcommand: ${subcommand}. Use 'list', 'audit', 'drift', 'reach', or 'rank'.`,
243
+ )
244
+ outro()
245
+ process.exitCode = 1
246
+ })
247
+
248
+ skills
249
+ .command('list')
250
+ .description('List the plugin skills shipped under claude/skills/')
251
+ .helpOption('-h, --help', 'Show this help message')
252
+ .option('--json', 'Emit JSON with name and description')
253
+ .option('--names', 'Only list skill names, one per line')
254
+ .addHelpText(
255
+ 'after',
256
+ [
257
+ '',
258
+ 'Notes:',
259
+ ' Internal skills under .claude/skills/ are excluded, since they',
260
+ ' never install into a target project.',
261
+ '',
262
+ ].join('\n'),
263
+ )
264
+ .action((opts: SkillsListOptions) => {
265
+ process.exitCode = runSkillsList(opts)
266
+ })
267
+
268
+ skills
269
+ .command('audit')
270
+ .description(
271
+ 'Report both skill corpora against the mechanical rules in standards/skill.md',
272
+ )
273
+ .argument('[path]', 'Project root, defaulting to the current directory')
274
+ .helpOption('-h, --help', 'Show this help message')
275
+ .option('--json', 'Add a machine-readable record on stdout')
276
+ .option(
277
+ '--requirements-only',
278
+ 'Run the gating requirement-presence check alone',
279
+ )
280
+ .addHelpText(
281
+ 'after',
282
+ [
283
+ '',
284
+ 'Exit codes:',
285
+ ' 0 the audit completed with every skill carrying a requirement',
286
+ ' 1 refused, with the reason on stderr',
287
+ ' 2 a skill folder carries no REQUIREMENT.md',
288
+ '',
289
+ 'Only a missing REQUIREMENT.md sets a failing exit code. Name, description,',
290
+ 'folder, and requirement-section findings are advisory.',
291
+ '',
292
+ 'Examples:',
293
+ ' canon claude skills audit',
294
+ ' canon claude skills audit --json',
295
+ ' canon claude skills audit --requirements-only',
296
+ '',
297
+ ].join('\n'),
298
+ )
299
+ .action(async (path: string | undefined, opts: SkillsAuditOptions) => {
300
+ process.exitCode = await runSkillsAudit(path, opts)
301
+ })
302
+
303
+ skills
304
+ .command('drift')
305
+ .description('Name the shipped skill bodies rewritten since a given ref')
306
+ .argument('<ref>', 'The commit a session started from')
307
+ .helpOption('-h, --help', 'Show this help message')
308
+ .option('--json', 'Add a machine-readable record on stdout')
309
+ .addHelpText(
310
+ 'after',
311
+ [
312
+ '',
313
+ 'Exit codes:',
314
+ ' 0 history was read, whether or not a body moved',
315
+ ' 1 the question could not be answered, with the reason on stderr',
316
+ '',
317
+ 'A moved body means the file changed, not that a session holds a stale',
318
+ 'copy. Passing a ref older than the oldest load over-reports, which is',
319
+ 'the safe direction. Confirm a name by reading the body.',
320
+ '',
321
+ 'Every run also reports the installed version against the newest',
322
+ 'published one. That report never changes the exit code, so an offline',
323
+ 'machine reads it as unknown rather than as a failure.',
324
+ '',
325
+ 'Examples:',
326
+ ' canon claude skills drift HEAD~20',
327
+ ' canon claude skills drift 02d7b265 --json',
328
+ '',
329
+ ].join('\n'),
330
+ )
331
+ .action(async (ref: string, opts: SkillsDriftOptions) => {
332
+ process.exitCode = await runSkillsDrift(ref, opts)
333
+ })
334
+
335
+ skills
336
+ .command('reach')
337
+ .description('Report skill bodies citing a path no target receives')
338
+ .argument('[path]', 'Project root, defaulting to the current directory')
339
+ .helpOption('-h, --help', 'Show this help message')
340
+ .option('--json', 'Add a machine-readable record on stdout')
341
+ .addHelpText(
342
+ 'after',
343
+ [
344
+ '',
345
+ 'Scope:',
346
+ ' Every markdown file under claude/skills/, which is the tree that',
347
+ ' installs into a target, or under .claude/skills/ in a project',
348
+ ' carrying that corpus alone. A cited path counts when it sits under',
349
+ ' an authoring root no install channel delivers and the project',
350
+ ' holds it. A path under src/, scripts/, or bare docs/ names the',
351
+ " reader's own tree and is not measured, and .claude/context/ joins",
352
+ " them when the corpus read is a project's own.",
353
+ '',
354
+ 'Exit codes:',
355
+ ' 0 every citation names the toolkit as the owner',
356
+ ' 1 refused, with the reason on stderr',
357
+ ' 2 at least one citation is unqualified',
358
+ '',
359
+ 'Reports rather than gates. A toolkit-scoped instruction is sometimes',
360
+ 'meant for a session in this repository, so the verdict is a reading',
361
+ 'and the repair is to name the owner in the sentence.',
362
+ '',
363
+ 'Examples:',
364
+ ' canon claude skills reach',
365
+ ' canon claude skills reach --json',
366
+ ' canon claude skills reach ~/repos/my-project',
367
+ '',
368
+ ].join('\n'),
369
+ )
370
+ .action((path: string | undefined, opts: SkillsReachOptions) => {
371
+ process.exitCode = runSkillsReach(path, opts)
372
+ })
373
+
374
+ skills
375
+ .command('rank')
376
+ .description('Score a skill catalog against a routing case corpus')
377
+ .argument('[path]', 'Project root, defaulting to the current directory')
378
+ .helpOption('-h, --help', 'Show this help message')
379
+ .option('--json', 'Add a machine-readable record on stdout')
380
+ .option(
381
+ '--cases <path>',
382
+ "A project's own case corpus as JSON, replacing the toolkit's",
383
+ )
384
+ .addHelpText(
385
+ 'after',
386
+ [
387
+ '',
388
+ 'Scope:',
389
+ ' TF-IDF cosine similarity over every SKILL.md frontmatter',
390
+ ' description under claude/skills/, or under .claude/skills/ in a',
391
+ ' project carrying that corpus alone, scored against the',
392
+ ' hand-authored corpus at src/claude/cases/. A necessary condition',
393
+ ' rather than a report of real routing behavior: it asks whether the',
394
+ ' descriptions are separable by the words they use, and Claude Code',
395
+ ' does not route this way.',
396
+ '',
397
+ 'The case corpus:',
398
+ ' --cases takes a JSON array of { "prompt", "expect" } objects, the',
399
+ ' shape src/claude/cases/ already holds, where expect is a skill',
400
+ " folder name. A project's own skills need its own prompts, so the",
401
+ ' toolkit corpus is not a default anything else can measure against.',
402
+ ' No standard stands behind the file until a third project needs one.',
403
+ '',
404
+ 'Exit codes:',
405
+ ' 0 the catalog was read, whether or not a case missed rank one',
406
+ ' 1 refused, with the reason on stderr',
407
+ '',
408
+ 'Reports rather than gates. The corpus is a first run with no',
409
+ 'baseline to fail a push against, so `canon audits run` registers',
410
+ 'this with no gating exit and joins the ratchet instead.',
411
+ '',
412
+ 'Examples:',
413
+ ' canon claude skills rank',
414
+ ' canon claude skills rank --json',
415
+ ' canon claude skills rank ~/repos/my-project --cases cases.json',
416
+ '',
417
+ ].join('\n'),
418
+ )
419
+ .action((path: string | undefined, opts: SkillsRankOptions) => {
420
+ process.exitCode = runSkillsRank(path, opts)
421
+ })
422
+ }
423
+
424
+ function succeed(message: string): number {
425
+ const { GREEN, NC } = palette(process.stderr)
426
+ outro()
427
+ process.stderr.write(`${GREEN}✓ ${message}${NC}\n`)
428
+ return 0
429
+ }
430
+
431
+ async function runInit(target: string): Promise<number> {
432
+ intro('canon claude')
433
+
434
+ const resolved = resolveTarget(target, PROJECT_ROOT)
435
+ if (typeof resolved === 'number') return resolved
436
+
437
+ logStep('Scanning .claude/')
438
+ const seedEntries = planSeeds(PROJECT_ROOT, resolved)
439
+ for (const entry of seedEntries) {
440
+ if (entry.present) logInfo(entry.seed.scanLabel)
441
+ else logAdd(entry.seed.scanLabel)
442
+ }
443
+ const seeds = pendingSeeds(seedEntries)
444
+
445
+ logStep('Scanning .gitignore')
446
+ const chain = claudeChain(PROJECT_ROOT)
447
+ const gitignoreEntries = planGitignore(chain, resolved)
448
+ for (const entry of gitignoreEntries) {
449
+ if (entry.present) logInfo(entry.entry)
450
+ else logAdd(entry.entry)
451
+ }
452
+ const missing = pendingEntries(gitignoreEntries)
453
+
454
+ const total = seeds.length + missing.length
455
+ if (total === 0) return succeed('Claude already initialized')
456
+
457
+ const shouldApply = await select({
458
+ message: `Apply ${total} change(s) (${summarize(seeds, missing.length)})?`,
459
+ options: [
460
+ { value: true, label: 'Apply all' },
461
+ { value: false, label: 'Cancel' },
462
+ ],
463
+ nonInteractiveDefault: true,
464
+ })
465
+
466
+ if (!shouldApply) {
467
+ logWarn('Cancelled')
468
+ outro()
469
+ return 0
470
+ }
471
+
472
+ logStep('Applying changes')
473
+ for (const label of await applySeeds(seeds)) logAdd(label)
474
+ if (missing.length > 0) await injectGitignore(chain, resolved)
475
+
476
+ return succeed('Claude ready')
477
+ }
478
+
479
+ function summarize(seeds: readonly Seed[], gitignoreCount: number): string {
480
+ const counts = countByScope(seeds)
481
+ const parts: string[] = []
482
+ if (counts.claude > 0) parts.push(`${counts.claude} .claude`)
483
+ if (counts.root > 0) parts.push(`${counts.root} CLAUDE.md`)
484
+ if (gitignoreCount > 0) parts.push(`${gitignoreCount} .gitignore`)
485
+ return parts.join(', ')
486
+ }
487
+
488
+ async function runSync(target: string): Promise<number> {
489
+ intro('canon claude')
490
+
491
+ const resolved = resolveTarget(target, PROJECT_ROOT)
492
+ if (typeof resolved === 'number') return resolved
493
+
494
+ logStep('Seeded')
495
+ for (const name of SEEDED_FILES) {
496
+ if (existsSync(join(resolved, '.claude', name))) logInfo(name)
497
+ else logWarn(`${name} missing. Run \`canon claude init\``)
498
+ }
499
+ for (const name of SEEDED_DIRS) {
500
+ if (isDirectory(join(resolved, '.claude', name))) logInfo(`${name}/`)
501
+ else logWarn(`${name}/ missing. Run \`canon claude init\``)
502
+ }
503
+
504
+ logStep('Scanning .gitignore')
505
+ const chain = claudeChain(PROJECT_ROOT)
506
+ const pruned = await pruneGitignore(chain, resolved)
507
+ const gitignoreEntries = planGitignore(chain, resolved)
508
+ for (const entry of gitignoreEntries) {
509
+ if (entry.present) logInfo(entry.entry)
510
+ else logAdd(entry.entry)
511
+ }
512
+ const missing = pendingEntries(gitignoreEntries)
513
+
514
+ if (missing.length === 0 && pruned.length === 0) {
515
+ return succeed('Claude workflow up to date')
516
+ }
517
+
518
+ if (missing.length > 0) {
519
+ if (isNonInteractive()) {
520
+ logInfo(`Applying ${missing.length} update(s) (non-interactive)`)
521
+ } else {
522
+ const shouldApply = await select({
523
+ message: `Apply ${missing.length} update(s) (${missing.length} .gitignore)?`,
524
+ options: [
525
+ { value: true, label: 'Apply all' },
526
+ { value: false, label: 'Cancel' },
527
+ ],
528
+ })
529
+
530
+ if (!shouldApply) {
531
+ logWarn('Cancelled')
532
+ outro()
533
+ return 0
534
+ }
535
+ }
536
+
537
+ logStep('Applying changes')
538
+ await injectGitignore(chain, resolved)
539
+ }
540
+
541
+ return succeed('Claude workflow synced')
542
+ }
543
+
544
+ /**
545
+ * Writes to the operator's own machine rather than a project, so the
546
+ * destination is a parameter. That lets a test cover the merge without
547
+ * pointing it at a real home directory.
548
+ */
549
+ async function runSetup(dest: string): Promise<number> {
550
+ intro('canon claude')
551
+
552
+ const resolved = resolve(dest)
553
+ if (resolved === join(PROJECT_ROOT, '.claude')) {
554
+ logError(
555
+ 'Cannot run against the toolkit .claude/. Files here are the source of truth.',
556
+ )
557
+ outro()
558
+ return 1
559
+ }
560
+
561
+ const userDir = join(PROJECT_ROOT, USER_DIR)
562
+ const scriptSrc = join(userDir, STATUSLINE)
563
+ const scriptDest = join(resolved, STATUSLINE)
564
+ const settingsPath = join(resolved, 'settings.json')
565
+
566
+ logStep('Statusline script')
567
+ if (await sameContent(scriptSrc, scriptDest)) {
568
+ logInfo(STATUSLINE)
569
+ } else {
570
+ await copyPreservingMode(scriptSrc, scriptDest)
571
+ await chmod(scriptDest, 0o755)
572
+ logAdd(STATUSLINE)
573
+ }
574
+
575
+ logStep('Settings')
576
+ const template = await readSettings(join(userDir, 'settings.template.json'))
577
+
578
+ let current: Awaited<ReturnType<typeof readSettings>>
579
+ try {
580
+ current = await readSettings(settingsPath)
581
+ } catch (error) {
582
+ logError(error instanceof Error ? error.message : String(error))
583
+ logError('Fix the file by hand, or move it aside and rerun.')
584
+ outro()
585
+ return 1
586
+ }
587
+
588
+ const { value, indent } = current
589
+ const plan = planSettings(value, template.value, `bash ${scriptDest}`)
590
+
591
+ for (const key of plan.keys) {
592
+ if (key.changed) logAdd(key.label)
593
+ else logInfo(key.label)
594
+ }
595
+
596
+ if (plan.changed) {
597
+ await writeSettings(settingsPath, serializeSettings(plan.next, indent))
598
+ }
599
+
600
+ return succeed('Claude user config ready')
601
+ }
602
+
603
+ async function sameContent(src: string, dest: string): Promise<boolean> {
604
+ if (!existsSync(dest)) return false
605
+ const [left, right] = await Promise.all([
606
+ readFile(src, 'utf8'),
607
+ readFile(dest, 'utf8'),
608
+ ])
609
+ return left === right
610
+ }
611
+
612
+ /**
613
+ * `--json` and `--names` write to stdout so a skill can pipe them, while the
614
+ * human listing stays on the timeline. Only the human mode opens a frame.
615
+ */
616
+ async function runSeedsList(opts: SeedsListOptions): Promise<number> {
617
+ const listings = listSeeds(PROJECT_ROOT)
618
+
619
+ if (opts.json) {
620
+ const withContent = await readSeedContents(listings)
621
+ process.stdout.write(`${JSON.stringify(withContent)}\n`)
622
+ return 0
623
+ }
624
+
625
+ if (opts.names) {
626
+ process.stdout.write(
627
+ listings.map((listing) => listing.target).join('\n') + '\n',
628
+ )
629
+ return 0
630
+ }
631
+
632
+ const { GREY, NC } = palette(process.stderr)
633
+ intro('canon claude')
634
+ logStep('Seed docs')
635
+ for (const listing of listings) {
636
+ logInfo(`${listing.target} ${GREY}← ${listing.source}${NC}`)
637
+ }
638
+ outro()
639
+ return 0
640
+ }
641
+
642
+ function runSkillsList(opts: SkillsListOptions): number {
643
+ const listings = listSkills(PROJECT_ROOT)
644
+
645
+ if (opts.json) {
646
+ process.stdout.write(`${JSON.stringify({ skills: listings })}\n`)
647
+ return 0
648
+ }
649
+
650
+ if (opts.names) {
651
+ process.stdout.write(
652
+ listings.map((listing) => listing.name).join('\n') + '\n',
653
+ )
654
+ return 0
655
+ }
656
+
657
+ intro('canon claude')
658
+ logStep('Plugin skills')
659
+ for (const listing of listings) {
660
+ logInfo(listing.name)
661
+ }
662
+ outro()
663
+ return 0
664
+ }
665
+
666
+ /**
667
+ * Measures the cwd for the same reason the audit does, and takes the ref as a
668
+ * required argument with no default. `HEAD` would be the only defensible one and
669
+ * it answers every run with nothing moved, which is the silence this reports
670
+ * against.
671
+ */
672
+ async function runSkillsDrift(
673
+ ref: string,
674
+ opts: SkillsDriftOptions,
675
+ ): Promise<number> {
676
+ const root = process.cwd()
677
+ const report = readDrift(root, ref)
678
+ const skew = await readSkew()
679
+
680
+ if (report.kind === 'measured') {
681
+ intro('canon claude skills drift')
682
+ reportSkew(skew)
683
+ reportDrift(report, ref)
684
+ outro()
685
+ } else {
686
+ frameError(report.reason)
687
+ // The refusal path names the binary too. A project consuming the plugin
688
+ // from a marketplace cache is refused here for having no history, and that
689
+ // is the moment a skew warning is worth most, since an old binary is one
690
+ // reason the cache and the CLI disagree in the first place.
691
+ if (skew.state === 'behind') {
692
+ const { GREY, NC } = palette(process.stderr)
693
+ process.stderr.write(`${GREY}${describeSkew(skew)}${NC}\n`)
694
+ }
695
+ }
696
+
697
+ if (opts.json) {
698
+ process.stdout.write(
699
+ `${JSON.stringify(
700
+ report.kind === 'measured'
701
+ ? {
702
+ root,
703
+ ref,
704
+ base: report.base,
705
+ head: report.head,
706
+ moved: report.moved,
707
+ skew,
708
+ }
709
+ : { root, ref, unreadable: report.reason, skew },
710
+ )}\n`,
711
+ )
712
+ }
713
+
714
+ return report.kind === 'measured' ? 0 : 1
715
+ }
716
+
717
+ /**
718
+ * The binary reports before the range does, for the reason the range section
719
+ * states about itself: the command answers what changed on disk, and a binary
720
+ * behind the published one is a second way the tree a session reads differs
721
+ * from the tree it holds.
722
+ */
723
+ function reportSkew(skew: SkewReport): void {
724
+ logStep('Toolkit version')
725
+ if (skew.state === 'behind') logWarn(describeSkew(skew))
726
+ else logInfo(describeSkew(skew))
727
+ }
728
+
729
+ /** What a reader does about each way the reading cannot be taken. */
730
+ const ROUTING_REFUSALS: Record<RoutingRefusal, string> = {
731
+ 'no-claude-md': 'No CLAUDE.md here, so this tree has no always-loaded file.',
732
+ 'no-rules':
733
+ 'No path-scoped rules under .claude/rules/, so nothing covers a path yet.',
734
+ }
735
+
736
+ /**
737
+ * Measures the cwd rather than the toolkit root, matching the reach and drift
738
+ * verbs, so a linked worktree reads its own branch instead of `main`.
739
+ */
740
+ function runRouting(path: string | undefined, opts: RoutingOptions): number {
741
+ const root = resolve(path ?? process.cwd())
742
+ const report = scanRouting(root)
743
+
744
+ if (report.kind === 'refused') {
745
+ frameError(ROUTING_REFUSALS[report.reason])
746
+ if (opts.json) {
747
+ process.stdout.write(
748
+ `${JSON.stringify({
749
+ root,
750
+ reason: report.reason,
751
+ message: ROUTING_REFUSALS[report.reason],
752
+ })}\n`,
753
+ )
754
+ }
755
+ return 1
756
+ }
757
+
758
+ intro('canon claude routing')
759
+ reportRouting(report)
760
+ outro()
761
+
762
+ if (opts.json) {
763
+ process.stdout.write(
764
+ `${JSON.stringify({
765
+ root,
766
+ rules: report.rules,
767
+ sections: report.sections,
768
+ })}\n`,
769
+ )
770
+ }
771
+
772
+ return 0
773
+ }
774
+
775
+ /**
776
+ * States the corpus on every run, so a section naming no path reads as
777
+ * measured rather than as skipped. A reader deciding what to cut needs the
778
+ * sections that stay as much as the ones that move.
779
+ */
780
+ function reportRouting(
781
+ report: Extract<RoutingReport, { kind: 'measured' }>,
782
+ ): void {
783
+ const bullets = report.sections.reduce(
784
+ (total, section) => total + section.bullets,
785
+ 0,
786
+ )
787
+ const pathScoped = report.sections.reduce(
788
+ (total, section) => total + section.pathScoped,
789
+ 0,
790
+ )
791
+
792
+ logStep('Corpus')
793
+ logInfo(
794
+ `${plural(report.sections.length, 'section')} carrying ${plural(bullets, 'bullet')}, read against ${plural(report.rules, 'path-scoped rule')}`,
795
+ )
796
+
797
+ logStep('Sections')
798
+ logInfo(`${pathScoped} of ${bullets} bullets name a path`)
799
+ pipeOutput(
800
+ report.sections
801
+ .map(
802
+ (section) =>
803
+ `${section.pathScoped}/${section.bullets} path-scoped, ${section.covered} covered ${section.heading}${
804
+ section.uncovered.length === 0
805
+ ? ''
806
+ : ` [uncovered: ${section.uncovered.join(', ')}]`
807
+ }`,
808
+ )
809
+ .join('\n'),
810
+ )
811
+ }
812
+
813
+ /** What a reader does about the one way the corpus fails to build. */
814
+ const REACH_REFUSALS: Record<ReachRefusal, string> = {
815
+ 'no-skills':
816
+ 'Neither claude/skills/ nor .claude/skills/ here, so this project carries no skill body to measure.',
817
+ }
818
+
819
+ /**
820
+ * Measures the cwd rather than the toolkit root, matching the audit and drift
821
+ * verbs, so a linked worktree reads its own branch instead of `main`, and a
822
+ * target carrying `.claude/skills/` alone is in scope the way the audit
823
+ * already has it.
824
+ */
825
+ function runSkillsReach(
826
+ path: string | undefined,
827
+ opts: SkillsReachOptions,
828
+ ): number {
829
+ const root = resolve(path ?? process.cwd())
830
+ const report = scanReach(root)
831
+
832
+ if (report.kind === 'refused') {
833
+ frameError(REACH_REFUSALS[report.reason])
834
+ if (opts.json) {
835
+ process.stdout.write(
836
+ `${JSON.stringify({
837
+ root,
838
+ reason: report.reason,
839
+ message: REACH_REFUSALS[report.reason],
840
+ })}\n`,
841
+ )
842
+ }
843
+ return 1
844
+ }
845
+
846
+ intro('canon claude skills reach')
847
+ reportReach(report)
848
+ outro()
849
+
850
+ if (opts.json) {
851
+ process.stdout.write(
852
+ `${JSON.stringify({
853
+ root,
854
+ corpus: report.corpus,
855
+ bodies: report.bodies,
856
+ qualified: report.qualified,
857
+ unqualified: report.unqualified,
858
+ })}\n`,
859
+ )
860
+ }
861
+
862
+ return report.unqualified.length === 0 ? 0 : 2
863
+ }
864
+
865
+ /**
866
+ * States the corpus on every run, including the clean one. A count of what
867
+ * failed reads as a verdict on the catalog unless the run also says how many
868
+ * bodies it opened and how many citations it already accepted.
869
+ */
870
+ function reportReach(report: Extract<ReachReport, { kind: 'measured' }>): void {
871
+ logStep('Corpus')
872
+ logInfo(
873
+ `${report.corpus}: ${plural(report.bodies, 'file')} read, ${plural(report.qualified.length, 'citation')} already naming the toolkit as owner`,
874
+ )
875
+
876
+ logStep('Unqualified citations')
877
+ if (report.unqualified.length === 0) {
878
+ logInfo('Every toolkit-owned path a shipped body cites names its owner.')
879
+ return
880
+ }
881
+
882
+ logWarn(plural(report.unqualified.length, 'citation'))
883
+ pipeOutput(
884
+ report.unqualified
885
+ .map((citation) => `${citation.file}:${citation.line} ${citation.path}`)
886
+ .join('\n'),
887
+ )
888
+ }
889
+
890
+ /** What a reader does about each way the measure fails to build. */
891
+ const RANK_REFUSALS: Record<RankRefusal, string> = {
892
+ 'no-skills':
893
+ 'Neither claude/skills/ nor .claude/skills/ here, so this project carries no skill body to measure.',
894
+ 'no-cases': 'No case corpus at the path given to --cases.',
895
+ 'bad-cases':
896
+ 'The case corpus is not a JSON array of { "prompt", "expect" } objects.',
897
+ }
898
+
899
+ /**
900
+ * Measures the cwd rather than the toolkit root, matching the reach and audit
901
+ * verbs, so a linked worktree reads its own branch instead of `main`, and a
902
+ * target carrying `.claude/skills/` alone is in scope.
903
+ *
904
+ * The toolkit's own cases are the default and answer for this catalog alone.
905
+ * A project measuring its own skills supplies its own prompts through
906
+ * `--cases`, since a corpus written against skills it did not author scores
907
+ * vocabulary it never uses.
908
+ */
909
+ function runSkillsRank(
910
+ path: string | undefined,
911
+ opts: SkillsRankOptions,
912
+ ): number {
913
+ const root = resolve(path ?? process.cwd())
914
+
915
+ let cases: readonly SkillCase[] = SKILL_CASES
916
+ if (opts.cases !== undefined) {
917
+ const corpus = loadCaseCorpus(resolve(opts.cases))
918
+ if (corpus.kind === 'refused') {
919
+ return refuseRank(root, corpus.reason, corpus.detail, opts)
920
+ }
921
+ cases = corpus.cases
922
+ }
923
+
924
+ const report = scanRank(root, cases)
925
+
926
+ if (report.kind === 'refused') {
927
+ return refuseRank(root, report.reason, '', opts)
928
+ }
929
+
930
+ intro('canon claude skills rank')
931
+ reportRank(report)
932
+ outro()
933
+
934
+ if (opts.json) {
935
+ process.stdout.write(
936
+ `${JSON.stringify({
937
+ root,
938
+ corpus: report.corpus,
939
+ skills: report.skills,
940
+ cases: report.cases,
941
+ rank1: report.rank1,
942
+ top3: report.top3,
943
+ misses: report.misses,
944
+ unmeasurable: report.unmeasurable,
945
+ })}\n`,
946
+ )
947
+ }
948
+
949
+ return 0
950
+ }
951
+
952
+ /**
953
+ * Carries the detail beside the reason, since three refusals share one verb
954
+ * and only one of them names a path the caller can correct without it.
955
+ */
956
+ function refuseRank(
957
+ root: string,
958
+ reason: RankRefusal,
959
+ detail: string,
960
+ opts: SkillsRankOptions,
961
+ ): number {
962
+ const message = RANK_REFUSALS[reason]
963
+ frameError(detail === '' ? message : `${message} ${detail}`)
964
+
965
+ if (opts.json) {
966
+ process.stdout.write(
967
+ `${JSON.stringify({ root, reason, message, detail })}\n`,
968
+ )
969
+ }
970
+
971
+ return 1
972
+ }
973
+
974
+ /**
975
+ * States the corpus and both counts on every run, including a clean one. A
976
+ * miss list alone reads as a verdict on the catalog unless the run also says
977
+ * how many skills and cases it measured against.
978
+ */
979
+ function reportRank(report: Extract<RankReport, { kind: 'measured' }>): void {
980
+ logStep('Corpus')
981
+ logInfo(
982
+ `${report.corpus}: ${plural(report.skills, 'skill')} scored against ${plural(report.cases, 'case')}`,
983
+ )
984
+
985
+ logStep('Score')
986
+ logInfo(
987
+ `rank one: ${report.rank1}/${report.cases}, top three: ${report.top3}/${report.cases}`,
988
+ )
989
+
990
+ if (report.unmeasurable.length > 0) {
991
+ logWarn(plural(report.unmeasurable.length, 'unmeasurable case'))
992
+ pipeOutput(
993
+ report.unmeasurable
994
+ .map((skillCase) => `${skillCase.expect} ${skillCase.prompt}`)
995
+ .join('\n'),
996
+ )
997
+ }
998
+
999
+ if (report.misses.length === 0) {
1000
+ logInfo('Every measurable case ranked its expected skill first.')
1001
+ return
1002
+ }
1003
+
1004
+ logWarn(plural(report.misses.length, 'collision'))
1005
+ pipeOutput(
1006
+ report.misses
1007
+ .map(
1008
+ (miss) =>
1009
+ `${miss.expect} lost to ${miss.won} (rank ${miss.rank}) ${miss.prompt}`,
1010
+ )
1011
+ .join('\n'),
1012
+ )
1013
+ }
1014
+
1015
+ /**
1016
+ * States the bound on every run, including the run that names nothing. A report
1017
+ * listing only what moved reads as a verdict on what a session holds, and the
1018
+ * command has no access to that.
1019
+ */
1020
+ function reportDrift(
1021
+ report: Extract<DriftReport, { kind: 'measured' }>,
1022
+ ref: string,
1023
+ ): void {
1024
+ logStep('Range')
1025
+ logInfo(`${ref} to HEAD, resolved as ${report.base}..${report.head}.`)
1026
+ logInfo(
1027
+ 'A body here changed on disk. Whether a session still holds the old one is what reading it settles.',
1028
+ )
1029
+
1030
+ logStep('Moved bodies')
1031
+ if (report.moved.length === 0) {
1032
+ logInfo('No shipped body changed in this range.')
1033
+ return
1034
+ }
1035
+
1036
+ const count = report.moved.length
1037
+ logWarn(
1038
+ `${count} skill ${count === 1 ? 'body' : 'bodies'} rewritten since ${ref}`,
1039
+ )
1040
+ pipeOutput(
1041
+ report.moved
1042
+ .map((moved) => `${moved.name} ${moved.commit.slice(0, 8)}`)
1043
+ .join('\n'),
1044
+ )
1045
+ }
1046
+
1047
+ /**
1048
+ * Measures the tree at the cwd rather than the toolkit root the catalog reads,
1049
+ * so a linked worktree audits its own branch instead of reporting on `main`. A
1050
+ * target carrying `.claude/skills/` alone is in scope for the same reason.
1051
+ */
1052
+ async function runSkillsAudit(
1053
+ path: string | undefined,
1054
+ opts: SkillsAuditOptions,
1055
+ ): Promise<number> {
1056
+ const root = resolve(path ?? process.cwd())
1057
+ const gateOnly = opts.requirementsOnly ?? false
1058
+ const report = await auditSkills(root)
1059
+
1060
+ if (report.corpora.length === 0) {
1061
+ return refuseAudit(
1062
+ 'no-corpus',
1063
+ `No skill corpus under ${root}. Looked for ${CORPORA.join(' and ')}.`,
1064
+ gateOnly,
1065
+ root,
1066
+ opts.json ?? false,
1067
+ )
1068
+ }
1069
+
1070
+ if (gateOnly) {
1071
+ reportRequirementGate(report)
1072
+ } else {
1073
+ intro('canon claude skills audit')
1074
+ reportScope(report)
1075
+ reportRequirements(report)
1076
+ reportFrontmatter(report)
1077
+ reportFolder(report)
1078
+ reportRequirementShape(report)
1079
+ reportUnmeasured()
1080
+ outro()
1081
+ }
1082
+
1083
+ if (opts.json) {
1084
+ process.stdout.write(
1085
+ `${JSON.stringify({
1086
+ root,
1087
+ corpora: report.corpora.map((corpus) => ({
1088
+ path: corpus.rel,
1089
+ skills: corpus.skills,
1090
+ })),
1091
+ skills: report.skills,
1092
+ findings: {
1093
+ missingRequirement: report.missingRequirement,
1094
+ nameMismatch: report.nameMismatch,
1095
+ missingDescription: report.missingDescription,
1096
+ longDescription: report.longDescription,
1097
+ readme: report.readme,
1098
+ folderName: report.folderName,
1099
+ requirementSections: report.requirementSections,
1100
+ },
1101
+ checkpoints: {
1102
+ descriptionLimit: DESCRIPTION_LIMIT,
1103
+ requirementSections: REQUIREMENT_SECTIONS,
1104
+ corpora: CORPORA,
1105
+ },
1106
+ })}\n`,
1107
+ )
1108
+ }
1109
+
1110
+ return auditExitCode(report)
1111
+ }
1112
+
1113
+ function refuseAudit(
1114
+ reason: SkillsAuditRefusal,
1115
+ message: string,
1116
+ gateOnly: boolean,
1117
+ root: string,
1118
+ emitJson: boolean,
1119
+ ): number {
1120
+ if (gateOnly) {
1121
+ frameError(message)
1122
+ } else {
1123
+ intro('canon claude skills audit')
1124
+ logStep('Refused')
1125
+ logWarn(message)
1126
+ outro()
1127
+ }
1128
+
1129
+ if (emitJson) {
1130
+ process.stdout.write(`${JSON.stringify({ root, reason, message })}\n`)
1131
+ }
1132
+
1133
+ return 1
1134
+ }
1135
+
1136
+ /**
1137
+ * Prints nothing when every skill carries a requirement.
1138
+ *
1139
+ * `--requirements-only` is what the merge gate runs on every push, and it pipes
1140
+ * a stage's whole output into its own frame. A passing gate that printed
1141
+ * its frame would nest one inside the other on every contributor's push.
1142
+ */
1143
+ function reportRequirementGate(report: SkillsAudit): void {
1144
+ const missing = report.missingRequirement
1145
+ if (missing.length === 0) return
1146
+
1147
+ intro('canon claude skills audit')
1148
+ logError(
1149
+ missing.length === 1
1150
+ ? '1 skill folder carries no REQUIREMENT.md'
1151
+ : `${missing.length} skill folders carry no REQUIREMENT.md`,
1152
+ )
1153
+ pipeOutput(missing.join('\n'))
1154
+ outro()
1155
+ }
1156
+
1157
+ function reportFindings(findings: readonly SkillFinding[]): void {
1158
+ pipeOutput(
1159
+ findings.map((found) => `${found.rel} ${found.detail}`).join('\n'),
1160
+ )
1161
+ }
1162
+
1163
+ /**
1164
+ * Names each corpus that resolved, since a corpus the tree does not carry is
1165
+ * skipped silently and a count taken over one of the two reads as the whole.
1166
+ */
1167
+ function reportScope(report: SkillsAudit): void {
1168
+ logStep('Scope')
1169
+ for (const corpus of report.corpora) {
1170
+ logInfo(`${corpus.rel}: ${plural(corpus.skills, 'skill')}`)
1171
+ }
1172
+ }
1173
+
1174
+ function reportRequirements(report: SkillsAudit): void {
1175
+ logStep('Requirements')
1176
+ logInfo('Every skill folder carries REQUIREMENT.md beside SKILL.md.')
1177
+ logInfo('This is the only measure here that fails a run.')
1178
+
1179
+ if (report.missingRequirement.length === 0) {
1180
+ logInfo('Every skill carries one.')
1181
+ return
1182
+ }
1183
+
1184
+ logWarn(`${plural(report.missingRequirement.length, 'skill')} without one`)
1185
+ pipeOutput(report.missingRequirement.join('\n'))
1186
+ }
1187
+
1188
+ function reportFrontmatter(report: SkillsAudit): void {
1189
+ logStep('Frontmatter')
1190
+ logInfo(
1191
+ `name matches the folder, and description is present and under ${DESCRIPTION_LIMIT} characters.`,
1192
+ )
1193
+ logInfo('A body whose frontmatter does not parse reads as declaring neither.')
1194
+
1195
+ const findings =
1196
+ report.nameMismatch.length +
1197
+ report.missingDescription.length +
1198
+ report.longDescription.length
1199
+ if (findings === 0) {
1200
+ logInfo('Every body declares both fields.')
1201
+ return
1202
+ }
1203
+
1204
+ if (report.nameMismatch.length > 0) {
1205
+ logWarn(
1206
+ `${plural(report.nameMismatch.length, 'body')} whose name does not match its folder`,
1207
+ )
1208
+ reportFindings(report.nameMismatch)
1209
+ }
1210
+
1211
+ if (report.missingDescription.length > 0) {
1212
+ logWarn(
1213
+ `${plural(report.missingDescription.length, 'body')} without a description`,
1214
+ )
1215
+ pipeOutput(report.missingDescription.join('\n'))
1216
+ }
1217
+
1218
+ if (report.longDescription.length > 0) {
1219
+ logWarn(
1220
+ `${plural(report.longDescription.length, 'description')} past the ${DESCRIPTION_LIMIT}-character ceiling`,
1221
+ )
1222
+ reportFindings(report.longDescription)
1223
+ }
1224
+ }
1225
+
1226
+ function reportFolder(report: SkillsAudit): void {
1227
+ logStep('Folder')
1228
+ logInfo(
1229
+ 'No README.md inside a skill folder, and a folder name in kebab-case carrying no capital or underscore.',
1230
+ )
1231
+
1232
+ if (report.readme.length === 0 && report.folderName.length === 0) {
1233
+ logInfo('Every folder conforms.')
1234
+ return
1235
+ }
1236
+
1237
+ if (report.readme.length > 0) {
1238
+ logWarn(`${plural(report.readme.length, 'folder')} carrying a README.md`)
1239
+ pipeOutput(report.readme.join('\n'))
1240
+ }
1241
+
1242
+ if (report.folderName.length > 0) {
1243
+ logWarn(
1244
+ `${plural(report.folderName.length, 'folder name')} outside kebab-case`,
1245
+ )
1246
+ pipeOutput(report.folderName.join('\n'))
1247
+ }
1248
+ }
1249
+
1250
+ function reportRequirementShape(report: SkillsAudit): void {
1251
+ logStep('Requirement shape')
1252
+ logInfo(
1253
+ `Each REQUIREMENT.md declares ${REQUIREMENT_SECTIONS.join(' and ')}, matched at any heading level.`,
1254
+ )
1255
+ logInfo(
1256
+ 'A folder carrying no requirement is reported above rather than counted twice here.',
1257
+ )
1258
+
1259
+ if (report.requirementSections.length === 0) {
1260
+ logInfo('Every requirement declares both.')
1261
+ return
1262
+ }
1263
+
1264
+ logWarn(
1265
+ `${plural(report.requirementSections.length, 'requirement')} short a declared section`,
1266
+ )
1267
+ reportFindings(report.requirementSections)
1268
+ }
1269
+
1270
+ /**
1271
+ * Stated on every run, including the run where everything above passed. A
1272
+ * report that lists only what it measured reads as a verdict on the standard
1273
+ * rather than on the half of it a parser can reach.
1274
+ */
1275
+ function reportUnmeasured(): void {
1276
+ logStep('Unmeasured')
1277
+ logInfo(
1278
+ 'The standard states rules no parser reads, and a pass above says nothing about them.',
1279
+ )
1280
+ logInfo(
1281
+ 'Whether each Must traces to a stated gap, whether a gap reads as an observed failure rather than an intent, and whether a description routes.',
1282
+ )
1283
+ logInfo(
1284
+ 'The 150-line body checkpoint is mechanical and still absent here, and it would print a count rather than a defect.',
1285
+ )
1286
+ }