@erclx/canon 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (643) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/claude/.claude-plugin/plugin.json +19 -0
  4. package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
  5. package/claude/skills/bash-cli-script/SKILL.md +48 -0
  6. package/claude/skills/bash-cli-script/references/template.md +43 -0
  7. package/claude/skills/bash-script/REQUIREMENT.md +36 -0
  8. package/claude/skills/bash-script/SKILL.md +100 -0
  9. package/claude/skills/bash-script/references/patterns.md +349 -0
  10. package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
  11. package/claude/skills/canon-cli/SKILL.md +103 -0
  12. package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
  13. package/claude/skills/canon-feedback-file/SKILL.md +80 -0
  14. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
  15. package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
  16. package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
  17. package/claude/skills/canon-operator/SKILL.md +108 -0
  18. package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
  19. package/claude/skills/canon-rollout/SKILL.md +147 -0
  20. package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
  21. package/claude/skills/canon-screencast/SKILL.md +167 -0
  22. package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
  23. package/claude/skills/canon-slides-draft/SKILL.md +62 -0
  24. package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
  25. package/claude/skills/ci-workflow/SKILL.md +65 -0
  26. package/claude/skills/ci-workflow/references/workflows.md +98 -0
  27. package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
  28. package/claude/skills/claude-address-review/SKILL.md +212 -0
  29. package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
  30. package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
  31. package/claude/skills/claude-autoship/SKILL.md +207 -0
  32. package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
  33. package/claude/skills/claude-design-extract/SKILL.md +102 -0
  34. package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
  35. package/claude/skills/claude-diagram/SKILL.md +177 -0
  36. package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
  37. package/claude/skills/claude-docs/SKILL.md +287 -0
  38. package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
  39. package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
  40. package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
  41. package/claude/skills/claude-feature/SKILL.md +115 -0
  42. package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
  43. package/claude/skills/claude-groundwork/SKILL.md +142 -0
  44. package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
  45. package/claude/skills/claude-intake/SKILL.md +114 -0
  46. package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
  47. package/claude/skills/claude-intake-answer/SKILL.md +90 -0
  48. package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
  49. package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
  50. package/claude/skills/claude-markdown-propose/references/format.md +107 -0
  51. package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
  52. package/claude/skills/claude-memory-capture/SKILL.md +101 -0
  53. package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
  54. package/claude/skills/claude-memory-review/SKILL.md +210 -0
  55. package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
  56. package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
  57. package/claude/skills/claude-orchestrate/SKILL.md +241 -0
  58. package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
  59. package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
  60. package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
  61. package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
  62. package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
  63. package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
  64. package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
  65. package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
  66. package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
  67. package/claude/skills/claude-pr-review/SKILL.md +295 -0
  68. package/claude/skills/claude-review/REQUIREMENT.md +39 -0
  69. package/claude/skills/claude-review/SKILL.md +142 -0
  70. package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
  71. package/claude/skills/claude-seed-sync/SKILL.md +156 -0
  72. package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
  73. package/claude/skills/claude-standards-audit/SKILL.md +99 -0
  74. package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
  75. package/claude/skills/claude-tasks/SKILL.md +161 -0
  76. package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
  77. package/claude/skills/claude-teach/SKILL.md +196 -0
  78. package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
  79. package/claude/skills/claude-teach/references/pedagogy.md +67 -0
  80. package/claude/skills/claude-teach/references/promotion.md +54 -0
  81. package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
  82. package/claude/skills/claude-ui-test/SKILL.md +77 -0
  83. package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
  84. package/claude/skills/claude-ux-audit/SKILL.md +79 -0
  85. package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
  86. package/claude/skills/claude-ux-measure/SKILL.md +122 -0
  87. package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
  88. package/claude/skills/claude-worker/SKILL.md +96 -0
  89. package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
  90. package/claude/skills/claude-worktree/SKILL.md +136 -0
  91. package/claude/skills/create-rule/REQUIREMENT.md +45 -0
  92. package/claude/skills/create-rule/SKILL.md +68 -0
  93. package/claude/skills/create-skill/REQUIREMENT.md +38 -0
  94. package/claude/skills/create-skill/SKILL.md +32 -0
  95. package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
  96. package/claude/skills/create-snippet/SKILL.md +30 -0
  97. package/claude/skills/create-standard/REQUIREMENT.md +35 -0
  98. package/claude/skills/create-standard/SKILL.md +29 -0
  99. package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
  100. package/claude/skills/decision-escalate/SKILL.md +79 -0
  101. package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
  102. package/claude/skills/docs-sync/SKILL.md +95 -0
  103. package/claude/skills/git-branch/REQUIREMENT.md +38 -0
  104. package/claude/skills/git-branch/SKILL.md +60 -0
  105. package/claude/skills/git-commit/REQUIREMENT.md +36 -0
  106. package/claude/skills/git-commit/SKILL.md +49 -0
  107. package/claude/skills/git-followup/REQUIREMENT.md +43 -0
  108. package/claude/skills/git-followup/SKILL.md +48 -0
  109. package/claude/skills/git-issue/REQUIREMENT.md +38 -0
  110. package/claude/skills/git-issue/SKILL.md +65 -0
  111. package/claude/skills/git-pr/REQUIREMENT.md +50 -0
  112. package/claude/skills/git-pr/SKILL.md +164 -0
  113. package/claude/skills/git-pr/references/labels.md +95 -0
  114. package/claude/skills/git-ship/REQUIREMENT.md +42 -0
  115. package/claude/skills/git-ship/SKILL.md +55 -0
  116. package/claude/skills/git-split/REQUIREMENT.md +39 -0
  117. package/claude/skills/git-split/SKILL.md +162 -0
  118. package/claude/skills/git-stage/REQUIREMENT.md +39 -0
  119. package/claude/skills/git-stage/SKILL.md +73 -0
  120. package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
  121. package/claude/skills/git-worktree/SKILL.md +130 -0
  122. package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
  123. package/claude/skills/migration-claude-md/SKILL.md +76 -0
  124. package/claude/skills/migration-context/REQUIREMENT.md +36 -0
  125. package/claude/skills/migration-context/SKILL.md +95 -0
  126. package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
  127. package/claude/skills/migration-standards-drop/SKILL.md +113 -0
  128. package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
  129. package/claude/skills/migration-superseded/SKILL.md +115 -0
  130. package/claude/skills/project-commands/REQUIREMENT.md +42 -0
  131. package/claude/skills/project-commands/SKILL.md +85 -0
  132. package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
  133. package/claude/skills/restate-plainly/SKILL.md +39 -0
  134. package/claude/skills/session-map/REQUIREMENT.md +57 -0
  135. package/claude/skills/session-map/SKILL.md +70 -0
  136. package/claude/skills/session-resume/REQUIREMENT.md +49 -0
  137. package/claude/skills/session-resume/SKILL.md +51 -0
  138. package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
  139. package/claude/skills/setup-gov/SKILL.md +77 -0
  140. package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
  141. package/claude/skills/setup-indexes/SKILL.md +153 -0
  142. package/claude/skills/setup-init/REQUIREMENT.md +45 -0
  143. package/claude/skills/setup-init/SKILL.md +127 -0
  144. package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
  145. package/claude/skills/setup-plugins/SKILL.md +81 -0
  146. package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
  147. package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
  148. package/claude/skills/setup-verify/SKILL.md +51 -0
  149. package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
  150. package/claude/skills/systematic-debugging/SKILL.md +70 -0
  151. package/claude/skills/write-human/REQUIREMENT.md +46 -0
  152. package/claude/skills/write-human/SKILL.md +68 -0
  153. package/claude/skills/write-human/references/density.md +38 -0
  154. package/claude/skills/write-human/references/machine-tells.md +107 -0
  155. package/claude/skills/write-human/references/source-material.md +37 -0
  156. package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
  157. package/claude/skills/youtube-transcripts/SKILL.md +34 -0
  158. package/docs/agents/audits.md +98 -0
  159. package/docs/agents/capture.md +37 -0
  160. package/docs/agents/census.md +23 -0
  161. package/docs/agents/commands.md +146 -0
  162. package/docs/agents/comments.md +34 -0
  163. package/docs/agents/context-audit-checks.md +120 -0
  164. package/docs/agents/context-audit.md +83 -0
  165. package/docs/agents/counts.md +76 -0
  166. package/docs/agents/demo.md +86 -0
  167. package/docs/agents/docs.md +17 -0
  168. package/docs/agents/gate.md +84 -0
  169. package/docs/agents/index.md +47 -0
  170. package/docs/agents/indexes.md +35 -0
  171. package/docs/agents/install-and-sync.md +385 -0
  172. package/docs/agents/intake.md +81 -0
  173. package/docs/agents/key-changes.md +103 -0
  174. package/docs/agents/label-coverage.md +73 -0
  175. package/docs/agents/markdown-audit.md +197 -0
  176. package/docs/agents/output-shape.md +70 -0
  177. package/docs/agents/overview.md +26 -0
  178. package/docs/agents/records.md +170 -0
  179. package/docs/agents/restated.md +81 -0
  180. package/docs/agents/review-classification.md +77 -0
  181. package/docs/agents/routing.md +61 -0
  182. package/docs/agents/rule-citations.md +98 -0
  183. package/docs/agents/sandbox.md +71 -0
  184. package/docs/agents/scripting.md +149 -0
  185. package/docs/agents/sessions.md +120 -0
  186. package/docs/agents/skills-audit.md +94 -0
  187. package/docs/agents/skills-reach.md +64 -0
  188. package/docs/agents/standards-audit.md +38 -0
  189. package/docs/agents/state-scoped-risk.md +105 -0
  190. package/docs/agents/superseded.md +85 -0
  191. package/docs/agents/targets.md +83 -0
  192. package/docs/agents/tasks.md +200 -0
  193. package/docs/agents/teach.md +158 -0
  194. package/docs/agents/test-order.md +56 -0
  195. package/docs/agents/worktrees.md +62 -0
  196. package/docs/ai-workflow.md +317 -0
  197. package/docs/index.md +23 -0
  198. package/docs/operating-model.md +223 -0
  199. package/docs/target-projects.md +258 -0
  200. package/docs/visual-design-workflow.md +151 -0
  201. package/docs/zshrc-aliases.md +65 -0
  202. package/governance/rules/ci/700-ci-workflow.md +44 -0
  203. package/governance/rules/claude/500-prose.md +15 -0
  204. package/governance/rules/claude/501-markdown.md +14 -0
  205. package/governance/rules/claude/510-context.md +28 -0
  206. package/governance/rules/claude/511-indexes.md +14 -0
  207. package/governance/rules/claude/520-wireframes.md +12 -0
  208. package/governance/rules/claude/530-requirements.md +11 -0
  209. package/governance/rules/claude/540-architecture.md +11 -0
  210. package/governance/rules/claude/550-design.md +11 -0
  211. package/governance/rules/claude/555-tasks.md +12 -0
  212. package/governance/rules/claude/556-groundwork.md +11 -0
  213. package/governance/rules/claude/557-intake.md +11 -0
  214. package/governance/rules/claude/558-plan.md +22 -0
  215. package/governance/rules/claude/559-memory.md +11 -0
  216. package/governance/rules/claude/560-diagrams.md +18 -0
  217. package/governance/rules/claude/561-teach.md +13 -0
  218. package/governance/rules/claude/562-session.md +15 -0
  219. package/governance/rules/claude/570-skill.md +24 -0
  220. package/governance/rules/claude/575-hooks.md +17 -0
  221. package/governance/rules/claude/576-settings.md +14 -0
  222. package/governance/rules/claude/580-readme.md +11 -0
  223. package/governance/rules/claude/590-rule-authoring.md +12 -0
  224. package/governance/rules/claude/591-standard-authoring.md +12 -0
  225. package/governance/rules/claude/592-claude-md.md +19 -0
  226. package/governance/rules/core/000-constitution.md +30 -0
  227. package/governance/rules/core/005-behavior.md +27 -0
  228. package/governance/rules/core/010-testing.md +35 -0
  229. package/governance/rules/core/015-output.md +20 -0
  230. package/governance/rules/core/020-concurrency.md +22 -0
  231. package/governance/rules/core/025-indexes.md +9 -0
  232. package/governance/rules/core/030-error-handling.md +31 -0
  233. package/governance/rules/core/035-tasks.md +13 -0
  234. package/governance/rules/core/040-performance.md +20 -0
  235. package/governance/rules/core/045-memory.md +12 -0
  236. package/governance/rules/core/050-logging.md +20 -0
  237. package/governance/rules/core/055-scratch.md +9 -0
  238. package/governance/rules/core/060-naming.md +19 -0
  239. package/governance/rules/core/065-spelling.md +19 -0
  240. package/governance/rules/core/070-planning.md +18 -0
  241. package/governance/rules/core/075-dependencies.md +25 -0
  242. package/governance/rules/core/080-config-comments.md +22 -0
  243. package/governance/rules/core/085-worktrees.md +17 -0
  244. package/governance/rules/core/087-git.md +11 -0
  245. package/governance/rules/core/090-code-comments.md +39 -0
  246. package/governance/rules/framework/200-react.md +51 -0
  247. package/governance/rules/framework/210-astro.md +41 -0
  248. package/governance/rules/framework/220-fastapi.md +43 -0
  249. package/governance/rules/framework/230-nextjs.md +48 -0
  250. package/governance/rules/framework/250-tailwind.md +32 -0
  251. package/governance/rules/framework/260-shadcn.md +34 -0
  252. package/governance/rules/lang/100-typescript.md +40 -0
  253. package/governance/rules/lang/110-python.md +42 -0
  254. package/governance/rules/lang/120-bash.md +19 -0
  255. package/governance/rules/lib/300-testing-ts.md +39 -0
  256. package/governance/rules/lib/305-e2e-reliability.md +34 -0
  257. package/governance/rules/lib/306-test-scope.md +25 -0
  258. package/governance/rules/lib/310-zod.md +25 -0
  259. package/governance/rules/lib/320-tanstack-query.md +32 -0
  260. package/governance/rules/lib/330-testing-py.md +44 -0
  261. package/governance/rules/lib/340-pydantic.md +38 -0
  262. package/governance/rules/lib/350-security-web.md +32 -0
  263. package/governance/rules/lib/360-security-server.md +39 -0
  264. package/governance/rules/lib/370-database.md +35 -0
  265. package/governance/rules/snippets/505-at-references.md +9 -0
  266. package/governance/rules/ui/400-ui.md +36 -0
  267. package/governance/rules/ui/410-a11y.md +48 -0
  268. package/governance/rules/ui/420-forms.md +36 -0
  269. package/governance/rules/ui/430-ux-completeness.md +65 -0
  270. package/governance/rules/ui/440-surface-capture.md +34 -0
  271. package/governance/rules/ui/450-link-behavior.md +19 -0
  272. package/governance/stacks/astro.toml +2 -0
  273. package/governance/stacks/base.toml +8 -0
  274. package/governance/stacks/node-server.toml +2 -0
  275. package/governance/stacks/node.toml +2 -0
  276. package/governance/stacks/python-fastapi.toml +2 -0
  277. package/governance/stacks/python.toml +2 -0
  278. package/governance/stacks/react.toml +2 -0
  279. package/package.json +69 -0
  280. package/scripts/config.sh +11 -0
  281. package/scripts/core/bootstrap.sh +81 -0
  282. package/scripts/core/check-color-source.sh +41 -0
  283. package/scripts/core/check-ignore-parity.sh +162 -0
  284. package/scripts/core/check-plugin-boundary.sh +45 -0
  285. package/scripts/core/check-seed-independence.sh +59 -0
  286. package/scripts/core/check-skill-paths.sh +24 -0
  287. package/scripts/core/clean.sh +36 -0
  288. package/scripts/core/install-check.sh +101 -0
  289. package/scripts/core/list-seed-roots.sh +18 -0
  290. package/scripts/core/regen-claude-copies.sh +10 -0
  291. package/scripts/core/regen-hero.sh +217 -0
  292. package/scripts/core/regen-indexes.sh +10 -0
  293. package/scripts/core/regen-tooling-paths.sh +61 -0
  294. package/scripts/core/repair-bare-flag.sh +19 -0
  295. package/scripts/core/snapshot.sh +134 -0
  296. package/scripts/core/update.sh +35 -0
  297. package/scripts/docs/list.sh +165 -0
  298. package/scripts/lib/frontmatter.sh +30 -0
  299. package/scripts/lib/gov.sh +14 -0
  300. package/scripts/lib/sandbox-fixtures.sh +191 -0
  301. package/scripts/lib/sandbox-git.sh +125 -0
  302. package/scripts/lib/sandbox-path.sh +206 -0
  303. package/scripts/lib/tooling.sh +35 -0
  304. package/scripts/lib/ui.sh +266 -0
  305. package/scripts/lib/worktree.sh +20 -0
  306. package/scripts/manage-sandbox.sh +466 -0
  307. package/scripts/snippets/create.sh +156 -0
  308. package/scripts/standards/list.sh +115 -0
  309. package/scripts/tooling/create.sh +109 -0
  310. package/scripts/tooling/verify.sh +179 -0
  311. package/snippets/align.md +12 -0
  312. package/snippets/claude/decision-memo.md +39 -0
  313. package/snippets/claude/feature-recap.md +19 -0
  314. package/snippets/claude/figma-steps.md +24 -0
  315. package/snippets/compact-summary.md +5 -0
  316. package/snippets/decision-help.md +6 -0
  317. package/snippets/meta-prompt.md +14 -0
  318. package/snippets/research-prompt.md +7 -0
  319. package/snippets/session-notes.md +11 -0
  320. package/snippets/snippets.toml +5 -0
  321. package/snippets/step-by-step.md +10 -0
  322. package/snippets/web-research.md +21 -0
  323. package/src/audits/baseline.ts +201 -0
  324. package/src/audits/catalog.ts +876 -0
  325. package/src/audits/run.ts +204 -0
  326. package/src/autoship/classify.ts +75 -0
  327. package/src/autoship/paths.ts +51 -0
  328. package/src/binary.ts +16 -0
  329. package/src/browser/engine.ts +40 -0
  330. package/src/census/count.ts +113 -0
  331. package/src/claude/cases/all.ts +24 -0
  332. package/src/claude/cases/authoring.ts +53 -0
  333. package/src/claude/cases/claude-workflow.ts +158 -0
  334. package/src/claude/cases/git.ts +44 -0
  335. package/src/claude/cases/misc.ts +27 -0
  336. package/src/claude/cases/setup.ts +94 -0
  337. package/src/claude/gitignore.ts +51 -0
  338. package/src/claude/routing.ts +283 -0
  339. package/src/claude/seeds-list.ts +47 -0
  340. package/src/claude/seeds.ts +150 -0
  341. package/src/claude/settings.ts +151 -0
  342. package/src/claude/skills-audit.ts +228 -0
  343. package/src/claude/skills-drift.ts +156 -0
  344. package/src/claude/skills-list.ts +99 -0
  345. package/src/claude/skills-rank.ts +320 -0
  346. package/src/claude/skills-reach.ts +227 -0
  347. package/src/cli-run.ts +43 -0
  348. package/src/cli.ts +200 -0
  349. package/src/commands/audits.ts +350 -0
  350. package/src/commands/autoship.ts +129 -0
  351. package/src/commands/capture.ts +133 -0
  352. package/src/commands/census.ts +105 -0
  353. package/src/commands/claude.ts +1286 -0
  354. package/src/commands/comments.ts +240 -0
  355. package/src/commands/context.ts +857 -0
  356. package/src/commands/demo.ts +389 -0
  357. package/src/commands/deps.ts +173 -0
  358. package/src/commands/design.ts +36 -0
  359. package/src/commands/docs.ts +60 -0
  360. package/src/commands/feedback-format.ts +23 -0
  361. package/src/commands/feedback.ts +112 -0
  362. package/src/commands/gate.ts +189 -0
  363. package/src/commands/gov.ts +1265 -0
  364. package/src/commands/indexes.ts +184 -0
  365. package/src/commands/init.ts +113 -0
  366. package/src/commands/intake.ts +406 -0
  367. package/src/commands/inventory.ts +256 -0
  368. package/src/commands/labels.ts +361 -0
  369. package/src/commands/markdown.ts +544 -0
  370. package/src/commands/migrate.ts +175 -0
  371. package/src/commands/pass-through.ts +39 -0
  372. package/src/commands/pr.ts +411 -0
  373. package/src/commands/records.ts +728 -0
  374. package/src/commands/sandbox.ts +468 -0
  375. package/src/commands/secrets.ts +132 -0
  376. package/src/commands/serve.ts +159 -0
  377. package/src/commands/sessions.ts +408 -0
  378. package/src/commands/slides.ts +126 -0
  379. package/src/commands/snippets.ts +84 -0
  380. package/src/commands/standards.ts +247 -0
  381. package/src/commands/sync.ts +428 -0
  382. package/src/commands/targets.ts +319 -0
  383. package/src/commands/tasks.ts +743 -0
  384. package/src/commands/teach.ts +786 -0
  385. package/src/commands/tooling.ts +573 -0
  386. package/src/commands/transcripts.ts +44 -0
  387. package/src/commands/upgrade.ts +231 -0
  388. package/src/commands/wiki.ts +100 -0
  389. package/src/commands/worktrees.ts +191 -0
  390. package/src/comments/scan.ts +338 -0
  391. package/src/comments/trend.ts +207 -0
  392. package/src/comments/vocabulary.ts +85 -0
  393. package/src/context/architecture.ts +364 -0
  394. package/src/context/audit.ts +790 -0
  395. package/src/context/citations.ts +196 -0
  396. package/src/context/folders.ts +186 -0
  397. package/src/context/gate.ts +57 -0
  398. package/src/context/index-drift.ts +64 -0
  399. package/src/context/narration.ts +99 -0
  400. package/src/copy.ts +30 -0
  401. package/src/counts/catalogs.ts +96 -0
  402. package/src/counts/numbers.ts +79 -0
  403. package/src/counts/scan.ts +314 -0
  404. package/src/demo/beats.ts +135 -0
  405. package/src/demo/compile.ts +326 -0
  406. package/src/demo/container.ts +63 -0
  407. package/src/demo/cursors.ts +55 -0
  408. package/src/demo/drive.ts +357 -0
  409. package/src/demo/pointer.ts +178 -0
  410. package/src/demo/theme.ts +112 -0
  411. package/src/deps/audit.ts +153 -0
  412. package/src/design/parse.ts +116 -0
  413. package/src/design/render.ts +249 -0
  414. package/src/docs/read.ts +77 -0
  415. package/src/exec.ts +16 -0
  416. package/src/exempt-marker.ts +43 -0
  417. package/src/frontmatter.ts +13 -0
  418. package/src/gate/measures.ts +682 -0
  419. package/src/gate/sequencer.ts +386 -0
  420. package/src/gate/stages.ts +412 -0
  421. package/src/git-env.ts +36 -0
  422. package/src/git-files.ts +105 -0
  423. package/src/git-ignore.ts +46 -0
  424. package/src/github-format.ts +13 -0
  425. package/src/github.ts +24 -0
  426. package/src/gov/adapter.ts +103 -0
  427. package/src/gov/citations.ts +514 -0
  428. package/src/gov/consumed.ts +129 -0
  429. package/src/gov/install.ts +132 -0
  430. package/src/gov/list.ts +106 -0
  431. package/src/gov/payload.ts +39 -0
  432. package/src/gov/restated.ts +814 -0
  433. package/src/gov/stacks.ts +205 -0
  434. package/src/gov/superseded.ts +415 -0
  435. package/src/gov/test-order.ts +407 -0
  436. package/src/indexes/frontmatter.ts +46 -0
  437. package/src/indexes/regen.ts +84 -0
  438. package/src/indexes/render.ts +201 -0
  439. package/src/indexes/walk.ts +83 -0
  440. package/src/init/flags.ts +60 -0
  441. package/src/init/plan.ts +114 -0
  442. package/src/init/run.ts +46 -0
  443. package/src/init/steps.ts +77 -0
  444. package/src/intake/folder.ts +320 -0
  445. package/src/intake/items.ts +174 -0
  446. package/src/inventory/config.ts +117 -0
  447. package/src/inventory/group.ts +76 -0
  448. package/src/inventory/subjects.ts +114 -0
  449. package/src/inventory/walk.ts +129 -0
  450. package/src/labels/audit.ts +82 -0
  451. package/src/labels/coverage.ts +79 -0
  452. package/src/labels/map.ts +101 -0
  453. package/src/labels/phase.ts +95 -0
  454. package/src/markdown/bans.ts +94 -0
  455. package/src/markdown/files.ts +102 -0
  456. package/src/markdown/gate.ts +28 -0
  457. package/src/markdown/scan.ts +295 -0
  458. package/src/markdown/structure.ts +730 -0
  459. package/src/migrate/apply.ts +115 -0
  460. package/src/migrate/plan.ts +103 -0
  461. package/src/migrate/rename.ts +183 -0
  462. package/src/pr/bijection.ts +145 -0
  463. package/src/pr/paths.ts +335 -0
  464. package/src/process/harness.ts +167 -0
  465. package/src/project-root.ts +19 -0
  466. package/src/records/backup.ts +455 -0
  467. package/src/records/migrate.ts +78 -0
  468. package/src/records/size.ts +260 -0
  469. package/src/records/validate.ts +1162 -0
  470. package/src/sandbox/census.ts +228 -0
  471. package/src/sandbox/coverage.ts +115 -0
  472. package/src/sandbox/expect.ts +629 -0
  473. package/src/sandbox/tree.ts +47 -0
  474. package/src/secrets/marker.ts +30 -0
  475. package/src/secrets/patterns.ts +142 -0
  476. package/src/secrets/scan.ts +123 -0
  477. package/src/secrets/shipped.ts +111 -0
  478. package/src/seed-marker.ts +74 -0
  479. package/src/serve/static.ts +322 -0
  480. package/src/sessions/claim.ts +85 -0
  481. package/src/sessions/live.ts +79 -0
  482. package/src/sessions/registry.ts +137 -0
  483. package/src/sessions/resolve.ts +333 -0
  484. package/src/slides/layouts.ts +391 -0
  485. package/src/slides/open.ts +18 -0
  486. package/src/slides/parse.ts +84 -0
  487. package/src/slides/render.ts +88 -0
  488. package/src/slides/styles.ts +44 -0
  489. package/src/snippets/categories.ts +66 -0
  490. package/src/snippets/list.ts +32 -0
  491. package/src/snippets/presets.ts +50 -0
  492. package/src/standards/audit.ts +132 -0
  493. package/src/standards/read.ts +100 -0
  494. package/src/sync/check.ts +692 -0
  495. package/src/sync/engine.ts +576 -0
  496. package/src/sync/git.ts +225 -0
  497. package/src/sync/history.ts +123 -0
  498. package/src/sync/layout.ts +140 -0
  499. package/src/sync/reverse.ts +268 -0
  500. package/src/sync/seeds-report.ts +126 -0
  501. package/src/sync/stamp.ts +358 -0
  502. package/src/sync/target.ts +70 -0
  503. package/src/sync/workflow.ts +200 -0
  504. package/src/target.ts +43 -0
  505. package/src/targets/pulls.ts +250 -0
  506. package/src/targets/registry.ts +203 -0
  507. package/src/targets/resolve.ts +145 -0
  508. package/src/targets/sweep.ts +246 -0
  509. package/src/tasks/archive.ts +506 -0
  510. package/src/tasks/record.ts +311 -0
  511. package/src/tasks/trunk.ts +89 -0
  512. package/src/tasks/validate.ts +1001 -0
  513. package/src/teach/lesson.ts +180 -0
  514. package/src/teach/workspace.ts +842 -0
  515. package/src/tooling/gitignore.ts +122 -0
  516. package/src/tooling/inject.ts +193 -0
  517. package/src/tooling/list.ts +39 -0
  518. package/src/tooling/manifest.ts +176 -0
  519. package/src/tooling/package.ts +166 -0
  520. package/src/tooling/read.ts +65 -0
  521. package/src/tooling/scan.ts +147 -0
  522. package/src/tooling/stamp.ts +44 -0
  523. package/src/transcripts/fetch.ts +156 -0
  524. package/src/transcripts/metadata.ts +54 -0
  525. package/src/transcripts/vtt.ts +114 -0
  526. package/src/ui.ts +266 -0
  527. package/src/version/compare.ts +53 -0
  528. package/src/version/installed.ts +40 -0
  529. package/src/version/manager.ts +67 -0
  530. package/src/version/skew.ts +192 -0
  531. package/src/wiki/init.ts +85 -0
  532. package/src/worktree.ts +143 -0
  533. package/src/worktrees/reclaim.ts +306 -0
  534. package/standards/architecture.md +72 -0
  535. package/standards/branch.md +59 -0
  536. package/standards/commit.md +72 -0
  537. package/standards/context.md +151 -0
  538. package/standards/design.md +93 -0
  539. package/standards/diagrams.md +152 -0
  540. package/standards/glossary.md +75 -0
  541. package/standards/groundwork.md +211 -0
  542. package/standards/index.md +36 -0
  543. package/standards/intake.md +192 -0
  544. package/standards/issue.md +94 -0
  545. package/standards/markdown.md +137 -0
  546. package/standards/memory.md +144 -0
  547. package/standards/plan.md +172 -0
  548. package/standards/pr.md +139 -0
  549. package/standards/publish.md +51 -0
  550. package/standards/readme.md +208 -0
  551. package/standards/requirements.md +70 -0
  552. package/standards/rule.md +118 -0
  553. package/standards/session.md +109 -0
  554. package/standards/skill.md +300 -0
  555. package/standards/slug.md +39 -0
  556. package/standards/snippets.md +76 -0
  557. package/standards/standard.md +170 -0
  558. package/standards/tasks.md +254 -0
  559. package/standards/teach.md +153 -0
  560. package/standards/versioning.md +71 -0
  561. package/standards/wireframes.md +113 -0
  562. package/tooling/astro/configs/astro.config.mjs +31 -0
  563. package/tooling/astro/configs/eslint.config.js +79 -0
  564. package/tooling/astro/configs/playwright.config.ts +26 -0
  565. package/tooling/astro/configs/tsconfig.json +12 -0
  566. package/tooling/astro/configs/vitest.config.ts +22 -0
  567. package/tooling/astro/manifest.toml +34 -0
  568. package/tooling/astro/reference.md +60 -0
  569. package/tooling/base/configs/.editorconfig +5 -0
  570. package/tooling/base/configs/.github/pull_request_template.md +18 -0
  571. package/tooling/base/configs/.github/workflows/verify.yml +35 -0
  572. package/tooling/base/configs/.husky/commit-msg +1 -0
  573. package/tooling/base/configs/.husky/post-merge +61 -0
  574. package/tooling/base/configs/.husky/post-rewrite +21 -0
  575. package/tooling/base/configs/.husky/pre-commit +1 -0
  576. package/tooling/base/configs/.husky/pre-push +1 -0
  577. package/tooling/base/configs/.prettierrc +12 -0
  578. package/tooling/base/configs/.shellcheckrc +1 -0
  579. package/tooling/base/configs/.vscode/extensions.json +9 -0
  580. package/tooling/base/configs/.vscode/settings.json +3 -0
  581. package/tooling/base/configs/commitlint.config.js +11 -0
  582. package/tooling/base/configs/scripts/verify.sh +64 -0
  583. package/tooling/base/manifest.toml +30 -0
  584. package/tooling/base/reference.md +98 -0
  585. package/tooling/base/seeds/.claude/context/ci.md +33 -0
  586. package/tooling/base/seeds/.claude/context/development.md +37 -0
  587. package/tooling/base/seeds/.claude/context/index.md +11 -0
  588. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  589. package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
  590. package/tooling/base/seeds/.lintstagedrc +8 -0
  591. package/tooling/base/seeds/.prettierignore +0 -0
  592. package/tooling/base/seeds/cspell.json +20 -0
  593. package/tooling/claude/manifest.toml +14 -0
  594. package/tooling/claude/reference.md +79 -0
  595. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
  596. package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
  597. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
  598. package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
  599. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
  600. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
  601. package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
  602. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
  603. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
  604. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
  605. package/tooling/claude/seeds/.claude/memory/index.md +8 -0
  606. package/tooling/claude/seeds/.claude/settings.json +47 -0
  607. package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
  608. package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
  609. package/tooling/claude/seeds/CLAUDE.md +29 -0
  610. package/tooling/claude/user/settings.template.json +10 -0
  611. package/tooling/claude/user/statusline-command.sh +53 -0
  612. package/tooling/python/configs/.coveragerc +14 -0
  613. package/tooling/python/configs/.python-version +1 -0
  614. package/tooling/python/configs/mypy.ini +6 -0
  615. package/tooling/python/configs/pytest.ini +4 -0
  616. package/tooling/python/configs/ruff.toml +15 -0
  617. package/tooling/python/configs/scripts/verify.sh +77 -0
  618. package/tooling/python/manifest.toml +16 -0
  619. package/tooling/python/reference.md +66 -0
  620. package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
  621. package/tooling/python/seeds/tests/test_smoke.py +2 -0
  622. package/tooling/vite-react/configs/playwright.config.ts +26 -0
  623. package/tooling/vite-react/configs/tsconfig.json +35 -0
  624. package/tooling/vite-react/configs/vite.config.ts +24 -0
  625. package/tooling/vite-react/configs/vitest.config.ts +26 -0
  626. package/tooling/vite-react/manifest.toml +23 -0
  627. package/tooling/vite-react/reference.md +55 -0
  628. package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
  629. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
  630. package/tooling/web/configs/.github/workflows/verify.yml +134 -0
  631. package/tooling/web/configs/.vscode/extensions.json +13 -0
  632. package/tooling/web/configs/.vscode/settings.json +10 -0
  633. package/tooling/web/configs/e2e/home.spec.ts +6 -0
  634. package/tooling/web/configs/e2e/screenshot.ts +53 -0
  635. package/tooling/web/configs/eslint.config.js +82 -0
  636. package/tooling/web/configs/scripts/screenshot.sh +28 -0
  637. package/tooling/web/configs/scripts/verify.sh +80 -0
  638. package/tooling/web/configs/scripts/worktree-port.sh +74 -0
  639. package/tooling/web/configs/src/test/setup.ts +8 -0
  640. package/tooling/web/manifest.toml +58 -0
  641. package/tooling/web/reference.md +115 -0
  642. package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
  643. package/tsconfig.json +14 -0
@@ -0,0 +1,629 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ /**
5
+ * Three states rather than two. An arm with no declaration is `unchecked`: it
6
+ * cannot pass, since nothing was asserted, and it does not fail, since failing
7
+ * every undeclared arm would make the harness unusable while expectations roll
8
+ * out. `canon sandbox coverage` is what keeps the count from hiding.
9
+ */
10
+ export type VerdictState = 'pass' | 'fail' | 'unchecked'
11
+
12
+ export interface ContentAssertion {
13
+ readonly path: string
14
+ readonly pattern: string
15
+ }
16
+
17
+ export interface Expectation {
18
+ readonly paths: readonly string[]
19
+ readonly absent: readonly string[]
20
+ readonly content: readonly ContentAssertion[]
21
+ readonly writeScope: readonly string[]
22
+ /**
23
+ * Undefined means the arm makes no claim about escapes, which is every arm
24
+ * today. Present, even as `[]`, means the arm asserts a bound: the empty
25
+ * form declares that a correct run produces none, so `stringArray`'s
26
+ * collapse of "absent" and "empty" into one `[]` would erase that claim.
27
+ */
28
+ readonly escapeScope?: readonly string[]
29
+ readonly reply: readonly string[]
30
+ readonly manual: readonly string[]
31
+ readonly maxTurns?: number
32
+ }
33
+
34
+ export interface AssertionResult {
35
+ readonly ok: boolean
36
+ readonly message: string
37
+ }
38
+
39
+ /**
40
+ * `reply` is optional because an absent reply and an empty one mean different
41
+ * things. A run whose envelope was never supplied has nothing to assert against
42
+ * and skips. A run that genuinely returned no text carries the empty string and
43
+ * fails every reply assertion, which is the finding.
44
+ */
45
+ export interface RunEnvelope {
46
+ readonly isError: boolean
47
+ readonly turns: number
48
+ readonly denials: number
49
+ readonly reply?: string
50
+ }
51
+
52
+ export interface Verdict {
53
+ readonly state: VerdictState
54
+ readonly asserted: number
55
+ readonly failed: number
56
+ readonly unchecked: number
57
+ readonly results: readonly AssertionResult[]
58
+ readonly manual: readonly string[]
59
+ readonly skipped: readonly string[]
60
+ readonly note?: string
61
+ }
62
+
63
+ /**
64
+ * `writes`, `escapes`, and `envelope` are absent when the caller supplied no
65
+ * data for them, which is not the same as a run that wrote nothing, escaped
66
+ * nowhere, or reported nothing. The assertion kinds that depend on them report
67
+ * as skipped rather than silently dropping out of the count, so the cheap
68
+ * standalone path cannot claim more coverage than it had.
69
+ */
70
+ export interface CheckInput {
71
+ readonly sandboxDir: string
72
+ readonly writes?: readonly string[]
73
+ readonly escapes?: readonly string[]
74
+ /**
75
+ * Whether any watched escape root held one of the four directories this run.
76
+ * Undefined when the caller supplied no escapes at all, which already skips.
77
+ * False is what separates a watch that ran and found nothing from one with
78
+ * nothing to watch, both of which produce the same empty `escapes` list.
79
+ */
80
+ readonly escapesWatched?: boolean
81
+ readonly envelope?: RunEnvelope
82
+ }
83
+
84
+ interface KindOutcome {
85
+ readonly results: AssertionResult[]
86
+ readonly skipped: string[]
87
+ }
88
+
89
+ const EXIT_CODE: Record<VerdictState, number> = {
90
+ pass: 0,
91
+ fail: 1,
92
+ unchecked: 0,
93
+ }
94
+
95
+ /**
96
+ * `unchecked` exits zero so an undeclared arm does not break a gate while
97
+ * expectations roll out. Pass `strict` to make it exit one, which is how a
98
+ * caller that has finished arming its scenarios keeps them armed.
99
+ */
100
+ export function verdictExitCode(state: VerdictState, strict = false): number {
101
+ if (strict && state === 'unchecked') return 1
102
+
103
+ return EXIT_CODE[state]
104
+ }
105
+
106
+ export interface ScenarioTarget {
107
+ readonly category: string
108
+ readonly command: string
109
+ }
110
+
111
+ /**
112
+ * Splits `<category>:<command>`. Returns undefined rather than a partial target,
113
+ * so a typo cannot resolve to a declaration path that happens not to exist and
114
+ * report `unchecked` on what is really a caller error.
115
+ */
116
+ export function parseTarget(target: string): ScenarioTarget | undefined {
117
+ const [category, command, ...rest] = target.split(':')
118
+ if (rest.length > 0) return undefined
119
+ if (category === undefined || category === '') return undefined
120
+ if (command === undefined || command === '') return undefined
121
+
122
+ return { category, command }
123
+ }
124
+
125
+ export function expectFilePath(
126
+ root: string,
127
+ category: string,
128
+ command: string,
129
+ arm: string,
130
+ ): string {
131
+ return join(
132
+ root,
133
+ 'scripts',
134
+ 'sandbox',
135
+ 'fixtures',
136
+ category,
137
+ command,
138
+ arm,
139
+ 'expect.toml',
140
+ )
141
+ }
142
+
143
+ function stringArray(value: unknown): string[] {
144
+ if (!Array.isArray(value)) return []
145
+
146
+ return value.filter(
147
+ (entry): entry is string => typeof entry === 'string' && entry !== '',
148
+ )
149
+ }
150
+
151
+ function contentArray(value: unknown): ContentAssertion[] {
152
+ if (!Array.isArray(value)) return []
153
+
154
+ const assertions: ContentAssertion[] = []
155
+ for (const entry of value) {
156
+ if (typeof entry !== 'object' || entry === null) continue
157
+
158
+ const record = entry as Record<string, unknown>
159
+
160
+ // Dropping a half-written entry rather than throwing is deliberate, and it
161
+ // reads as the opposite of the stray-key check below. An entry missing its
162
+ // path or pattern declares no assertion to lose, and `checkExpectation`
163
+ // fails an arm whose surviving declaration asserts nothing, so the vacuous
164
+ // pass is already closed one level up. A stray key is the reverse: the entry
165
+ // is well-formed and the declaration around it silently lost a key it
166
+ // appears to carry, which nothing downstream can see.
167
+ if (typeof record.path !== 'string' || record.path === '') continue
168
+ if (typeof record.pattern !== 'string' || record.pattern === '') continue
169
+
170
+ // A bare key written below a `[[content]]` header belongs to that table in
171
+ // TOML, not to the document, so a declaration listing `manual` or
172
+ // `max_turns` after its content blocks parses clean and silently asserts
173
+ // neither. The `claude/ui-test` arm shipped that way: a turn ceiling that
174
+ // never ran and five manual entries that never reached the unchecked count,
175
+ // while `canon sandbox coverage` counted the arm as armed. Nothing at the
176
+ // top level can see the difference, so the check belongs here.
177
+ const stray = Object.keys(record).filter(
178
+ (key) => key !== 'path' && key !== 'pattern',
179
+ )
180
+ if (stray.length > 0) {
181
+ throw new Error(
182
+ `content entry for ${record.path} carries ${stray.join(', ')}. Move top-level keys above the first [[content]] block.`,
183
+ )
184
+ }
185
+
186
+ assertions.push({ path: record.path, pattern: record.pattern })
187
+ }
188
+
189
+ return assertions
190
+ }
191
+
192
+ /**
193
+ * Throws on malformed TOML. `resolveVerdict` turns that into a failed verdict, so
194
+ * a typo in a declaration reads the same way a pattern that does not compile
195
+ * does, rather than surfacing as a stack trace.
196
+ */
197
+ export function parseExpectation(source: string): Expectation {
198
+ const parsed = Bun.TOML.parse(source) as Record<string, unknown>
199
+
200
+ return {
201
+ paths: stringArray(parsed.paths),
202
+ absent: stringArray(parsed.absent),
203
+ content: contentArray(parsed.content),
204
+ writeScope: stringArray(parsed.write_scope),
205
+ escapeScope:
206
+ parsed.escape_scope === undefined
207
+ ? undefined
208
+ : stringArray(parsed.escape_scope),
209
+ reply: stringArray(parsed.reply),
210
+ manual: stringArray(parsed.manual),
211
+ maxTurns:
212
+ typeof parsed.max_turns === 'number' ? parsed.max_turns : undefined,
213
+ }
214
+ }
215
+
216
+ /**
217
+ * `manual` is deliberately excluded. Counting it would let an arm declare five
218
+ * prose lines, assert nothing, and still read green, which is the vacuous case
219
+ * `scripts/core/install-check.sh` warns about in its own comment.
220
+ */
221
+ export function countMechanicalAssertions(expectation: Expectation): number {
222
+ return (
223
+ expectation.paths.length +
224
+ expectation.absent.length +
225
+ expectation.content.length +
226
+ expectation.writeScope.length +
227
+ (expectation.escapeScope === undefined ? 0 : 1) +
228
+ expectation.reply.length
229
+ )
230
+ }
231
+
232
+ /**
233
+ * An entry carrying `*` is matched as a glob, so an arm can assert a file whose
234
+ * name a run derives. Reports the matched path rather than the pattern, since a
235
+ * pass on `lessons/0001-*.html` says nothing until the name it found is named.
236
+ */
237
+ function checkPaths(
238
+ expectation: Expectation,
239
+ sandboxDir: string,
240
+ ): AssertionResult[] {
241
+ return expectation.paths.map((path) => {
242
+ const written = writtenUnder(path, sandboxDir)
243
+
244
+ return written
245
+ ? { ok: true, message: `exists: ${written}` }
246
+ : { ok: false, message: `missing: ${path}` }
247
+ })
248
+ }
249
+
250
+ /**
251
+ * The first file an entry matches, or undefined when it matches none. An entry
252
+ * carrying `*` is matched as a glob, which is what lets an arm name a file whose
253
+ * name a run derives rather than fixes. Pinning one spelling of a derived name
254
+ * passes vacuously against every other spelling, which reads as coverage the arm
255
+ * does not have.
256
+ *
257
+ * Returning the match rather than a boolean is what lets a result name the file
258
+ * the run wrote instead of the pattern that found it. A glob matching several
259
+ * files answers with one of them in no fixed order, so an arm asserting content
260
+ * through a glob seeds a folder holding one.
261
+ */
262
+ function writtenUnder(pattern: string, sandboxDir: string): string | undefined {
263
+ if (!pattern.includes('*')) {
264
+ return existsSync(join(sandboxDir, pattern)) ? pattern : undefined
265
+ }
266
+
267
+ for (const match of new Bun.Glob(pattern).scanSync({
268
+ cwd: sandboxDir,
269
+ dot: true,
270
+ })) {
271
+ return match
272
+ }
273
+
274
+ return undefined
275
+ }
276
+
277
+ function checkAbsent(
278
+ expectation: Expectation,
279
+ sandboxDir: string,
280
+ ): AssertionResult[] {
281
+ return expectation.absent.map((path) => {
282
+ const written = writtenUnder(path, sandboxDir)
283
+
284
+ return written
285
+ ? { ok: false, message: `should not exist: ${written}` }
286
+ : { ok: true, message: `absent: ${path}` }
287
+ })
288
+ }
289
+
290
+ /**
291
+ * A pattern against a missing file reports as a content miss rather than
292
+ * throwing, since an arm may assert content without also listing the path. A
293
+ * pattern that does not compile is a defect in the declaration, so it fails the
294
+ * assertion it belongs to rather than aborting the whole verdict.
295
+ *
296
+ * A path carrying `*` resolves to the file it matched, and falls back to itself
297
+ * when it matched none so the miss is reported against the entry as written.
298
+ */
299
+ function checkContent(
300
+ expectation: Expectation,
301
+ sandboxDir: string,
302
+ ): AssertionResult[] {
303
+ return expectation.content.map(({ path, pattern }) => {
304
+ const matched = writtenUnder(path, sandboxDir) ?? path
305
+ const full = join(sandboxDir, matched)
306
+ const label = `${matched} =~ ${pattern}`
307
+
308
+ let matcher: RegExp
309
+ try {
310
+ matcher = new RegExp(pattern, 'm')
311
+ } catch {
312
+ return { ok: false, message: `invalid pattern for ${path}: ${pattern}` }
313
+ }
314
+
315
+ if (!existsSync(full) || !statSync(full).isFile()) {
316
+ return { ok: false, message: `no file to match: ${path}` }
317
+ }
318
+
319
+ if (matcher.test(readFileSync(full, 'utf8'))) {
320
+ return { ok: true, message: `matches: ${label}` }
321
+ }
322
+
323
+ return { ok: false, message: `no match: ${label}` }
324
+ })
325
+ }
326
+
327
+ /**
328
+ * The probe on 2026-07-30 found no permission mode that both allows writes under
329
+ * `.claude/` and scopes them, so the run goes wide and this assertion carries the
330
+ * property the permission layer used to. An arm declaring no scope skips it.
331
+ */
332
+ function checkWriteScope(
333
+ expectation: Expectation,
334
+ writes: readonly string[] | undefined,
335
+ ): KindOutcome {
336
+ if (expectation.writeScope.length === 0) return { results: [], skipped: [] }
337
+
338
+ if (writes === undefined) {
339
+ return {
340
+ results: [],
341
+ skipped: ['write scope: no write data supplied, pass --writes'],
342
+ }
343
+ }
344
+
345
+ // A scope produces one result per write, so a run that wrote nothing produces
346
+ // none, and without this the declaration vanishes from the verdict entirely:
347
+ // no result, no skipped entry, and no contribution to the unchecked count that
348
+ // exists to surface exactly this. The `undefined` branch above cannot stand in,
349
+ // since `run.sh` always passes `--writes` and `readPathList` returns `[]` for an
350
+ // empty file. An arm whose output escaped the snapshot reads as a clean run,
351
+ // which is the vacuous pass the harness exists to remove.
352
+ if (writes.length === 0) {
353
+ return {
354
+ results: [],
355
+ skipped: ['write scope: the run wrote nothing, so no path was checked'],
356
+ }
357
+ }
358
+
359
+ const globs = expectation.writeScope.map((glob) => new Bun.Glob(glob))
360
+
361
+ return {
362
+ results: writes.map((path) =>
363
+ globs.some((glob) => glob.match(path))
364
+ ? { ok: true, message: `in scope: ${path}` }
365
+ : { ok: false, message: `wrote outside declared scope: ${path}` },
366
+ ),
367
+ skipped: [],
368
+ }
369
+ }
370
+
371
+ /**
372
+ * `run.sh` watches two toolkit roots for a write to shared session scratch
373
+ * during a run, and reports every one it finds as an unattributed escape with
374
+ * no arm able to fail on it. An arm whose skill legitimately reaches outside
375
+ * the sandbox tree declares the destinations here, and the harness asserts
376
+ * them instead of trusting the skill to bound itself.
377
+ *
378
+ * Declaring the scope inverts the empty case against `checkWriteScope`. A
379
+ * write-scope declaration exists to bound required output, so a run that wrote
380
+ * nothing skips rather than passing on a fabricated zero. An escape-scope
381
+ * declaration exists to bound a side effect nothing requires, so zero escapes
382
+ * is the outcome a correct run produces and reports as a pass outright,
383
+ * provided a watched root held something to watch. `run.sh`'s `snapshot_root`
384
+ * returns an empty manifest both when a watch ran clean and when none of the
385
+ * four watched directories existed under a root, and the two produce the same
386
+ * empty `escapes` list. `watched` is what tells them apart: a run that had
387
+ * nothing to watch reports unmeasured rather than passing on a diff it never
388
+ * had the target to take.
389
+ */
390
+ function checkEscapeScope(
391
+ expectation: Expectation,
392
+ escapes: readonly string[] | undefined,
393
+ watched: boolean | undefined,
394
+ ): KindOutcome {
395
+ if (expectation.escapeScope === undefined) return { results: [], skipped: [] }
396
+
397
+ if (escapes === undefined) {
398
+ return {
399
+ results: [],
400
+ skipped: ['escape scope: no escape data supplied, pass --escapes'],
401
+ }
402
+ }
403
+
404
+ if (escapes.length === 0) {
405
+ if (watched === false) {
406
+ return {
407
+ results: [],
408
+ skipped: ['escape scope: no watched root held a target, unmeasured'],
409
+ }
410
+ }
411
+
412
+ return {
413
+ results: [{ ok: true, message: 'no escape during this run' }],
414
+ skipped: [],
415
+ }
416
+ }
417
+
418
+ const globs = expectation.escapeScope.map((glob) => new Bun.Glob(glob))
419
+
420
+ return {
421
+ results: escapes.map((path) =>
422
+ globs.some((glob) => glob.match(path))
423
+ ? { ok: true, message: `declared escape: ${path}` }
424
+ : { ok: false, message: `unbounded escape: ${path}` },
425
+ ),
426
+ skipped: [],
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Plain substrings, matched case-sensitively, against the text the run replied
432
+ * with. A substring rather than a regex because the pattern a reply assertion
433
+ * wants is a load-bearing token, a path or a command, and a regex invites the
434
+ * anchored sentence that goes red on any rewording.
435
+ *
436
+ * Declare only positives. A negative substring passes on every reply that
437
+ * phrases the thing differently, which is the vacuous pass
438
+ * `countMechanicalAssertions` excludes `manual` to prevent. An entry asserting
439
+ * what a run must not have said stays in `manual` with its reason.
440
+ */
441
+ function checkReply(
442
+ expectation: Expectation,
443
+ envelope: RunEnvelope | undefined,
444
+ ): KindOutcome {
445
+ if (expectation.reply.length === 0) return { results: [], skipped: [] }
446
+
447
+ if (envelope?.reply === undefined) {
448
+ return {
449
+ results: [],
450
+ skipped: ['reply: no reply text supplied, pass --envelope'],
451
+ }
452
+ }
453
+
454
+ const reply = envelope.reply
455
+
456
+ return {
457
+ results: expectation.reply.map((fragment) =>
458
+ reply.includes(fragment)
459
+ ? { ok: true, message: `reply says: ${fragment}` }
460
+ : { ok: false, message: `reply never says: ${fragment}` },
461
+ ),
462
+ skipped: [],
463
+ }
464
+ }
465
+
466
+ /**
467
+ * The envelope never determines a pass. It can only fail a run the expectations
468
+ * would otherwise have passed. Under `bypassPermissions` the denial count is
469
+ * always zero, so the write-scope assertion is what replaced it.
470
+ */
471
+ function checkEnvelope(
472
+ expectation: Expectation,
473
+ envelope: RunEnvelope | undefined,
474
+ ): KindOutcome {
475
+ if (envelope === undefined) {
476
+ const skipped =
477
+ expectation.maxTurns === undefined
478
+ ? []
479
+ : ['turn ceiling: no envelope supplied, pass --envelope']
480
+
481
+ return { results: [], skipped }
482
+ }
483
+
484
+ const results: AssertionResult[] = []
485
+
486
+ if (envelope.isError) {
487
+ results.push({ ok: false, message: 'envelope: is_error true' })
488
+ }
489
+ if (envelope.denials > 0) {
490
+ results.push({
491
+ ok: false,
492
+ message: `envelope: ${envelope.denials} permission denials`,
493
+ })
494
+ }
495
+ if (
496
+ expectation.maxTurns !== undefined &&
497
+ envelope.turns > expectation.maxTurns
498
+ ) {
499
+ results.push({
500
+ ok: false,
501
+ message: `envelope: ${envelope.turns} turns over ceiling of ${expectation.maxTurns}`,
502
+ })
503
+ }
504
+
505
+ return { results, skipped: [] }
506
+ }
507
+
508
+ export function checkExpectation(
509
+ expectation: Expectation,
510
+ input: CheckInput,
511
+ ): Verdict {
512
+ const scope = checkWriteScope(expectation, input.writes)
513
+ const escapeScope = checkEscapeScope(
514
+ expectation,
515
+ input.escapes,
516
+ input.escapesWatched,
517
+ )
518
+ const reply = checkReply(expectation, input.envelope)
519
+ const envelope = checkEnvelope(expectation, input.envelope)
520
+
521
+ const results = [
522
+ ...checkPaths(expectation, input.sandboxDir),
523
+ ...checkAbsent(expectation, input.sandboxDir),
524
+ ...checkContent(expectation, input.sandboxDir),
525
+ ...reply.results,
526
+ ...scope.results,
527
+ ...escapeScope.results,
528
+ ...envelope.results,
529
+ ]
530
+ const skipped = [
531
+ ...scope.skipped,
532
+ ...escapeScope.skipped,
533
+ ...reply.skipped,
534
+ ...envelope.skipped,
535
+ ]
536
+
537
+ const failed = results.filter((result) => !result.ok).length
538
+
539
+ // A declaration counts what it declares, this counts what ran, and the two
540
+ // diverge on `write_scope`, which produces one result per write rather than one
541
+ // per glob. An arm declaring only a write scope against a run that wrote
542
+ // nothing would otherwise report pass having asserted nothing, which is the
543
+ // vacuous pass the whole feature exists to remove.
544
+ if (results.length === 0) {
545
+ return {
546
+ state: 'fail',
547
+ asserted: 0,
548
+ failed: 1,
549
+ unchecked: expectation.manual.length + skipped.length,
550
+ results: [
551
+ { ok: false, message: 'no assertion ran against this sandbox' },
552
+ ...results,
553
+ ],
554
+ manual: expectation.manual,
555
+ skipped,
556
+ note: 'The declaration asserts something, but nothing was checkable here. A pass with zero assertions is not a pass.',
557
+ }
558
+ }
559
+
560
+ return {
561
+ state: failed > 0 ? 'fail' : 'pass',
562
+ asserted: results.length,
563
+ failed,
564
+ unchecked: expectation.manual.length + skipped.length,
565
+ results,
566
+ manual: expectation.manual,
567
+ skipped,
568
+ }
569
+ }
570
+
571
+ /**
572
+ * A missing declaration and an empty one are different verdicts. Absence is the
573
+ * rollout state for most arms. A declaration that exists and asserts nothing is
574
+ * silent truncation, and it goes red.
575
+ */
576
+ export function resolveVerdict(expectFile: string, input: CheckInput): Verdict {
577
+ if (!existsSync(expectFile)) {
578
+ return {
579
+ state: 'unchecked',
580
+ asserted: 0,
581
+ failed: 0,
582
+ unchecked: 0,
583
+ results: [],
584
+ manual: [],
585
+ skipped: [],
586
+ note: `No expect.toml at ${expectFile}. Nothing was asserted.`,
587
+ }
588
+ }
589
+
590
+ let expectation: Expectation
591
+ try {
592
+ expectation = parseExpectation(readFileSync(expectFile, 'utf8'))
593
+ } catch (error) {
594
+ const reason = error instanceof Error ? error.message : String(error)
595
+
596
+ return {
597
+ state: 'fail',
598
+ asserted: 0,
599
+ failed: 1,
600
+ unchecked: 0,
601
+ results: [
602
+ { ok: false, message: `expect.toml does not parse: ${reason}` },
603
+ ],
604
+ manual: [],
605
+ skipped: [],
606
+ note: `Fix the declaration at ${expectFile}.`,
607
+ }
608
+ }
609
+
610
+ if (countMechanicalAssertions(expectation) === 0) {
611
+ return {
612
+ state: 'fail',
613
+ asserted: 0,
614
+ failed: 1,
615
+ unchecked: expectation.manual.length,
616
+ results: [
617
+ {
618
+ ok: false,
619
+ message: `expect.toml declares no mechanical assertion: ${expectFile}`,
620
+ },
621
+ ],
622
+ manual: expectation.manual,
623
+ skipped: [],
624
+ note: 'An expectation file that asserts nothing passes every run. Declare one or delete the file.',
625
+ }
626
+ }
627
+
628
+ return checkExpectation(expectation, input)
629
+ }
@@ -0,0 +1,47 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ /**
6
+ * Mints a short per-run identifier the first time it is asked for and holds it
7
+ * in `CANON_SANDBOX_RUN_ID` for the rest of this process, so a script that
8
+ * spawns a child inheriting `process.env` — `run.sh` calling `manage-sandbox.sh`
9
+ * and then `canon sandbox check` — resolves the same tree in every one of them.
10
+ * A process that already carries the variable, inherited from such a parent,
11
+ * reuses it rather than minting a new one.
12
+ *
13
+ * Twin of `mint_sandbox_run_id` in `scripts/lib/sandbox-path.sh`.
14
+ */
15
+ export function mintSandboxRunId(): string {
16
+ const existing = process.env.CANON_SANDBOX_RUN_ID
17
+ if (existing !== undefined && existing !== '') return existing
18
+
19
+ const id = randomBytes(4).toString('hex')
20
+ process.env.CANON_SANDBOX_RUN_ID = id
21
+ return id
22
+ }
23
+
24
+ /**
25
+ * The provisioned tree's path, split out of `src/commands/sandbox.ts` so the
26
+ * per-run default is unit-testable on its own rather than only through the
27
+ * command's registration.
28
+ *
29
+ * Twin of `resolve_sandbox_dir` in `scripts/lib/sandbox-path.sh`. The exec
30
+ * boundary rules out a shared constant, so a change to the default lands on
31
+ * both sides. The fall-through used to be one path per machine, so two
32
+ * sessions each resolving the default at once provisioned over each other
33
+ * with neither told; `mintSandboxRunId` gives the path a per-run component
34
+ * instead, which is what makes two such sessions land on two different trees.
35
+ */
36
+ export function sandboxTree(): string {
37
+ const override = process.env.CANON_SANDBOX_DIR
38
+ if (override !== undefined && override !== '') return override
39
+
40
+ const state = process.env.XDG_STATE_HOME
41
+ const base =
42
+ state !== undefined && state !== ''
43
+ ? state
44
+ : join(homedir(), '.local', 'state')
45
+
46
+ return join(base, 'canon', `sandbox-${mintSandboxRunId()}`)
47
+ }
@@ -0,0 +1,30 @@
1
+ import { isMarked } from '@/exempt-marker'
2
+
3
+ /**
4
+ * The inline token exempting one line from the secret scan.
5
+ *
6
+ * Shaped on the `stub: true` precedent in `src/seed-marker.ts`, which answers
7
+ * a check whose own comment records a false-positive class. The exemption
8
+ * travels with the line it exempts rather than sitting in a path list away
9
+ * from it, so a reader meeting a muted match finds the reason on the spot.
10
+ *
11
+ * The set of files carrying one is empty today. What empties it is the keying
12
+ * rather than this mechanism: `patterns.ts` matches issued values and never
13
+ * the words around them, so nothing in the shipped tree matches on purpose.
14
+ * A path allow-list was declined for the same reason, since the noise it would
15
+ * target is word-keyed and spread past the fixture trees, so it would hide
16
+ * part of the noise and none of the risk.
17
+ */
18
+ export const SECRET_MARKER = 'canon-allow-secret'
19
+
20
+ /**
21
+ * Whether the line at `index` is exempt, reading itself and the line above it.
22
+ *
23
+ * Where a marker may sit and what makes one count are `isMarked`, which the
24
+ * superseded-value sweep reads through the same call. Only the token differs
25
+ * between the two, so the placement rule has one answer rather than two that
26
+ * can drift.
27
+ */
28
+ export function isExempt(lines: readonly string[], index: number): boolean {
29
+ return isMarked(lines, index, SECRET_MARKER)
30
+ }