@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,304 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises'
3
+ import { join, relative, resolve, sep } from 'node:path'
4
+ import { regenOne } from '@/indexes/regen'
5
+
6
+ const TASKS_DIR = join('.claude', 'tasks')
7
+ const ARCHIVE_DIR = join('.claude', '.tmp', 'task-archive')
8
+ const PLANS_DIR = join('.claude', 'plans')
9
+
10
+ /**
11
+ * Generated and hand-maintained siblings that sit on the board without being
12
+ * tasks. The post-merge hook skips the same two names.
13
+ */
14
+ const RESERVED_STEMS = ['index', 'priority'] as const
15
+
16
+ export const ARCHIVE_REFUSALS = [
17
+ 'no-board',
18
+ 'no-match',
19
+ 'ambiguous',
20
+ 'no-outcomes',
21
+ 'open-outcomes',
22
+ 'plan-unswept',
23
+ ] as const
24
+
25
+ export type ArchiveRefusal = (typeof ARCHIVE_REFUSALS)[number]
26
+
27
+ export type TaskSelector =
28
+ | { readonly kind: 'stem'; readonly stem: string }
29
+ | { readonly kind: 'pull-request'; readonly number: number }
30
+
31
+ export interface ArchiveSuccess {
32
+ readonly ok: true
33
+ readonly stem: string
34
+ readonly from: string
35
+ readonly to: string
36
+ readonly priorityRowRemoved: boolean
37
+ readonly indexRegenerated: boolean
38
+ }
39
+
40
+ export interface ArchiveRefused {
41
+ readonly ok: false
42
+ readonly reason: ArchiveRefusal
43
+ readonly message: string
44
+ readonly detail: readonly string[]
45
+ }
46
+
47
+ export type ArchiveOutcome = ArchiveSuccess | ArchiveRefused
48
+
49
+ export interface TaskOutcomes {
50
+ readonly open: readonly string[]
51
+ readonly closed: readonly string[]
52
+ }
53
+
54
+ export function tasksDir(root: string): string {
55
+ return join(root, TASKS_DIR)
56
+ }
57
+
58
+ export function archiveDir(root: string): string {
59
+ return join(root, ARCHIVE_DIR)
60
+ }
61
+
62
+ /**
63
+ * Splits a task's outcome list by checkbox state. The board format puts every
64
+ * outcome at the top level of `## Outcomes`, so an anchored match is enough and
65
+ * no heading tracking is needed.
66
+ */
67
+ export function readOutcomes(text: string): TaskOutcomes {
68
+ const open: string[] = []
69
+ const closed: string[] = []
70
+
71
+ for (const line of text.split('\n')) {
72
+ const match = /^- \[([ xX])\] ?(.*)$/.exec(line)
73
+ if (!match) continue
74
+
75
+ const [, box, body] = match
76
+ if (box === ' ') {
77
+ open.push(body.trim())
78
+ } else {
79
+ closed.push(body.trim())
80
+ }
81
+ }
82
+
83
+ return { open, closed }
84
+ }
85
+
86
+ /**
87
+ * Reads the `Pull request:` line `git-pr` writes when a pull request opens.
88
+ * The number is a bare `#NNN` the way `Issue:` is, since neither is a path and
89
+ * a full URL would write the remote into a gitignored file.
90
+ */
91
+ export function readPullRequest(text: string): number | undefined {
92
+ const match = /^Pull request:\s*#(\d+)\s*$/m.exec(text)
93
+ return match ? Number(match[1]) : undefined
94
+ }
95
+
96
+ /**
97
+ * Reads the `Plan:` target out of a markdown link, falling back to the older
98
+ * bare-path form. The path is returned as written, relative to the board.
99
+ */
100
+ export function readPlanTarget(text: string): string | undefined {
101
+ const match = /^Plan:\s*(?:\[[^\]]*\]\(([^)]+)\)|(\S+))\s*$/m.exec(text)
102
+ if (!match) return undefined
103
+ return match[1] ?? match[2]
104
+ }
105
+
106
+ /**
107
+ * Drops the archived task's row from the ordering table. Rows are matched by
108
+ * the link they carry rather than by a line pattern, because a row holds links
109
+ * and prose that a stream edit against it would rewrite in place.
110
+ */
111
+ export function removePriorityRow(
112
+ text: string,
113
+ stem: string,
114
+ ): { readonly text: string; readonly removed: boolean } {
115
+ const target = `](${stem}.md)`
116
+ const lines = text.split('\n')
117
+ const kept = lines.filter((line) => !isRowFor(line, target))
118
+
119
+ return { text: kept.join('\n'), removed: kept.length !== lines.length }
120
+ }
121
+
122
+ /**
123
+ * A row is about a task when its first cell links to it. A later cell naming
124
+ * another task is a reference, such as a blocker pointing at what it waits on,
125
+ * and matching anywhere in the line would delete that task's row alongside it.
126
+ */
127
+ function isRowFor(line: string, target: string): boolean {
128
+ const trimmed = line.trimStart()
129
+ if (!trimmed.startsWith('|')) return false
130
+
131
+ const [, first] = trimmed.split('|')
132
+ return first !== undefined && first.includes(target)
133
+ }
134
+
135
+ /**
136
+ * Tests containment rather than a string prefix, so a sibling whose name merely
137
+ * extends the folder's is not read as being inside it.
138
+ */
139
+ function isUnder(path: string, dir: string): boolean {
140
+ return path === dir || path.startsWith(`${dir}${sep}`)
141
+ }
142
+
143
+ /**
144
+ * Resolves the `Plan:` target against the board and against the project root
145
+ * both, which is how `claude-docs` reads the same line. It accepts `../plans/x.md`
146
+ * and `.claude/plans/x.md` as one file, so a gate reading only the first form
147
+ * would pass the second and strand the plan this exists to protect.
148
+ */
149
+ function isLivePlan(target: string, dir: string, root: string): boolean {
150
+ const plans = join(root, PLANS_DIR)
151
+
152
+ return (
153
+ isUnder(resolve(dir, target), plans) ||
154
+ isUnder(resolve(root, target), plans)
155
+ )
156
+ }
157
+
158
+ async function listTaskStems(dir: string): Promise<string[]> {
159
+ const entries = await readdir(dir)
160
+ const reserved: readonly string[] = RESERVED_STEMS
161
+
162
+ return entries
163
+ .filter((entry) => entry.endsWith('.md'))
164
+ .map((entry) => entry.slice(0, -'.md'.length))
165
+ .filter((stem) => !reserved.includes(stem))
166
+ .sort()
167
+ }
168
+
169
+ async function matchByPullRequest(
170
+ dir: string,
171
+ stems: readonly string[],
172
+ number: number,
173
+ ): Promise<string[]> {
174
+ const read = await Promise.all(
175
+ stems.map(async (stem) => ({
176
+ stem,
177
+ number: readPullRequest(await readFile(join(dir, `${stem}.md`), 'utf8')),
178
+ })),
179
+ )
180
+
181
+ return read.filter((entry) => entry.number === number).map(({ stem }) => stem)
182
+ }
183
+
184
+ function refuse(
185
+ reason: ArchiveRefusal,
186
+ message: string,
187
+ detail: readonly string[] = [],
188
+ ): ArchiveRefused {
189
+ return { ok: false, reason, message, detail }
190
+ }
191
+
192
+ async function resolveStem(
193
+ dir: string,
194
+ selector: TaskSelector,
195
+ ): Promise<string | ArchiveRefused> {
196
+ const stems = await listTaskStems(dir)
197
+
198
+ if (selector.kind === 'stem') {
199
+ if (!stems.includes(selector.stem)) {
200
+ return refuse(
201
+ 'no-match',
202
+ `No task named ${selector.stem} on the board.`,
203
+ stems,
204
+ )
205
+ }
206
+ return selector.stem
207
+ }
208
+
209
+ const matched = await matchByPullRequest(dir, stems, selector.number)
210
+
211
+ if (matched.length === 0) {
212
+ return refuse('no-match', `No task names pull request #${selector.number}.`)
213
+ }
214
+
215
+ if (matched.length > 1) {
216
+ return refuse(
217
+ 'ambiguous',
218
+ `${matched.length} tasks name pull request #${selector.number}. One task, one pull request.`,
219
+ matched,
220
+ )
221
+ }
222
+
223
+ return matched[0]
224
+ }
225
+
226
+ /**
227
+ * Archives one task as a single unit: the move, the ordering-row removal, and
228
+ * the index regen. The hook and `claude-tasks` both call this, so every gate
229
+ * refuses rather than reports. A caller with nobody watching cannot act on a
230
+ * warning, and two callers gating differently is the drift this exists to stop.
231
+ */
232
+ export async function archiveTask(
233
+ root: string,
234
+ selector: TaskSelector,
235
+ ): Promise<ArchiveOutcome> {
236
+ const dir = tasksDir(root)
237
+
238
+ if (!existsSync(dir)) {
239
+ return refuse('no-board', `No task board at ${relative(root, dir)}.`)
240
+ }
241
+
242
+ const resolved = await resolveStem(dir, selector)
243
+ if (typeof resolved !== 'string') return resolved
244
+
245
+ const stem = resolved
246
+ const from = join(dir, `${stem}.md`)
247
+ const text = await readFile(from, 'utf8')
248
+
249
+ const { open, closed } = readOutcomes(text)
250
+
251
+ if (open.length > 0) {
252
+ return refuse(
253
+ 'open-outcomes',
254
+ `${stem} has ${open.length} open outcome(s). Close them or cut them from the task, then archive.`,
255
+ open,
256
+ )
257
+ }
258
+
259
+ if (closed.length === 0) {
260
+ return refuse(
261
+ 'no-outcomes',
262
+ `${stem} carries no outcomes, so nothing marks it shipped.`,
263
+ )
264
+ }
265
+
266
+ const planTarget = readPlanTarget(text)
267
+ if (planTarget && isLivePlan(planTarget, dir, root)) {
268
+ return refuse(
269
+ 'plan-unswept',
270
+ `${stem} still points at a live plan. Run /claude-docs to sweep it first, then archive.`,
271
+ [planTarget],
272
+ )
273
+ }
274
+
275
+ const destination = archiveDir(root)
276
+ await mkdir(destination, { recursive: true })
277
+ const to = join(destination, `${stem}.md`)
278
+ await rename(from, to)
279
+
280
+ const priorityRowRemoved = await clearPriorityRow(dir, stem)
281
+ const regen = await regenOne(dir, { dryRun: false })
282
+
283
+ return {
284
+ ok: true,
285
+ stem,
286
+ from,
287
+ to,
288
+ priorityRowRemoved,
289
+ indexRegenerated: regen.action === 'written',
290
+ }
291
+ }
292
+
293
+ async function clearPriorityRow(dir: string, stem: string): Promise<boolean> {
294
+ const path = join(dir, 'priority.md')
295
+ if (!existsSync(path)) return false
296
+
297
+ const { text, removed } = removePriorityRow(
298
+ await readFile(path, 'utf8'),
299
+ stem,
300
+ )
301
+ if (removed) await writeFile(path, text)
302
+
303
+ return removed
304
+ }
@@ -0,0 +1,122 @@
1
+ import type { GitignoreSection } from '@/tooling/manifest'
2
+
3
+ export interface MergeResult {
4
+ readonly content: string
5
+ readonly added: readonly string[]
6
+ }
7
+
8
+ export interface PruneResult {
9
+ readonly content: string
10
+ readonly removed: readonly string[]
11
+ }
12
+
13
+ /**
14
+ * Splits a gitignore body the way `while IFS= read -r line` did, so a file
15
+ * with or without a trailing newline yields the same logical lines.
16
+ */
17
+ function toLines(content: string): string[] {
18
+ if (content === '') return []
19
+ const lines = content.split('\n')
20
+ if (lines[lines.length - 1] === '') lines.pop()
21
+ return lines
22
+ }
23
+
24
+ function fromLines(lines: readonly string[]): string {
25
+ if (lines.length === 0) return ''
26
+ return `${lines.join('\n')}\n`
27
+ }
28
+
29
+ function withoutTrailingSlash(entry: string): string {
30
+ return entry.endsWith('/') ? entry.slice(0, -1) : entry
31
+ }
32
+
33
+ function hasEntry(lines: readonly string[], entry: string): boolean {
34
+ return lines.includes(entry) || lines.includes(withoutTrailingSlash(entry))
35
+ }
36
+
37
+ /**
38
+ * Appends every manifest entry the file does not already carry.
39
+ *
40
+ * Two behaviors are load-bearing and preserved from `merge_gitignore`. The
41
+ * section header is written only when the whole section is missing, so a
42
+ * partially present section appends its entries bare at the end of the file.
43
+ * Presence is re-checked against the growing file, so an entry added by an
44
+ * earlier section or an earlier stack in the chain is not added twice.
45
+ */
46
+ export function mergeSections(
47
+ content: string,
48
+ sections: readonly GitignoreSection[],
49
+ ): MergeResult {
50
+ let next = content
51
+ const added: string[] = []
52
+
53
+ for (const section of sections) {
54
+ const lines = toLines(next)
55
+ const missing = section.entries.filter((entry) => !hasEntry(lines, entry))
56
+ if (missing.length === 0) continue
57
+
58
+ if (missing.length === section.entries.length) {
59
+ next += `\n${section.header}\n`
60
+ }
61
+
62
+ for (const entry of missing) {
63
+ next += `${entry}\n`
64
+ added.push(entry)
65
+ }
66
+ }
67
+
68
+ return { content: next, added }
69
+ }
70
+
71
+ /**
72
+ * Drops entries that sit under a managed header but are no longer in the
73
+ * manifest. A section runs from its header line until the first blank line or
74
+ * the next comment, matching `prune_gitignore_section`.
75
+ */
76
+ export function pruneSections(
77
+ content: string,
78
+ sections: readonly GitignoreSection[],
79
+ ): PruneResult {
80
+ let lines = toLines(content)
81
+ const removed: string[] = []
82
+
83
+ for (const section of sections) {
84
+ const kept: string[] = []
85
+ let inSection = false
86
+
87
+ for (const line of lines) {
88
+ if (line === section.header) {
89
+ inSection = true
90
+ kept.push(line)
91
+ continue
92
+ }
93
+
94
+ if (!inSection) {
95
+ kept.push(line)
96
+ continue
97
+ }
98
+
99
+ if (line === '' || line.startsWith('#')) {
100
+ inSection = false
101
+ kept.push(line)
102
+ continue
103
+ }
104
+
105
+ const isAllowed = section.entries.some(
106
+ (entry) =>
107
+ line === entry ||
108
+ withoutTrailingSlash(line) === withoutTrailingSlash(entry),
109
+ )
110
+
111
+ if (isAllowed) {
112
+ kept.push(line)
113
+ } else {
114
+ removed.push(line)
115
+ }
116
+ }
117
+
118
+ lines = kept
119
+ }
120
+
121
+ return { content: removed.length > 0 ? fromLines(lines) : content, removed }
122
+ }
@@ -0,0 +1,214 @@
1
+ import { existsSync } from 'node:fs'
2
+ import {
3
+ copyFile,
4
+ mkdir,
5
+ readFile,
6
+ rm,
7
+ rmdir,
8
+ writeFile,
9
+ } from 'node:fs/promises'
10
+ import { dirname, join } from 'node:path'
11
+ import { $ } from 'bun'
12
+ import { copyPreservingMode } from '@/copy'
13
+ import { mergeSections, pruneSections } from '@/tooling/gitignore'
14
+ import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
15
+ import {
16
+ applyScripts,
17
+ collectDeps,
18
+ readPackage,
19
+ serializePackage,
20
+ } from '@/tooling/package'
21
+ import { logAdd, logRemove, logStep } from '@/ui'
22
+
23
+ export async function injectConfigs(
24
+ chain: readonly Manifest[],
25
+ target: string,
26
+ ): Promise<string[]> {
27
+ const applied: string[] = []
28
+
29
+ for (const manifest of ancestorsFirst(chain)) {
30
+ const files = listFiles(manifest.configsDir)
31
+ if (files.length === 0) continue
32
+
33
+ logStep(`Applying ${manifest.name} configs`)
34
+ for (const rel of files) {
35
+ await copyPreservingMode(
36
+ join(manifest.configsDir, rel),
37
+ join(target, rel),
38
+ )
39
+ logAdd(rel)
40
+ applied.push(rel)
41
+ }
42
+ }
43
+
44
+ return applied
45
+ }
46
+
47
+ /**
48
+ * Copies a seed when the target lacks it. When the target already has one and
49
+ * the seed is a `.txt` word list, missing lines are appended and the file is
50
+ * re-sorted. Every other existing file is left untouched.
51
+ */
52
+ async function mergeSeedFile(src: string, dest: string): Promise<void> {
53
+ await mkdir(dirname(dest), { recursive: true })
54
+
55
+ if (!existsSync(dest)) {
56
+ await copyFile(src, dest)
57
+ return
58
+ }
59
+
60
+ if (!src.endsWith('.txt')) return
61
+
62
+ const existing = (await readFile(dest, 'utf8')).split('\n')
63
+ const incoming = (await readFile(src, 'utf8')).split('\n')
64
+ const missing = incoming.filter(
65
+ (word) => word !== '' && !existing.includes(word),
66
+ )
67
+
68
+ if (missing.length === 0) return
69
+
70
+ const merged = [...existing.filter((line) => line !== ''), ...missing].sort()
71
+ await writeFile(dest, `${merged.join('\n')}\n`)
72
+ }
73
+
74
+ export async function injectSeeds(
75
+ chain: readonly Manifest[],
76
+ target: string,
77
+ ): Promise<string[]> {
78
+ const applied: string[] = []
79
+
80
+ for (const manifest of ancestorsFirst(chain)) {
81
+ const files = listFiles(manifest.seedsDir)
82
+ if (files.length === 0) continue
83
+
84
+ logStep(`Applying ${manifest.name} seeds`)
85
+ for (const rel of files) {
86
+ await mergeSeedFile(join(manifest.seedsDir, rel), join(target, rel))
87
+ logAdd(rel)
88
+ applied.push(rel)
89
+ }
90
+ }
91
+
92
+ return applied
93
+ }
94
+
95
+ export async function injectGitignore(
96
+ chain: readonly Manifest[],
97
+ target: string,
98
+ ): Promise<string[]> {
99
+ const path = join(target, '.gitignore')
100
+ const added: string[] = []
101
+
102
+ for (const manifest of ancestorsFirst(chain)) {
103
+ if (manifest.gitignore.length === 0) continue
104
+
105
+ const content = existsSync(path) ? await readFile(path, 'utf8') : ''
106
+ const result = mergeSections(content, manifest.gitignore)
107
+ if (result.added.length === 0) continue
108
+
109
+ logStep(`Applying ${manifest.name} gitignore`)
110
+ await writeFile(path, result.content)
111
+ for (const entry of result.added) {
112
+ logAdd(entry)
113
+ added.push(entry)
114
+ }
115
+ }
116
+
117
+ if (added.length === 0 && !existsSync(path)) await writeFile(path, '')
118
+
119
+ return added
120
+ }
121
+
122
+ export async function pruneGitignore(
123
+ chain: readonly Manifest[],
124
+ target: string,
125
+ ): Promise<string[]> {
126
+ const path = join(target, '.gitignore')
127
+ if (!existsSync(path)) return []
128
+
129
+ const removed: string[] = []
130
+
131
+ for (const manifest of ancestorsFirst(chain)) {
132
+ if (manifest.gitignore.length === 0) continue
133
+
134
+ const content = await readFile(path, 'utf8')
135
+ const result = pruneSections(content, manifest.gitignore)
136
+ if (result.removed.length === 0) continue
137
+
138
+ await writeFile(path, result.content)
139
+ for (const entry of result.removed) {
140
+ logRemove(entry)
141
+ removed.push(entry)
142
+ }
143
+ }
144
+
145
+ return removed
146
+ }
147
+
148
+ /**
149
+ * Installs missing dev dependencies, fills package scripts, and merges
150
+ * gitignore entries. This is the `inject_tooling_manifest` sequence, which
151
+ * assumed a target that already has a package.json.
152
+ */
153
+ export async function injectManifest(
154
+ chain: readonly Manifest[],
155
+ target: string,
156
+ ): Promise<void> {
157
+ const packagePath = join(target, 'package.json')
158
+ const pkg = readPackage(packagePath)
159
+
160
+ if (pkg) {
161
+ const missing = collectDeps(chain, pkg)
162
+ .filter((dep) => dep.state === 'missing')
163
+ .map((dep) => dep.spec)
164
+
165
+ if (missing.length > 0) {
166
+ logStep('Installing missing dependencies')
167
+ await $`bun add -D ${missing}`.cwd(target)
168
+ for (const spec of missing) logAdd(spec)
169
+ }
170
+
171
+ const fresh = readPackage(packagePath)
172
+ if (fresh) {
173
+ const result = applyScripts(fresh, chain)
174
+ if (result.added.length > 0 || result.overridden.length > 0) {
175
+ logStep('Applying scripts')
176
+ for (const key of result.added) logAdd(key)
177
+ for (const key of result.overridden) logAdd(key)
178
+ await writeFile(packagePath, serializePackage(result.pkg))
179
+ }
180
+ }
181
+ }
182
+
183
+ await injectGitignore(chain, target)
184
+ }
185
+
186
+ /**
187
+ * Drops reference docs into `.claude/tooling/`, clearing the legacy
188
+ * `tooling/` location the earlier layout used.
189
+ */
190
+ export async function applyReferences(
191
+ chain: readonly Manifest[],
192
+ target: string,
193
+ stacks: readonly string[],
194
+ ): Promise<string[]> {
195
+ const destDir = join(target, '.claude', 'tooling')
196
+ await mkdir(destDir, { recursive: true })
197
+
198
+ const applied: string[] = []
199
+ const byName = new Map(chain.map((manifest) => [manifest.name, manifest]))
200
+
201
+ for (const stack of stacks) {
202
+ const manifest = byName.get(stack)
203
+ if (!manifest) continue
204
+
205
+ await copyFile(manifest.referenceFile, join(destDir, `${stack}.md`))
206
+ logAdd(`.claude/tooling/${stack}.md`)
207
+ applied.push(stack)
208
+ await rm(join(target, 'tooling', `${stack}.md`), { force: true })
209
+ }
210
+
211
+ await rmdir(join(target, 'tooling')).catch(() => {})
212
+
213
+ return applied
214
+ }
@@ -0,0 +1,39 @@
1
+ import { listStacks, loadManifest } from '@/tooling/manifest'
2
+
3
+ export interface StackSummary {
4
+ readonly name: string
5
+ readonly extends: string | null
6
+ readonly devDeps: number
7
+ readonly scripts: number
8
+ readonly gitignoreGroups: number
9
+ }
10
+
11
+ /**
12
+ * Summarizes each installable stack. The counts come from `loadManifest`
13
+ * rather than the three `awk` programs the bash carried, so the list and the
14
+ * sync it previews read the same parse.
15
+ */
16
+ export function buildStackSummaries(root: string): StackSummary[] {
17
+ const summaries: StackSummary[] = []
18
+
19
+ for (const stack of listStacks(root)) {
20
+ const manifest = loadManifest(root, stack)
21
+ if (!manifest) continue
22
+
23
+ summaries.push({
24
+ name: manifest.name,
25
+ extends: manifest.parent ?? null,
26
+ devDeps: manifest.devPackages.length,
27
+ scripts: Object.keys(manifest.scripts).length,
28
+ gitignoreGroups: manifest.gitignore.length,
29
+ })
30
+ }
31
+
32
+ return summaries
33
+ }
34
+
35
+ export function describeStack(summary: StackSummary): string {
36
+ const counts = `${summary.devDeps} dev deps, ${summary.scripts} scripts, ${summary.gitignoreGroups} gitignore groups`
37
+ const prefix = summary.extends === null ? '' : `extends: ${summary.extends}, `
38
+ return `${summary.name} (${prefix}${counts})`
39
+ }