@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,315 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { Command } from 'commander'
4
+ import { PROJECT_ROOT, execScript } from '@/exec'
5
+ import {
6
+ collectCoverage,
7
+ coveragePercent,
8
+ type CoverageReport,
9
+ } from '@/sandbox/coverage'
10
+ import {
11
+ expectFilePath,
12
+ parseTarget,
13
+ resolveVerdict,
14
+ verdictExitCode,
15
+ type RunEnvelope,
16
+ type Verdict,
17
+ } from '@/sandbox/expect'
18
+ import {
19
+ intro,
20
+ logError,
21
+ logInfo,
22
+ logRemove,
23
+ logStep,
24
+ logWarn,
25
+ outro,
26
+ select,
27
+ } from '@/ui'
28
+
29
+ const SANDBOX_DIR = join(PROJECT_ROOT, 'scripts', 'sandbox')
30
+
31
+ /**
32
+ * Holds fixture content for scenarios rather than scenarios of its own.
33
+ * Twin of the `-not -name fixtures` filter in `scripts/manage-sandbox.sh`.
34
+ */
35
+ const FIXTURES_DIR = 'fixtures'
36
+
37
+ /** A run that reported nothing counts as clean, so only real signals fail. */
38
+ const CLEAN_ENVELOPE: RunEnvelope = { isError: false, turns: 0, denials: 0 }
39
+
40
+ interface CheckOptions {
41
+ readonly envelope?: string
42
+ readonly writes?: string
43
+ readonly json?: boolean
44
+ readonly strict?: boolean
45
+ }
46
+
47
+ interface CoverageOptions {
48
+ readonly json?: boolean
49
+ readonly strict?: boolean
50
+ }
51
+
52
+ function getCategories(): string[] {
53
+ return readdirSync(SANDBOX_DIR, { withFileTypes: true })
54
+ .filter((d) => d.isDirectory() && d.name !== FIXTURES_DIR)
55
+ .map((d) => d.name)
56
+ .sort()
57
+ }
58
+
59
+ function getCommands(category: string): string[] {
60
+ return readdirSync(join(SANDBOX_DIR, category), { withFileTypes: true })
61
+ .filter((f) => f.isFile() && f.name.endsWith('.sh'))
62
+ .map((f) => f.name.replace(/\.sh$/, ''))
63
+ .sort()
64
+ }
65
+
66
+ async function interactivePicker(): Promise<string> {
67
+ intro('aitk sandbox')
68
+
69
+ const categories = getCategories()
70
+ const category = await select({
71
+ message: 'Select category:',
72
+ options: categories.map((c) => ({ value: c, label: c })),
73
+ })
74
+
75
+ const commands = getCommands(category)
76
+ const command = await select({
77
+ message: 'Select command:',
78
+ options: commands.map((c) => ({ value: c, label: c })),
79
+ })
80
+
81
+ return `${category}:${command}`
82
+ }
83
+
84
+ /**
85
+ * Reads the `claude -p --output-format json` envelope. Returns undefined when no
86
+ * file was given, so the turn ceiling reports as skipped rather than passing on a
87
+ * fabricated zero. A file that exists but does not parse falls back to clean,
88
+ * since decision 8 lets the envelope fail a run but never pass one.
89
+ */
90
+ function readEnvelope(path: string | undefined): RunEnvelope | undefined {
91
+ if (path === undefined) return undefined
92
+ if (!existsSync(path)) return CLEAN_ENVELOPE
93
+
94
+ try {
95
+ const parsed = JSON.parse(readFileSync(path, 'utf8')) as Record<
96
+ string,
97
+ unknown
98
+ >
99
+ const denials = parsed.permission_denials
100
+
101
+ return {
102
+ isError: parsed.is_error === true,
103
+ turns: typeof parsed.num_turns === 'number' ? parsed.num_turns : 0,
104
+ denials: Array.isArray(denials) ? denials.length : 0,
105
+ }
106
+ } catch {
107
+ return CLEAN_ENVELOPE
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Undefined when no file was given, which is not the same as a run that wrote
113
+ * nothing. The write-scope assertion needs that distinction: an empty list is a
114
+ * finding, an absent list is a gap in what the caller supplied.
115
+ */
116
+ function readWrites(path: string | undefined): string[] | undefined {
117
+ if (path === undefined) return undefined
118
+ if (!existsSync(path)) return []
119
+
120
+ return readFileSync(path, 'utf8')
121
+ .split('\n')
122
+ .map((line) => line.trim())
123
+ .filter((line) => line !== '')
124
+ }
125
+
126
+ /**
127
+ * The unchecked count rides the summary line in every verdict, including a pass.
128
+ * It is the number the rollout is measured by, and a count that surfaces only on
129
+ * arms that happen to carry prose is a count nobody drains.
130
+ */
131
+ function reportVerdict(verdict: Verdict): void {
132
+ if (verdict.results.length > 0) {
133
+ logStep('Checking expectations')
134
+ for (const result of verdict.results) {
135
+ if (result.ok) logInfo(result.message)
136
+ else logRemove(result.message)
137
+ }
138
+ }
139
+
140
+ if (verdict.manual.length > 0) {
141
+ logStep('Not checked (needs a reader)')
142
+ for (const line of verdict.manual) logWarn(line)
143
+ }
144
+
145
+ if (verdict.skipped.length > 0) {
146
+ logStep('Not checked (no data supplied)')
147
+ for (const line of verdict.skipped) logWarn(line)
148
+ }
149
+
150
+ logStep(`Verdict: ${verdict.state.toUpperCase()}`)
151
+ if (verdict.note !== undefined) logWarn(verdict.note)
152
+
153
+ const summary = `${verdict.asserted} asserted, ${verdict.failed} failed, ${verdict.unchecked} unchecked`
154
+ if (verdict.state === 'unchecked') logWarn(summary)
155
+ else logInfo(summary)
156
+ }
157
+
158
+ /**
159
+ * Lists the undeclared scenarios by name rather than counting them. A percentage
160
+ * alone is a number nobody acts on, while a name is the next thing to arm.
161
+ */
162
+ function reportCoverage(report: CoverageReport): void {
163
+ const armed = report.scenarios.filter((s) => s.armed.length > 0)
164
+ const bare = report.scenarios.filter((s) => s.armed.length === 0)
165
+
166
+ if (armed.length > 0) {
167
+ logStep('Declares expectations')
168
+ for (const scenario of armed) {
169
+ logInfo(
170
+ `${scenario.category}:${scenario.command} (${scenario.armed.join(', ')})`,
171
+ )
172
+ }
173
+ }
174
+
175
+ if (bare.length > 0) {
176
+ logStep('Provisions only, asserts nothing')
177
+ for (const scenario of bare)
178
+ logWarn(`${scenario.category}:${scenario.command}`)
179
+ }
180
+
181
+ logStep('Coverage')
182
+ const summary =
183
+ `${report.armedScenarios}/${report.totalScenarios} scenarios declared ` +
184
+ `(${coveragePercent(report)}%), ${report.armedArms} armed arms`
185
+ if (bare.length > 0) logWarn(summary)
186
+ else logInfo(summary)
187
+ }
188
+
189
+ function runCoverage(options: CoverageOptions): void {
190
+ intro('aitk sandbox coverage')
191
+
192
+ const report = collectCoverage(PROJECT_ROOT)
193
+ reportCoverage(report)
194
+ if (options.json === true) process.stdout.write(`${JSON.stringify(report)}\n`)
195
+
196
+ outro()
197
+ process.exitCode =
198
+ options.strict === true && report.armedScenarios < report.totalScenarios
199
+ ? 1
200
+ : 0
201
+ }
202
+
203
+ function runCheck(
204
+ target: string,
205
+ arm: string | undefined,
206
+ options: CheckOptions,
207
+ ): void {
208
+ intro('aitk sandbox check')
209
+
210
+ const parsed = parseTarget(target)
211
+ if (parsed === undefined) {
212
+ logError('Invalid target. Use <category>:<command>, e.g. claude:docs.')
213
+ outro()
214
+ process.exitCode = 1
215
+ return
216
+ }
217
+
218
+ // A sandbox that was never provisioned fails every path assertion, reading as a
219
+ // skill that did nothing rather than a caller that ran check too early. The
220
+ // whole point of a verdict is that it means what it says.
221
+ const sandboxDir = join(PROJECT_ROOT, '.sandbox')
222
+ if (!existsSync(sandboxDir)) {
223
+ logError(`No sandbox at ${sandboxDir}. Provision one with aitk sandbox.`)
224
+ outro()
225
+ process.exitCode = 1
226
+ return
227
+ }
228
+
229
+ const verdict = resolveVerdict(
230
+ expectFilePath(PROJECT_ROOT, parsed.category, parsed.command, arm ?? ''),
231
+ {
232
+ sandboxDir,
233
+ writes: readWrites(options.writes),
234
+ envelope: readEnvelope(options.envelope),
235
+ },
236
+ )
237
+
238
+ // The report always renders on stderr. `--json` adds the machine copy on
239
+ // stdout rather than replacing the frame, per the stream contract in
240
+ // `docs/agents.md`, so one invocation serves a human and a caller at once.
241
+ reportVerdict(verdict)
242
+ if (options.json === true)
243
+ process.stdout.write(`${JSON.stringify(verdict)}\n`)
244
+
245
+ outro()
246
+ process.exitCode = verdictExitCode(verdict.state, options.strict === true)
247
+ }
248
+
249
+ export function register(program: Command): void {
250
+ const sandbox = program
251
+ .command('sandbox')
252
+ .description('Provision and run sandbox scenarios')
253
+ .allowUnknownOption()
254
+ .allowExcessArguments(true)
255
+ .passThroughOptions()
256
+ .action(async (_opts: unknown, cmd: Command) => {
257
+ const args = cmd.args
258
+
259
+ if (args.length === 0) {
260
+ const resolved = await interactivePicker()
261
+ await execScript('manage-sandbox.sh', ['--no-header', resolved])
262
+ } else {
263
+ await execScript('manage-sandbox.sh', args)
264
+ }
265
+ })
266
+
267
+ sandbox
268
+ .command('check')
269
+ .description('Check a provisioned sandbox against a scenario expectation')
270
+ .argument('<target>', 'Scenario as <category>:<command>, e.g. claude:docs')
271
+ .argument('[arm]', 'Named scenario arm, e.g. drift')
272
+ .helpOption('-h, --help', 'Show this help message')
273
+ .option('--envelope <file>', 'Run envelope JSON from claude -p')
274
+ .option('--writes <file>', 'Newline-delimited paths the session wrote')
275
+ .option('--json', 'Emit the verdict as JSON on stdout')
276
+ .option('--strict', 'Exit non-zero when the arm declares no expectation')
277
+ .addHelpText(
278
+ 'after',
279
+ [
280
+ '',
281
+ 'Examples:',
282
+ ' aitk sandbox check claude:docs drift',
283
+ ' aitk sandbox check claude:docs drift --envelope run.json --json',
284
+ '',
285
+ 'Exit codes: 0 on pass or unchecked, 1 on failure.',
286
+ 'With --strict, unchecked exits 1 as well.',
287
+ ].join('\n'),
288
+ )
289
+ .action(
290
+ (target: string, arm: string | undefined, options: CheckOptions) => {
291
+ runCheck(target, arm, options)
292
+ },
293
+ )
294
+
295
+ sandbox
296
+ .command('coverage')
297
+ .description('Report which scenarios declare expectations')
298
+ .helpOption('-h, --help', 'Show this help message')
299
+ .option('--json', 'Emit the report as JSON on stdout')
300
+ .option('--strict', 'Exit non-zero while any scenario declares nothing')
301
+ .addHelpText(
302
+ 'after',
303
+ [
304
+ '',
305
+ 'Examples:',
306
+ ' aitk sandbox coverage',
307
+ ' aitk sandbox coverage --json',
308
+ '',
309
+ 'Exit codes: 0 always, unless --strict and a scenario declares nothing.',
310
+ ].join('\n'),
311
+ )
312
+ .action((options: CoverageOptions) => {
313
+ runCoverage(options)
314
+ })
315
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import type { Command } from 'commander'
4
+ import { LAYOUTS } from '@/slides/layouts'
5
+ import { openDeck } from '@/slides/open'
6
+ import { renderSlidesDoc } from '@/slides/render'
7
+ import type { Variant } from '@/slides/styles'
8
+ import { intro, outro } from '@/ui'
9
+
10
+ const GREY = '\x1b[0;90m'
11
+ const GREEN = '\x1b[0;32m'
12
+ const RED = '\x1b[0;31m'
13
+ const NC = '\x1b[0m'
14
+
15
+ export function register(program: Command): void {
16
+ const slides = program
17
+ .command('slides')
18
+ .description('Slide deck commands (render, list)')
19
+
20
+ slides
21
+ .command('render')
22
+ .description('Render a SLIDES.md source into a PowerPoint deck')
23
+ .option('-s, --source <path>', 'Source SLIDES.md path', '.claude/SLIDES.md')
24
+ .option('-o, --out <path>', 'Output directory', '.claude/review/slides')
25
+ .option('-v, --variant <variant>', 'Override variant (light or dark)')
26
+ .option(
27
+ '-m, --mirror <path>',
28
+ 'Copy the deck to this directory after writing',
29
+ )
30
+ .option('--open', 'Open the deck after writing')
31
+ .action(
32
+ async (opts: {
33
+ source: string
34
+ out: string
35
+ variant?: string
36
+ mirror?: string
37
+ open?: boolean
38
+ }) => {
39
+ const sourcePath = resolve(process.cwd(), opts.source)
40
+ const outDir = resolve(process.cwd(), opts.out)
41
+ if (!existsSync(sourcePath)) {
42
+ fail(`${opts.source} not found`)
43
+ }
44
+ const variant = parseVariant(opts.variant)
45
+ const mirror = resolveMirror(opts.mirror)
46
+ intro('Render slides')
47
+ const result = await renderSlidesDoc(sourcePath, outDir, {
48
+ variant,
49
+ mirror,
50
+ })
51
+ process.stderr.write(
52
+ `${GREY}│${NC} ${GREEN}✓${NC} ${result.slideCount} slides\n${GREY}│${NC} ${GREEN}✓${NC} ${result.pptxPath}\n`,
53
+ )
54
+ if (result.mirrorPath) {
55
+ process.stderr.write(
56
+ `${GREY}│${NC} ${GREEN}✓${NC} mirrored to ${result.mirrorPath}\n`,
57
+ )
58
+ }
59
+ if (opts.open) {
60
+ const target = result.mirrorPath ?? result.pptxPath
61
+ const opened = await openDeck(target)
62
+ const mark = opened ? `${GREEN}✓${NC}` : `${RED}✗${NC}`
63
+ process.stderr.write(
64
+ `${GREY}│${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
65
+ )
66
+ }
67
+ outro()
68
+ },
69
+ )
70
+
71
+ slides
72
+ .command('list')
73
+ .description('List the available slide layouts')
74
+ .option('--json', 'Output the layout catalog as JSON')
75
+ .action((opts: { json?: boolean }) => {
76
+ if (opts.json) {
77
+ process.stdout.write(`${JSON.stringify(LAYOUTS)}\n`)
78
+ return
79
+ }
80
+ intro('Slide layouts')
81
+ for (const layout of LAYOUTS) {
82
+ process.stderr.write(
83
+ `${GREY}│${NC} ${GREEN}✓${NC} ${layout.name} ${GREY}${layout.description}${NC}\n`,
84
+ )
85
+ }
86
+ outro()
87
+ })
88
+ }
89
+
90
+ function parseVariant(value: string | undefined): Variant | undefined {
91
+ if (value === undefined) return undefined
92
+ if (value === 'light' || value === 'dark') return value
93
+ fail(`Invalid variant "${value}". Use light or dark.`)
94
+ }
95
+
96
+ function resolveMirror(value: string | undefined): string | undefined {
97
+ const mirror = value ?? process.env.AITK_SLIDES_MIRROR
98
+ return mirror ? resolve(process.cwd(), mirror) : undefined
99
+ }
100
+
101
+ function fail(message: string): never {
102
+ process.stderr.write(
103
+ `${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${message}\n${GREY}└${NC}\n`,
104
+ )
105
+ process.exit(1)
106
+ }
@@ -0,0 +1,236 @@
1
+ import type { Command } from 'commander'
2
+ import { registerPassThroughVerbs } from '@/commands/pass-through'
3
+ import { PROJECT_ROOT } from '@/exec'
4
+ import { createSnippetsAdapter } from '@/snippets/adapter'
5
+ import { BASE_CATEGORY } from '@/snippets/categories'
6
+ import {
7
+ ALL_CATEGORY,
8
+ installSnippets,
9
+ installableCategories,
10
+ resolveSnippets,
11
+ } from '@/snippets/install'
12
+ import { buildSnippetsCatalog } from '@/snippets/list'
13
+ import { recordStamp, runDomainSync } from '@/sync/engine'
14
+ import { resolveTarget } from '@/target'
15
+ import {
16
+ intro,
17
+ isNonInteractive,
18
+ logAdd,
19
+ logError,
20
+ logInfo,
21
+ logStep,
22
+ logWarn,
23
+ outro,
24
+ select,
25
+ } from '@/ui'
26
+
27
+ const GREEN = '\x1b[0;32m'
28
+ const NC = '\x1b[0m'
29
+
30
+ const SNIPPETS_REL = '.claude/snippets'
31
+
32
+ const PASS_THROUGH_VERBS = ['create'] as const
33
+
34
+ interface ListOptions {
35
+ readonly categories?: boolean
36
+ readonly entries?: boolean
37
+ readonly json?: boolean
38
+ }
39
+
40
+ export function register(program: Command): void {
41
+ const snippets = program
42
+ .command('snippets')
43
+ .description('Snippets commands (install, sync, create, list)')
44
+ .helpOption('-h, --help', 'Show this help message')
45
+
46
+ snippets
47
+ .command('install')
48
+ .description('Install snippets into .claude/snippets/')
49
+ .argument('[category]', "Preset, folder, or 'all'")
50
+ .argument('[target]', 'Target directory', '.')
51
+ .helpOption('-h, --help', 'Show this help message')
52
+ .addHelpText(
53
+ 'after',
54
+ [
55
+ '',
56
+ 'Examples:',
57
+ ' aitk snippets install essentials',
58
+ ' aitk snippets install all',
59
+ ' aitk snippets install claude ../my-app',
60
+ '',
61
+ ].join('\n'),
62
+ )
63
+ .action(async (category: string | undefined, target: string) => {
64
+ process.exitCode = await runInstall(category, target)
65
+ })
66
+
67
+ snippets
68
+ .command('sync')
69
+ .description('Update snippets already installed under .claude/snippets/')
70
+ .argument('[target]', 'Target directory', '.')
71
+ .helpOption('-h, --help', 'Show this help message')
72
+ .action(async (target: string) => {
73
+ process.exitCode = await runDomainSync(
74
+ createSnippetsAdapter(PROJECT_ROOT),
75
+ target,
76
+ { protectedRoot: PROJECT_ROOT },
77
+ )
78
+ })
79
+
80
+ snippets
81
+ .command('list')
82
+ .description('List snippet presets, categories, and entries')
83
+ .helpOption('-h, --help', 'Show this help message')
84
+ .option('--categories', 'Only list presets and category names')
85
+ .option('--entries', 'Only list entry slugs grouped by category')
86
+ .option('--json', 'Emit machine-readable JSON')
87
+ .action((opts: ListOptions) => {
88
+ process.exitCode = runList(opts)
89
+ })
90
+
91
+ registerPassThroughVerbs(snippets, 'snippets', PASS_THROUGH_VERBS)
92
+ }
93
+
94
+ /**
95
+ * Refuses rather than picking, the same judgment `aitk gov install` applies to
96
+ * its stack picker. Headless, `select_option` returned the first option, which
97
+ * here was `all`, so an agent omitting the argument installed every category.
98
+ */
99
+ async function chooseCategory(root: string): Promise<string | number> {
100
+ const categories = installableCategories(root)
101
+
102
+ if (categories.length === 0) {
103
+ logError('No categories found in snippets source.')
104
+ outro()
105
+ return 1
106
+ }
107
+
108
+ if (isNonInteractive()) {
109
+ logError(
110
+ `Category argument is required in non-interactive mode. One of: ${[ALL_CATEGORY, ...categories].join(', ')}.`,
111
+ )
112
+ outro()
113
+ return 1
114
+ }
115
+
116
+ return select({
117
+ message: 'Select category to install:',
118
+ options: [ALL_CATEGORY, ...categories].map((name) => ({
119
+ value: name,
120
+ label: name,
121
+ })),
122
+ })
123
+ }
124
+
125
+ async function runInstall(
126
+ category: string | undefined,
127
+ target: string,
128
+ ): Promise<number> {
129
+ intro('aitk snippets install')
130
+
131
+ const resolved = resolveTarget(target, PROJECT_ROOT)
132
+ if (typeof resolved === 'number') return resolved
133
+
134
+ let selected = category
135
+ if (selected === undefined) {
136
+ const choice = await chooseCategory(PROJECT_ROOT)
137
+ if (typeof choice === 'number') return choice
138
+ selected = choice
139
+ }
140
+
141
+ const resolution = resolveSnippets(PROJECT_ROOT, selected)
142
+ if (!resolution.ok) {
143
+ logError(`Category not found: ${resolution.unknownCategory}`)
144
+ outro()
145
+ return 1
146
+ }
147
+
148
+ logStep(resolution.step)
149
+ for (const slug of resolution.missing) {
150
+ logWarn(`${slug} (source not found, skipping)`)
151
+ }
152
+
153
+ if (resolution.files.length === 0) {
154
+ logWarn(`No snippets found for category: ${selected}`)
155
+ outro()
156
+ return 0
157
+ }
158
+
159
+ for (const file of resolution.files) logInfo(file.relPath)
160
+
161
+ const display = target.replace(/\/$/, '')
162
+ const shouldInstall = await select({
163
+ message: `Install ${resolution.files.length} snippets to ${display}/${SNIPPETS_REL}?`,
164
+ options: [
165
+ { value: true, label: 'Yes' },
166
+ { value: false, label: 'No' },
167
+ ],
168
+ nonInteractiveDefault: true,
169
+ })
170
+
171
+ if (!shouldInstall) {
172
+ logWarn('Cancelled')
173
+ outro()
174
+ return 0
175
+ }
176
+
177
+ logStep('Installing snippets')
178
+ for (const rel of await installSnippets(resolution.files, resolved)) {
179
+ logAdd(rel)
180
+ }
181
+ await recordStamp(createSnippetsAdapter(PROJECT_ROOT), resolved, new Date())
182
+
183
+ outro()
184
+ process.stderr.write(`${GREEN}✓ Snippets installed${NC}\n`)
185
+ return 0
186
+ }
187
+
188
+ /**
189
+ * `JSON.stringify` replaces a `printf` template that interpolated names into a
190
+ * JSON string literal unescaped, so a slug or category carrying a quote emitted
191
+ * output no consuming skill could parse.
192
+ *
193
+ * The frame opens after the `--json` return, since the bash verb only ever
194
+ * emitted section headers and the pass-through above it owned the `┌`.
195
+ */
196
+ function runList(opts: ListOptions): number {
197
+ const catalog = buildSnippetsCatalog(PROJECT_ROOT)
198
+
199
+ if (opts.json) {
200
+ process.stdout.write(`${JSON.stringify(catalog)}\n`)
201
+ return 0
202
+ }
203
+
204
+ intro('aitk snippets list')
205
+
206
+ const showCategories = opts.entries !== true
207
+ const showEntries = opts.categories !== true
208
+
209
+ if (showCategories) {
210
+ if (catalog.presets.length > 0) {
211
+ logStep('Presets')
212
+ for (const preset of catalog.presets) {
213
+ logInfo(`${preset.name} (${preset.slugs.length} entries)`)
214
+ }
215
+ }
216
+
217
+ logStep('Categories')
218
+ for (const category of catalog.categories) {
219
+ logInfo(`${category.name} (${category.entries.length} entries)`)
220
+ }
221
+ }
222
+
223
+ if (showEntries) {
224
+ logStep('Entries')
225
+ for (const category of catalog.categories) {
226
+ for (const entry of category.entries) {
227
+ logInfo(
228
+ category.name === BASE_CATEGORY ? entry : `${category.name}/${entry}`,
229
+ )
230
+ }
231
+ }
232
+ }
233
+
234
+ outro()
235
+ return 0
236
+ }