@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,202 @@
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
+
7
+ /**
8
+ * Domains the stamp can attribute. Tooling runs its own inject and manifest
9
+ * machinery rather than the sync engine, so it is absent by design.
10
+ */
11
+ export const STAMP_DOMAINS = ['standards', 'snippets', 'governance'] as const
12
+
13
+ export type StampDomain = (typeof STAMP_DOMAINS)[number]
14
+
15
+ /** What a domain needs to stamp itself: its key, and the clone to date it against. */
16
+ export interface StampSource {
17
+ readonly domain: StampDomain
18
+ readonly toolkitRoot: string
19
+ }
20
+
21
+ /** Target-relative posix path to content hash, for one domain. */
22
+ export type DomainHashes = Readonly<Record<string, string>>
23
+
24
+ /**
25
+ * One domain's record. The anchor sits here rather than at the top level
26
+ * because domains sync independently, and a shared anchor would let a gov sync
27
+ * advance the revision standards measures its upstream range from.
28
+ */
29
+ export interface DomainStamp {
30
+ /** Toolkit revision this domain last synced from. Absent outside a git clone. */
31
+ readonly commit?: string
32
+ readonly syncedAt: string
33
+ readonly files: DomainHashes
34
+ }
35
+
36
+ export interface Stamp {
37
+ /** Domains actually stamped in this target, so an unstamped one is legible. */
38
+ readonly covers: readonly StampDomain[]
39
+ readonly domains: Readonly<Partial<Record<StampDomain, DomainStamp>>>
40
+ }
41
+
42
+ export function stampPath(target: string): string {
43
+ return join(target, '.claude', 'aitk.json')
44
+ }
45
+
46
+ export function hashContent(content: Buffer | string): string {
47
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`
48
+ }
49
+
50
+ export function hashFile(path: string): string {
51
+ return hashContent(readFileSync(path))
52
+ }
53
+
54
+ /**
55
+ * Keys are stored posix-style so a stamp written on one platform still
56
+ * resolves on another.
57
+ */
58
+ export function toStampKey(rel: string): string {
59
+ return rel.split(sep).join('/')
60
+ }
61
+
62
+ /**
63
+ * A missing or corrupt stamp reads as absent rather than failing, which is what
64
+ * keeps every unstamped target on the existing unattributed path.
65
+ */
66
+ export function readStamp(target: string): Stamp | undefined {
67
+ const path = stampPath(target)
68
+ if (!existsSync(path)) return undefined
69
+
70
+ try {
71
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
72
+ return isStamp(parsed) ? parsed : undefined
73
+ } catch {
74
+ return undefined
75
+ }
76
+ }
77
+
78
+ /** The revision this domain last synced from, which bounds its upstream range. */
79
+ export function stampedCommit(
80
+ stamp: Stamp | undefined,
81
+ domain: StampDomain,
82
+ ): string | undefined {
83
+ return stamp?.domains[domain]?.commit
84
+ }
85
+
86
+ export function stampedHashes(
87
+ stamp: Stamp | undefined,
88
+ domain: StampDomain | undefined,
89
+ ): DomainHashes {
90
+ if (stamp === undefined || domain === undefined) return {}
91
+ return stamp.domains[domain]?.files ?? {}
92
+ }
93
+
94
+ /**
95
+ * Replaces one domain's record and leaves the others untouched, because domains
96
+ * install and sync independently but share the one file.
97
+ */
98
+ export async function writeStamp(
99
+ target: string,
100
+ source: StampSource,
101
+ hashes: DomainHashes,
102
+ now: Date,
103
+ ): Promise<void> {
104
+ const previous = readStamp(target)
105
+ const commit = await toolkitCommit(source.toolkitRoot)
106
+
107
+ const record: DomainStamp = {
108
+ ...(commit === undefined ? {} : { commit }),
109
+ syncedAt: now.toISOString(),
110
+ files: sortKeys(hashes),
111
+ }
112
+
113
+ const domains = sortDomains({
114
+ ...previous?.domains,
115
+ [source.domain]: record,
116
+ })
117
+
118
+ const stamp: Stamp = {
119
+ covers: STAMP_DOMAINS.filter((domain) => domains[domain] !== undefined),
120
+ domains,
121
+ }
122
+
123
+ const path = stampPath(target)
124
+ await mkdir(dirname(path), { recursive: true })
125
+ await writeFile(path, `${JSON.stringify(stamp, null, 2)}\n`)
126
+ }
127
+
128
+ const commitCache = new Map<string, Promise<string | undefined>>()
129
+
130
+ /**
131
+ * Resolved once per clone per process. A toolkit reached outside a git clone
132
+ * yields no commit, which costs the report its upstream range but not its
133
+ * per-file attribution.
134
+ */
135
+ export function toolkitCommit(root: string): Promise<string | undefined> {
136
+ const cached = commitCache.get(root)
137
+ if (cached !== undefined) return cached
138
+
139
+ const pending = readCommit(root)
140
+ commitCache.set(root, pending)
141
+ return pending
142
+ }
143
+
144
+ async function readCommit(root: string): Promise<string | undefined> {
145
+ const result = await execa(
146
+ 'git',
147
+ ['-C', root, 'rev-parse', '--short', 'HEAD'],
148
+ { reject: false },
149
+ )
150
+
151
+ return result.exitCode === 0 && result.stdout.trim() !== ''
152
+ ? result.stdout.trim()
153
+ : undefined
154
+ }
155
+
156
+ /** Deterministic key order keeps a re-sync diff empty and a merge conflict local. */
157
+ function sortKeys(hashes: DomainHashes): DomainHashes {
158
+ return Object.fromEntries(
159
+ Object.entries(hashes).sort(([left], [right]) => left.localeCompare(right)),
160
+ )
161
+ }
162
+
163
+ function sortDomains(
164
+ domains: Partial<Record<StampDomain, DomainStamp>>,
165
+ ): Partial<Record<StampDomain, DomainStamp>> {
166
+ const sorted: Partial<Record<StampDomain, DomainStamp>> = {}
167
+ for (const domain of STAMP_DOMAINS) {
168
+ const record = domains[domain]
169
+ if (record !== undefined) sorted[domain] = record
170
+ }
171
+
172
+ return sorted
173
+ }
174
+
175
+ /**
176
+ * Validates every domain record rather than only the container. A stamp whose
177
+ * domain value is the wrong shape would otherwise reach `attribute`, where a
178
+ * bad hash lookup silently reads as a customization.
179
+ */
180
+ function isStamp(value: unknown): value is Stamp {
181
+ if (!isRecord(value) || !isRecord(value.domains)) return false
182
+
183
+ return Object.entries(value.domains).every(
184
+ ([domain, record]) =>
185
+ (STAMP_DOMAINS as readonly string[]).includes(domain) &&
186
+ isDomainStamp(record),
187
+ )
188
+ }
189
+
190
+ function isDomainStamp(value: unknown): value is DomainStamp {
191
+ if (!isRecord(value) || !isRecord(value.files)) return false
192
+ if (typeof value.syncedAt !== 'string') return false
193
+ if (value.commit !== undefined && typeof value.commit !== 'string') {
194
+ return false
195
+ }
196
+
197
+ return Object.values(value.files).every((hash) => typeof hash === 'string')
198
+ }
199
+
200
+ function isRecord(value: unknown): value is Record<string, unknown> {
201
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
202
+ }
@@ -0,0 +1,79 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { isDirectory } from '@/target'
4
+
5
+ export const SYNC_DOMAINS = [
6
+ 'standards',
7
+ 'snippets',
8
+ 'governance',
9
+ 'claude',
10
+ ] as const
11
+
12
+ export type SyncDomain = (typeof SYNC_DOMAINS)[number]
13
+
14
+ export interface DomainState {
15
+ readonly domain: SyncDomain
16
+ readonly installed: boolean
17
+ }
18
+
19
+ const DOMAIN_MARKERS: Record<SyncDomain, string> = {
20
+ standards: join('.claude', 'standards'),
21
+ snippets: join('.claude', 'snippets'),
22
+ governance: join('.claude', 'rules'),
23
+ claude: '.claude',
24
+ }
25
+
26
+ /**
27
+ * Paths whose git status decides whether a domain contributed to the sync
28
+ * commit. Governance carries the retired `.claude/GOV.md` because a sync that
29
+ * removes it is still a governance change, and `claude` watches `.gitignore`
30
+ * because that is the only file `aitk claude sync` writes.
31
+ */
32
+ const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
33
+ standards: ['.claude/standards/'],
34
+ snippets: ['.claude/snippets/'],
35
+ governance: ['.claude/rules/', '.claude/GOV.md'],
36
+ claude: ['.gitignore'],
37
+ }
38
+
39
+ export function domainPaths(domain: SyncDomain): readonly string[] {
40
+ return DOMAIN_PATHS[domain]
41
+ }
42
+
43
+ export function detectDomains(target: string): DomainState[] {
44
+ return SYNC_DOMAINS.map((domain) => ({
45
+ domain,
46
+ installed: isDirectory(join(target, DOMAIN_MARKERS[domain])),
47
+ }))
48
+ }
49
+
50
+ export function installedDomains(states: readonly DomainState[]): SyncDomain[] {
51
+ return states.filter((state) => state.installed).map((state) => state.domain)
52
+ }
53
+
54
+ /**
55
+ * Whether a domain's sync command should run, which is not the same question
56
+ * `detectDomains` answers. Governance reports as absent without
57
+ * `.claude/rules/`, yet still syncs when only the retired `.claude/GOV.md`
58
+ * remains, so that the sync can delete it. The bash drew the same distinction
59
+ * between `detect_domains` and `run_syncs`.
60
+ */
61
+ export function shouldSync(target: string, domain: SyncDomain): boolean {
62
+ if (domain === 'governance') {
63
+ return (
64
+ isDirectory(join(target, '.claude', 'rules')) ||
65
+ existsSync(join(target, '.claude', 'GOV.md'))
66
+ )
67
+ }
68
+
69
+ return isDirectory(join(target, DOMAIN_MARKERS[domain]))
70
+ }
71
+
72
+ /**
73
+ * A target with no `.git/` reports clean, because `git status` fails there and
74
+ * the bash swallowed that failure. The guard exists to make the sync commit
75
+ * survivable, and a directory outside git has no commit to protect.
76
+ */
77
+ export function isTreeClean(status: string): boolean {
78
+ return status.trim() === ''
79
+ }
@@ -0,0 +1,201 @@
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
+ pipeOutput,
23
+ select,
24
+ } from '@/ui'
25
+
26
+ const GREEN = '\x1b[0;32m'
27
+ const NC = '\x1b[0m'
28
+
29
+ const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
30
+
31
+ export type WorkflowChoice = 'pr' | 'commit' | 'cancel'
32
+
33
+ export interface WorkflowDeps {
34
+ readonly git: GitRunner
35
+ /** Absent when `gh` is not installed, which hides the pull request option. */
36
+ readonly pullRequests: PullRequestOpener | undefined
37
+ readonly now: Date
38
+ readonly nonInteractive: boolean
39
+ /**
40
+ * Defaults to the interactive prompt. A test injects it to reach the commit
41
+ * and pull request branches, which `select` otherwise gates behind a TTY.
42
+ */
43
+ readonly choose?: (canOpenPullRequest: boolean) => Promise<WorkflowChoice>
44
+ }
45
+
46
+ /**
47
+ * Groups the target's pending changes by the domain that produced them. Each
48
+ * domain is read independently, so the four status calls run together.
49
+ */
50
+ export async function collectChanges(git: GitRunner): Promise<DomainChange[]> {
51
+ const results = await Promise.all(
52
+ SYNC_DOMAINS.map(async (domain) => {
53
+ const status = await git.status(domainPaths(domain))
54
+ return { domain, status }
55
+ }),
56
+ )
57
+
58
+ const changes: DomainChange[] = []
59
+
60
+ for (const { domain, status } of results) {
61
+ const names = changedNames(status)
62
+ if (names.length === 0) continue
63
+
64
+ changes.push({
65
+ domain,
66
+ verb: classifyStatus(status),
67
+ names,
68
+ paths: changedPaths(status),
69
+ })
70
+ }
71
+
72
+ return changes
73
+ }
74
+
75
+ /**
76
+ * Commits what the domain syncs wrote and optionally opens a pull request.
77
+ *
78
+ * This is the only toolkit code that writes to a git remote, which is why a
79
+ * headless run refuses it outright. `select_option` resolved to its first
80
+ * option under `AITK_NON_INTERACTIVE=1`, and the first option here was
81
+ * `Commit and open PR` whenever `gh` was installed, so an agent following the
82
+ * documented non-interactive path pushed a branch and opened a pull request on
83
+ * someone's repository without being asked.
84
+ */
85
+ export async function runGitWorkflow(
86
+ target: string,
87
+ deps: WorkflowDeps,
88
+ ): Promise<number> {
89
+ if (!isDirectory(join(target, '.git'))) return 0
90
+
91
+ const changes = await collectChanges(deps.git)
92
+ if (changes.length === 0) return 0
93
+
94
+ const domains = changes.map((change) => change.domain)
95
+ const message = commitMessage(domains)
96
+ const body = pullRequestBody(changes)
97
+ const branch = syncBranchName(deps.now)
98
+
99
+ intro('aitk sync → git')
100
+
101
+ const [localTaken, remoteTaken] = await Promise.all([
102
+ deps.git.branchExists(branch),
103
+ deps.git.remoteBranchExists(branch),
104
+ ])
105
+
106
+ if (localTaken || remoteTaken) {
107
+ logWarn(
108
+ `${branch} already exists. Wait a minute or delete it, then re-run.`,
109
+ )
110
+ outro()
111
+ return 0
112
+ }
113
+
114
+ const currentBranch = await deps.git.currentBranch()
115
+ const onProtected =
116
+ currentBranch === '' || PROTECTED_BRANCHES.includes(currentBranch)
117
+
118
+ logStep('Preview')
119
+ logInfo(`Domains: ${domains.join(', ')}`)
120
+ logInfo(
121
+ onProtected
122
+ ? `Branch: ${branch} (source: ${currentBranch || 'detached'})`
123
+ : `Branch: ${currentBranch} (commit only) or ${branch} (PR)`,
124
+ )
125
+ logInfo(`Commit: ${message}`)
126
+
127
+ logStep('PR body')
128
+ pipeOutput(body)
129
+
130
+ if (deps.nonInteractive) {
131
+ logWarn('Refusing the git workflow in non-interactive mode.')
132
+ logInfo(`Would commit ${domains.join(', ')} to ${branch}.`)
133
+ logInfo('Run interactively to commit, push, and open the pull request.')
134
+ outro()
135
+ return 0
136
+ }
137
+
138
+ const opener = deps.pullRequests
139
+ if (opener === undefined) {
140
+ logWarn(
141
+ 'gh CLI not found — PR option unavailable. Install from https://cli.github.com',
142
+ )
143
+ }
144
+
145
+ const choose = deps.choose ?? promptChoice
146
+ const choice = await choose(opener !== undefined)
147
+
148
+ if (choice === 'cancel') {
149
+ logWarn('Skipped')
150
+ outro()
151
+ return 0
152
+ }
153
+
154
+ const paths = [...new Set(changes.flatMap((change) => change.paths))]
155
+
156
+ try {
157
+ logStep('Committing')
158
+
159
+ if (choice === 'pr' || onProtected) await deps.git.createBranch(branch)
160
+ await deps.git.stage(paths)
161
+ await deps.git.commit(message)
162
+
163
+ if (choice === 'commit' || opener === undefined) {
164
+ return succeed(
165
+ onProtected
166
+ ? `Committed to ${branch}. Push and open a PR when ready.`
167
+ : `Committed to ${currentBranch}.`,
168
+ )
169
+ }
170
+
171
+ await deps.git.push(branch)
172
+
173
+ logStep('Opening PR')
174
+ const url = await opener.create(message, body)
175
+
176
+ return succeed(`Synced: ${url}`)
177
+ } catch (error) {
178
+ logError(error instanceof Error ? error.message : String(error))
179
+ outro()
180
+ return 1
181
+ }
182
+ }
183
+
184
+ function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
185
+ return select({
186
+ message: 'Review changes, then?',
187
+ options: [
188
+ ...(canOpenPullRequest
189
+ ? [{ value: 'pr' as const, label: 'Commit and open PR' }]
190
+ : []),
191
+ { value: 'commit' as const, label: 'Commit only' },
192
+ { value: 'cancel' as const, label: 'Cancel' },
193
+ ],
194
+ })
195
+ }
196
+
197
+ function succeed(message: string): number {
198
+ outro()
199
+ process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
200
+ return 0
201
+ }
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
+ }