@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,358 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { mkdir, writeFile } from 'node:fs/promises'
4
+ import { dirname, join, sep } from 'node:path'
5
+ import { execa } from 'execa'
6
+ import { recordTarget } from '@/targets/registry'
7
+
8
+ /**
9
+ * Domains the stamp can record. Governance attributes file by file through the
10
+ * sync engine. Tooling runs its own inject and manifest machinery, so it
11
+ * records the stack chain it resolved instead and carries no file hashes.
12
+ *
13
+ * A stamp written before the standards or snippets install channel closed
14
+ * still carries a `standards` or `snippets` record. `isStamp` ignores the key
15
+ * and `sortDomains` drops it on the next write, so the target loses a domain
16
+ * nothing can refresh rather than losing the whole file.
17
+ */
18
+ export const STAMP_DOMAINS = ['governance', 'tooling'] as const
19
+
20
+ export type StampDomain = (typeof STAMP_DOMAINS)[number]
21
+
22
+ /** What a domain needs to stamp itself: its key, and the clone to date it against. */
23
+ export interface StampSource {
24
+ readonly domain: StampDomain
25
+ readonly toolkitRoot: string
26
+ }
27
+
28
+ /** Target-relative posix path to content hash, for one domain. */
29
+ export type DomainHashes = Readonly<Record<string, string>>
30
+
31
+ /**
32
+ * One domain's record. The anchor sits here rather than at the top level
33
+ * because domains sync independently, and a shared anchor would let a gov sync
34
+ * advance the revision standards measures its upstream range from.
35
+ */
36
+ export interface DomainStamp {
37
+ /** Toolkit revision this domain last synced from. Absent outside a git clone. */
38
+ readonly commit?: string
39
+ readonly syncedAt: string
40
+ readonly files: DomainHashes
41
+ /**
42
+ * Stack names the install resolved. Tooling records the full ancestor chain,
43
+ * nearest stack first, because a stack that extends another cannot be
44
+ * reinstalled from its leaf alone, and a `--skip` run installs fewer layers
45
+ * than the leaf's own chain would reproduce. Governance records the single
46
+ * stack `canon gov install` was given, since `resolveRules` walks its
47
+ * ancestors internally and a reader needs only the leaf to ask it again.
48
+ */
49
+ readonly chain?: readonly string[]
50
+ }
51
+
52
+ export interface Stamp {
53
+ /** Domains actually stamped in this target, so an unstamped one is legible. */
54
+ readonly covers: readonly StampDomain[]
55
+ readonly domains: Readonly<Partial<Record<StampDomain, DomainStamp>>>
56
+ }
57
+
58
+ export function stampPath(target: string): string {
59
+ return join(target, '.claude', 'canon', 'config.json')
60
+ }
61
+
62
+ /**
63
+ * Where `106115ba` moved the stamp from. No migration shipped with that move,
64
+ * so a target stamped before it still carries its config here, and `readStamp`
65
+ * falls back to this path when the current one is absent.
66
+ *
67
+ * The old tool name is deliberate and this path never renames. It names what a
68
+ * target already has on disk, so rewriting it to the current spelling would
69
+ * point the fallback at a file that has never existed anywhere.
70
+ */
71
+ export function legacyStampPath(target: string): string {
72
+ // canon-keep-retired
73
+ return join(target, '.claude', 'aitk.json')
74
+ }
75
+
76
+ /**
77
+ * The stamp folder under the retired tool name, which is where every target
78
+ * stamped between `106115ba` and the rename carries its config. Retired for
79
+ * the same reason as the path above and kept readable on the same terms.
80
+ */
81
+ export function retiredNameStampPath(target: string): string {
82
+ // canon-keep-retired
83
+ return join(target, '.claude', 'aitk', 'config.json')
84
+ }
85
+
86
+ /**
87
+ * Every spelling a stamp has been written under, current first. The order is
88
+ * the read order, so a target carrying more than one resolves to the newest.
89
+ *
90
+ * The fallback carries no end date. It costs two path reads on a command that
91
+ * already touches the filesystem, and dropping it later is a second breaking
92
+ * change aimed at exactly the targets that were slowest to migrate the first
93
+ * time.
94
+ */
95
+ export function stampPaths(target: string): readonly string[] {
96
+ return [
97
+ stampPath(target),
98
+ retiredNameStampPath(target),
99
+ legacyStampPath(target),
100
+ ]
101
+ }
102
+
103
+ /**
104
+ * Whether `readStamp` would resolve to a retired path, so a caller can report
105
+ * that a target's config still sits at one. False when no path exists, since
106
+ * there is nothing to migrate off of.
107
+ */
108
+ export function isLegacyStamped(target: string): boolean {
109
+ if (existsSync(stampPath(target))) return false
110
+ return (
111
+ existsSync(retiredNameStampPath(target)) ||
112
+ existsSync(legacyStampPath(target))
113
+ )
114
+ }
115
+
116
+ export function hashContent(content: Buffer | string): string {
117
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`
118
+ }
119
+
120
+ export function hashFile(path: string): string {
121
+ return hashContent(readFileSync(path))
122
+ }
123
+
124
+ /**
125
+ * Keys are stored posix-style so a stamp written on one platform still
126
+ * resolves on another.
127
+ */
128
+ export function toStampKey(rel: string): string {
129
+ return rel.split(sep).join('/')
130
+ }
131
+
132
+ /**
133
+ * A missing or corrupt stamp reads as absent rather than failing, which is what
134
+ * keeps every unstamped target on the existing unattributed path.
135
+ *
136
+ * Falls back to the retired path only when the current one does not exist,
137
+ * read only: nothing here migrates a target's config as a side effect of a
138
+ * report. The check is existence rather than a successful parse, so a
139
+ * corrupt current stamp reads as absent rather than silently serving the
140
+ * retired one beside it. `isLegacyStamped` tests the same existence check,
141
+ * which is what keeps the two agreeing on which path a corrupt current file
142
+ * was read from.
143
+ */
144
+ export function readStamp(target: string): Stamp | undefined {
145
+ const found = stampPaths(target).find((path) => existsSync(path))
146
+ return found === undefined ? undefined : readStampFile(found)
147
+ }
148
+
149
+ function readStampFile(path: string): Stamp | undefined {
150
+ if (!existsSync(path)) return undefined
151
+
152
+ try {
153
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
154
+ return isStamp(parsed) ? parsed : undefined
155
+ } catch {
156
+ return undefined
157
+ }
158
+ }
159
+
160
+ /** The revision this domain last synced from, which bounds its upstream range. */
161
+ export function stampedCommit(
162
+ stamp: Stamp | undefined,
163
+ domain: StampDomain,
164
+ ): string | undefined {
165
+ return stamp?.domains[domain]?.commit
166
+ }
167
+
168
+ export function stampedHashes(
169
+ stamp: Stamp | undefined,
170
+ domain: StampDomain | undefined,
171
+ ): DomainHashes {
172
+ if (stamp === undefined || domain === undefined) return {}
173
+ return stamp.domains[domain]?.files ?? {}
174
+ }
175
+
176
+ /**
177
+ * The stack chain a domain's install last recorded. An empty result is the
178
+ * state every target predating that domain's chain recording sits in, and a
179
+ * reader treats it as unmeasured rather than as clean.
180
+ */
181
+ export function stampedChain(
182
+ stamp: Stamp | undefined,
183
+ domain: StampDomain,
184
+ ): readonly string[] {
185
+ return stamp?.domains[domain]?.chain ?? []
186
+ }
187
+
188
+ /**
189
+ * Replaces one domain's file hashes and leaves the others, including that
190
+ * domain's own chain, untouched. Domains install and sync independently but
191
+ * share the one file, and a chain an install recorded is a separate fact a
192
+ * later file-only sync must not erase.
193
+ */
194
+ export async function writeStamp(
195
+ target: string,
196
+ source: StampSource,
197
+ hashes: DomainHashes,
198
+ now: Date,
199
+ ): Promise<void> {
200
+ await putDomain(target, source, { files: sortKeys(hashes) }, now)
201
+ }
202
+
203
+ /**
204
+ * Records the stack chain an install resolved and leaves that domain's file
205
+ * hashes untouched. Tooling calls this with no files ever recorded, since
206
+ * `src/tooling/` never runs the sync engine and the chain is its whole record.
207
+ * Governance calls it alongside `writeStamp`, since it records both.
208
+ */
209
+ export async function writeChainStamp(
210
+ target: string,
211
+ source: StampSource,
212
+ chain: readonly string[],
213
+ now: Date,
214
+ ): Promise<void> {
215
+ await putDomain(target, source, { chain: [...chain] }, now)
216
+ }
217
+
218
+ async function putDomain(
219
+ target: string,
220
+ source: StampSource,
221
+ payload: Partial<Pick<DomainStamp, 'files' | 'chain'>>,
222
+ now: Date,
223
+ ): Promise<void> {
224
+ const previous = readStamp(target)
225
+ const previousRecord = previous?.domains[source.domain]
226
+ const commit = await toolkitCommit(source.toolkitRoot)
227
+
228
+ const record: DomainStamp = {
229
+ ...(commit === undefined ? {} : { commit }),
230
+ syncedAt: now.toISOString(),
231
+ files: payload.files ?? previousRecord?.files ?? {},
232
+ ...resolveChainField(payload.chain, previousRecord?.chain),
233
+ }
234
+
235
+ const domains = sortDomains({
236
+ ...previous?.domains,
237
+ [source.domain]: record,
238
+ })
239
+
240
+ const stamp: Stamp = {
241
+ covers: STAMP_DOMAINS.filter((domain) => domains[domain] !== undefined),
242
+ domains,
243
+ }
244
+
245
+ const path = stampPath(target)
246
+ await mkdir(dirname(path), { recursive: true })
247
+ await writeFile(path, `${JSON.stringify(stamp, null, 2)}\n`)
248
+
249
+ // Every install and sync that stamps a target passes through here, which is
250
+ // what makes this the one place the machine-level index can be kept without
251
+ // each command remembering to. Its outcome is dropped rather than reported:
252
+ // the stamp just written is the authoritative record of this install, the
253
+ // index is a cache over every such stamp, and a state folder nobody can
254
+ // write is not a reason to fail a sync that already landed its files.
255
+ recordTarget(target, now)
256
+ }
257
+
258
+ const commitCache = new Map<string, Promise<string | undefined>>()
259
+
260
+ /**
261
+ * Resolved once per clone per process. A toolkit reached outside a git clone
262
+ * yields no commit, which costs the report its upstream range but not its
263
+ * per-file attribution.
264
+ */
265
+ export function toolkitCommit(root: string): Promise<string | undefined> {
266
+ const cached = commitCache.get(root)
267
+ if (cached !== undefined) return cached
268
+
269
+ const pending = readCommit(root)
270
+ commitCache.set(root, pending)
271
+ return pending
272
+ }
273
+
274
+ async function readCommit(root: string): Promise<string | undefined> {
275
+ const result = await execa(
276
+ 'git',
277
+ ['-C', root, 'rev-parse', '--short', 'HEAD'],
278
+ { reject: false },
279
+ )
280
+
281
+ return result.exitCode === 0 && result.stdout.trim() !== ''
282
+ ? result.stdout.trim()
283
+ : undefined
284
+ }
285
+
286
+ /**
287
+ * A write naming no chain keeps the domain's previous one rather than dropping
288
+ * it, since `writeStamp` and `writeChainStamp` each touch one half of a
289
+ * governance record and neither should erase what the other wrote.
290
+ */
291
+ function resolveChainField(
292
+ chain: readonly string[] | undefined,
293
+ previous: readonly string[] | undefined,
294
+ ): Pick<DomainStamp, 'chain'> {
295
+ const resolved = chain ?? previous
296
+ return resolved === undefined ? {} : { chain: resolved }
297
+ }
298
+
299
+ /** Deterministic key order keeps a re-sync diff empty and a merge conflict local. */
300
+ function sortKeys(hashes: DomainHashes): DomainHashes {
301
+ return Object.fromEntries(
302
+ Object.entries(hashes).sort(([left], [right]) => left.localeCompare(right)),
303
+ )
304
+ }
305
+
306
+ function sortDomains(
307
+ domains: Partial<Record<StampDomain, DomainStamp>>,
308
+ ): Partial<Record<StampDomain, DomainStamp>> {
309
+ const sorted: Partial<Record<StampDomain, DomainStamp>> = {}
310
+ for (const domain of STAMP_DOMAINS) {
311
+ const record = domains[domain]
312
+ if (record !== undefined) sorted[domain] = record
313
+ }
314
+
315
+ return sorted
316
+ }
317
+
318
+ /**
319
+ * Validates every recognized domain record rather than only the container. A
320
+ * stamp whose domain value is the wrong shape would otherwise reach
321
+ * `attribute`, where a bad hash lookup silently reads as a customization.
322
+ *
323
+ * A key naming no current domain is ignored rather than failing the parse. A
324
+ * target stamped before the standards install channel closed carries one, and
325
+ * rejecting the file would read as unstamped and then drop the three records it
326
+ * does carry on the next write. `sortDomains` retires the key at that write.
327
+ */
328
+ function isStamp(value: unknown): value is Stamp {
329
+ if (!isRecord(value) || !isRecord(value.domains)) return false
330
+
331
+ return Object.entries(value.domains)
332
+ .filter(([domain]) => (STAMP_DOMAINS as readonly string[]).includes(domain))
333
+ .every(([, record]) => isDomainStamp(record))
334
+ }
335
+
336
+ /**
337
+ * `chain` is optional, which is what keeps a stamp written before tooling
338
+ * joined the domains readable rather than parsing as corrupt and discarding
339
+ * the three records it does carry.
340
+ */
341
+ function isDomainStamp(value: unknown): value is DomainStamp {
342
+ if (!isRecord(value) || !isRecord(value.files)) return false
343
+ if (typeof value.syncedAt !== 'string') return false
344
+ if (value.commit !== undefined && typeof value.commit !== 'string') {
345
+ return false
346
+ }
347
+ if (value.chain !== undefined && !isStringArray(value.chain)) return false
348
+
349
+ return Object.values(value.files).every((hash) => typeof hash === 'string')
350
+ }
351
+
352
+ function isStringArray(value: unknown): value is string[] {
353
+ return Array.isArray(value) && value.every((item) => typeof item === 'string')
354
+ }
355
+
356
+ function isRecord(value: unknown): value is Record<string, unknown> {
357
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
358
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { isDirectory } from '@/target'
4
+
5
+ export const SYNC_DOMAINS = ['governance', 'claude'] as const
6
+
7
+ export type SyncDomain = (typeof SYNC_DOMAINS)[number]
8
+
9
+ export interface DomainState {
10
+ readonly domain: SyncDomain
11
+ readonly installed: boolean
12
+ }
13
+
14
+ const DOMAIN_MARKERS: Record<SyncDomain, string> = {
15
+ governance: join('.claude', 'rules'),
16
+ claude: '.claude',
17
+ }
18
+
19
+ /**
20
+ * Paths whose git status decides whether a domain contributed to the sync
21
+ * commit. Governance carries the retired `.claude/GOV.md` because a sync that
22
+ * removes it is still a governance change, and `claude` watches `.gitignore`
23
+ * because that is the only file `canon claude sync` writes.
24
+ */
25
+ const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
26
+ governance: ['.claude/rules/', '.claude/GOV.md'],
27
+ claude: ['.gitignore'],
28
+ }
29
+
30
+ export function domainPaths(domain: SyncDomain): readonly string[] {
31
+ return DOMAIN_PATHS[domain]
32
+ }
33
+
34
+ export function detectDomains(target: string): DomainState[] {
35
+ return SYNC_DOMAINS.map((domain) => ({
36
+ domain,
37
+ installed: isDirectory(join(target, DOMAIN_MARKERS[domain])),
38
+ }))
39
+ }
40
+
41
+ export function installedDomains(states: readonly DomainState[]): SyncDomain[] {
42
+ return states.filter((state) => state.installed).map((state) => state.domain)
43
+ }
44
+
45
+ /**
46
+ * Whether a domain's sync command should run, which is not the same question
47
+ * `detectDomains` answers. Governance reports as absent without
48
+ * `.claude/rules/`, yet still syncs when only the retired `.claude/GOV.md`
49
+ * remains, so that the sync can delete it. The bash drew the same distinction
50
+ * between `detect_domains` and `run_syncs`.
51
+ */
52
+ export function shouldSync(target: string, domain: SyncDomain): boolean {
53
+ if (domain === 'governance') {
54
+ return (
55
+ isDirectory(join(target, '.claude', 'rules')) ||
56
+ existsSync(join(target, '.claude', 'GOV.md'))
57
+ )
58
+ }
59
+
60
+ return isDirectory(join(target, DOMAIN_MARKERS[domain]))
61
+ }
62
+
63
+ /**
64
+ * A target with no `.git/` reports clean, because `git status` fails there and
65
+ * the bash swallowed that failure. The guard exists to make the sync commit
66
+ * survivable, and a directory outside git has no commit to protect.
67
+ */
68
+ export function isTreeClean(status: string): boolean {
69
+ return status.trim() === ''
70
+ }
@@ -0,0 +1,200 @@
1
+ import { join } from 'node:path'
2
+ import {
3
+ changedNames,
4
+ changedPaths,
5
+ classifyStatus,
6
+ commitMessage,
7
+ type DomainChange,
8
+ type GitRunner,
9
+ type PullRequestOpener,
10
+ pullRequestBody,
11
+ syncBranchName,
12
+ } from '@/sync/git'
13
+ import { domainPaths, SYNC_DOMAINS } from '@/sync/target'
14
+ import { isDirectory } from '@/target'
15
+ import {
16
+ intro,
17
+ logError,
18
+ logInfo,
19
+ logStep,
20
+ logWarn,
21
+ outro,
22
+ palette,
23
+ pipeOutput,
24
+ select,
25
+ } from '@/ui'
26
+
27
+ const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
28
+
29
+ export type WorkflowChoice = 'pr' | 'commit' | 'cancel'
30
+
31
+ export interface WorkflowDeps {
32
+ readonly git: GitRunner
33
+ /** Absent when `gh` is not installed, which hides the pull request option. */
34
+ readonly pullRequests: PullRequestOpener | undefined
35
+ readonly now: Date
36
+ readonly nonInteractive: boolean
37
+ /**
38
+ * Defaults to the interactive prompt. A test injects it to reach the commit
39
+ * and pull request branches, which `select` otherwise gates behind a TTY.
40
+ */
41
+ readonly choose?: (canOpenPullRequest: boolean) => Promise<WorkflowChoice>
42
+ }
43
+
44
+ /**
45
+ * Groups the target's pending changes by the domain that produced them. Each
46
+ * domain is read independently, so the four status calls run together.
47
+ */
48
+ export async function collectChanges(git: GitRunner): Promise<DomainChange[]> {
49
+ const results = await Promise.all(
50
+ SYNC_DOMAINS.map(async (domain) => {
51
+ const status = await git.status(domainPaths(domain))
52
+ return { domain, status }
53
+ }),
54
+ )
55
+
56
+ const changes: DomainChange[] = []
57
+
58
+ for (const { domain, status } of results) {
59
+ const names = changedNames(status)
60
+ if (names.length === 0) continue
61
+
62
+ changes.push({
63
+ domain,
64
+ verb: classifyStatus(status),
65
+ names,
66
+ paths: changedPaths(status),
67
+ })
68
+ }
69
+
70
+ return changes
71
+ }
72
+
73
+ /**
74
+ * Commits what the domain syncs wrote and optionally opens a pull request.
75
+ *
76
+ * This is the only toolkit code that writes to a git remote, which is why a
77
+ * headless run refuses it outright. `select_option` resolved to its first
78
+ * option under `CANON_NON_INTERACTIVE=1`, and the first option here was
79
+ * `Commit and open PR` whenever `gh` was installed, so an agent following the
80
+ * documented non-interactive path pushed a branch and opened a pull request on
81
+ * someone's repository without being asked.
82
+ */
83
+ export async function runGitWorkflow(
84
+ target: string,
85
+ deps: WorkflowDeps,
86
+ ): Promise<number> {
87
+ if (!isDirectory(join(target, '.git'))) return 0
88
+
89
+ const changes = await collectChanges(deps.git)
90
+ if (changes.length === 0) return 0
91
+
92
+ const domains = changes.map((change) => change.domain)
93
+ const message = commitMessage(domains)
94
+ const body = pullRequestBody(changes)
95
+ const branch = syncBranchName(deps.now)
96
+
97
+ intro('canon sync → git')
98
+
99
+ const [localTaken, remoteTaken] = await Promise.all([
100
+ deps.git.branchExists(branch),
101
+ deps.git.remoteBranchExists(branch),
102
+ ])
103
+
104
+ if (localTaken || remoteTaken) {
105
+ logWarn(
106
+ `${branch} already exists. Wait a minute or delete it, then re-run.`,
107
+ )
108
+ outro()
109
+ return 0
110
+ }
111
+
112
+ const currentBranch = await deps.git.currentBranch()
113
+ const onProtected =
114
+ currentBranch === '' || PROTECTED_BRANCHES.includes(currentBranch)
115
+
116
+ logStep('Preview')
117
+ logInfo(`Domains: ${domains.join(', ')}`)
118
+ logInfo(
119
+ onProtected
120
+ ? `Branch: ${branch} (source: ${currentBranch || 'detached'})`
121
+ : `Branch: ${currentBranch} (commit only) or ${branch} (PR)`,
122
+ )
123
+ logInfo(`Commit: ${message}`)
124
+
125
+ logStep('PR body')
126
+ pipeOutput(body)
127
+
128
+ if (deps.nonInteractive) {
129
+ logWarn('Refusing the git workflow in non-interactive mode.')
130
+ logInfo(`Would commit ${domains.join(', ')} to ${branch}.`)
131
+ logInfo('Run interactively to commit, push, and open the pull request.')
132
+ outro()
133
+ return 0
134
+ }
135
+
136
+ const opener = deps.pullRequests
137
+ if (opener === undefined) {
138
+ logWarn(
139
+ 'gh CLI not found — PR option unavailable. Install from https://cli.github.com',
140
+ )
141
+ }
142
+
143
+ const choose = deps.choose ?? promptChoice
144
+ const choice = await choose(opener !== undefined)
145
+
146
+ if (choice === 'cancel') {
147
+ logWarn('Skipped')
148
+ outro()
149
+ return 0
150
+ }
151
+
152
+ const paths = [...new Set(changes.flatMap((change) => change.paths))]
153
+
154
+ try {
155
+ logStep('Committing')
156
+
157
+ if (choice === 'pr' || onProtected) await deps.git.createBranch(branch)
158
+ await deps.git.stage(paths)
159
+ await deps.git.commit(message)
160
+
161
+ if (choice === 'commit' || opener === undefined) {
162
+ return succeed(
163
+ onProtected
164
+ ? `Committed to ${branch}. Push and open a PR when ready.`
165
+ : `Committed to ${currentBranch}.`,
166
+ )
167
+ }
168
+
169
+ await deps.git.push(branch)
170
+
171
+ logStep('Opening PR')
172
+ const url = await opener.create(message, body)
173
+
174
+ return succeed(`Synced: ${url}`)
175
+ } catch (error) {
176
+ logError(error instanceof Error ? error.message : String(error))
177
+ outro()
178
+ return 1
179
+ }
180
+ }
181
+
182
+ function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
183
+ return select({
184
+ message: 'Review changes, then?',
185
+ options: [
186
+ ...(canOpenPullRequest
187
+ ? [{ value: 'pr' as const, label: 'Commit and open PR' }]
188
+ : []),
189
+ { value: 'commit' as const, label: 'Commit only' },
190
+ { value: 'cancel' as const, label: 'Cancel' },
191
+ ],
192
+ })
193
+ }
194
+
195
+ function succeed(message: string): number {
196
+ const { GREEN, NC } = palette(process.stderr)
197
+ outro()
198
+ process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
199
+ return 0
200
+ }
package/src/target.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { statSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { logError, outro } from '@/ui'
4
+
5
+ export function isDirectory(path: string): boolean {
6
+ try {
7
+ return statSync(path).isDirectory()
8
+ } catch {
9
+ return false
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Stands in for `validate_target` plus `guard_root`. The bash `guard_root` read
15
+ * as a toolkit-root check, but its body was `cd "$target" && pwd`, so it also
16
+ * rejected a target that does not exist. Porting the name alone would let a
17
+ * typo'd path scaffold a whole new tree, since every apply step creates parents.
18
+ *
19
+ * Returns the resolved path, or the exit code the caller should hand back. The
20
+ * frame is already open by the time this runs, so a rejection closes it.
21
+ */
22
+ export function resolveTarget(
23
+ target: string,
24
+ protectedRoot: string,
25
+ ): string | number {
26
+ const resolved = resolve(target)
27
+
28
+ if (!isDirectory(resolved)) {
29
+ logError(`Target directory not found: ${target}`)
30
+ outro()
31
+ return 1
32
+ }
33
+
34
+ if (resolved === protectedRoot) {
35
+ logError(
36
+ 'Cannot run against toolkit root. Files here are the source of truth.',
37
+ )
38
+ outro()
39
+ return 1
40
+ }
41
+
42
+ return resolved
43
+ }