@thedecipherist/claude-packs 0.0.0-stage → 1.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 (827) hide show
  1. package/CHANGELOG.md +680 -0
  2. package/LICENSE +21 -0
  3. package/README.md +316 -2
  4. package/bin/claude-packs.mjs +204 -0
  5. package/configurations/base/config.json +85 -0
  6. package/configurations/cpp/config.json +92 -0
  7. package/configurations/full-stack/config.json +152 -0
  8. package/configurations/go/config.json +96 -0
  9. package/configurations/node/config.json +100 -0
  10. package/configurations/python/config.json +97 -0
  11. package/configurations/synth-creator/config.json +148 -0
  12. package/global/hooks/gnu-shell.mjs +131 -0
  13. package/global/hooks/lib/program-read.mjs +196 -0
  14. package/global/hooks/lib/program-writes.mjs +177 -0
  15. package/global/hooks/lib/shell-writes.mjs +293 -0
  16. package/global/hooks/shell-traps.mjs +79 -0
  17. package/global/hooks/version-pin-guard.mjs +280 -0
  18. package/global/skills/latest-versions/SKILL.md +49 -0
  19. package/lib/backup.mjs +34 -0
  20. package/lib/claude-md.mjs +58 -0
  21. package/lib/config-cli.mjs +36 -0
  22. package/lib/configs.mjs +145 -0
  23. package/lib/coverage.mjs +86 -0
  24. package/lib/custom-packs.mjs +93 -0
  25. package/lib/doctor.mjs +115 -0
  26. package/lib/fix-plan.mjs +71 -0
  27. package/lib/git-tokens.mjs +64 -0
  28. package/lib/global.mjs +125 -0
  29. package/lib/hook-proof.mjs +28 -0
  30. package/lib/hook-timing.mjs +42 -0
  31. package/lib/hover-gate.mjs +29 -0
  32. package/lib/kit-lock.mjs +149 -0
  33. package/lib/layout.mjs +75 -0
  34. package/lib/logs.mjs +69 -0
  35. package/lib/manifest.mjs +24 -0
  36. package/lib/new.mjs +49 -0
  37. package/lib/once-only-report.mjs +54 -0
  38. package/lib/pack-manifest.mjs +20 -0
  39. package/lib/packs.mjs +107 -0
  40. package/lib/planner.mjs +59 -0
  41. package/lib/repeats.mjs +65 -0
  42. package/lib/repo-weight.mjs +28 -0
  43. package/lib/rules.mjs +40 -0
  44. package/lib/scan-detect.mjs +94 -0
  45. package/lib/scan.mjs +168 -0
  46. package/lib/selftest-cache.mjs +39 -0
  47. package/lib/settings.mjs +138 -0
  48. package/lib/shape-cli.mjs +76 -0
  49. package/lib/shape.mjs +4 -0
  50. package/lib/suggest.mjs +18 -0
  51. package/lib/switches.mjs +44 -0
  52. package/lib/symmetry-doctor.mjs +26 -0
  53. package/lib/sync.mjs +130 -0
  54. package/package.json +34 -4
  55. package/packs/ai/checks/boundary-computes-its-own-hash.json +30 -0
  56. package/packs/ai/checks/model-call-needs-boundary.json +29 -0
  57. package/packs/ai/checks/model-id-from-config.json +28 -0
  58. package/packs/ai/checks/model-output-validated.json +26 -0
  59. package/packs/ai/pack.json +38 -0
  60. package/packs/ai/rules/ai-input-sha-boundary.md +31 -0
  61. package/packs/ai/rules/llm-pipeline-security.md +26 -0
  62. package/packs/audio-core/checks/denormals-not-in-prepare.json +12 -0
  63. package/packs/audio-core/checks/dsp-without-apvts.json +12 -0
  64. package/packs/audio-core/checks/float-phase-or-rate.json +12 -0
  65. package/packs/audio-core/checks/juce-snap-to-zero-off.json +12 -0
  66. package/packs/audio-core/checks/keep-werror.json +12 -0
  67. package/packs/audio-core/checks/no-fast-math.json +14 -0
  68. package/packs/audio-core/checks/no-locale-change.json +12 -0
  69. package/packs/audio-core/checks/no-march-native.json +14 -0
  70. package/packs/audio-core/clang-tidy +28 -0
  71. package/packs/audio-core/hooks/lib/realtime-checks.mjs +56 -0
  72. package/packs/audio-core/hooks/param-id-lock.mjs +129 -0
  73. package/packs/audio-core/hooks/realtime-guard.mjs +67 -0
  74. package/packs/audio-core/pack.json +187 -0
  75. package/packs/audio-core/rules/audio-thread-realtime-safe.md +27 -0
  76. package/packs/audio-core/rules/audio-ui-thread-handoff.md +21 -0
  77. package/packs/audio-core/rules/boundary-tests-cross-the-boundary.md +16 -0
  78. package/packs/audio-core/rules/build-flags-safe.md +20 -0
  79. package/packs/audio-core/rules/cached-parameter-pointers.md +28 -0
  80. package/packs/audio-core/rules/clang-tidy-core-guidelines.md +21 -0
  81. package/packs/audio-core/rules/control-reaches-every-surface.md +23 -0
  82. package/packs/audio-core/rules/cpu-worst-case-per-block.md +26 -0
  83. package/packs/audio-core/rules/dc-block-after-asymmetry.md +22 -0
  84. package/packs/audio-core/rules/discrete-params-snap.md +19 -0
  85. package/packs/audio-core/rules/double-where-precision-matters.md +20 -0
  86. package/packs/audio-core/rules/filters-stable-under-modulation.md +29 -0
  87. package/packs/audio-core/rules/host-parameters-at-scale.md +24 -0
  88. package/packs/audio-core/rules/house-control-behaviours.md +34 -0
  89. package/packs/audio-core/rules/math-domain-guards.md +19 -0
  90. package/packs/audio-core/rules/modulated-delay-interpolation.md +22 -0
  91. package/packs/audio-core/rules/multiband-phase-coherent.md +22 -0
  92. package/packs/audio-core/rules/nonlinear-stages-antialiased.md +28 -0
  93. package/packs/audio-core/rules/note-ledger-and-pedals.md +22 -0
  94. package/packs/audio-core/rules/one-formula-per-value.md +20 -0
  95. package/packs/audio-core/rules/optimisations-are-null-tested.md +23 -0
  96. package/packs/audio-core/rules/oscillators-bandlimited.md +23 -0
  97. package/packs/audio-core/rules/output-limiter.md +23 -0
  98. package/packs/audio-core/rules/parameter-ids-never-change.md +20 -0
  99. package/packs/audio-core/rules/params-and-state-thread-safe.md +19 -0
  100. package/packs/audio-core/rules/params-snapshot-per-block.md +21 -0
  101. package/packs/audio-core/rules/patch-excludes-performance-controls.md +18 -0
  102. package/packs/audio-core/rules/per-sample-code-inlinable.md +22 -0
  103. package/packs/audio-core/rules/prepare-is-repeatable.md +23 -0
  104. package/packs/audio-core/rules/recursive-state-nan-guard.md +20 -0
  105. package/packs/audio-core/rules/reverb-loop-gain-bounded.md +24 -0
  106. package/packs/audio-core/rules/sample-accurate-midi.md +22 -0
  107. package/packs/audio-core/rules/sample-rate-and-block-size-from-host.md +23 -0
  108. package/packs/audio-core/rules/scoped-no-denormals.md +26 -0
  109. package/packs/audio-core/rules/smooth-continuous-parameters.md +22 -0
  110. package/packs/audio-core/rules/state-load-versioned-and-safe.md +22 -0
  111. package/packs/audio-core/rules/straight-on-and-aligned.md +21 -0
  112. package/packs/audio-core/rules/ui-matches-reference-look.md +21 -0
  113. package/packs/audio-core/rules/undo-one-gesture-one-step.md +21 -0
  114. package/packs/audio-core/rules/voice-steal-fades.md +21 -0
  115. package/packs/audio-core/rules/voice-sum-headroom.md +20 -0
  116. package/packs/audio-core/skills/analyse-reference-look/SKILL.md +21 -0
  117. package/packs/audio-core/skills/copy-and-prove/SKILL.md +98 -0
  118. package/packs/audio-core/skills/juce-realtime-audit/SKILL.md +22 -0
  119. package/packs/audio-core/skills/measure-tone/SKILL.md +20 -0
  120. package/packs/audio-core/skills/new-panel-control/SKILL.md +15 -0
  121. package/packs/audio-core/skills/prove-control/SKILL.md +23 -0
  122. package/packs/audio-core/skills/trace-parameter/SKILL.md +29 -0
  123. package/packs/audio-core/tools/lib/spectrum.mjs +112 -0
  124. package/packs/audio-core/tools/lib/wav.mjs +41 -0
  125. package/packs/audio-core/tools/tone-report.mjs +47 -0
  126. package/packs/base/checks/field-map-over-if-runs.json +25 -0
  127. package/packs/base/checks/lookup-over-if-chains.json +25 -0
  128. package/packs/base/checks/lookup-over-ternary-chains.json +20 -0
  129. package/packs/base/checks/no-code-after-midline-comment.json +12 -0
  130. package/packs/base/checks/one-walk-per-template.json +23 -0
  131. package/packs/base/checks/stub-reports-success.json +31 -0
  132. package/packs/base/checks/test-skip-on-failed-setup.json +17 -0
  133. package/packs/base/hooks/brief-guard.mjs +96 -0
  134. package/packs/base/hooks/code-shape.mjs +95 -0
  135. package/packs/base/hooks/dispatch-once.mjs +87 -0
  136. package/packs/base/hooks/file-size-guard.mjs +61 -0
  137. package/packs/base/hooks/folder-law.mjs +74 -0
  138. package/packs/base/hooks/git-guard.mjs +289 -0
  139. package/packs/base/hooks/lesson-checks.mjs +174 -0
  140. package/packs/base/hooks/lib/apply-edit.mjs +20 -0
  141. package/packs/base/hooks/lib/code-shapes.mjs +137 -0
  142. package/packs/base/hooks/lib/content.mjs +14 -0
  143. package/packs/base/hooks/lib/cpp-scan.mjs +94 -0
  144. package/packs/base/hooks/lib/dup-index.mjs +199 -0
  145. package/packs/base/hooks/lib/dup.mjs +127 -0
  146. package/packs/base/hooks/lib/elements.mjs +87 -0
  147. package/packs/base/hooks/lib/event-log.mjs +67 -0
  148. package/packs/base/hooks/lib/function-shapes.mjs +90 -0
  149. package/packs/base/hooks/lib/git-adds.mjs +20 -0
  150. package/packs/base/hooks/lib/git-guard-selftest.mjs +99 -0
  151. package/packs/base/hooks/lib/glob.mjs +33 -0
  152. package/packs/base/hooks/lib/hook-log.mjs +100 -0
  153. package/packs/base/hooks/lib/house.py +77 -0
  154. package/packs/base/hooks/lib/languages.mjs +66 -0
  155. package/packs/base/hooks/lib/layout.mjs +54 -0
  156. package/packs/base/hooks/lib/making-check-selftest.mjs +109 -0
  157. package/packs/base/hooks/lib/near-copies.mjs +76 -0
  158. package/packs/base/hooks/lib/program-read.mjs +196 -0
  159. package/packs/base/hooks/lib/program-writes.mjs +177 -0
  160. package/packs/base/hooks/lib/project.mjs +81 -0
  161. package/packs/base/hooks/lib/prose.mjs +11 -0
  162. package/packs/base/hooks/lib/pushed.mjs +201 -0
  163. package/packs/base/hooks/lib/repo-index.mjs +52 -0
  164. package/packs/base/hooks/lib/secret-patterns.mjs +18 -0
  165. package/packs/base/hooks/lib/selftest.mjs +45 -0
  166. package/packs/base/hooks/lib/shape-code.mjs +126 -0
  167. package/packs/base/hooks/lib/shape.mjs +294 -0
  168. package/packs/base/hooks/lib/shell-writes-selftest.mjs +65 -0
  169. package/packs/base/hooks/lib/shell-writes.mjs +293 -0
  170. package/packs/base/hooks/lib/turn.mjs +81 -0
  171. package/packs/base/hooks/lib/writes.mjs +71 -0
  172. package/packs/base/hooks/making-check.mjs +243 -0
  173. package/packs/base/hooks/making.mjs +106 -0
  174. package/packs/base/hooks/once-only.mjs +150 -0
  175. package/packs/base/hooks/placement-law.mjs +84 -0
  176. package/packs/base/hooks/repo-litter.mjs +75 -0
  177. package/packs/base/hooks/reuse-hint.mjs +75 -0
  178. package/packs/base/hooks/run-hooks.mjs +132 -0
  179. package/packs/base/hooks/secrets-guard.mjs +99 -0
  180. package/packs/base/hooks/shape-law.mjs +133 -0
  181. package/packs/base/hooks/suite-once.mjs +80 -0
  182. package/packs/base/hooks/template-law.mjs +148 -0
  183. package/packs/base/hooks/untrusted-boundary.mjs +53 -0
  184. package/packs/base/pack.json +214 -0
  185. package/packs/base/rules/a-control-is-seen-to-fire.md +27 -0
  186. package/packs/base/rules/behaviour-lives-with-its-kind.md +51 -0
  187. package/packs/base/rules/bug-fix-starts-red.md +25 -0
  188. package/packs/base/rules/field-map-over-if-runs.md +47 -0
  189. package/packs/base/rules/finished-work-lands.md +17 -0
  190. package/packs/base/rules/git-right-first-time.md +19 -0
  191. package/packs/base/rules/instances-by-index-identities-by-id.md +54 -0
  192. package/packs/base/rules/lookup-over-if-chains.md +49 -0
  193. package/packs/base/rules/making-line-first.md +14 -0
  194. package/packs/base/rules/open-items-tagged.md +17 -0
  195. package/packs/base/rules/plan-and-logs.md +22 -0
  196. package/packs/base/rules/references-use-anchors.md +19 -0
  197. package/packs/base/rules/refusals-are-judged.md +18 -0
  198. package/packs/base/rules/reuse-before-writing.md +15 -0
  199. package/packs/base/rules/secrets-never-committed.md +16 -0
  200. package/packs/base/rules/skill-writing.md +23 -0
  201. package/packs/base/rules/split-before-the-limit.md +14 -0
  202. package/packs/base/rules/subagent-dispatch.md +26 -0
  203. package/packs/base/rules/test-only-what-changed.md +17 -0
  204. package/packs/base/rules/test-the-template-once.md +22 -0
  205. package/packs/base/rules/untrusted-content-boundary.md +15 -0
  206. package/packs/base/skills/add-lesson-check/SKILL.md +26 -0
  207. package/packs/base/skills/fix-from-scan/SKILL.md +25 -0
  208. package/packs/base/skills/land-branch/SKILL.md +24 -0
  209. package/packs/base/skills/make-it-a-template/SKILL.md +23 -0
  210. package/packs/base/skills/pdf-to-md/SKILL.md +25 -0
  211. package/packs/base/skills/pick-tests/SKILL.md +20 -0
  212. package/packs/base/skills/pressure-test-skill/SKILL.md +23 -0
  213. package/packs/base/skills/prove-instances-identical/SKILL.md +37 -0
  214. package/packs/base/tools/shape-symmetry-selftest.mjs +66 -0
  215. package/packs/base/tools/shape-symmetry.mjs +178 -0
  216. package/packs/base/tools/test-verdict.mjs +75 -0
  217. package/packs/ci/checks/ci-permissions-stated.json +18 -0
  218. package/packs/ci/checks/ci-tools-pinned.json +14 -0
  219. package/packs/ci/pack.json +21 -0
  220. package/packs/ci/rules/ci-covers-what-ships.md +20 -0
  221. package/packs/ci/rules/generated-files-have-no-diff.md +15 -0
  222. package/packs/content-site/checks/captcha-is-not-constant.json +27 -0
  223. package/packs/content-site/checks/img-has-size.json +20 -0
  224. package/packs/content-site/checks/page-head-complete.json +26 -0
  225. package/packs/content-site/pack.json +22 -0
  226. package/packs/content-site/rules/content-has-one-source.md +23 -0
  227. package/packs/content-site/rules/media-is-sized.md +24 -0
  228. package/packs/content-site/rules/page-head-is-complete.md +26 -0
  229. package/packs/content-site/rules/public-forms-are-defended.md +27 -0
  230. package/packs/content-site/skills/publish-a-page/SKILL.md +21 -0
  231. package/packs/cpp/checks/cpp-clamp-before-cast.json +16 -0
  232. package/packs/cpp/checks/cpp-find-with-the-library.json +12 -0
  233. package/packs/cpp/checks/cpp-hardened-test-build.json +18 -0
  234. package/packs/cpp/checks/cpp-m-pi-not-standard.json +23 -0
  235. package/packs/cpp/checks/cpp-no-atoi.json +16 -0
  236. package/packs/cpp/checks/cpp-no-const-cast.json +16 -0
  237. package/packs/cpp/checks/cpp-no-float-loop-counter.json +16 -0
  238. package/packs/cpp/checks/cpp-no-getenv-in-product.json +22 -0
  239. package/packs/cpp/checks/cpp-no-mutable-global.json +20 -0
  240. package/packs/cpp/checks/cpp-no-mutable-local-static.json +23 -0
  241. package/packs/cpp/checks/cpp-no-unchecked-fwrite.json +17 -0
  242. package/packs/cpp/checks/cpp-no-using-namespace-in-header.json +19 -0
  243. package/packs/cpp/checks/cpp-no-zero-size-array.json +23 -0
  244. package/packs/cpp/checks/cpp-numbers-pi.json +16 -0
  245. package/packs/cpp/pack.json +22 -0
  246. package/packs/cpp/rules/cpp-absent-and-failed-are-types.md +24 -0
  247. package/packs/cpp/rules/cpp-builds-everywhere.md +26 -0
  248. package/packs/cpp/rules/cpp-faults-only-in-the-test-build.md +21 -0
  249. package/packs/cpp/rules/cpp-find-with-the-library.md +19 -0
  250. package/packs/cpp/rules/cpp-gates-share-one-reader.md +22 -0
  251. package/packs/cpp/rules/cpp-headers-declare.md +22 -0
  252. package/packs/cpp/rules/cpp-numbers-and-casts.md +22 -0
  253. package/packs/cpp/rules/cpp-repeats-become-templates.md +33 -0
  254. package/packs/cpp/rules/cpp-static-analysis-runs.md +22 -0
  255. package/packs/cpp/rules/cpp-thread-contracts-are-code.md +23 -0
  256. package/packs/cpp/skills/cpp-extract-template/SKILL.md +38 -0
  257. package/packs/docker/checks/alpine-with-native-build.json +13 -0
  258. package/packs/docker/checks/compose-db-needs-auth.json +14 -0
  259. package/packs/docker/checks/compose-image-pinned.json +14 -0
  260. package/packs/docker/checks/compose-no-hardcoded-ip.json +12 -0
  261. package/packs/docker/checks/compose-no-host-bind-for-data.json +18 -0
  262. package/packs/docker/checks/compose-no-plaintext-secret.json +13 -0
  263. package/packs/docker/checks/compose-no-swarm-ignored-keys.json +13 -0
  264. package/packs/docker/checks/compose-overlay-not-bridge.json +13 -0
  265. package/packs/docker/checks/compose-service-needs-healthcheck.json +11 -0
  266. package/packs/docker/checks/compose-service-needs-init.json +11 -0
  267. package/packs/docker/checks/copy-needs-chown-with-user.json +17 -0
  268. package/packs/docker/checks/copy-source-before-deps.json +13 -0
  269. package/packs/docker/checks/docker-exec-form.json +16 -0
  270. package/packs/docker/checks/dockerfile-needs-healthcheck.json +17 -0
  271. package/packs/docker/checks/dockerfile-needs-user.json +21 -0
  272. package/packs/docker/checks/dockerignore-has-the-basics.json +21 -0
  273. package/packs/docker/checks/gitattributes-forces-lf.json +17 -0
  274. package/packs/docker/checks/health-checks-nothing.json +25 -0
  275. package/packs/docker/checks/no-crlf-in-scripts.json +15 -0
  276. package/packs/docker/checks/no-latest-base.json +13 -0
  277. package/packs/docker/checks/no-secret-in-image.json +13 -0
  278. package/packs/docker/checks/no-sensitive-file-copy.json +13 -0
  279. package/packs/docker/checks/node-env-before-install.json +13 -0
  280. package/packs/docker/checks/npm-ci-not-install.json +13 -0
  281. package/packs/docker/pack.json +46 -0
  282. package/packs/docker/rules/compose-pinned-and-closed.md +21 -0
  283. package/packs/docker/rules/compose-swarm-safe.md +26 -0
  284. package/packs/docker/rules/docker-exec-form.md +19 -0
  285. package/packs/docker/rules/dockerfile-multistage.md +13 -0
  286. package/packs/docker/rules/dockerfile-production.md +25 -0
  287. package/packs/docker/skills/docker-diagnose/SKILL.md +23 -0
  288. package/packs/dynamic-data/checks/draft-on-the-server.json +27 -0
  289. package/packs/dynamic-data/checks/guard-does-its-job.json +28 -0
  290. package/packs/dynamic-data/checks/key-is-not-a-display-name.json +28 -0
  291. package/packs/dynamic-data/checks/no-code-from-data.json +29 -0
  292. package/packs/dynamic-data/checks/no-collection-from-request.json +27 -0
  293. package/packs/dynamic-data/checks/no-pipeline-from-stored-text.json +27 -0
  294. package/packs/dynamic-data/checks/no-regexp-from-stored-config.json +27 -0
  295. package/packs/dynamic-data/checks/no-template-from-stored-text.json +27 -0
  296. package/packs/dynamic-data/checks/save-takes-named-paths.json +27 -0
  297. package/packs/dynamic-data/pack.json +23 -0
  298. package/packs/dynamic-data/rules/a-list-is-a-saved-query.md +23 -0
  299. package/packs/dynamic-data/rules/definitions-are-validated-data.md +24 -0
  300. package/packs/dynamic-data/rules/every-dynamic-surface-has-a-boundary.md +29 -0
  301. package/packs/dynamic-data/rules/fields-have-owners.md +22 -0
  302. package/packs/dynamic-data/rules/imports-drop-nothing.md +26 -0
  303. package/packs/dynamic-data/rules/one-definition-many-surfaces.md +24 -0
  304. package/packs/dynamic-data/rules/one-fact-one-place.md +28 -0
  305. package/packs/dynamic-data/rules/rules-live-in-the-engine.md +29 -0
  306. package/packs/dynamic-data/rules/saves-are-patches.md +28 -0
  307. package/packs/dynamic-data/rules/tenant-costs-one-document.md +22 -0
  308. package/packs/dynamic-data/skills/design-a-definition/SKILL.md +24 -0
  309. package/packs/fastify/checks/api-routes-versioned.json +28 -0
  310. package/packs/fastify/checks/cookie-host-prefix.json +24 -0
  311. package/packs/fastify/checks/hijack-needs-try.json +28 -0
  312. package/packs/fastify/checks/no-cast-at-route-boundary.json +24 -0
  313. package/packs/fastify/checks/no-db-in-route-file.json +28 -0
  314. package/packs/fastify/checks/no-err-message-to-client.json +24 -0
  315. package/packs/fastify/checks/no-regexp-from-input.json +24 -0
  316. package/packs/fastify/checks/no-trust-proxy-true.json +25 -0
  317. package/packs/fastify/checks/one-error-body.json +26 -0
  318. package/packs/fastify/checks/origin-from-config-not-host.json +31 -0
  319. package/packs/fastify/checks/password-needs-slow-hash.json +30 -0
  320. package/packs/fastify/checks/sec-headers-present.json +28 -0
  321. package/packs/fastify/checks/secrets-compared-constant-time.json +24 -0
  322. package/packs/fastify/checks/sse-needs-error-listener.json +28 -0
  323. package/packs/fastify/checks/user-bytes-need-disposition.json +29 -0
  324. package/packs/fastify/pack.json +41 -0
  325. package/packs/fastify/rules/credentials-and-input.md +23 -0
  326. package/packs/fastify/rules/security-one-switch.md +28 -0
  327. package/packs/fastify/rules/streams-and-hijacks.md +22 -0
  328. package/packs/fastify/rules/tests-use-inject.md +13 -0
  329. package/packs/fastify/rules/three-layers.md +20 -0
  330. package/packs/fastify/rules/trust-config-not-headers.md +24 -0
  331. package/packs/full-stack/checks/no-express-no-vite.json +23 -0
  332. package/packs/full-stack/hooks/stack-guard.mjs +90 -0
  333. package/packs/full-stack/pack.json +76 -0
  334. package/packs/full-stack/rules/stack-is-fastify-and-nextjs.md +20 -0
  335. package/packs/go/checks/go-exec-result-ignored.json +15 -0
  336. package/packs/go/checks/go-unmarshal-error-ignored.json +15 -0
  337. package/packs/go/pack.json +21 -0
  338. package/packs/go/rules/go-errors-are-handled.md +22 -0
  339. package/packs/gpu-painter/checks/dawn-backends-and-validation.json +9 -0
  340. package/packs/gpu-painter/checks/dawn-no-spontaneous.json +10 -0
  341. package/packs/gpu-painter/checks/dawn-present-on-render-thread.json +17 -0
  342. package/packs/gpu-painter/checks/gpu-no-frame-stalls.json +12 -0
  343. package/packs/gpu-painter/checks/no-opengl-link.json +11 -0
  344. package/packs/gpu-painter/checks/no-opengl.json +13 -0
  345. package/packs/gpu-painter/hooks/browser-guard.py +196 -0
  346. package/packs/gpu-painter/pack.json +91 -0
  347. package/packs/gpu-painter/rules/dawn-build-and-shaders.md +27 -0
  348. package/packs/gpu-painter/rules/dawn-device-and-render-thread.md +26 -0
  349. package/packs/gpu-painter/rules/gpu-antialiasing.md +20 -0
  350. package/packs/gpu-painter/rules/gpu-blend-order.md +22 -0
  351. package/packs/gpu-painter/rules/gpu-bloom.md +20 -0
  352. package/packs/gpu-painter/rules/gpu-frame-budget.md +25 -0
  353. package/packs/gpu-painter/rules/gpu-frame-pacing-and-power.md +27 -0
  354. package/packs/gpu-painter/rules/gpu-frame-work.md +30 -0
  355. package/packs/gpu-painter/rules/gpu-golden-images.md +21 -0
  356. package/packs/gpu-painter/rules/gpu-hdr-and-tonemap.md +23 -0
  357. package/packs/gpu-painter/rules/gpu-picking-on-cpu.md +19 -0
  358. package/packs/gpu-painter/rules/gpu-render-lifecycle.md +25 -0
  359. package/packs/gpu-painter/rules/gpu-text-atlas.md +24 -0
  360. package/packs/gpu-painter/rules/gpu-travellers-from-snapshot.md +24 -0
  361. package/packs/gpu-painter/rules/hover-every-pressable.md +25 -0
  362. package/packs/gpu-painter/skills/gpu-capture/SKILL.md +15 -0
  363. package/packs/gpu-painter/skills/gpu-frame-bench/SKILL.md +14 -0
  364. package/packs/gpu-painter/skills/gpu-golden-check/SKILL.md +13 -0
  365. package/packs/gpu-painter/skills/gpu-golden-update/SKILL.md +16 -0
  366. package/packs/gpu-painter/skills/gpu-lifecycle-stress/SKILL.md +15 -0
  367. package/packs/gpu-painter/skills/hover-state/SKILL.md +33 -0
  368. package/packs/hook-proof/checks/hook-catch-must-not-allow.json +16 -0
  369. package/packs/hook-proof/checks/hook-command-no-pipe.json +16 -0
  370. package/packs/hook-proof/checks/hook-exit-1-does-not-block.json +17 -0
  371. package/packs/hook-proof/checks/hook-no-or-true.json +18 -0
  372. package/packs/hook-proof/checks/hook-refusal-on-stderr.json +17 -0
  373. package/packs/hook-proof/pack.json +38 -0
  374. package/packs/hook-proof/rules/a-refusal-is-exit-2.md +28 -0
  375. package/packs/hook-proof/skills/prove-a-hook/SKILL.md +21 -0
  376. package/packs/host-agent/checks/file-path-from-param.json +27 -0
  377. package/packs/host-agent/checks/integrity-check-optional.json +27 -0
  378. package/packs/host-agent/checks/quoted-value-into-config.json +24 -0
  379. package/packs/host-agent/checks/secret-on-argv.json +28 -0
  380. package/packs/host-agent/checks/ssh-verifies-host-key.json +27 -0
  381. package/packs/host-agent/checks/system-file-written-in-place.json +26 -0
  382. package/packs/host-agent/checks/tmp-path-predictable.json +27 -0
  383. package/packs/host-agent/pack.json +22 -0
  384. package/packs/host-agent/rules/root-on-other-machines.md +27 -0
  385. package/packs/html-ui/checks/email-html-escapes.json +25 -0
  386. package/packs/html-ui/checks/external-link-rel.json +21 -0
  387. package/packs/html-ui/checks/no-duplicate-escape.json +26 -0
  388. package/packs/html-ui/checks/no-inline-client-script.json +21 -0
  389. package/packs/html-ui/checks/no-inline-style.json +23 -0
  390. package/packs/html-ui/pack.json +33 -0
  391. package/packs/html-ui/rules/email-html-escapes.md +19 -0
  392. package/packs/html-ui/rules/server-html-escapes-everything.md +22 -0
  393. package/packs/http-api/checks/api-path-versioned.json +31 -0
  394. package/packs/http-api/checks/authz-from-request-body.json +26 -0
  395. package/packs/http-api/checks/client-calls-a-version.json +28 -0
  396. package/packs/http-api/checks/client-ip-not-leftmost-xff.json +28 -0
  397. package/packs/http-api/checks/cors-needs-origin.json +26 -0
  398. package/packs/http-api/checks/csp-not-switched-off.json +28 -0
  399. package/packs/http-api/checks/debug-route-not-shipped.json +26 -0
  400. package/packs/http-api/checks/encrypted-name-plaintext-value.json +26 -0
  401. package/packs/http-api/checks/err-text-stays-server-side.json +27 -0
  402. package/packs/http-api/checks/failure-is-not-empty.json +28 -0
  403. package/packs/http-api/checks/gate-is-not-a-todo.json +27 -0
  404. package/packs/http-api/checks/guard-fails-open.json +28 -0
  405. package/packs/http-api/checks/hmac-over-reserialised-body.json +26 -0
  406. package/packs/http-api/checks/jwt-verify-pins-algorithm.json +26 -0
  407. package/packs/http-api/checks/middleware-skips-without-user.json +26 -0
  408. package/packs/http-api/checks/next-api-route-versioned.json +47 -0
  409. package/packs/http-api/checks/no-security-off-switch.json +28 -0
  410. package/packs/http-api/checks/oauth-state-signed.json +26 -0
  411. package/packs/http-api/checks/one-time-code-compared-raw.json +26 -0
  412. package/packs/http-api/checks/query-cast-is-not-validation.json +28 -0
  413. package/packs/http-api/checks/queue-action-from-client.json +26 -0
  414. package/packs/http-api/checks/rate-limit-one-module.json +28 -0
  415. package/packs/http-api/checks/role-checked-in-one-guard.json +27 -0
  416. package/packs/http-api/checks/route-body-needs-schema.json +31 -0
  417. package/packs/http-api/checks/route-scopes-to-the-caller.json +31 -0
  418. package/packs/http-api/checks/secret-compared-constant-time-any.json +28 -0
  419. package/packs/http-api/checks/secret-in-job-record.json +27 -0
  420. package/packs/http-api/checks/server-fetches-tenant-url.json +30 -0
  421. package/packs/http-api/checks/short-code-needs-limit.json +26 -0
  422. package/packs/http-api/checks/user-url-through-safe-fetch.json +28 -0
  423. package/packs/http-api/checks/webhook-dedupes-event-id.json +30 -0
  424. package/packs/http-api/pack.json +45 -0
  425. package/packs/http-api/rules/adapters-one-db-gateway.md +18 -0
  426. package/packs/http-api/rules/apis-are-versioned.md +27 -0
  427. package/packs/http-api/rules/credentials-handled-safely.md +22 -0
  428. package/packs/http-api/rules/handlers-own-the-work.md +17 -0
  429. package/packs/http-api/rules/health-and-correlation.md +17 -0
  430. package/packs/http-api/rules/one-error-contract.md +17 -0
  431. package/packs/http-api/rules/routes-are-thin.md +17 -0
  432. package/packs/http-api/rules/routes-scope-to-the-caller.md +26 -0
  433. package/packs/http-api/rules/routes-validate-and-answer.md +25 -0
  434. package/packs/http-api/skills/change-an-api/SKILL.md +20 -0
  435. package/packs/http-api/skills/secure-a-route/SKILL.md +21 -0
  436. package/packs/instrument-shell/checks/no-raw-struct-blob.json +23 -0
  437. package/packs/instrument-shell/pack.json +22 -0
  438. package/packs/instrument-shell/rules/fold-keeps-every-route.md +24 -0
  439. package/packs/instrument-shell/rules/quiet-announces-itself.md +22 -0
  440. package/packs/instrument-shell/rules/render-the-extremes.md +25 -0
  441. package/packs/instrument-shell/rules/seams-are-declared.md +27 -0
  442. package/packs/instrument-shell/rules/session-blob-append-only.md +25 -0
  443. package/packs/instrument-shell/skills/gates-from-commit-one/SKILL.md +20 -0
  444. package/packs/instrument-shell/skills/hit-the-painted-control/SKILL.md +18 -0
  445. package/packs/js-ts/checks/async-callback-unguarded.json +26 -0
  446. package/packs/js-ts/checks/background-loop-needs-stop.json +28 -0
  447. package/packs/js-ts/checks/child-process-error-handler.json +28 -0
  448. package/packs/js-ts/checks/entry-registers-shutdown.json +31 -0
  449. package/packs/js-ts/checks/fetch-needs-timeout.json +30 -0
  450. package/packs/js-ts/checks/job-only-in-memory.json +28 -0
  451. package/packs/js-ts/checks/json-only-through-helper.json +29 -0
  452. package/packs/js-ts/checks/line-under-200-chars.json +34 -0
  453. package/packs/js-ts/checks/localhost-default-in-server.json +29 -0
  454. package/packs/js-ts/checks/no-any.json +27 -0
  455. package/packs/js-ts/checks/no-cast-escape-hatches.json +24 -0
  456. package/packs/js-ts/checks/no-console-in-app.json +31 -0
  457. package/packs/js-ts/checks/no-dynamic-import-node-builtin.json +25 -0
  458. package/packs/js-ts/checks/no-inline-binary.json +20 -0
  459. package/packs/js-ts/checks/no-inline-zod-outside-schemas.json +27 -0
  460. package/packs/js-ts/checks/no-local-sleep.json +25 -0
  461. package/packs/js-ts/checks/no-nested-ternary.json +20 -0
  462. package/packs/js-ts/checks/no-secret-in-log.json +24 -0
  463. package/packs/js-ts/checks/no-shell-string-interpolation.json +24 -0
  464. package/packs/js-ts/checks/no-swallowed-errors.json +25 -0
  465. package/packs/js-ts/checks/no-ts-enum.json +20 -0
  466. package/packs/js-ts/checks/process-env-only-in-config.json +31 -0
  467. package/packs/js-ts/checks/race-leaves-work-running.json +23 -0
  468. package/packs/js-ts/checks/reject-as-catch-all-exists.json +24 -0
  469. package/packs/js-ts/checks/secret-has-no-fallback.json +24 -0
  470. package/packs/js-ts/checks/secret-needs-crypto-random.json +24 -0
  471. package/packs/js-ts/checks/server-close-awaited.json +27 -0
  472. package/packs/js-ts/checks/session-user-typed-once.json +24 -0
  473. package/packs/js-ts/checks/switch-is-a-table.json +24 -0
  474. package/packs/js-ts/checks/test-imports-the-source.json +19 -0
  475. package/packs/js-ts/checks/test-name-says-behaviour.json +16 -0
  476. package/packs/js-ts/checks/tsconfig-flags.json +21 -0
  477. package/packs/js-ts/checks/void-needs-catch.json +24 -0
  478. package/packs/js-ts/checks/zod-string-needs-max.json +25 -0
  479. package/packs/js-ts/checks/zod-v4-formats.json +23 -0
  480. package/packs/js-ts/pack.json +84 -0
  481. package/packs/js-ts/rules/await-handled-at-a-boundary.md +66 -0
  482. package/packs/js-ts/rules/env-through-config.md +36 -0
  483. package/packs/js-ts/rules/errors-never-swallowed.md +33 -0
  484. package/packs/js-ts/rules/files-under-300-lines.md +18 -0
  485. package/packs/js-ts/rules/guard-clauses-over-nesting.md +34 -0
  486. package/packs/js-ts/rules/js-repeats-become-templates.md +26 -0
  487. package/packs/js-ts/rules/json-only-through-one-helper.md +62 -0
  488. package/packs/js-ts/rules/named-constants-not-magic-numbers.md +34 -0
  489. package/packs/js-ts/rules/no-any.md +34 -0
  490. package/packs/js-ts/rules/no-console.md +30 -0
  491. package/packs/js-ts/rules/no-floating-promises.md +31 -0
  492. package/packs/js-ts/rules/no-param-mutation.md +28 -0
  493. package/packs/js-ts/rules/no-unsafe-assertions.md +30 -0
  494. package/packs/js-ts/rules/one-logger.md +24 -0
  495. package/packs/js-ts/rules/outbound-calls-have-a-limit.md +20 -0
  496. package/packs/js-ts/rules/parallel-independent-awaits.md +29 -0
  497. package/packs/js-ts/rules/process-lifecycle.md +47 -0
  498. package/packs/js-ts/rules/schema-single-source-of-truth.md +36 -0
  499. package/packs/js-ts/rules/secrets-stay-secret.md +22 -0
  500. package/packs/js-ts/rules/small-functions.md +20 -0
  501. package/packs/js-ts/rules/test-names-describe-behavior.md +28 -0
  502. package/packs/js-ts/rules/tests-can-fail.md +19 -0
  503. package/packs/js-ts/rules/tsconfig-strict.md +32 -0
  504. package/packs/js-ts/rules/typed-exports.md +27 -0
  505. package/packs/js-ts/rules/types-once-at-the-edge.md +18 -0
  506. package/packs/js-ts/rules/unions-over-enums.md +29 -0
  507. package/packs/js-ts/rules/zod-at-route-boundaries.md +40 -0
  508. package/packs/midi/checks/active-sensing-no-pulse.json +24 -0
  509. package/packs/midi/checks/clock-not-at-sample-zero.json +23 -0
  510. package/packs/midi/checks/midi-realtime-first.json +27 -0
  511. package/packs/midi/checks/no-tempo-writeback.json +23 -0
  512. package/packs/midi/checks/position-not-integrated.json +25 -0
  513. package/packs/midi/checks/stub-playhead-advances.json +27 -0
  514. package/packs/midi/checks/sysex-length-literal.json +23 -0
  515. package/packs/midi/checks/sysex-not-on-audio.json +24 -0
  516. package/packs/midi/checks/sysex-survives-realtime.json +27 -0
  517. package/packs/midi/pack.json +22 -0
  518. package/packs/midi/rules/clock-is-a-pulse-stream.md +30 -0
  519. package/packs/midi/rules/imports-count-every-loss.md +25 -0
  520. package/packs/midi/rules/measure-the-clock-in-audio.md +30 -0
  521. package/packs/midi/rules/sync-is-a-mode.md +29 -0
  522. package/packs/midi/rules/sysex-is-a-record.md +29 -0
  523. package/packs/midi/skills/clock-probe/SKILL.md +23 -0
  524. package/packs/midi/skills/clock-probe/clock-law.md +417 -0
  525. package/packs/midi/skills/does-it-sync/SKILL.md +19 -0
  526. package/packs/mongo/checks/bulk-over-many.json +28 -0
  527. package/packs/mongo/checks/created-at-immutable.json +25 -0
  528. package/packs/mongo/checks/driver-outside-gateway.json +24 -0
  529. package/packs/mongo/checks/key-lookup-is-hashed.json +24 -0
  530. package/packs/mongo/checks/mongoclient-cached-and-named.json +34 -0
  531. package/packs/mongo/checks/mongoclient-outside-gateway.json +24 -0
  532. package/packs/mongo/checks/no-count-plus-one-ids.json +25 -0
  533. package/packs/mongo/checks/no-db-write-in-loop.json +25 -0
  534. package/packs/mongo/checks/no-drop-in-code.json +27 -0
  535. package/packs/mongo/checks/no-empty-deletemany.json +26 -0
  536. package/packs/mongo/checks/no-find-reads.json +26 -0
  537. package/packs/mongo/checks/no-input-spread-into-write.json +24 -0
  538. package/packs/mongo/checks/no-mongoose.json +23 -0
  539. package/packs/mongo/checks/no-negation-delete.json +26 -0
  540. package/packs/mongo/checks/no-non-null-after-findone.json +25 -0
  541. package/packs/mongo/checks/objectid-equals.json +26 -0
  542. package/packs/mongo/checks/update-then-reread.json +24 -0
  543. package/packs/mongo/checks/watch-not-poll.json +25 -0
  544. package/packs/mongo/pack.json +52 -0
  545. package/packs/mongo/rules/deletes-name-what-dies.md +21 -0
  546. package/packs/mongo/rules/driver-only-in-gateway.md +26 -0
  547. package/packs/mongo/rules/keys-hashed-input-picked.md +21 -0
  548. package/packs/mongo/rules/one-client-one-pool.md +32 -0
  549. package/packs/mongo/rules/reads-aggregate-writes-bulk.md +25 -0
  550. package/packs/mongo/rules/writes-are-atomic.md +27 -0
  551. package/packs/multi-tenant/checks/role-sees-all-tenants.json +26 -0
  552. package/packs/multi-tenant/checks/tenant-filter-from-url.json +25 -0
  553. package/packs/multi-tenant/pack.json +22 -0
  554. package/packs/multi-tenant/rules/no-role-crosses-a-tenant.md +21 -0
  555. package/packs/multi-tenant/skills/prove-tenant-isolation/SKILL.md +19 -0
  556. package/packs/nginx/checks/modsec-no-blanket-off.json +16 -0
  557. package/packs/nginx/checks/nginx-csp-no-unsafe.json +15 -0
  558. package/packs/nginx/checks/nginx-headers-always.json +15 -0
  559. package/packs/nginx/checks/nginx-keepalive-http11.json +20 -0
  560. package/packs/nginx/checks/nginx-logs-to-stdout.json +15 -0
  561. package/packs/nginx/checks/nginx-no-crlf.json +17 -0
  562. package/packs/nginx/checks/nginx-resolver-ttl.json +19 -0
  563. package/packs/nginx/pack.json +43 -0
  564. package/packs/nginx/rules/modsecurity-tuning.md +22 -0
  565. package/packs/nginx/rules/nginx-production.md +26 -0
  566. package/packs/nginx/skills/waf-rollout/SKILL.md +20 -0
  567. package/packs/python/checks/py-no-bare-except.json +18 -0
  568. package/packs/python/checks/py-no-eval.json +18 -0
  569. package/packs/python/checks/py-no-mutable-default.json +18 -0
  570. package/packs/python/checks/py-no-shell-true.json +18 -0
  571. package/packs/python/checks/py-no-star-import.json +18 -0
  572. package/packs/python/checks/py-no-swallowed-exception.json +18 -0
  573. package/packs/python/checks/py-none-by-is.json +18 -0
  574. package/packs/python/checks/py-open-with-with.json +18 -0
  575. package/packs/python/checks/py-requests-timeout.json +18 -0
  576. package/packs/python/checks/py-subprocess-output-checked.json +18 -0
  577. package/packs/python/checks/py-yaml-safe-load.json +18 -0
  578. package/packs/python/pack.json +26 -0
  579. package/packs/python/rules/python-errors-and-resources.md +26 -0
  580. package/packs/python/rules/python-repeats-become-templates.md +27 -0
  581. package/packs/python/skills/python-extract-template/SKILL.md +35 -0
  582. package/packs/react/checks/dialog-needs-role.json +28 -0
  583. package/packs/react/checks/effect-not-for-derived-state.json +24 -0
  584. package/packs/react/checks/icon-button-needs-name.json +24 -0
  585. package/packs/react/checks/label-needs-a-field.json +24 -0
  586. package/packs/react/checks/ui-failed-fetch-says-so.json +23 -0
  587. package/packs/react/checks/ui-ok-branch-has-an-else.json +23 -0
  588. package/packs/react/pack.json +20 -0
  589. package/packs/react/rules/react-accessible-controls.md +20 -0
  590. package/packs/react/rules/react-state-and-effects.md +23 -0
  591. package/packs/reverse-engineering/checks/ear-constant-tagged.json +23 -0
  592. package/packs/reverse-engineering/checks/no-default-on-distrust.json +24 -0
  593. package/packs/reverse-engineering/checks/no-rom-table-literal.json +29 -0
  594. package/packs/reverse-engineering/checks/prose-never-decides.json +22 -0
  595. package/packs/reverse-engineering/hooks/firmware-out-of-git.mjs +49 -0
  596. package/packs/reverse-engineering/pack.json +35 -0
  597. package/packs/reverse-engineering/rules/a-negative-names-what-was-scanned.md +19 -0
  598. package/packs/reverse-engineering/rules/account-every-term.md +20 -0
  599. package/packs/reverse-engineering/rules/addresses-name-their-space.md +21 -0
  600. package/packs/reverse-engineering/rules/every-law-has-a-source.md +18 -0
  601. package/packs/reverse-engineering/rules/findings-are-withdrawn-in-place.md +17 -0
  602. package/packs/reverse-engineering/rules/firmware-stays-out.md +18 -0
  603. package/packs/reverse-engineering/rules/names-before-code.md +19 -0
  604. package/packs/reverse-engineering/rules/reproduction-or-original.md +18 -0
  605. package/packs/reverse-engineering/rules/two-sources-before-naming.md +21 -0
  606. package/packs/reverse-engineering/skills/bank-census/SKILL.md +18 -0
  607. package/packs/reverse-engineering/skills/gather-sources/SKILL.md +21 -0
  608. package/packs/reverse-engineering/skills/menu-map/SKILL.md +28 -0
  609. package/packs/reverse-engineering/skills/names-first/SKILL.md +41 -0
  610. package/packs/reverse-engineering/skills/re-deliverables/SKILL.md +21 -0
  611. package/packs/reverse-engineering/skills/tripwire-the-law/SKILL.md +19 -0
  612. package/packs/reverse-engineering/tools/lib/unwrap.mjs +115 -0
  613. package/packs/reverse-engineering/tools/names-sweep.mjs +264 -0
  614. package/packs/shell/checks/sh-mktemp.json +18 -0
  615. package/packs/shell/checks/sh-no-backticks.json +18 -0
  616. package/packs/shell/checks/sh-no-eval.json +18 -0
  617. package/packs/shell/checks/sh-no-parse-ls.json +18 -0
  618. package/packs/shell/checks/sh-no-pipe-to-shell.json +18 -0
  619. package/packs/shell/checks/sh-no-program-in-heredoc.json +18 -0
  620. package/packs/shell/checks/sh-quote-arguments.json +18 -0
  621. package/packs/shell/checks/sh-rm-guarded.json +18 -0
  622. package/packs/shell/checks/sh-strict-mode.json +20 -0
  623. package/packs/shell/pack.json +28 -0
  624. package/packs/shell/rules/shell-repeats-become-templates.md +23 -0
  625. package/packs/shell/rules/shell-strict-and-quoted.md +27 -0
  626. package/packs/shell/skills/shell-extract-template/SKILL.md +32 -0
  627. package/packs/shell-hooks/checks/gnu-only-in-shell.json +18 -0
  628. package/packs/shell-hooks/checks/hook-command-anchored.json +12 -0
  629. package/packs/shell-hooks/checks/hook-json-by-tool.json +18 -0
  630. package/packs/shell-hooks/checks/no-bash-c-with-variable.json +20 -0
  631. package/packs/shell-hooks/pack.json +36 -0
  632. package/packs/shell-hooks/rules/hooks-portable-anchored-quoted.md +26 -0
  633. package/packs/sound-design/checks/audio-seed-fixed.json +23 -0
  634. package/packs/sound-design/checks/mix-is-a-crossfade.json +30 -0
  635. package/packs/sound-design/checks/no-hash-in-the-signal.json +30 -0
  636. package/packs/sound-design/checks/no-literal-key-anchor.json +30 -0
  637. package/packs/sound-design/checks/test-compares-a-function-with-itself.json +22 -0
  638. package/packs/sound-design/pack.json +22 -0
  639. package/packs/sound-design/rules/algorithms-are-data.md +26 -0
  640. package/packs/sound-design/rules/meter-before-engine.md +27 -0
  641. package/packs/sound-design/rules/neutral-is-exact.md +26 -0
  642. package/packs/sound-design/rules/test-off-the-easy-point.md +28 -0
  643. package/packs/sound-design/rules/unreachable-is-said.md +25 -0
  644. package/packs/sound-design/rules/waveforms-and-octaves.md +24 -0
  645. package/packs/sound-design/skills/choose-observable/SKILL.md +18 -0
  646. package/packs/sound-design/skills/measure-a-law/SKILL.md +18 -0
  647. package/packs/sound-design/skills/measure-reverb/SKILL.md +19 -0
  648. package/packs/sound-design/skills/spread-probe/SKILL.md +18 -0
  649. package/packs/stellix/checks/no-keyboard-focus.json +12 -0
  650. package/packs/stellix/checks/one-seeded-random.json +16 -0
  651. package/packs/stellix/checks/world-never-includes-painter.json +12 -0
  652. package/packs/stellix/checks/world-no-libm.json +15 -0
  653. package/packs/stellix/checks/world-no-unordered.json +10 -0
  654. package/packs/stellix/hooks/no-shipped-images.mjs +88 -0
  655. package/packs/stellix/pack.json +61 -0
  656. package/packs/stellix/rules/stellix-deterministic-world.md +29 -0
  657. package/packs/stellix/rules/stellix-reset-generated.md +24 -0
  658. package/packs/stellix/rules/stellix-world-snapshot.md +27 -0
  659. package/packs/stellix/rules/world-continuous-collision.md +26 -0
  660. package/packs/stellix/rules/world-fp-determinism.md +28 -0
  661. package/packs/stored-text/checks/no-raw-html-from-data.json +31 -0
  662. package/packs/stored-text/checks/stored-text-libs-only-in-renderer.json +31 -0
  663. package/packs/stored-text/checks/stored-text-renderer-complete.json +24 -0
  664. package/packs/stored-text/checks/stored-text-through-renderer.json +25 -0
  665. package/packs/stored-text/pack.json +41 -0
  666. package/packs/stored-text/rules/stored-text-renders-once.md +26 -0
  667. package/packs/stored-text/skills/render-stored-text/SKILL.md +24 -0
  668. package/packs/synth-creator/checks/timing-from-synthcore.json +24 -0
  669. package/packs/synth-creator/checks/ui-drawn-in-code.json +23 -0
  670. package/packs/synth-creator/hooks/build-guard.mjs +232 -0
  671. package/packs/synth-creator/hooks/build-sweep.mjs +166 -0
  672. package/packs/synth-creator/hooks/frozen-files.mjs +63 -0
  673. package/packs/synth-creator/hooks/idle-wake.mjs +90 -0
  674. package/packs/synth-creator/hooks/protect-vendor.mjs +174 -0
  675. package/packs/synth-creator/hooks/spec-lock.mjs +117 -0
  676. package/packs/synth-creator/hooks/stage-guard.mjs +50 -0
  677. package/packs/synth-creator/hooks/time-guard.mjs +292 -0
  678. package/packs/synth-creator/hooks/user-asks.mjs +165 -0
  679. package/packs/synth-creator/pack.json +136 -0
  680. package/packs/synth-creator/rules/copy-trees-read-only.md +25 -0
  681. package/packs/synth-creator/rules/debug-by-measurement.md +20 -0
  682. package/packs/synth-creator/rules/done-means-fresh-evidence.md +18 -0
  683. package/packs/synth-creator/rules/hooks-and-gates.md +21 -0
  684. package/packs/synth-creator/rules/long-work-in-the-background.md +16 -0
  685. package/packs/synth-creator/rules/timing-from-synthcore.md +16 -0
  686. package/packs/synth-creator/skills/build-step/SKILL.md +125 -0
  687. package/packs/synth-creator/skills/build-through-the-queue/SKILL.md +19 -0
  688. package/packs/synth-creator/tools/freeze.mjs +36 -0
  689. package/packs/tui/checks/tui-entry-restores-terminal.json +27 -0
  690. package/packs/tui/checks/tui-gates-on-tty.json +25 -0
  691. package/packs/tui/checks/tui-no-ris-reset.json +22 -0
  692. package/packs/tui/checks/tui-resize-listener-removed.json +26 -0
  693. package/packs/tui/checks/tui-width-is-state.json +33 -0
  694. package/packs/tui/pack.json +44 -0
  695. package/packs/tui/rules/tui-resize-and-screen.md +25 -0
  696. package/packs/tui/skills/build-tui/SKILL.md +21 -0
  697. package/packs/ui-craft/checks/ui-input-needs-autocomplete.json +23 -0
  698. package/packs/ui-craft/checks/ui-intl-for-dates.json +21 -0
  699. package/packs/ui-craft/checks/ui-no-ai-palette.json +22 -0
  700. package/packs/ui-craft/checks/ui-no-animate-layout.json +22 -0
  701. package/packs/ui-craft/checks/ui-no-assistant-palette.json +23 -0
  702. package/packs/ui-craft/checks/ui-no-bounce-easing.json +22 -0
  703. package/packs/ui-craft/checks/ui-no-custom-cursor.json +22 -0
  704. package/packs/ui-craft/checks/ui-no-emoji-as-ui.json +22 -0
  705. package/packs/ui-craft/checks/ui-no-empty-image.json +21 -0
  706. package/packs/ui-craft/checks/ui-no-eyebrow.json +21 -0
  707. package/packs/ui-craft/checks/ui-no-furniture-text.json +22 -0
  708. package/packs/ui-craft/checks/ui-no-gradient-text.json +22 -0
  709. package/packs/ui-craft/checks/ui-no-index-numbering.json +21 -0
  710. package/packs/ui-craft/checks/ui-no-lorem.json +22 -0
  711. package/packs/ui-craft/checks/ui-no-marketing-buzzword.json +22 -0
  712. package/packs/ui-craft/checks/ui-no-paste-block.json +21 -0
  713. package/packs/ui-craft/checks/ui-no-placeholder-names.json +21 -0
  714. package/packs/ui-craft/checks/ui-no-scroll-hijack.json +21 -0
  715. package/packs/ui-craft/checks/ui-no-side-tab.json +21 -0
  716. package/packs/ui-craft/checks/ui-no-stock-labels.json +22 -0
  717. package/packs/ui-craft/checks/ui-no-transition-all.json +22 -0
  718. package/packs/ui-craft/checks/ui-no-uppercase-table-header.json +21 -0
  719. package/packs/ui-craft/checks/ui-no-zebra-rows.json +22 -0
  720. package/packs/ui-craft/checks/ui-no-zoom-lock.json +21 -0
  721. package/packs/ui-craft/pack.json +43 -0
  722. package/packs/ui-craft/rules/apps-are-not-pages.md +26 -0
  723. package/packs/ui-craft/rules/data-tells-the-truth.md +26 -0
  724. package/packs/ui-craft/rules/dates-and-numbers.md +18 -0
  725. package/packs/ui-craft/rules/forms-never-fight.md +22 -0
  726. package/packs/ui-craft/rules/interface-copy.md +26 -0
  727. package/packs/ui-craft/rules/motion-is-rare.md +31 -0
  728. package/packs/ui-craft/rules/no-generated-look.md +37 -0
  729. package/packs/ui-craft/rules/page-holds-its-frame.md +28 -0
  730. package/packs/ui-craft/rules/page-proved-rendered.md +19 -0
  731. package/packs/ui-craft/rules/pictures-do-a-job.md +25 -0
  732. package/packs/ui-craft/rules/pointer-feedback.md +28 -0
  733. package/packs/ui-craft/rules/screen-answers-one-question.md +24 -0
  734. package/packs/ui-craft/rules/survives-real-data.md +23 -0
  735. package/packs/ui-craft/rules/touch-and-small-screens.md +19 -0
  736. package/packs/ui-craft/rules/type-space-colour.md +26 -0
  737. package/packs/ui-craft/rules/url-holds-the-state.md +19 -0
  738. package/packs/ui-craft/skills/app-screen/SKILL.md +33 -0
  739. package/packs/ui-craft/skills/build-from-reference/SKILL.md +28 -0
  740. package/packs/ui-craft/skills/chart-choice/SKILL.md +34 -0
  741. package/packs/ui-craft/skills/hover-state/SKILL.md +28 -0
  742. package/packs/ui-craft/skills/page-critique/SKILL.md +33 -0
  743. package/packs/ui-craft/skills/prove-page/SKILL.md +23 -0
  744. package/packs/ui-craft/skills/redesign-page/SKILL.md +19 -0
  745. package/packs/ui-craft/skills/ui-code-review/SKILL.md +22 -0
  746. package/packs/ui-craft/skills/write-the-page/SKILL.md +21 -0
  747. package/packs/ui-craft/tools/page-measure.mjs +41 -0
  748. package/packs/web/checks/a11y-img-needs-alt.json +26 -0
  749. package/packs/web/checks/a11y-no-bare-outline-none.json +21 -0
  750. package/packs/web/checks/a11y-no-clickable-div.json +26 -0
  751. package/packs/web/checks/a11y-no-positive-tabindex.json +26 -0
  752. package/packs/web/checks/client-no-api-key.json +29 -0
  753. package/packs/web/checks/client-no-browser-global-at-module-scope.json +28 -0
  754. package/packs/web/checks/client-no-server-import.json +31 -0
  755. package/packs/web/checks/client-no-window-location-nav.json +28 -0
  756. package/packs/web/checks/css-no-inline-style.json +30 -0
  757. package/packs/web/checks/ds-pages-compose-primitives.json +28 -0
  758. package/packs/web/checks/key-from-index.json +23 -0
  759. package/packs/web/checks/prodbuild-no-cdn-runtime-deps.json +21 -0
  760. package/packs/web/checks/prodbuild-no-dev-flags.json +21 -0
  761. package/packs/web/checks/prodbuild-no-remote-fonts.json +22 -0
  762. package/packs/web/checks/rcss-button-cursor.json +30 -0
  763. package/packs/web/checks/rcss-no-100vw.json +21 -0
  764. package/packs/web/checks/rcss-no-fixed-wide-width.json +21 -0
  765. package/packs/web/checks/rcss-viewport-meta.json +28 -0
  766. package/packs/web/checks/secret-not-in-web-storage.json +25 -0
  767. package/packs/web/checks/tw-no-raw-hex-in-markup.json +34 -0
  768. package/packs/web/checks/wp-hero-not-lazy.json +27 -0
  769. package/packs/web/checks/wp-img-needs-dimensions.json +26 -0
  770. package/packs/web/pack.json +73 -0
  771. package/packs/web/rules/accessibility.md +26 -0
  772. package/packs/web/rules/client-boundaries.md +23 -0
  773. package/packs/web/rules/production-build.md +22 -0
  774. package/packs/web/rules/secrets-not-in-the-browser.md +18 -0
  775. package/packs/web/rules/styles-live-in-stylesheets.md +23 -0
  776. package/packs/web/rules/tailwind-v4.md +23 -0
  777. package/packs/web/rules/ui-primitives-one-module.md +30 -0
  778. package/packs/web/rules/web-performance.md +21 -0
  779. package/packs/web/skills/design-direction/SKILL.md +30 -0
  780. package/packs/web/skills/design-review/SKILL.md +28 -0
  781. package/packs/web/skills/pick-rendering-mode/SKILL.md +21 -0
  782. package/packs/web/skills/preflight-build/SKILL.md +23 -0
  783. package/packs/web/skills/seo-audit/SKILL.md +22 -0
  784. package/packs/websocket/checks/no-token-in-query.json +26 -0
  785. package/packs/websocket/checks/stale-socket-deletes-new-entry.json +25 -0
  786. package/packs/websocket/checks/ws-identity-from-message.json +26 -0
  787. package/packs/websocket/pack.json +21 -0
  788. package/packs/websocket/rules/identity-comes-from-the-connection.md +24 -0
  789. package/packs/writing/checks/ascii-no-em-dash.json +17 -0
  790. package/packs/writing/checks/no-copula-dodging.json +18 -0
  791. package/packs/writing/checks/no-dramatic-colon.json +18 -0
  792. package/packs/writing/checks/no-not-x-but-y.json +18 -0
  793. package/packs/writing/checks/no-participial-opener.json +18 -0
  794. package/packs/writing/checks/no-pseudo-wisdom.json +18 -0
  795. package/packs/writing/checks/no-slop-words.json +18 -0
  796. package/packs/writing/checks/no-stock-connectors.json +18 -0
  797. package/packs/writing/checks/no-time-or-date.json +27 -0
  798. package/packs/writing/checks/no-trailing-ing.json +18 -0
  799. package/packs/writing/checks/no-vague-attribution.json +18 -0
  800. package/packs/writing/pack.json +43 -0
  801. package/packs/writing/rules/ascii-no-em-dash.md +21 -0
  802. package/packs/writing/rules/cut-the-padding.md +20 -0
  803. package/packs/writing/rules/direct-voice.md +22 -0
  804. package/packs/writing/rules/no-cargo-cult.md +16 -0
  805. package/packs/writing/rules/no-scaffolding.md +19 -0
  806. package/packs/writing/rules/no-stock-language.md +27 -0
  807. package/packs/writing/rules/no-time-or-date.md +18 -0
  808. package/packs/writing/rules/rhythm-like-talk.md +20 -0
  809. package/packs/writing/rules/write-from-your-own-corner.md +20 -0
  810. package/packs/writing/skills/argument-post/SKILL.md +28 -0
  811. package/packs/writing/skills/humanize/SKILL.md +28 -0
  812. package/packs/writing/skills/launch-post/SKILL.md +38 -0
  813. package/packs/writing/skills/pick-subreddit/SKILL.md +69 -0
  814. package/packs/writing/skills/post-voice/SKILL.md +22 -0
  815. package/packs/writing/skills/reddit-post/SKILL.md +63 -0
  816. package/packs/writing/skills/reddit-title/SKILL.md +23 -0
  817. package/packs/writing/skills/reply-moves/SKILL.md +40 -0
  818. package/packs/writing/skills/reply-stance/SKILL.md +23 -0
  819. package/packs/writing/skills/should-i-reply/SKILL.md +21 -0
  820. package/packs/zod/hooks/schemas-only.mjs +53 -0
  821. package/packs/zod/pack.json +44 -0
  822. package/packs/zod/rules/env-shapes-only.md +14 -0
  823. package/packs/zod/rules/zod-4-schemas.md +14 -0
  824. package/profiles/stellix.layout.json +315 -0
  825. package/profiles/sub37.layout.json +247 -0
  826. package/prompts/scan-review.md +40 -0
  827. package/prompts/survey.md +82 -0
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse (Bash): what lands in git stays in its history for ever. `git add` of a file over the size limit, a working screenshot in the repo root or a debug
3
+ // dump is refused. One real repo carried 991 MB, with 22 screenshots in its root and 64 MB files. Limits are in layout.json "guards": { "repoLitter": { "maxFileMb": 10,
4
+ // "allow": ["<glob>"] } }. `--selftest` proves it.
5
+ import { spawnSync } from 'node:child_process';
6
+ import { statSync } from 'node:fs';
7
+ import { join, relative, resolve } from 'node:path';
8
+ import { gitAdds } from './lib/git-adds.mjs';
9
+ import { matchesAny } from './lib/glob.mjs';
10
+ import { readLayout } from './lib/layout.mjs';
11
+ import { checkoutRoot, claudeRoot } from './lib/project.mjs';
12
+ import { bashOf, git, put, runHook, scratchProject, verdicts } from './lib/selftest.mjs';
13
+ import { judgeEvent } from './lib/writes.mjs';
14
+ import { asHook, isMain } from './lib/hook-log.mjs';
15
+
16
+ export const BIG_MB = 10;
17
+ const MB = 1024 * 1024;
18
+ const KEPT_IMAGE = /^(favicon|apple-touch-icon|logo|icon|og-image|social|android-chrome|mstile|safari-pinned)/i;
19
+ const DUMP = /\.(har|heapsnapshot|dmp|core|log)$/i;
20
+
21
+ export const isRootImage = (rel, allow) => /^[^/]+\.(png|jpe?g|gif|webp|bmp|mov|mp4)$/i.test(rel) && !KEPT_IMAGE.test(rel) && !matchesAny(rel, allow);
22
+
23
+ function faultOf(rel, abs, cfg) {
24
+ if (matchesAny(rel, cfg.allow ?? [])) return null;
25
+ if (isRootImage(rel, [])) return `${rel} looks like a working screenshot in the repo root. Keep it out of git, or put an asset in its folder.`;
26
+ if (/:Zone\.Identifier$/.test(rel)) return `${rel} is a browser download marker. It is an invalid path on Windows and breaks checkout. Add *:Zone.Identifier to .gitignore.`;
27
+ if (DUMP.test(rel)) return `${rel} is a log or a debug dump. Add it to .gitignore.`;
28
+ let mb = 0;
29
+ try { mb = statSync(abs).size / MB; } catch { return null; }
30
+ const limit = cfg.maxFileMb ?? BIG_MB;
31
+ return mb > limit ? `${rel} is ${Math.round(mb)} MB (limit ${limit}). Git keeps it for ever: use object storage or a release.` : null;
32
+ }
33
+
34
+ // A named file, or what a folder would stage.
35
+ function staged(dir, arg) {
36
+ const r = spawnSync('git', ['ls-files', '--others', '--modified', '--exclude-standard', '--', arg], { cwd: dir, encoding: 'utf8' });
37
+ return (r.stdout ?? '').split('\n').filter(Boolean).map((f) => resolve(dir, f));
38
+ }
39
+
40
+ function judgeAdd(command, cwd) {
41
+ for (const { dir, args } of gitAdds(command, cwd)) {
42
+ const root = checkoutRoot(join(dir, 'x'));
43
+ const cfg = readLayout(claudeRoot(join(dir, 'x'))).guards?.repoLitter ?? {};
44
+ for (const abs of args.filter((a) => !a.startsWith('-')).flatMap((a) => staged(dir, a))) {
45
+ const fault = faultOf(relative(root, abs).replace(/\\/g, '/'), abs, cfg);
46
+ if (fault) return `${fault}\nA real asset can be named in guards.repoLitter.allow.\n`;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ const main = (text) => judgeEvent(text, { name: 'repo-litter', command: judgeAdd });
53
+
54
+ function selftest() {
55
+ const hook = new URL(import.meta.url).pathname;
56
+ const dir = scratchProject({ 'src/a.pdf:Zone.Identifier': 'x', 'shot.png': 'x', 'favicon.png': 'x', 'src/a.ts': 'x', 'src/hero.png': 'x', 'trace.har': 'x', 'kept.png': 'x', '.claude/layout.json': JSON.stringify({ guards: { repoLitter: { maxFileMb: 1, allow: ['kept.png'] } } }) });
57
+ git(dir, 'init', '-q');
58
+ put(join(dir, 'media/big.bin'), 'x'.repeat(2 * MB));
59
+ const add = (paths) => runHook(hook, dir, bashOf(`git add ${paths}`, dir));
60
+ return verdicts('repo-litter', [
61
+ ['a screenshot in the root', add('shot.png'), 2],
62
+ ['a big file', add('media/big.bin'), 2],
63
+ ['a folder holding a big file', add('media'), 2],
64
+ ['a debug dump', add('trace.har'), 2],
65
+ ['a download marker', add('src/a.pdf:Zone.Identifier'), 2],
66
+ ['source', add('src/a.ts'), 0],
67
+ ['an image in its folder', add('src/hero.png'), 0],
68
+ ['a favicon in the root', add('favicon.png'), 0],
69
+ ['a file the layout allows', add('kept.png'), 0],
70
+ ['something else entirely', runHook(hook, dir, bashOf('git status', dir)), 0],
71
+ ]);
72
+ }
73
+
74
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
75
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('repo-litter', main));
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit: repetition is cheapest to stop before a line is drafted. When a prompt's words name something the repo already has, what exists
3
+ // goes in front of the prompt, so it is called and never written again. Only real matches, six at most; a prompt that matches nothing adds nothing.
4
+ // `--selftest` proves it.
5
+ import { TESTS, skippedBy } from './lib/elements.mjs';
6
+ import { readLayout } from './lib/layout.mjs';
7
+ import { checkoutRoot, claudeRoot } from './lib/project.mjs';
8
+ import { repoIndex } from './lib/repo-index.mjs';
9
+ import { scratchProject, verdicts } from './lib/selftest.mjs';
10
+ import { asHook, isMain } from './lib/hook-log.mjs';
11
+
12
+ const SHOWN = 6;
13
+ const COMMON = new Set(`this that with from have make need want when what where which their there then them they will would should could into only also more
14
+ some just like been were does each every code file files function method class value data line test tests work works using used change update write read
15
+ please thing things sure right added adding`.split(/\s+/));
16
+
17
+ const stem = (w) => w.toLowerCase().replace(/(?:ing|ers|er|es|ed|s)$/, '');
18
+ const piecesOf = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[\s_-]+/).filter((w) => w.length >= 3).map(stem);
19
+ const NEAR = 5; // the pieces of a name lie within this many words of each other: scattered over a long prompt they are chance
20
+ const LONG = 150; // words: past this a prompt is a document, and any name can be spelled out of it
21
+
22
+ // { word: [its places in the prompt] }.
23
+ function placesOf(prompt) {
24
+ const places = new Map();
25
+ (prompt.match(/[A-Za-z]+/g) ?? []).forEach((w, i) => {
26
+ if (w.length < 4 || COMMON.has(w.toLowerCase())) return;
27
+ const k = stem(w);
28
+ places.set(k, [...(places.get(k) ?? []), i]);
29
+ });
30
+ return places;
31
+ }
32
+
33
+ const closeTogether = (pieces, places) => pieces.some((p) => (places.get(p) ?? []).some((at) => pieces.every((q) => (places.get(q) ?? []).some((x) => Math.abs(x - at) <= NEAR))));
34
+
35
+ // A compound name, with all its pieces in the prompt, close together. One word in common says nothing.
36
+ export function matches(prompt, index) {
37
+ if (prompt.split(/\s+/).length > LONG) return [];
38
+ const places = placesOf(prompt);
39
+ const out = [];
40
+ for (const [file, e] of Object.entries(index)) {
41
+ for (const [name, line] of e.defs ?? []) {
42
+ const pieces = piecesOf(name);
43
+ if (pieces.length >= 2 && closeTogether(pieces, places)) out.push({ name, at: `${file}:${line}`, size: pieces.length });
44
+ }
45
+ }
46
+ return out.sort((a, b) => b.size - a.size).slice(0, SHOWN);
47
+ }
48
+
49
+ function main(text) {
50
+ const event = JSON.parse(text || '{}');
51
+ const at = `${event.cwd ?? process.cwd()}/x`;
52
+ const found = matches(event.prompt ?? '', repoIndex(checkoutRoot(at), [...TESTS, ...skippedBy(readLayout(claudeRoot(at)))], readLayout(claudeRoot(at))));
53
+ if (found.length) console.log(`Already in this repo, call it rather than write it again:\n${found.map((f) => `- ${f.name} (${f.at})`).join('\n')}`);
54
+ return 0;
55
+ }
56
+
57
+ function selftest() {
58
+ const dir = scratchProject({ 'src/dates.ts': 'export function formatDateRange(a, b) { return a + b; }\n', 'src/a.py': 'def parse_price_table(text):\n return text\n', 'src/one.ts': 'export function render(a) { return a; }\n' });
59
+ const index = repoIndex(dir, []);
60
+ const named = (prompt) => matches(prompt, index).map((m) => m.name).join();
61
+ return verdicts('reuse-hint', [
62
+ ['a prompt naming what exists, camelCase', named('add a page that will format the date range of a booking'), 'formatDateRange'],
63
+ ['a prompt naming what exists, snake_case', named('parse the price table from the upload'), 'parse_price_table'],
64
+ ['it says where', matches('format the date range', index)[0]?.at, 'src/dates.ts:1'],
65
+ ['one word in common says nothing', named('the date of the last deploy'), ''],
66
+ ['a one-word name is never matched', named('render the render view'), ''],
67
+ ['an empty prompt', named(''), ''],
68
+ ['the pieces far apart', named('format the page first, then later on, much further down, show a date and a range'), ''],
69
+ ['a long document', named(`format the date range ${'and more words here '.repeat(40)}`), ''],
70
+ ['a prompt about nothing here', named('rename the settings page title'), ''],
71
+ ]);
72
+ }
73
+
74
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
75
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('reuse-hint', main));
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // run-hooks: one node process runs every kit hook of one settings entry, instead of one node process per hook (KWB-24:
3
+ // The user saw 17 hooks take 48 s on one rm; each was its own node start, all at once, on a loaded machine). settings.json
4
+ // names the runner and its hooks in one command: node ".../run-hooks.mjs" .claude/hooks/a.mjs .claude/hooks/b.mjs
5
+ // Each hook module exports hook(text) (the event's JSON text, as stdin was) and returns its exit code; the runner calls
6
+ // them in turn, catches each one's stdout and stderr, writes each one's timing line to .claude/hooks.log (lib/hook-log.mjs)
7
+ // and its own (hook "run-hooks": the whole process, node's start-up included), and exits with the worst code: 2 if any
8
+ // hook refused, else the first other non-zero. A hook that throws counts as exit 1 (a non-blocking error), as it would alone.
9
+ // Every hook still runs on its own too (node .claude/hooks/a.mjs, its selftest, a project's hand-made entry).
10
+ // node run-hooks.mjs <hook path or name>... (stdin: the event)
11
+ // node run-hooks.mjs --selftest
12
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, writeSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { basename, dirname, isAbsolute, join } from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { logCall } from './lib/event-log.mjs';
17
+ import { appendRotated, isMain, logHook, projectRoot, readHookLog } from './lib/hook-log.mjs';
18
+
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ export const RUNNER = 'run-hooks';
21
+ export const nameOf = (arg) => basename(arg).replace(/\.mjs$/, '');
22
+ const fileOf = (arg) => (isAbsolute(arg) ? arg : join(HERE, `${nameOf(arg)}.mjs`));
23
+
24
+ // Several hooks' stdout: one is passed as it is; JSON objects are merged (their messages joined), anything else joined by lines.
25
+ export function mergeOut(outs) {
26
+ const said = outs.filter((o) => o.trim());
27
+ if (said.length < 2) return said[0] ?? '';
28
+ const objs = said.map((o) => { try { const v = JSON.parse(o); return v && typeof v === 'object' && !Array.isArray(v) ? v : null; } catch { return null; } });
29
+ if (objs.some((o) => o === null)) return said.map((o) => o.replace(/\n$/, '')).join('\n') + '\n';
30
+ const out = {};
31
+ for (const o of objs) {
32
+ for (const [k, v] of Object.entries(o)) {
33
+ if (k === 'hookSpecificOutput' && out[k]) {
34
+ const ctx = [out[k].additionalContext, v.additionalContext].filter(Boolean).join('\n');
35
+ out[k] = { ...out[k], ...v, ...(ctx ? { additionalContext: ctx } : {}) };
36
+ } else if (typeof v === 'string' && typeof out[k] === 'string' && ['systemMessage', 'reason', 'stopReason'].includes(k)) out[k] = `${out[k]}\n${v}`;
37
+ else out[k] = v;
38
+ }
39
+ }
40
+ return JSON.stringify(out);
41
+ }
42
+
43
+ const capture = (stream, sink) => {
44
+ const write = stream.write;
45
+ stream.write = (chunk, ...rest) => { sink.push(String(chunk)); const cb = rest.find((r) => typeof r === 'function'); if (cb) cb(); return true; };
46
+ return () => { stream.write = write; };
47
+ };
48
+
49
+ // -> { code, stdout, stderr }. Each hook's own time is logged (its module's first import included).
50
+ export async function runAll(args, text, root = projectRoot()) {
51
+ let ev = {};
52
+ try { ev = JSON.parse(text) ?? {}; } catch { /* the hooks read it themselves */ }
53
+ const outs = [];
54
+ const err = [];
55
+ const judged = [];
56
+ let code = 0;
57
+ for (const arg of args) {
58
+ const name = nameOf(arg);
59
+ const out = [];
60
+ const before = err.length;
61
+ const t0 = performance.now();
62
+ const undo = [capture(process.stdout, out), capture(process.stderr, err)];
63
+ let c;
64
+ try {
65
+ const mod = await import(pathToFileURL(fileOf(arg)).href);
66
+ if (typeof mod.hook !== 'function') throw new Error(`${name} exports no hook(text)`);
67
+ c = await mod.hook(text);
68
+ } catch (e) {
69
+ c = 1;
70
+ err.push(`run-hooks: ${name} failed: ${e?.stack ?? e}\n`);
71
+ } finally {
72
+ for (const u of undo) u();
73
+ }
74
+ c = Number.isInteger(c) ? c : 0;
75
+ outs.push(out.join(''));
76
+ const ms = performance.now() - t0;
77
+ logHook(root, { event: ev.hook_event_name, hook: name, ms, code: c, tool: ev.tool_name ?? ev.source });
78
+ judged.push({ hook: name, ms, code: c, reason: err.slice(before).join(''), out: out.join('') });
79
+ if (c === 2) code = 2;
80
+ else if (c !== 0 && code === 0) code = c;
81
+ }
82
+ logCall(root, text, judged, appendRotated, performance.now());
83
+ return { code, stdout: mergeOut(outs), stderr: err.join('') };
84
+ }
85
+
86
+ async function selftest() {
87
+ const dir = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), 'run-hooks-'));
88
+ mkdirSync(join(dir, '.claude'));
89
+ const fake = (name, body) => { const f = join(dir, `${name}.mjs`); writeFileSync(f, `export function hook(text) {\n${body}\n}\n`); return f; };
90
+ const ok = fake('says-ok', "process.stdout.write('{\"systemMessage\":\"a\"}'); return 0;");
91
+ const no = fake('refuses', "process.stderr.write('BLOCKED by refuses\\n'); return JSON.parse(text).tool_name === 'Bash' ? 2 : 0;");
92
+ const also = fake('also-says', "console.log(JSON.stringify({ systemMessage: 'b' })); return 0;");
93
+ const boom = fake('throws', "throw new Error('planted');");
94
+ const bash = JSON.stringify({ hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'rm x' } });
95
+ const cases = [];
96
+ const r1 = await runAll([ok, no, also], bash, dir);
97
+ cases.push(['a refusal is the runner\'s exit 2', r1.code === 2]);
98
+ cases.push(['its message reaches stderr', r1.stderr.includes('BLOCKED by refuses')]);
99
+ cases.push(['two JSON outputs merge into one', JSON.parse(r1.stdout).systemMessage === 'a\nb']);
100
+ const r2 = await runAll([ok, no], JSON.stringify({ hook_event_name: 'PreToolUse', tool_name: 'Write' }), dir);
101
+ cases.push(['no refusal: exit 0', r2.code === 0 && r2.stdout === '{"systemMessage":"a"}']);
102
+ const r3 = await runAll([boom, ok], bash, dir);
103
+ cases.push(['a hook that throws is exit 1 and the next still runs', r3.code === 1 && r3.stderr.includes('throws failed') && r3.stdout.includes('"a"')]);
104
+ const rows = readHookLog(dir);
105
+ cases.push(['one timing line per hook per call', rows.length === 7 && rows.filter((r) => r.hook === 'refuses').map((r) => r.code).join() === '2,0']);
106
+ cases.push(['the line names the event and the tool', rows[0].event === 'PreToolUse' && rows[0].tool === 'Bash']);
107
+ const lines = (f) => readFileSync(join(dir, '.claude/logs', f), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
108
+ const refused = lines('refusals.jsonl');
109
+ cases.push(['every call is a line of events.jsonl', lines('events.jsonl').length === 3]);
110
+ cases.push(['a refusal keeps its reason and what was tried', refused[0].hook === 'refuses' && refused[0].reason.includes('BLOCKED by refuses') && refused[0].command === 'rm x']);
111
+ cases.push(['a crash is kept too', refused.some((r) => r.hook === 'throws' && r.code === 1)]);
112
+ rmSync(dir, { recursive: true, force: true });
113
+ const bad = cases.filter(([, good]) => !good);
114
+ for (const [label] of bad) console.error(`run-hooks selftest FAILED: ${label}`);
115
+ if (!bad.length) console.log(`run-hooks selftest: OK (${cases.length} cases)`);
116
+ return bad.length ? 1 : 0;
117
+ }
118
+
119
+ if (isMain(import.meta.url)) {
120
+ const args = process.argv.slice(2);
121
+ if (args.includes('--selftest')) process.exit(await selftest());
122
+ let text = '';
123
+ try { text = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
124
+ const root = projectRoot();
125
+ const r = await runAll(args, text, root);
126
+ let ev = {};
127
+ try { ev = JSON.parse(text) ?? {}; } catch { /* logged as '-' */ }
128
+ logHook(root, { event: ev.hook_event_name, hook: RUNNER, ms: performance.now(), code: r.code, tool: ev.tool_name ?? ev.source });
129
+ // written at once, then out: a hook may leave a timer or a child behind, and the event never waits on it
130
+ for (const [fd, s] of [[1, r.stdout], [2, r.stderr]]) if (s) try { writeSync(fd, s); } catch { /* a closed pipe */ }
131
+ process.exit(r.code);
132
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // secrets-guard (PreToolUse, Write|Edit|MultiEdit|NotebookEdit|Bash): a secret never lands in git (KWB-33; STEP 3: ".env
3
+ // never committed"). Refused, by every write route (lib/writes.mjs) and on `git add`:
4
+ // 1. staging a secret file: `git add` (with -f too) of a .env (.env.example, .sample, .template and .dist are fine), a key
5
+ // file (*.pem, *.key, *.p12, *.pfx, *.jks, *.keystore, id_rsa and its kin) or credentials.json; and `git add -A` / `.` /
6
+ // `--all` / a folder while such a file in the checkout is not ignored;
7
+ // 2. a write that ADDS a literal secret to any file but an ignored .env: a GitHub, Anthropic, OpenAI, AWS, Slack or Stripe
8
+ // key, a private key block, a connection string carrying its password (a ${VAR} or $VAR in its place is fine);
9
+ // 3. writing a .env that git does not ignore (add `.env` to .gitignore first).
10
+ // A token is named on the command that uses it ($PERSONAL_GITHUB_TOKEN), never written. Another project's files are not judged.
11
+ // node secrets-guard.mjs --selftest
12
+ import { spawnSync } from 'node:child_process';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { basename, join, relative, resolve } from 'node:path';
15
+ import { judgeEvent } from './lib/writes.mjs';
16
+ import { checkoutRoot } from './lib/project.mjs';
17
+ import { bashOf, git, runHook, scratchProject, verdicts, writeOf } from './lib/selftest.mjs';
18
+ import { gitAdds } from './lib/git-adds.mjs';
19
+ import { SECRETS } from './lib/secret-patterns.mjs';
20
+ import { asHook, isMain } from './lib/hook-log.mjs';
21
+
22
+ const ENV = /^\.env(\..+)?$/;
23
+ const ENV_OK = /\.(example|sample|template|dist)$/;
24
+ const KEY = /\.(pem|key|private|p12|pfx|jks|keystore)$|^id_(rsa|dsa|ecdsa|ed25519)$|^credentials\.json$/;
25
+ export const isSecretFile = (p) => { const b = basename(p); return (ENV.test(b) && !ENV_OK.test(b)) || KEY.test(b); };
26
+ const isEnv = (p) => ENV.test(basename(p)) && !ENV_OK.test(basename(p));
27
+
28
+ export { SECRETS };
29
+ const count = (text, re) => (text.match(re) ?? []).length;
30
+ export const addedSecret = (before, after) => SECRETS.find(([, re]) => count(after, re) > count(before ?? '', re))?.[0] ?? null;
31
+
32
+ function ignored(root, path) {
33
+ const r = spawnSync('git', ['check-ignore', '-q', relative(root, path)], { cwd: root });
34
+ if (r.status === 0 || r.status === 1) return r.status === 0;
35
+ const gi = join(root, '.gitignore'); // not a git repo: read .gitignore
36
+ return existsSync(gi) && readFileSync(gi, 'utf8').split('\n').some((l) => /^\/?(\.env\*?|\.env\.\*|\*\.env|\.env\/?)\s*$/.test(l.trim()) && ENV.test(basename(path)));
37
+ }
38
+
39
+ function judgeWrite(w) {
40
+ const root = checkoutRoot(w.path);
41
+ if (isEnv(w.path)) return ignored(root, w.path) ? null : `${relative(root, w.path)} is a .env file git does not ignore. Add \`.env\` (and \`.env.*\`, with !.env.example) to .gitignore first; a .env is never committed.`;
42
+ if (w.after === null) return null;
43
+ const what = addedSecret(w.before, w.after);
44
+ return what ? `this write puts ${what} into ${relative(root, w.path)}. A secret lives in an ignored .env (or the machine's own store) and is read at run time; code reads it by name (process.env.X), a command names it ($X). Take the value out.` : null;
45
+ }
46
+
47
+ // `git add` of a secret file by name, or a sweep (-A, ., --all, a folder) while one is not ignored.
48
+ function judgeAdd(command, cwd) {
49
+ for (const { dir, args: raw } of gitAdds(command, cwd)) {
50
+ const args = raw.filter((a) => !a.startsWith('-') || ['-A', '--all'].includes(a));
51
+ const named = args.find((a) => isSecretFile(a));
52
+ if (named) return `\`git add ${named}\`: a secret file is never staged (.env, keys, credentials). Keep it ignored; commit a .env.example with the names and no values.`;
53
+ if (args.some((a) => ['-A', '--all', '.', '*'].includes(a) || a.endsWith('/'))) {
54
+ const r = spawnSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd: dir, encoding: 'utf8' });
55
+ const loose = (r.stdout ?? '').split('\n').filter((f) => f && isSecretFile(f));
56
+ if (loose.length) return `this \`git add\` would stage ${loose.join(', ')}, a secret file git does not ignore. Add it to .gitignore first, then stage by path.`;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ const main = (text) => judgeEvent(text, {
63
+ name: 'secrets-guard',
64
+ command: judgeAdd,
65
+ write: (w) => (w.op === 'delete' ? null : judgeWrite(w)),
66
+ });
67
+
68
+ function selftest() {
69
+ const hook = new URL(import.meta.url).pathname;
70
+ const gh = `gh${'p'}_${'a1B2'.repeat(9)}`; // built here, so this file holds no token
71
+ const pem = `-----BEGIN RSA ${'PRIVATE'} KEY-----`;
72
+ const dir = scratchProject({ '.gitignore': '.env\n', 'src/app.ts': 'export const x = 1;\n', 'src/old.ts': `const t = '${gh}';\nconst x = 1;\n` });
73
+ git(dir, 'init', '-q');
74
+ const open = scratchProject({ 'src/app.ts': '\n' });
75
+ git(open, 'init', '-q');
76
+ const run = (d, payload) => runHook(hook, d, payload);
77
+ const cases = [
78
+ ['a GitHub token written into source', run(dir, writeOf(join(dir, 'src/app.ts'), `const t = '${gh}';\n`)), 2],
79
+ ['a private key written into a doc', run(dir, writeOf(join(dir, 'docs/key.md'), `${pem}\n${'MIIEvQIBADANBgkqhkiG9w0BAQEFAASC'.repeat(3)}\n`)), 2],
80
+ ['a bare header in a doc is a placeholder', run(dir, writeOf(join(dir, 'docs/format.md'), `${pem}\n...\n`)), 0],
81
+ ['a connection string with its password', run(dir, writeOf(join(dir, 'src/db.ts'), "const u = 'mongodb+srv://user:hunter22@c0.x.net/db';\n")), 2],
82
+ ['a connection string that names its password ($VAR)', run(dir, writeOf(join(dir, 'src/db.ts'), 'const u = `mongodb+srv://user:${process.env.PW}@c0.x.net/db`;\n')), 0],
83
+ ['code that reads the token by name', run(dir, writeOf(join(dir, 'src/app.ts'), 'const t = process.env.GITHUB_TOKEN;\n')), 0],
84
+ ['the token into an ignored .env', run(dir, writeOf(join(dir, '.env'), `GITHUB_TOKEN=${gh}\n`)), 0],
85
+ ['a .env git does not ignore', run(open, writeOf(join(open, '.env'), 'A=1\n')), 2],
86
+ ['a .env.example with names only', run(open, writeOf(join(open, '.env.example'), 'GITHUB_TOKEN=\n')), 0],
87
+ ['git add .env', run(dir, bashOf('git add .env src/app.ts', dir)), 2],
88
+ ['git add -f of a key file', run(dir, bashOf('git add -f deploy/server.pem', dir)), 2],
89
+ ['git add by path, no secret', run(dir, bashOf('git add src/app.ts', dir)), 0],
90
+ ['git add -A while a .env is not ignored', (spawnSync('touch', [join(open, '.env')]), run(open, bashOf('git add -A', open))), 2],
91
+ ['git add -A while the .env is ignored', (spawnSync('touch', [join(dir, '.env')]), run(dir, bashOf('git add -A', dir))), 0],
92
+ ['an edit beside a token already there adds none', run(dir, { tool_name: 'Edit', tool_input: { file_path: join(dir, 'src/old.ts'), old_string: 'x = 1', new_string: 'x = 2' } }), 0],
93
+ ['git -C <dir> add of a .env', run(dir, bashOf(`git -C ${dir} add .env`, dir)), 2],
94
+ ];
95
+ return verdicts('secrets-guard', cases);
96
+ }
97
+
98
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
99
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('secrets-guard', main));
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse (Write|Edit|MultiEdit|NotebookEdit|Bash): the shape law at write time, by every write route (lib/writes.mjs),
3
+ // in every folder; only design/shape.json "exclude" (generated files, vendored code) is left out. KWB-1: a hard-coded
4
+ // "slot 2" got past checks scoped to the engine folders. What a write may not ADD (what was there before is scan's job):
5
+ // - an instance literal of a concept (lib/shape.mjs, the number rule): a pattern hit, a concept word with a number or an
6
+ // ordinal, [N] on a per-instance array, `x == N` / `case N:` on a concept-named expression, a call that drives one
7
+ // instance by number, a hand-listed set (one line per instance), a role name before a concept word ("the PAD slot");
8
+ // allowed only as an example ("seen in:"), a listed selector, or a declared constant: "example, rule, or constant?";
9
+ // - in a test (under test/, tests/, probes/, or named *test* / *_probe*): the same, read as a test that drives one
10
+ // instance; the test loops over every instance or takes the instance as a parameter;
11
+ // - a selector for a concept (pickSlot, slotFor, firstSlot, ownerSlot) not listed in the manifest;
12
+ // - a new type that holds instances (an index member, or made N times in an array) whose concept has no row.
13
+ // No design/shape.json: nothing is declared, so nothing is judged (doctor fails a project of a configuration that needs one). `--selftest`.
14
+ import { readFileSync } from 'node:fs';
15
+ import { dirname, join } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { checkoutRoot, insideProject, relInCheckout, ROOT } from './lib/project.mjs';
18
+ import { addedFindings, findInstanceLiterals, formatFinding, isExcluded, isTestFile, logRefusal, readShape } from './lib/shape.mjs';
19
+ import { addedDefs, findSelectorDefs, findUnknownTypes } from './lib/shape-code.mjs';
20
+ import { bashOf, editOf, runHook, scratchProject, verdicts, writeOf } from './lib/selftest.mjs';
21
+ import { writesOf } from './lib/writes.mjs';
22
+ import { asHook, isMain } from './lib/hook-log.mjs';
23
+
24
+ const TEST_NOTE = 'a test that drives one instance proves that instance only: loop over every instance, or take the instance as a parameter';
25
+
26
+ function judgeText(rel, before, after, shape) {
27
+ const lits = addedFindings(before, after, (t) => findInstanceLiterals(t, rel, shape));
28
+ const test = isTestFile(rel);
29
+ const out = lits.map((f) => `${formatFinding(f, rel)}${test ? `\n ${TEST_NOTE}` : ''}`);
30
+ const concepts = lits.map((f) => f.concept);
31
+ for (const d of [...addedDefs(before, after, (t) => findSelectorDefs(t, rel, shape)), ...addedDefs(before, after, (t) => findUnknownTypes(t, rel, shape))]) {
32
+ out.push(formatFinding(d, rel));
33
+ concepts.push(d.concept ?? 'new-type');
34
+ }
35
+ return { out, concepts };
36
+ }
37
+
38
+ function main(text) {
39
+ let payload;
40
+ try { payload = JSON.parse(text); } catch { return 0; }
41
+ for (const w of writesOf(payload, payload.cwd ?? process.cwd())) {
42
+ if (w.after === null || w.op === 'delete' || !insideProject(w.path) || w.after.includes('\0')) continue;
43
+ const root = checkoutRoot(w.path);
44
+ const shape = readShape(root) ?? readShape(ROOT);
45
+ if (!shape || shape.error) continue;
46
+ const rel = relInCheckout(w.path);
47
+ if (rel.startsWith('..') || isExcluded(rel, shape)) continue;
48
+ const { out, concepts } = judgeText(rel, w.before, w.after, shape);
49
+ if (!out.length) continue;
50
+ logRefusal(ROOT, 'shape-law', concepts);
51
+ process.stderr.write(`BLOCKED by shape-law: this write names one instance of a concept that design/shape.json declares (KWB-1).\n${out.slice(0, 10).map((l) => ` ${l}`).join('\n')}${out.length > 10 ? `\n ... and ${out.length - 10} more` : ''}\n` +
52
+ 'Build for the shape: a loop over every instance or the generator; an instance seen in a bug goes on a "seen in:" line.\n');
53
+ return 2;
54
+ }
55
+ return 0;
56
+ }
57
+
58
+ const SELFTEST_SHAPE = {
59
+ concepts: {
60
+ slot: {
61
+ shape: 'many', count: 4, generator: 'slotId(i, field)', words: ['slot', 'slots'], patterns: ['\\bSlot::(amber|cyan|violet|green)\\b'],
62
+ arrays: ['kSlotNames'], selectors: { selectedSlot: 'the slot the player has open', firstEmpty: 'the first slot with nothing in it' },
63
+ constants: { initSlot: { instance: 1, reason: 'INIT plays one saw in slot 1' } },
64
+ },
65
+ mixer: { shape: 'one', id: 'mixer', words: ['mixer'] },
66
+ planet: { shape: 'many', generator: 'planetId(kind, field)', words: ['planet', 'planets'], names: ['DELAY', 'FILTER'] },
67
+ },
68
+ exclude: ['build/**', 'third_party/**'],
69
+ values: ['Cell'],
70
+ };
71
+
72
+ function selftest() {
73
+ const hook = fileURLToPath(import.meta.url);
74
+ const layout = { folders: { engines: { path: 'src/engines', purpose: 'engines' }, world: { path: 'src/world', purpose: 'world' }, ui: { path: 'src/ui', purpose: 'ui' }, tests: { path: 'tests', purpose: 'tests' }, docs: { path: 'docs', purpose: 'docs' } }, concepts: {} };
75
+ const dir = scratchProject({ 'design/shape.json': JSON.stringify(SELFTEST_SHAPE), 'src/world/Old.cpp': 'void f() { if (slot == 2) g(); }\n// PLACE\n' }, layout);
76
+ const bare = scratchProject({}, layout);
77
+ const w = (rel, text, at = dir) => runHook(hook, at, writeOf(join(at, rel), text));
78
+ const sh = (cmd) => runHook(hook, dir, bashOf(cmd, dir));
79
+ const loop = 'for (int slot = 0; slot < kSlots; ++slot) set(slotId(slot, "reverb.mix"), v);\n';
80
+ return verdicts('shape-law', [
81
+ // planted: an engine file
82
+ ['engine: slotIx == 1', w('src/engines/Engine.cpp', 'void f() { if (slotIx == 1) startCrew(); }\n'), 2],
83
+ ['engine: slots[1]', w('src/engines/Engine.cpp', 'auto& s = slots[1];\n'), 2],
84
+ ['engine: "slot2.reverb.mix"', w('src/engines/Bank.cpp', 'void f() { set("slot2.reverb.mix", v); }\n'), 2],
85
+ ['engine: Slot::cyan (a manifest pattern)', w('src/engines/Bank.cpp', 'auto c = Slot::cyan;\n'), 2],
86
+ ['engine: the one concept by number ("mixer2.bus")', w('src/engines/Bus.cpp', 'route("mixer2.bus");\n'), 2],
87
+ // planted: a world file and a UI file
88
+ ['world: s == 2 inside a loop over the slots', w('src/world/Space.cpp', 'for (int s = 0; s < kSlots; ++s) if (s == 2) addDelay(s);\n'), 2],
89
+ ['world: case 2 in a switch on the slot', w('src/world/Space.cpp', 'switch (slot) {\ncase 2: addDelay(); break;\n}\n'), 2],
90
+ ['ui: a hand-listed set, one line per slot', w('src/ui/View.cpp', 'slotGain.set(0, g);\nslotGain.set(1, g);\nslotGain.set(2, g);\nslotGain.set(3, g);\n'), 2],
91
+ ['ui: "the PAD slot"', w('src/ui/View.cpp', '// the PAD slot gets the arp\n'), 2],
92
+ ['ui: an unlisted name before a concept word ("the PAD planet", KWB-12)', w('src/ui/View.cpp', '// the PAD planet gets the arp\n'), 2],
93
+ ['ui: a caps concept word with no label before it, by number ("SLOTS 2", KWB-22)', w('src/ui/View.cpp', '// SLOTS 2 gets the arp\n'), 2],
94
+ ['ui: kSlotNames[2]', w('src/ui/View.cpp', 'draw(kSlotNames[2]);\n'), 2],
95
+ ['world: a selector not in the manifest', w('src/world/Pick.cpp', 'int pickSlot(int pad) {\n return pad % 4;\n}\n'), 2],
96
+ ['world: a new instance type with no row', w('src/world/Lane.h', 'struct Lane { int index; float gain; };\n'), 2],
97
+ ['world: a value-like type not in "values" (KWB-10)', w('src/world/Tex.h', 'struct Tex { int index; float u; };\n'), 2],
98
+ // planted: a test
99
+ ['test: drives slot 2 only', w('tests/slot_test.cpp', 'TEST(Slots, Plays) { Engine e; e.playSlot(2); EXPECT_TRUE(e.sounding()); }\n'), 2],
100
+ ['test: named for one instance', w('tests/play_test.cpp', 'TEST(Play, Slot2Sounds) { run(); }\n'), 2],
101
+ // planted: a doc
102
+ ['doc: "Drive slot 2"', w('docs/notes.md', 'Drive slot 2 with the pad.\n'), 2],
103
+ ['doc: "the second slot"', w('docs/notes.md', 'The second slot holds the pad.\n'), 2],
104
+ // a shell route
105
+ ['bash heredoc: slotIx == 1', sh("cat > src/engines/Engine.cpp <<'EOF'\nif (slotIx == 1) go();\nEOF"), 2],
106
+ ['bash sed -i adds a literal', sh("sed -i 's|// PLACE|if (slot == 3) h();|' src/world/Old.cpp"), 2],
107
+ // the allowed forms
108
+ ['allowed: the generator in a loop', w('src/engines/Bank.cpp', loop), 0],
109
+ ['allowed: a test that loops over every slot', w('tests/slot_test.cpp', `TEST(Slots, Plays) {\n ${loop}}\n`), 0],
110
+ ['allowed: a gate verdict before the word ("FAIL slot", KWB-15)', w('tools/gates/look.sh', 'for i in 0 1 2 3; do run "$i" || echo "FAIL slot $i"; done\n'), 0],
111
+ ['allowed: a seen in: line (doc)', w('docs/notes.md', 'seen in: slot 2, build for all slots\n'), 0],
112
+ ['allowed: a seen in: comment (code)', w('src/world/Space.cpp', '// seen in: slot 3 had no delay (B-1); every slot gets one\nfor (int s = 0; s < kSlots; ++s) addDelay(s);\n'), 0],
113
+ ['allowed: a listed selector', w('src/world/Pick.cpp', 'int selectedSlot() const {\n return open;\n}\nif (selectedSlot == 1) x();\n'), 0],
114
+ ['allowed: a declared constant', w('src/engines/Init.cpp', 'play(slotId(initSlot, "osc.wave")); // INIT: slot 1\n'), 0],
115
+ ['allowed: a type listed in "values" (KWB-10)', w('src/world/Cell.h', 'struct Cell { int index; float x; };\n'), 0],
116
+ ['allowed: a listed identity name ("the DELAY planet", KWB-12)', w('src/ui/Space.cpp', '// the DELAY planet sits by the FILTER planet\n'), 0],
117
+ ['allowed: a type of a known concept', w('src/world/Card.h', 'struct SlotCard { int index; };\n'), 0],
118
+ ['allowed: not instances (buffer[1], channel == 1)', w('src/engines/Mix.cpp', 'float s = buffer[1]; if (channel == 1) s = pos[0];\nif (slots == kSlots) full = true;\n'), 0],
119
+ ['allowed: a multi-word caps label\'s value ("AIM PLANETS 100", KWB-22)', w('src/ui/View.cpp', '// every planet off, AIM PLANETS 100: nothing heard\n'), 0],
120
+ ['allowed: a multi-word caps label before the word ("a NOTES MONO slot", KWB-22)', w('src/engines/Engine.cpp', '// a NOTES MONO slot plays only its newest note\n'), 0],
121
+ ['allowed: a number past the count is a value ("slot 40 ms", KWB-22)', w('docs/notes.md', 'Each slot 40 ms later than the last.\n'), 0],
122
+ ['allowed: the whole range "slots 1-4"', w('docs/notes.md', 'Slots 1-4 all play every note.\n'), 0],
123
+ ['allowed: a line already there, edited and moved (KWB-23)', runHook(hook, dir, editOf(join(dir, 'src/world/Old.cpp'), 'void f() { if (slot == 2) g(); }\n// PLACE', '// PLACE\nvoid f() { if (slot == 2) g(); } // closed: B-1')), 0],
124
+ ['an edit adding a second copy of a literal already there (KWB-23)', runHook(hook, dir, editOf(join(dir, 'src/world/Old.cpp'), '// PLACE', 'void h() { if (slot == 2) g(); }')), 2],
125
+ ['allowed: a literal already there, untouched', runHook(hook, dir, editOf(join(dir, 'src/world/Old.cpp'), '// PLACE', '// nothing')), 0],
126
+ ['allowed: excluded (build/)', w('build/gen/Table.cpp', 'auto& s = slots[1];\n'), 0],
127
+ ['allowed: no design/shape.json', w('src/engines/Engine.cpp', 'if (slotIx == 1) go();\n', bare), 0],
128
+ ['allowed: another project', runHook(hook, dir, writeOf(join(dirname(dir), 'elsewhere/src/x.cpp'), 'if (slotIx == 1) go();\n')), 0],
129
+ ]);
130
+ }
131
+
132
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
133
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('shape-law', main));
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // suite-once (PreToolUse, Bash): the whole test suite runs once, as the gate before landing, never while working (one
3
+ // agent ran all 425 tests on 1 core; KWB-41: a session ran the kit's suite again and again, 12 minutes each, on code that had not
4
+ // changed). A whole-suite command (`npm test`, `pnpm|yarn|bun test`, a bare `ctest`, the project's "commands".test in layout.json) is
5
+ // refused while the checkout it runs in has uncommitted tracked changes: uncommitted means still working, and working runs only the
6
+ // tests the change reaches (rule: test-only-what-changed; skill: pick-tests). Once the work is committed the gate passes. Named tests
7
+ // (`npm test -- a.test.mjs`, `ctest -R x`) are always allowed. `--selftest` proves it.
8
+ import { execFileSync } from 'node:child_process';
9
+ import { resolve } from 'node:path';
10
+ import { readLayout } from './lib/layout.mjs';
11
+ import { checkoutRoot, claudeRoot } from './lib/project.mjs';
12
+ import { bashOf, git, put, runHook, scratchProject, verdicts } from './lib/selftest.mjs';
13
+ import { judgeEvent } from './lib/writes.mjs';
14
+ import { asHook, isMain } from './lib/hook-log.mjs';
15
+
16
+ const WHOLE = /^(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test|ctest(?:\s+(?!-R\b|--tests-regex\b|-L\b|-I\b)\S+)*)$/;
17
+ const PREFIX = /^(?:[A-Za-z_]\w*=\S*\s+|(?:env|command|nohup|time|exec)\s+|timeout\s+\S+\s+)+/;
18
+ const REDIRECT = /\s*(?:\d?>>?|&>)&?\s*\S+/g;
19
+
20
+ const clean = (part) => part.trim().replace(PREFIX, '').replace(REDIRECT, '').trim();
21
+
22
+ function uncommitted(dir) {
23
+ try {
24
+ return execFileSync('git', ['-C', dir, '--no-optional-locks', 'status', '--porcelain', '--untracked-files=no'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).split('\n').filter((l) => l.trim());
25
+ } catch {
26
+ return []; // not a repository: nothing says the work is unfinished
27
+ }
28
+ }
29
+
30
+ export function refusal(command, cwd) {
31
+ const body = command.replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n\1\b/g, '').replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "''");
32
+ let here = cwd;
33
+ for (const part of body.split(/&&|\|\||;|\||\n/)) {
34
+ const cd = part.trim().match(/^cd\s+(\S+)$/);
35
+ if (cd) { here = resolve(here, cd[1]); continue; }
36
+ const words = clean(part);
37
+ const { commands = {} } = readLayout(claudeRoot(here));
38
+ if (!WHOLE.test(words) && words !== commands.test) continue;
39
+ const dirty = uncommitted(checkoutRoot(resolve(here, 'x')));
40
+ if (!dirty.length) continue;
41
+ return `\`${words}\` runs the whole suite, and this checkout has uncommitted work:\n ${dirty.slice(0, 8).join('\n ')}\n` +
42
+ `The whole suite runs once, as the gate before landing, never while working. Run the tests this change reaches${commands['test-changed'] ? ` (\`${commands['test-changed']}\`)` : ' (the pick-tests skill)'}; commit; then run the whole suite once.\n`;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ const main = (text) => judgeEvent(text, { name: 'suite-once', command: refusal });
48
+
49
+ function selftest() {
50
+ const hook = new URL(import.meta.url).pathname;
51
+ const dir = scratchProject({ 'a.txt': 'one\n', '.claude/layout.json': JSON.stringify({ commands: { test: 'bash tools/all.sh', 'test-changed': 'npm run test:changed' } }) });
52
+ git(dir, 'init', '-q');
53
+ git(dir, 'add', 'a.txt', '.claude/layout.json');
54
+ git(dir, 'commit', '-qm', 'x');
55
+ const sh = (cmd) => runHook(hook, dir, bashOf(cmd, dir));
56
+ const committed = [
57
+ ['the gate, every change committed', sh('npm test'), 0],
58
+ ['the project\'s own whole-suite command, committed', sh('bash tools/all.sh'), 0],
59
+ ];
60
+ put(`${dir}/a.txt`, 'two\n');
61
+ const said = runHook(hook, dir, bashOf('npm test', dir));
62
+ return verdicts('suite-once', [
63
+ ...committed,
64
+ ['npm test on uncommitted work', said, 2],
65
+ ['pnpm, a run form and a prefix', sh('CI=1 timeout 300 pnpm run test'), 2],
66
+ ['a redirect and a pipe change nothing', sh('npm test > /tmp/log 2>&1 | tail -3'), 2],
67
+ ['a bare ctest', sh('ctest --output-on-failure'), 2],
68
+ ['the project\'s own whole-suite command', sh('bash tools/all.sh'), 2],
69
+ ['after other steps', sh('git status && npm test'), 2],
70
+ ['named tests', sh('npm test -- a.test.mjs'), 0],
71
+ ['the changed-only run', sh('npm run test:changed'), 0],
72
+ ['ctest of one pattern', sh("ctest -R 'svf'"), 0],
73
+ ['words about the suite', sh("echo 'npm test'"), 0],
74
+ ['a heredoc body', sh("cat <<'EOF'\nnpm test\nEOF"), 0],
75
+ ['another project', sh('cd /tmp && npm test'), 0],
76
+ ]);
77
+ }
78
+
79
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
80
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('suite-once', main));