@erclx/aitk 0.8.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 (359) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/claude/.claude-plugin/plugin.json +19 -0
  4. package/claude/skills/bash-script/SKILL.md +96 -0
  5. package/claude/skills/bash-script/references/patterns.md +349 -0
  6. package/claude/skills/ci-workflow/SKILL.md +52 -0
  7. package/claude/skills/ci-workflow/references/workflows.md +74 -0
  8. package/claude/skills/claude-address-review/SKILL.md +107 -0
  9. package/claude/skills/claude-autoship/SKILL.md +137 -0
  10. package/claude/skills/claude-design-extract/SKILL.md +69 -0
  11. package/claude/skills/claude-design-propose/SKILL.md +75 -0
  12. package/claude/skills/claude-diagram/SKILL.md +126 -0
  13. package/claude/skills/claude-docs/SKILL.md +204 -0
  14. package/claude/skills/claude-feature/REQUIREMENT.md +35 -0
  15. package/claude/skills/claude-feature/SKILL.md +148 -0
  16. package/claude/skills/claude-groundwork/SKILL.md +148 -0
  17. package/claude/skills/claude-groundwork/references/folder-format.md +107 -0
  18. package/claude/skills/claude-memory-capture/SKILL.md +80 -0
  19. package/claude/skills/claude-memory-review/REQUIREMENT.md +37 -0
  20. package/claude/skills/claude-memory-review/SKILL.md +206 -0
  21. package/claude/skills/claude-orchestrate/SKILL.md +125 -0
  22. package/claude/skills/claude-pr-review/SKILL.md +150 -0
  23. package/claude/skills/claude-review/SKILL.md +136 -0
  24. package/claude/skills/claude-roadmap/SKILL.md +61 -0
  25. package/claude/skills/claude-roadmap/references/roadmap.md +52 -0
  26. package/claude/skills/claude-screencast/SKILL.md +160 -0
  27. package/claude/skills/claude-seed-sync/SKILL.md +141 -0
  28. package/claude/skills/claude-slides-draft/SKILL.md +62 -0
  29. package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
  30. package/claude/skills/claude-standards-audit/SKILL.md +79 -0
  31. package/claude/skills/claude-standards-audit/references/branch.md +49 -0
  32. package/claude/skills/claude-standards-audit/references/pr.md +124 -0
  33. package/claude/skills/claude-tasks/SKILL.md +123 -0
  34. package/claude/skills/claude-ui-test/SKILL.md +75 -0
  35. package/claude/skills/claude-ux-audit/SKILL.md +78 -0
  36. package/claude/skills/claude-worktree/SKILL.md +79 -0
  37. package/claude/skills/cli-script/SKILL.md +47 -0
  38. package/claude/skills/cli-script/references/template.md +43 -0
  39. package/claude/skills/create-rule/SKILL.md +68 -0
  40. package/claude/skills/create-skill/REQUIREMENT.md +30 -0
  41. package/claude/skills/create-skill/SKILL.md +24 -0
  42. package/claude/skills/create-snippet/SKILL.md +28 -0
  43. package/claude/skills/create-snippet/references/snippets.md +57 -0
  44. package/claude/skills/create-standard/SKILL.md +31 -0
  45. package/claude/skills/create-standard/references/standard.md +52 -0
  46. package/claude/skills/docs-sync/SKILL.md +87 -0
  47. package/claude/skills/git-branch/REQUIREMENT.md +38 -0
  48. package/claude/skills/git-branch/SKILL.md +60 -0
  49. package/claude/skills/git-branch/references/branch.md +49 -0
  50. package/claude/skills/git-commit/REQUIREMENT.md +33 -0
  51. package/claude/skills/git-commit/SKILL.md +51 -0
  52. package/claude/skills/git-commit/references/commit.md +63 -0
  53. package/claude/skills/git-followup/REQUIREMENT.md +38 -0
  54. package/claude/skills/git-followup/SKILL.md +35 -0
  55. package/claude/skills/git-issue/REQUIREMENT.md +38 -0
  56. package/claude/skills/git-issue/SKILL.md +69 -0
  57. package/claude/skills/git-issue/references/issue.md +84 -0
  58. package/claude/skills/git-pr/REQUIREMENT.md +37 -0
  59. package/claude/skills/git-pr/SKILL.md +119 -0
  60. package/claude/skills/git-pr/references/branch.md +49 -0
  61. package/claude/skills/git-pr/references/pr.md +124 -0
  62. package/claude/skills/git-ship/REQUIREMENT.md +34 -0
  63. package/claude/skills/git-ship/SKILL.md +39 -0
  64. package/claude/skills/git-split/REQUIREMENT.md +38 -0
  65. package/claude/skills/git-split/SKILL.md +160 -0
  66. package/claude/skills/git-split/references/branch.md +49 -0
  67. package/claude/skills/git-split/references/pr.md +124 -0
  68. package/claude/skills/git-stage/REQUIREMENT.md +36 -0
  69. package/claude/skills/git-stage/SKILL.md +74 -0
  70. package/claude/skills/git-stage/references/commit.md +63 -0
  71. package/claude/skills/git-worktree/REQUIREMENT.md +37 -0
  72. package/claude/skills/git-worktree/SKILL.md +120 -0
  73. package/claude/skills/migration-claude-md/SKILL.md +74 -0
  74. package/claude/skills/migration-context/SKILL.md +93 -0
  75. package/claude/skills/migration-standards/SKILL.md +75 -0
  76. package/claude/skills/project-commands/SKILL.md +73 -0
  77. package/claude/skills/session-resume/SKILL.md +36 -0
  78. package/claude/skills/setup-gov/SKILL.md +71 -0
  79. package/claude/skills/setup-indexes/SKILL.md +148 -0
  80. package/claude/skills/setup-init/SKILL.md +114 -0
  81. package/claude/skills/setup-plugins/SKILL.md +81 -0
  82. package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
  83. package/claude/skills/setup-verify/SKILL.md +51 -0
  84. package/claude/skills/systematic-debugging/SKILL.md +70 -0
  85. package/claude/skills/toolkit-cli/SKILL.md +38 -0
  86. package/claude/skills/toolkit-feedback/SKILL.md +80 -0
  87. package/claude/skills/toolkit-operator/SKILL.md +45 -0
  88. package/claude/skills/toolkit-triage/SKILL.md +63 -0
  89. package/claude/skills/youtube-transcripts/SKILL.md +34 -0
  90. package/docs/agents.md +371 -0
  91. package/docs/ai-workflow.md +157 -0
  92. package/docs/index.md +20 -0
  93. package/docs/operating-model.md +106 -0
  94. package/docs/target-projects.md +183 -0
  95. package/docs/visual-design-workflow.md +136 -0
  96. package/docs/zshrc-aliases.md +65 -0
  97. package/governance/rules/claude/500-prose.md +12 -0
  98. package/governance/rules/claude/510-context.md +16 -0
  99. package/governance/rules/claude/520-wireframes.md +12 -0
  100. package/governance/rules/claude/530-requirements.md +11 -0
  101. package/governance/rules/claude/540-architecture.md +11 -0
  102. package/governance/rules/claude/550-design.md +11 -0
  103. package/governance/rules/claude/555-tasks.md +12 -0
  104. package/governance/rules/claude/560-diagrams.md +11 -0
  105. package/governance/rules/claude/570-skill.md +18 -0
  106. package/governance/rules/claude/580-readme.md +11 -0
  107. package/governance/rules/claude/590-rule-authoring.md +12 -0
  108. package/governance/rules/core/000-constitution.md +30 -0
  109. package/governance/rules/core/010-testing.md +35 -0
  110. package/governance/rules/core/020-concurrency.md +22 -0
  111. package/governance/rules/core/030-error-handling.md +31 -0
  112. package/governance/rules/core/040-performance.md +20 -0
  113. package/governance/rules/core/050-logging.md +20 -0
  114. package/governance/rules/core/060-naming.md +19 -0
  115. package/governance/rules/core/070-planning.md +13 -0
  116. package/governance/rules/core/080-config-comments.md +22 -0
  117. package/governance/rules/framework/200-react.md +51 -0
  118. package/governance/rules/framework/210-astro.md +41 -0
  119. package/governance/rules/framework/220-fastapi.md +43 -0
  120. package/governance/rules/framework/230-nextjs.md +48 -0
  121. package/governance/rules/framework/250-tailwind.md +32 -0
  122. package/governance/rules/framework/260-shadcn.md +34 -0
  123. package/governance/rules/lang/100-typescript.md +40 -0
  124. package/governance/rules/lang/110-python.md +42 -0
  125. package/governance/rules/lib/300-testing-ts.md +38 -0
  126. package/governance/rules/lib/310-zod.md +25 -0
  127. package/governance/rules/lib/320-tanstack-query.md +32 -0
  128. package/governance/rules/lib/330-testing-py.md +44 -0
  129. package/governance/rules/lib/340-pydantic.md +38 -0
  130. package/governance/rules/lib/350-security-web.md +31 -0
  131. package/governance/rules/ui/400-ui.md +34 -0
  132. package/governance/rules/ui/410-a11y.md +44 -0
  133. package/governance/rules/ui/420-forms.md +34 -0
  134. package/governance/rules/ui/430-ux-completeness.md +62 -0
  135. package/governance/stacks/astro.toml +2 -0
  136. package/governance/stacks/base.toml +2 -0
  137. package/governance/stacks/node.toml +2 -0
  138. package/governance/stacks/python-fastapi.toml +2 -0
  139. package/governance/stacks/python.toml +2 -0
  140. package/governance/stacks/react.toml +2 -0
  141. package/package.json +67 -0
  142. package/scripts/config.sh +11 -0
  143. package/scripts/core/bootstrap.sh +81 -0
  144. package/scripts/core/check-plugin-boundary.sh +45 -0
  145. package/scripts/core/check-skill-paths.sh +25 -0
  146. package/scripts/core/clean.sh +36 -0
  147. package/scripts/core/install-check.sh +93 -0
  148. package/scripts/core/regen-claude-copies.sh +26 -0
  149. package/scripts/core/regen-indexes.sh +10 -0
  150. package/scripts/core/regen-skill-references.sh +27 -0
  151. package/scripts/core/snapshot.sh +132 -0
  152. package/scripts/core/update.sh +35 -0
  153. package/scripts/core/verify.sh +201 -0
  154. package/scripts/docs/list.sh +129 -0
  155. package/scripts/gov/list.sh +234 -0
  156. package/scripts/lib/frontmatter.sh +30 -0
  157. package/scripts/lib/gov.sh +14 -0
  158. package/scripts/lib/sandbox-fixtures.sh +109 -0
  159. package/scripts/lib/sandbox-git.sh +73 -0
  160. package/scripts/lib/tooling.sh +24 -0
  161. package/scripts/lib/ui.sh +173 -0
  162. package/scripts/lib/worktree.sh +20 -0
  163. package/scripts/manage-sandbox.sh +453 -0
  164. package/scripts/snippets/create.sh +156 -0
  165. package/scripts/standards/list.sh +91 -0
  166. package/scripts/tooling/create.sh +109 -0
  167. package/scripts/tooling/ref.sh +141 -0
  168. package/scripts/tooling/verify.sh +179 -0
  169. package/snippets/claude/decision-memo.md +39 -0
  170. package/snippets/claude/feature-recap.md +19 -0
  171. package/snippets/claude/figma-steps.md +18 -0
  172. package/snippets/claude/orchestrator-resume.md +23 -0
  173. package/snippets/claude/orchestrator-sweep.md +10 -0
  174. package/snippets/compact-summary.md +5 -0
  175. package/snippets/create-snippet.md +7 -0
  176. package/snippets/decision-help.md +6 -0
  177. package/snippets/meta-prompt.md +14 -0
  178. package/snippets/research-prompt.md +7 -0
  179. package/snippets/session-notes.md +11 -0
  180. package/snippets/snippets.toml +6 -0
  181. package/snippets/step-by-step.md +10 -0
  182. package/snippets/web-research.md +21 -0
  183. package/src/claude/gitignore.ts +51 -0
  184. package/src/claude/seeds-list.ts +47 -0
  185. package/src/claude/seeds.ts +124 -0
  186. package/src/claude/settings.ts +151 -0
  187. package/src/cli-run.ts +43 -0
  188. package/src/cli.ts +111 -0
  189. package/src/commands/claude.ts +349 -0
  190. package/src/commands/design.ts +39 -0
  191. package/src/commands/docs.ts +59 -0
  192. package/src/commands/feedback-format.ts +23 -0
  193. package/src/commands/feedback.ts +103 -0
  194. package/src/commands/gov.ts +268 -0
  195. package/src/commands/indexes.ts +182 -0
  196. package/src/commands/init.ts +117 -0
  197. package/src/commands/pass-through.ts +39 -0
  198. package/src/commands/sandbox.ts +315 -0
  199. package/src/commands/slides.ts +106 -0
  200. package/src/commands/snippets.ts +236 -0
  201. package/src/commands/standards.ts +89 -0
  202. package/src/commands/sync.ts +211 -0
  203. package/src/commands/tasks.ts +185 -0
  204. package/src/commands/tooling.ts +432 -0
  205. package/src/commands/transcripts.ts +48 -0
  206. package/src/commands/wiki.ts +101 -0
  207. package/src/copy.ts +30 -0
  208. package/src/design/parse.ts +79 -0
  209. package/src/design/render.ts +170 -0
  210. package/src/docs/read.ts +59 -0
  211. package/src/exec.ts +17 -0
  212. package/src/frontmatter.ts +13 -0
  213. package/src/github-format.ts +13 -0
  214. package/src/github.ts +20 -0
  215. package/src/gov/adapter.ts +73 -0
  216. package/src/gov/install.ts +103 -0
  217. package/src/gov/payload.ts +39 -0
  218. package/src/gov/stacks.ts +124 -0
  219. package/src/indexes/frontmatter.ts +46 -0
  220. package/src/indexes/regen.ts +84 -0
  221. package/src/indexes/render.ts +181 -0
  222. package/src/indexes/walk.ts +111 -0
  223. package/src/init/flags.ts +66 -0
  224. package/src/init/plan.ts +128 -0
  225. package/src/init/run.ts +46 -0
  226. package/src/init/steps.ts +91 -0
  227. package/src/sandbox/coverage.ts +115 -0
  228. package/src/sandbox/expect.ts +412 -0
  229. package/src/slides/layouts.ts +391 -0
  230. package/src/slides/open.ts +18 -0
  231. package/src/slides/parse.ts +84 -0
  232. package/src/slides/render.ts +88 -0
  233. package/src/slides/styles.ts +44 -0
  234. package/src/snippets/adapter.ts +34 -0
  235. package/src/snippets/categories.ts +59 -0
  236. package/src/snippets/install.ts +125 -0
  237. package/src/snippets/list.ts +32 -0
  238. package/src/snippets/presets.ts +50 -0
  239. package/src/standards/adapter.ts +51 -0
  240. package/src/standards/index-refresh.ts +44 -0
  241. package/src/standards/install.ts +52 -0
  242. package/src/sync/check.ts +269 -0
  243. package/src/sync/engine.ts +416 -0
  244. package/src/sync/git.ts +204 -0
  245. package/src/sync/stamp.ts +202 -0
  246. package/src/sync/target.ts +79 -0
  247. package/src/sync/workflow.ts +201 -0
  248. package/src/target.ts +43 -0
  249. package/src/tasks/archive.ts +304 -0
  250. package/src/tooling/gitignore.ts +122 -0
  251. package/src/tooling/inject.ts +214 -0
  252. package/src/tooling/list.ts +39 -0
  253. package/src/tooling/manifest.ts +178 -0
  254. package/src/tooling/package.ts +166 -0
  255. package/src/tooling/scan.ts +186 -0
  256. package/src/transcripts/fetch.ts +156 -0
  257. package/src/transcripts/metadata.ts +54 -0
  258. package/src/transcripts/vtt.ts +114 -0
  259. package/src/ui.ts +187 -0
  260. package/src/wiki/init.ts +85 -0
  261. package/standards/architecture.md +41 -0
  262. package/standards/bundled/branch.md +49 -0
  263. package/standards/bundled/commit.md +63 -0
  264. package/standards/bundled/issue.md +84 -0
  265. package/standards/bundled/pr.md +124 -0
  266. package/standards/bundled/roadmap.md +52 -0
  267. package/standards/bundled/snippets.md +57 -0
  268. package/standards/bundled/standard.md +52 -0
  269. package/standards/context.md +132 -0
  270. package/standards/design.md +31 -0
  271. package/standards/diagrams.md +55 -0
  272. package/standards/index.md +21 -0
  273. package/standards/prose.md +122 -0
  274. package/standards/readme.md +163 -0
  275. package/standards/requirements.md +61 -0
  276. package/standards/rule.md +79 -0
  277. package/standards/skill.md +257 -0
  278. package/standards/tasks.md +140 -0
  279. package/standards/versioning.md +47 -0
  280. package/standards/wireframes.md +67 -0
  281. package/tooling/astro/configs/astro.config.mjs +17 -0
  282. package/tooling/astro/configs/eslint.config.js +79 -0
  283. package/tooling/astro/configs/playwright.config.ts +25 -0
  284. package/tooling/astro/configs/tsconfig.json +12 -0
  285. package/tooling/astro/configs/vitest.config.ts +22 -0
  286. package/tooling/astro/manifest.toml +32 -0
  287. package/tooling/astro/reference.md +60 -0
  288. package/tooling/base/configs/.editorconfig +5 -0
  289. package/tooling/base/configs/.github/pull_request_template.md +18 -0
  290. package/tooling/base/configs/.github/workflows/verify.yml +35 -0
  291. package/tooling/base/configs/.husky/commit-msg +1 -0
  292. package/tooling/base/configs/.husky/post-merge +61 -0
  293. package/tooling/base/configs/.husky/post-rewrite +21 -0
  294. package/tooling/base/configs/.husky/pre-commit +1 -0
  295. package/tooling/base/configs/.husky/pre-push +1 -0
  296. package/tooling/base/configs/.prettierrc +12 -0
  297. package/tooling/base/configs/.shellcheckrc +1 -0
  298. package/tooling/base/configs/.vscode/extensions.json +9 -0
  299. package/tooling/base/configs/.vscode/settings.json +3 -0
  300. package/tooling/base/configs/commitlint.config.js +11 -0
  301. package/tooling/base/configs/scripts/clean.sh +45 -0
  302. package/tooling/base/configs/scripts/update.sh +49 -0
  303. package/tooling/base/configs/scripts/verify.sh +64 -0
  304. package/tooling/base/manifest.toml +32 -0
  305. package/tooling/base/reference.md +114 -0
  306. package/tooling/base/seeds/.claude/context/ci.md +27 -0
  307. package/tooling/base/seeds/.claude/context/development.md +32 -0
  308. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  309. package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
  310. package/tooling/base/seeds/.lintstagedrc +8 -0
  311. package/tooling/base/seeds/.prettierignore +0 -0
  312. package/tooling/base/seeds/cspell.json +20 -0
  313. package/tooling/claude/manifest.toml +11 -0
  314. package/tooling/claude/reference.md +62 -0
  315. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
  316. package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
  317. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
  318. package/tooling/claude/seeds/.claude/context/index.md +8 -0
  319. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +43 -0
  320. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +45 -0
  321. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +61 -0
  322. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +60 -0
  323. package/tooling/claude/seeds/.claude/settings.json +39 -0
  324. package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
  325. package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
  326. package/tooling/claude/seeds/CLAUDE.md +87 -0
  327. package/tooling/claude/user/settings.template.json +10 -0
  328. package/tooling/claude/user/statusline-command.sh +53 -0
  329. package/tooling/python/configs/.coveragerc +14 -0
  330. package/tooling/python/configs/.python-version +1 -0
  331. package/tooling/python/configs/mypy.ini +6 -0
  332. package/tooling/python/configs/pytest.ini +4 -0
  333. package/tooling/python/configs/ruff.toml +15 -0
  334. package/tooling/python/configs/scripts/verify.sh +77 -0
  335. package/tooling/python/manifest.toml +16 -0
  336. package/tooling/python/reference.md +66 -0
  337. package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
  338. package/tooling/python/seeds/tests/test_smoke.py +2 -0
  339. package/tooling/vite-react/configs/playwright.config.ts +25 -0
  340. package/tooling/vite-react/configs/tsconfig.json +35 -0
  341. package/tooling/vite-react/configs/vite.config.ts +14 -0
  342. package/tooling/vite-react/configs/vitest.config.ts +26 -0
  343. package/tooling/vite-react/manifest.toml +21 -0
  344. package/tooling/vite-react/reference.md +55 -0
  345. package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
  346. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
  347. package/tooling/web/configs/.github/workflows/verify.yml +134 -0
  348. package/tooling/web/configs/.vscode/extensions.json +13 -0
  349. package/tooling/web/configs/.vscode/settings.json +10 -0
  350. package/tooling/web/configs/e2e/home.spec.ts +6 -0
  351. package/tooling/web/configs/e2e/screenshot.ts +41 -0
  352. package/tooling/web/configs/eslint.config.js +82 -0
  353. package/tooling/web/configs/scripts/screenshot.sh +28 -0
  354. package/tooling/web/configs/scripts/verify.sh +80 -0
  355. package/tooling/web/configs/src/test/setup.ts +8 -0
  356. package/tooling/web/manifest.toml +57 -0
  357. package/tooling/web/reference.md +95 -0
  358. package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
  359. package/tsconfig.json +14 -0
@@ -0,0 +1,59 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { isDirectory } from '@/target'
4
+
5
+ export const BASE_CATEGORY = 'base'
6
+
7
+ export function snippetsSourceDir(root: string): string {
8
+ return join(root, 'snippets')
9
+ }
10
+
11
+ /**
12
+ * Lists the folder categories. `base` is the synthetic name for snippets
13
+ * sitting directly under `snippets/`, so it is not a directory and callers
14
+ * prepend it themselves.
15
+ *
16
+ * Nothing is filtered: toolkit-internal snippets are authored under `internal/`,
17
+ * which this never reads, so every category found here is publishable.
18
+ */
19
+ export function listFolderCategories(root: string): string[] {
20
+ const dir = snippetsSourceDir(root)
21
+ if (!isDirectory(dir)) return []
22
+
23
+ return readdirSync(dir, { withFileTypes: true })
24
+ .filter((entry) => entry.isDirectory())
25
+ .map((entry) => entry.name)
26
+ .sort()
27
+ }
28
+
29
+ export function listCategories(root: string): string[] {
30
+ return [BASE_CATEGORY, ...listFolderCategories(root)]
31
+ }
32
+
33
+ export function categoryDir(root: string, category: string): string {
34
+ const source = snippetsSourceDir(root)
35
+ return category === BASE_CATEGORY ? source : join(source, category)
36
+ }
37
+
38
+ /**
39
+ * Tests for a directory rather than mere existence. `snippets/` holds files
40
+ * alongside its category folders, so an argument naming one of them resolves
41
+ * to a real path that cannot be scanned as a category.
42
+ */
43
+ export function categoryExists(root: string, category: string): boolean {
44
+ return isDirectory(categoryDir(root, category))
45
+ }
46
+
47
+ /**
48
+ * Lists one category's entry slugs. Only the immediate level is read, matching
49
+ * the `find -maxdepth 1` the bash used, so a nested folder is its own category
50
+ * rather than part of its parent.
51
+ */
52
+ export function listEntries(root: string, category: string): string[] {
53
+ const dir = categoryDir(root, category)
54
+ if (!isDirectory(dir)) return []
55
+
56
+ return [...new Bun.Glob('*.md').scanSync({ cwd: dir, onlyFiles: true })]
57
+ .map((name) => name.slice(0, -'.md'.length))
58
+ .sort()
59
+ }
@@ -0,0 +1,125 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { basename, dirname, join } from 'node:path'
3
+ import { copyPreservingMode } from '@/copy'
4
+ import {
5
+ BASE_CATEGORY,
6
+ categoryDir,
7
+ categoryExists,
8
+ listCategories,
9
+ listEntries,
10
+ snippetsSourceDir,
11
+ } from '@/snippets/categories'
12
+ import { findPreset, loadPresets } from '@/snippets/presets'
13
+
14
+ export const ALL_CATEGORY = 'all'
15
+
16
+ export interface SnippetFile {
17
+ readonly src: string
18
+ readonly relPath: string
19
+ }
20
+
21
+ export type SnippetResolution =
22
+ | {
23
+ readonly ok: true
24
+ readonly step: string
25
+ readonly files: readonly SnippetFile[]
26
+ readonly missing: readonly string[]
27
+ }
28
+ | { readonly ok: false; readonly unknownCategory: string }
29
+
30
+ export function installedSnippetsDir(target: string): string {
31
+ return join(target, '.claude', 'snippets')
32
+ }
33
+
34
+ /**
35
+ * Derives the destination path. A snippet directly under `snippets/` installs
36
+ * flat and a nested one keeps its immediate parent, so the installed tree
37
+ * mirrors the source one level deep.
38
+ */
39
+ export function deriveDestRelPath(root: string, src: string): string {
40
+ const parent = dirname(src)
41
+ const filename = basename(src)
42
+
43
+ return parent === snippetsSourceDir(root)
44
+ ? filename
45
+ : `${basename(parent)}/${filename}`
46
+ }
47
+
48
+ function filesInCategory(root: string, category: string): SnippetFile[] {
49
+ return listEntries(root, category).map((slug) => {
50
+ const src = join(categoryDir(root, category), `${slug}.md`)
51
+ return { src, relPath: deriveDestRelPath(root, src) }
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Resolves the one argument three ways. `all` wins, then a preset name, then a
57
+ * folder, which is the bash precedence and matters because a folder sharing a
58
+ * preset name resolves to the preset.
59
+ *
60
+ * Preset slugs pass the internal-category filter too. A slug is a path relative
61
+ * to `snippets/`, so a preset naming `aitk/<slug>` would otherwise reach an
62
+ * internal snippet through the one install path the filter did not cover.
63
+ */
64
+ export function resolveSnippets(
65
+ root: string,
66
+ category: string,
67
+ ): SnippetResolution {
68
+ if (category === ALL_CATEGORY) {
69
+ const files = listCategories(root).flatMap((name) =>
70
+ filesInCategory(root, name),
71
+ )
72
+ return { ok: true, step: 'Resolving all categories', files, missing: [] }
73
+ }
74
+
75
+ const preset = findPreset(root, category)
76
+ if (preset) {
77
+ const files: SnippetFile[] = []
78
+ const missing: string[] = []
79
+
80
+ for (const slug of preset.slugs) {
81
+ const src = join(snippetsSourceDir(root), `${slug}.md`)
82
+ if (existsSync(src)) files.push({ src, relPath: `${slug}.md` })
83
+ else missing.push(slug)
84
+ }
85
+
86
+ return { ok: true, step: `Resolving preset: ${category}`, files, missing }
87
+ }
88
+
89
+ if (category !== BASE_CATEGORY && !categoryExists(root, category)) {
90
+ return { ok: false, unknownCategory: category }
91
+ }
92
+
93
+ return {
94
+ ok: true,
95
+ step: `Resolving category: ${category}`,
96
+ files: filesInCategory(root, category),
97
+ missing: [],
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Lists everything the picker offers: presets first, then `base`, then the
103
+ * folder categories, matching the order the bash built the menu in.
104
+ */
105
+ export function installableCategories(root: string): string[] {
106
+ return [
107
+ ...loadPresets(root).map((preset) => preset.name),
108
+ ...listCategories(root),
109
+ ]
110
+ }
111
+
112
+ export async function installSnippets(
113
+ files: readonly SnippetFile[],
114
+ target: string,
115
+ ): Promise<string[]> {
116
+ const destDir = installedSnippetsDir(target)
117
+ const installed: string[] = []
118
+
119
+ for (const file of files) {
120
+ await copyPreservingMode(file.src, join(destDir, file.relPath))
121
+ installed.push(join('.claude', 'snippets', file.relPath))
122
+ }
123
+
124
+ return installed
125
+ }
@@ -0,0 +1,32 @@
1
+ import { listCategories, listEntries } from '@/snippets/categories'
2
+ import { loadPresets } from '@/snippets/presets'
3
+
4
+ export interface CategoryCatalogEntry {
5
+ readonly name: string
6
+ readonly entries: readonly string[]
7
+ }
8
+
9
+ export interface SnippetsCatalog {
10
+ readonly presets: readonly {
11
+ readonly name: string
12
+ readonly slugs: readonly string[]
13
+ }[]
14
+ readonly categories: readonly CategoryCatalogEntry[]
15
+ }
16
+
17
+ /**
18
+ * Builds the catalog both output modes read, so the human list and the `--json`
19
+ * one cannot describe different corpora.
20
+ */
21
+ export function buildSnippetsCatalog(root: string): SnippetsCatalog {
22
+ return {
23
+ presets: loadPresets(root).map((preset) => ({
24
+ name: preset.name,
25
+ slugs: preset.slugs,
26
+ })),
27
+ categories: listCategories(root).map((name) => ({
28
+ name,
29
+ entries: listEntries(root, name),
30
+ })),
31
+ }
32
+ }
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { snippetsSourceDir } from '@/snippets/categories'
4
+
5
+ export interface Preset {
6
+ readonly name: string
7
+ readonly slugs: readonly string[]
8
+ }
9
+
10
+ export function presetsPath(root: string): string {
11
+ return join(snippetsSourceDir(root), 'snippets.toml')
12
+ }
13
+
14
+ /**
15
+ * Reads every preset from `snippets.toml`. The bash read section headers with
16
+ * `grep '^\['` and then re-walked the file per preset with a `BASH_REMATCH`
17
+ * loop, so a name carrying a bracket or a slug split across lines parsed by
18
+ * accident rather than by rule.
19
+ */
20
+ export function loadPresets(root: string): Preset[] {
21
+ const path = presetsPath(root)
22
+ if (!existsSync(path)) return []
23
+
24
+ const parsed = Bun.TOML.parse(readFileSync(path, 'utf8')) as Record<
25
+ string,
26
+ unknown
27
+ >
28
+ const presets: Preset[] = []
29
+
30
+ for (const [name, table] of Object.entries(parsed)) {
31
+ if (typeof table !== 'object' || table === null || Array.isArray(table)) {
32
+ continue
33
+ }
34
+ const names = (table as Record<string, unknown>).names
35
+ presets.push({
36
+ name,
37
+ slugs: Array.isArray(names)
38
+ ? names.filter(
39
+ (slug): slug is string => typeof slug === 'string' && slug !== '',
40
+ )
41
+ : [],
42
+ })
43
+ }
44
+
45
+ return presets.sort((left, right) => left.name.localeCompare(right.name))
46
+ }
47
+
48
+ export function findPreset(root: string, name: string): Preset | undefined {
49
+ return loadPresets(root).find((preset) => preset.name === name)
50
+ }
@@ -0,0 +1,51 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+ import {
4
+ INDEX_FILE,
5
+ refreshIndex,
6
+ standardsInstallDir,
7
+ } from '@/standards/index-refresh'
8
+ import type { InstalledFile, SyncAdapter } from '@/sync/engine'
9
+
10
+ export function standardsSourceDir(root: string): string {
11
+ return join(root, 'standards')
12
+ }
13
+
14
+ /**
15
+ * Matches installed standards to sources by filename against the flat
16
+ * `standards/` root. A standard that lives in a source subfolder, such as
17
+ * `bundled/`, has no flat sibling and so reads as project-authored, which is
18
+ * what keeps `install` and `sync` agreeing on the same set.
19
+ *
20
+ * Standards are seeds a project is expected to edit, unlike gov rules and
21
+ * snippets, so a headless run refuses to overwrite drift rather than applying
22
+ * it. That refusal is the reason the engine grew a per-adapter policy.
23
+ */
24
+ export function createStandardsAdapter(root: string): SyncAdapter {
25
+ const sourceDir = standardsSourceDir(root)
26
+
27
+ return {
28
+ banner: 'aitk standards sync',
29
+ label: 'standards',
30
+ missingMessage:
31
+ "No .claude/standards/ found in target. Run 'aitk standards install' first.",
32
+ unit: 'standards',
33
+ installedRoot: standardsInstallDir,
34
+ isExcluded: (file: InstalledFile) => basename(file.path) === INDEX_FILE,
35
+ locateSource: (file: InstalledFile) =>
36
+ locateSource(sourceDir, basename(file.path)),
37
+ nonInteractive: {
38
+ kind: 'refuse',
39
+ message:
40
+ 'Drifts detected. Refusing to auto-apply in non-interactive mode.',
41
+ hint: 'Run interactively, or use /claude-seed-sync for per-section audit that preserves customizations.',
42
+ },
43
+ onComplete: (target: string) => refreshIndex(sourceDir, target),
44
+ stamp: { domain: 'standards', toolkitRoot: root },
45
+ }
46
+ }
47
+
48
+ function locateSource(sourceDir: string, name: string): string | undefined {
49
+ const source = join(sourceDir, name)
50
+ return existsSync(source) ? source : undefined
51
+ }
@@ -0,0 +1,44 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { copyPreservingMode } from '@/copy'
4
+ import { regenOne } from '@/indexes/regen'
5
+ import { logAdd, logWarn } from '@/ui'
6
+
7
+ export const STANDARDS_REL = join('.claude', 'standards')
8
+ export const INDEX_FILE = 'index.md'
9
+
10
+ export function standardsInstallDir(target: string): string {
11
+ return join(target, STANDARDS_REL)
12
+ }
13
+
14
+ /**
15
+ * Replaces the target's `index.md` from source and rebuilds it against what
16
+ * actually landed. It runs on every completed install and every completed sync,
17
+ * including one with no changes, because the catalog can go stale from a file
18
+ * the toolkit stopped shipping rather than from drift in a file it still does.
19
+ *
20
+ * `install` and `sync` are peers, so this sits beside both rather than inside
21
+ * the sync adapter, where an install would be importing from the other verb.
22
+ */
23
+ export async function refreshIndex(
24
+ sourceDir: string,
25
+ target: string,
26
+ ): Promise<void> {
27
+ const installedDir = standardsInstallDir(target)
28
+ const source = join(sourceDir, INDEX_FILE)
29
+
30
+ if (!existsSync(source)) {
31
+ logWarn(`No ${INDEX_FILE} in toolkit standards, leaving the target catalog`)
32
+ return
33
+ }
34
+
35
+ await copyPreservingMode(source, join(installedDir, INDEX_FILE))
36
+ const result = await regenOne(installedDir, { dryRun: false })
37
+
38
+ if (result.action === 'error') {
39
+ logWarn(`${join(STANDARDS_REL, INDEX_FILE)} regen failed: ${result.reason}`)
40
+ return
41
+ }
42
+
43
+ logAdd(join(STANDARDS_REL, INDEX_FILE))
44
+ }
@@ -0,0 +1,52 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import { mkdir } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { copyPreservingMode } from '@/copy'
5
+ import { INDEX_FILE, STANDARDS_REL } from '@/standards/index-refresh'
6
+
7
+ export interface StandardsSource {
8
+ readonly path: string
9
+ readonly name: string
10
+ }
11
+
12
+ /**
13
+ * Lists the flat `standards/` root only, which is the same set the sync adapter
14
+ * matches installed files against. A standard in a source subfolder such as
15
+ * `bundled/` is not installed wholesale, so the two verbs agree on scope.
16
+ *
17
+ * `index.md` is excluded because install copies it separately and then rebuilds
18
+ * it against what landed, rather than shipping the toolkit's own catalog.
19
+ */
20
+ export function planInstall(sourceDir: string): StandardsSource[] {
21
+ if (!existsSync(sourceDir)) return []
22
+
23
+ return readdirSync(sourceDir, { withFileTypes: true })
24
+ .filter(
25
+ (entry) =>
26
+ entry.isFile() &&
27
+ entry.name.endsWith('.md') &&
28
+ entry.name !== INDEX_FILE,
29
+ )
30
+ .map((entry) => ({ path: join(sourceDir, entry.name), name: entry.name }))
31
+ .sort((left, right) => left.name.localeCompare(right.name))
32
+ }
33
+
34
+ /**
35
+ * Copies every standard and returns the labels to log. Routes through
36
+ * `copyPreservingMode` because the `cp` it replaces left an existing
37
+ * destination's mode alone, so a target file held at 600 stays there.
38
+ */
39
+ export async function applyInstall(
40
+ files: readonly StandardsSource[],
41
+ destDir: string,
42
+ ): Promise<string[]> {
43
+ await mkdir(destDir, { recursive: true })
44
+
45
+ await Promise.all(
46
+ files.map((file) =>
47
+ copyPreservingMode(file.path, join(destDir, file.name)),
48
+ ),
49
+ )
50
+
51
+ return files.map((file) => join(STANDARDS_REL, file.name))
52
+ }
@@ -0,0 +1,269 @@
1
+ import { join } from 'node:path'
2
+ import { execa } from 'execa'
3
+ import { createGovAdapter } from '@/gov/adapter'
4
+ import { createSnippetsAdapter } from '@/snippets/adapter'
5
+ import { planSync, type ScanEntry, type SyncAdapter } from '@/sync/engine'
6
+ import {
7
+ readStamp,
8
+ STAMP_DOMAINS,
9
+ type Stamp,
10
+ stampedCommit,
11
+ type StampDomain,
12
+ } from '@/sync/stamp'
13
+ import { createStandardsAdapter } from '@/standards/adapter'
14
+ import { isDirectory } from '@/target'
15
+
16
+ /**
17
+ * The toolkit path whose commits change what each domain holds. `claude/skills/`
18
+ * is deliberately absent: skills load live from the plugin directory, so they
19
+ * never go stale and belong in the read-only section instead.
20
+ */
21
+ const SYNCED_SOURCES: Record<StampDomain, string> = {
22
+ standards: 'standards/',
23
+ snippets: 'snippets/',
24
+ governance: 'governance/rules/',
25
+ }
26
+
27
+ const ADAPTERS: Record<StampDomain, (root: string) => SyncAdapter> = {
28
+ standards: createStandardsAdapter,
29
+ snippets: createSnippetsAdapter,
30
+ governance: createGovAdapter,
31
+ }
32
+
33
+ const INSTALL_MARKERS: Record<StampDomain, readonly string[]> = {
34
+ standards: ['.claude', 'standards'],
35
+ snippets: ['.claude', 'snippets'],
36
+ governance: ['.claude', 'rules'],
37
+ }
38
+
39
+ export interface StateCounts {
40
+ readonly matching: number
41
+ readonly stale: number
42
+ readonly customized: number
43
+ readonly drifted: number
44
+ readonly orphaned: number
45
+ readonly stranded: number
46
+ }
47
+
48
+ export interface DomainReport {
49
+ readonly domain: StampDomain
50
+ readonly stamped: boolean
51
+ /** This domain's own anchor, not the target's most recent sync. */
52
+ readonly commit?: string
53
+ readonly syncedAt?: string
54
+ readonly counts: StateCounts
55
+ readonly entries: readonly ScanEntry[]
56
+ readonly upstream: readonly UpstreamCommit[]
57
+ }
58
+
59
+ export interface UpstreamCommit {
60
+ readonly sha: string
61
+ readonly subject: string
62
+ }
63
+
64
+ export interface CheckReport {
65
+ readonly covers: readonly StampDomain[]
66
+ readonly domains: readonly DomainReport[]
67
+ readonly newSkills: readonly string[]
68
+ }
69
+
70
+ export function installedStampDomains(target: string): StampDomain[] {
71
+ return STAMP_DOMAINS.filter((domain) =>
72
+ isDirectory(join(target, ...INSTALL_MARKERS[domain])),
73
+ )
74
+ }
75
+
76
+ export function countStates(entries: readonly ScanEntry[]): StateCounts {
77
+ return {
78
+ matching: count(entries, 'matching'),
79
+ stale: count(entries, 'stale'),
80
+ customized: count(entries, 'customized'),
81
+ drifted: count(entries, 'drifted'),
82
+ orphaned: count(entries, 'orphaned'),
83
+ stranded: count(entries, 'stranded'),
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Whether the target has diverged from the toolkit in a way a sync could close.
89
+ * Orphaned files are excluded: a project-authored rule never converges, and
90
+ * counting it would leave `--exit-code` failing forever with no remedy.
91
+ */
92
+ export function hasDrift(report: CheckReport): boolean {
93
+ return report.domains.some(
94
+ (domain) =>
95
+ domain.counts.stale +
96
+ domain.counts.customized +
97
+ domain.counts.drifted +
98
+ domain.counts.stranded >
99
+ 0,
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Bounds each domain's upstream read by that domain's own anchor and its own
105
+ * source path. A shared anchor would let a gov sync advance the revision
106
+ * standards measures from, silently dropping a standards change out of the read.
107
+ */
108
+ export async function buildCheckReport(
109
+ toolkitRoot: string,
110
+ target: string,
111
+ ): Promise<CheckReport> {
112
+ const stamp = readStamp(target)
113
+
114
+ const domains = await Promise.all(
115
+ installedStampDomains(target).map((domain) =>
116
+ buildDomainReport(toolkitRoot, target, stamp, domain),
117
+ ),
118
+ )
119
+
120
+ const anchors = domains
121
+ .map((domain) => domain.commit)
122
+ .filter((commit): commit is string => commit !== undefined)
123
+
124
+ return {
125
+ covers: stamp?.covers ?? [],
126
+ domains,
127
+ newSkills: await readNewSkills(toolkitRoot, anchors),
128
+ }
129
+ }
130
+
131
+ async function buildDomainReport(
132
+ toolkitRoot: string,
133
+ target: string,
134
+ stamp: Stamp | undefined,
135
+ domain: StampDomain,
136
+ ): Promise<DomainReport> {
137
+ const plan = planSync(ADAPTERS[domain](toolkitRoot), target)
138
+ const record = stamp?.domains[domain]
139
+ const since = stampedCommit(stamp, domain)
140
+
141
+ return {
142
+ domain,
143
+ stamped: record !== undefined,
144
+ commit: since,
145
+ syncedAt: record?.syncedAt,
146
+ counts: countStates(plan.entries),
147
+ entries: plan.entries,
148
+ upstream:
149
+ since === undefined
150
+ ? []
151
+ : await readUpstream(toolkitRoot, since, SYNCED_SOURCES[domain]),
152
+ }
153
+ }
154
+
155
+ export function parseUpstream(log: string): UpstreamCommit[] {
156
+ const commits: UpstreamCommit[] = []
157
+
158
+ for (const line of log.split('\n')) {
159
+ const trimmed = line.trim()
160
+ if (trimmed === '') continue
161
+
162
+ const boundary = trimmed.indexOf(' ')
163
+ if (boundary === -1) continue
164
+
165
+ commits.push({
166
+ sha: trimmed.slice(0, boundary),
167
+ subject: trimmed.slice(boundary + 1),
168
+ })
169
+ }
170
+
171
+ return commits
172
+ }
173
+
174
+ /** A skill is new when its `SKILL.md` was added, not when a support file was. */
175
+ export function parseNewSkills(paths: string): string[] {
176
+ const names = paths
177
+ .split('\n')
178
+ .map((line) => line.trim())
179
+ .filter((line) => line.endsWith('/SKILL.md'))
180
+ .map((line) => line.split('/').at(-2))
181
+ .filter((name): name is string => name !== undefined)
182
+
183
+ return [...new Set(names)].sort()
184
+ }
185
+
186
+ async function readUpstream(
187
+ root: string,
188
+ since: string,
189
+ sourcePath: string,
190
+ ): Promise<UpstreamCommit[]> {
191
+ const log = await read(root, [
192
+ 'log',
193
+ '--oneline',
194
+ '--no-decorate',
195
+ `${since}..HEAD`,
196
+ '--',
197
+ sourcePath,
198
+ ])
199
+
200
+ return parseUpstream(log)
201
+ }
202
+
203
+ /**
204
+ * Skills are not domain-scoped, so the read runs from the oldest anchor across
205
+ * domains. Over-reporting a skill costs a line, while measuring from the newest
206
+ * would hide one that arrived before the most recent domain sync.
207
+ */
208
+ async function readNewSkills(
209
+ root: string,
210
+ anchors: readonly string[],
211
+ ): Promise<string[]> {
212
+ const since = await oldestAnchor(root, anchors)
213
+ if (since === undefined) return []
214
+
215
+ const paths = await read(root, [
216
+ 'diff',
217
+ '--name-only',
218
+ '--diff-filter=A',
219
+ `${since}..HEAD`,
220
+ '--',
221
+ 'claude/skills/',
222
+ ])
223
+
224
+ return parseNewSkills(paths)
225
+ }
226
+
227
+ async function oldestAnchor(
228
+ root: string,
229
+ anchors: readonly string[],
230
+ ): Promise<string | undefined> {
231
+ const unique = [...new Set(anchors)]
232
+ let oldest = unique[0]
233
+
234
+ for (const candidate of unique.slice(1)) {
235
+ if (await isAncestor(root, candidate, oldest)) oldest = candidate
236
+ }
237
+
238
+ return oldest
239
+ }
240
+
241
+ async function isAncestor(
242
+ root: string,
243
+ candidate: string,
244
+ reference: string,
245
+ ): Promise<boolean> {
246
+ const result = await execa(
247
+ 'git',
248
+ ['-C', root, 'merge-base', '--is-ancestor', candidate, reference],
249
+ { reject: false },
250
+ )
251
+
252
+ return result.exitCode === 0
253
+ }
254
+
255
+ /**
256
+ * A toolkit outside a git clone, or a stamped revision this clone has never
257
+ * seen, yields no range. The per-file report still stands on its own.
258
+ */
259
+ async function read(root: string, args: readonly string[]): Promise<string> {
260
+ const result = await execa('git', ['-C', root, ...args], { reject: false })
261
+ return result.exitCode === 0 ? result.stdout : ''
262
+ }
263
+
264
+ function count(
265
+ entries: readonly ScanEntry[],
266
+ state: ScanEntry['state'],
267
+ ): number {
268
+ return entries.filter((entry) => entry.state === state).length
269
+ }