@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,174 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse (Write|Edit|MultiEdit|NotebookEdit|Bash): code this project did not write is read-only, to every
3
+ // route that writes (WB-69): a tool, or a shell command that writes, copies, moves or deletes there.
4
+ // - Vendored and generated code (JUCE, third_party, _deps, build output) and the project's own read-only paths
5
+ // (.claude/layout.json "protected"). Agents "fixing" a bug inside JUCE is a reported
6
+ // failure mode; fix our code or wrap the library instead.
7
+ // - COPY TREES: every layout folder whose "kind" is "copy" (another project's code, copied unchanged and proved, from
8
+ // the copy-and-prove skill). Let through: git, and the project's sanctioned copy tools, run as `python3 <tool>`:
9
+ // .claude/layout.json "guards": { "copy": { "tools": ["tools/copy_x.py", ...] } }. A new copy tree is guarded the
10
+ // moment its folder is declared with "kind": "copy".
11
+ // One law, read-only paths: the separate copy-tree guard (copy-tree-guard.py) is merged in here.
12
+ // A path in an agent worktree (.claude/worktrees/<name>/...) is judged as the same path in the project.
13
+ // A nested clone under build/_deps is vendored code too (Sub37 WB-2: checkoutRoot counts only this repo's worktrees).
14
+ // `--selftest` plants a write into each kind of read-only path and lets the allowed ones through.
15
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath, pathToFileURL } from 'node:url';
19
+ import { matchesAny } from './lib/glob.mjs';
20
+ import { foldersOfKind, readLayout } from './lib/layout.mjs';
21
+ import * as project from './lib/project.mjs';
22
+ import { editOf, git, put, runHook, scratchProject, verdicts } from './lib/selftest.mjs';
23
+ import { writesOf } from './lib/writes.mjs';
24
+ import { asHook, isMain } from './lib/hook-log.mjs';
25
+
26
+ const DEFAULT_GLOBS = ['JUCE/**', '**/JUCE/modules/**', 'JuceLibraryCode/**', 'libs/**', 'third_party/**', 'external/**', '_deps/**'];
27
+ // The build output; a project whose build lives elsewhere names it in layout.json "buildOutput" (a build in out/, while build/
28
+ // holds the report and the pictures, which agents write). Any nested clone under build/_deps stays vendored either way.
29
+ const buildGlobs = (layout) => [...(layout.buildOutput ?? ['build/**']), 'build/_deps/**'];
30
+ // What makes a write to this file wrong, or null. `p` is the project library (lib/project.mjs).
31
+ // layout.json "writeOnce" globs (KWB-21): evidence a command leaves; a new file may be added, an existing one is never changed,
32
+ // moved or deleted, so what a gate recorded stays what it recorded.
33
+ function judge(p, file, base, op = 'write') {
34
+ const abs = resolve(base, file);
35
+ if (!p.insideProject(abs)) return null;
36
+ const rel = p.relInCheckout(abs).replace(/^\.claude\/worktrees\/[^/]+\//, '');
37
+ const layout = readLayout(p.claudeRoot(abs));
38
+ const tree = foldersOfKind(layout, 'copy').find((t) => rel === t || rel.startsWith(`${t}/`));
39
+ if (tree) return { rel, tree, layout };
40
+ if (matchesAny(rel, [...DEFAULT_GLOBS, ...buildGlobs(layout), ...(layout.protected ?? [])])) return { rel, layout };
41
+ if (matchesAny(rel, layout.writeOnce ?? []) && (op === 'delete' || existsSync(abs))) return { rel, layout, once: true };
42
+ return null;
43
+ }
44
+
45
+ // Every file the call writes, by any route (lib/writes.mjs): Write, Edit, MultiEdit, NotebookEdit, and the
46
+ // shell's >, >>, tee, sed -i, perl -i, cp, mv (its source too), ln, rm, truncate, touch, dd of=, a heredoc.
47
+ export function verdict(payload, p = project) {
48
+ for (const w of writesOf(payload, payload.cwd ?? p.ROOT)) {
49
+ const hit = judge(p, w.path, p.ROOT, w.op);
50
+ if (hit) return { ...hit, why: payload.tool_name === 'Bash' ? `the command writes ${hit.rel}` : hit.rel };
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function message({ why, tree, layout, once }) {
56
+ if (once) {
57
+ return `BLOCKED by protect-vendor: ${why}, which already exists under a "writeOnce" path of .claude/layout.json: evidence is\n` +
58
+ 'written once and never changed, moved or deleted. Write the new run to a new file name beside it.\n';
59
+ }
60
+ if (!tree) {
61
+ return `BLOCKED by protect-vendor: ${why} is vendored/generated code and is read-only.\n` +
62
+ 'Fix our own code, wrap the library, or ask the user if the library itself must change.\n';
63
+ }
64
+ const tools = layout.guards?.copy?.tools ?? [];
65
+ return `BLOCKED by protect-vendor: ${why}, in the copy tree ${tree}. A copy tree (a folder of kind "copy" in .claude/layout.json) is read-only:\n` +
66
+ `its files come only from the project's copy tools${tools.length ? ` (${tools.map((t) => `python3 ${t}`).join(', ')})` : ''}, each with its origin line and licence row.\n` +
67
+ 'Fix the code that uses the copy. If the copy itself must change, that is the user\'s to say, then a pinned change under null tests (.claude/rules/copy-trees-read-only.md).\n';
68
+ }
69
+
70
+ function main(text) {
71
+ let payload;
72
+ try {
73
+ payload = JSON.parse(text);
74
+ } catch {
75
+ return 0;
76
+ }
77
+ const v = verdict(payload);
78
+ if (!v) return 0;
79
+ process.stderr.write(message(v));
80
+ return 2;
81
+ }
82
+
83
+ async function selftest() {
84
+ const root = realpathSync(mkdtempSync(join(tmpdir(), 'pv-')));
85
+ mkdirSync(join(root, '.claude'));
86
+ writeFileSync(join(root, '.claude/layout.json'), JSON.stringify({
87
+ folders: { a: { path: 'lib/engines/src/tabula', kind: 'copy' }, b: { path: 'lib/app/tabula', kind: 'copy' }, c: { path: 'lib/effects/src/tabula', kind: 'copy' }, d: { path: 'lib/app' } },
88
+ protected: ['design/reference/**'],
89
+ guards: { copy: { tools: ['tools/copy_tabula.py', 'tools/copy_reimport.py', 'tools/copy_promote.py'] } },
90
+ }));
91
+ const saved = process.env.CLAUDE_PROJECT_DIR;
92
+ process.env.CLAUDE_PROJECT_DIR = root;
93
+ const p = await import(`${pathToFileURL(join(import.meta.dirname, 'lib', 'project.mjs')).href}?selftest`);
94
+ process.env.CLAUDE_PROJECT_DIR = saved;
95
+ const W = (f) => ({ tool_name: 'Write', tool_input: { file_path: join(root, f) } });
96
+ const E = (f) => ({ tool_name: 'Edit', tool_input: { file_path: join(root, f), old_string: 'a', new_string: 'b' } });
97
+ const N = (f) => ({ tool_name: 'NotebookEdit', tool_input: { notebook_path: join(root, f) } });
98
+ const B = (c) => ({ tool_name: 'Bash', tool_input: { command: c } });
99
+ const refused = [
100
+ W('lib/app/tabula/tabula/src/engine/PianoBridge.cpp'), E('lib/engines/src/tabula/tabula/src/engine/Voice.h'), W('lib/effects/src/tabula/x/New.h'),
101
+ W('.claude/worktrees/agent-x/lib/app/tabula/tabula/tools/p.cpp'), N('lib/app/tabula/n.ipynb'), W('JUCE/modules/juce_core/x.h'), E('design/reference/look.md'),
102
+ B("sed -i 's/0.5f/0.6f/' lib/app/tabula/tabula/src/engine/PianoBridge.cpp"), B('echo x > lib/engines/src/tabula/tabula/src/util/Dev.h'),
103
+ B('cp /tmp/x.h lib/app/tabula/tabula/src/engine/'), B("cd lib && perl -pi -e 's/a/b/' engines/src/tabula/dspblocks/include/dsp/Drive.h"), B('mv lib/app/tabula/a.h lib/app/src/a.h'), B("cat > build/x.h <<'EOF'\nx\nEOF"),
104
+ B('rm lib/app/tabula/tabula/tools/piano_prebuild.cpp'), B('cat a | tee -a lib/effects/src/tabula/x.h'),
105
+ B('python3 tools/my_copy_tabula.py; rm lib/app/tabula/tabula/tools/piano_prebuild.cpp'), B('python3 out/copy_promote.py && cp x lib/app/tabula/y.h'),
106
+ B('dd if=/dev/zero of=lib/app/tabula/z.bin bs=1 count=1'),
107
+ ];
108
+ const passed = [
109
+ W('lib/app/src/pianos.h'), E('lib/engines/src/engines.cpp'), W('lib/app/tabulax/y.h'), W('/elsewhere/lib/app/tabula/x.h'),
110
+ B('python3 tools/copy_tabula.py'), B('python3 tools/copy_reimport.py --no-proofs'), B('python3 tools/copy_promote.py lib/engines/src/tabula/tabula/src/engine/FireSaw.h --proof true'),
111
+ B('grep -rn loadFrom lib/app/tabula > out/grep.txt'), B('cp lib/app/tabula/tabula/src/engine/PianoBridge.h /tmp/x.h'),
112
+ B('git rm -q lib/app/tabula/tabula/tools/old.cpp'), B('cat lib/engines/src/tabula/tabula/src/engine/Voice.h 2>&1 | head'),
113
+ B('cmake --build build -j8 > out/log.txt'), B('cd /elsewhere/other && rm lib/app/tabula/x.h'), B("echo 'rm lib/app/tabula/x.h'"),
114
+ ];
115
+ const cwd = process.cwd();
116
+ process.chdir(root); // a hook runs in the project's folder
117
+ const bad = [...refused.filter((x) => !verdict(x, p)), ...passed.filter((x) => verdict(x, p))];
118
+ process.chdir(cwd);
119
+ for (const x of bad) console.error(`protect-vendor selftest FAILED on ${JSON.stringify(x.tool_input)}`);
120
+ rmSync(root, { recursive: true, force: true });
121
+ console.log(`protect-vendor routes: ${refused.length} writes into read-only paths refused, ${passed.length} others let through: ${bad.length ? 'FAIL' : 'PASS'}`);
122
+ return bad.length ? 1 : 0;
123
+ }
124
+
125
+ // A git project with FetchContent's JUCE clone under build/_deps and an agent worktree.
126
+ function selftestRepo() {
127
+ const hook = fileURLToPath(import.meta.url);
128
+ const dir = scratchProject({ 'src/dsp/Svf.cpp': '// svf\n', 'design/ref.md': 'x\n' }, { folders: {}, concepts: {}, protected: ['design/**'] });
129
+ git(dir, 'init', '-q');
130
+ git(dir, 'add', 'src', '.claude');
131
+ git(dir, 'commit', '-qm', 'init');
132
+ git(dir, 'worktree', 'add', '-q', '.claude/worktrees/agent-x', '-b', 'work/x');
133
+ put(join(dir, 'build/_deps/juce-src/modules/juce_core/juce_core.h'), '// juce\n');
134
+ git(join(dir, 'build/_deps/juce-src'), 'init', '-q');
135
+ const wt = join(dir, '.claude/worktrees/agent-x');
136
+ const e = (file) => runHook(hook, dir, editOf(join(dir, file)));
137
+ return verdicts('protect-vendor', [
138
+ ['our own source', e('src/dsp/Svf.cpp'), 0],
139
+ ['JUCE modules', e('JUCE/modules/juce_core/juce_core.h'), 2],
140
+ ['a nested clone under build/_deps (Sub37 WB-2)', e('build/_deps/juce-src/modules/juce_core/juce_core.h'), 2],
141
+ ['a layout "protected" glob', e('design/ref.md'), 2],
142
+ ['a worktree file is judged in the worktree', runHook(hook, dir, editOf(join(wt, 'src/dsp/Svf.cpp'))), 0],
143
+ ['a worktree\'s vendored folder', runHook(hook, dir, editOf(join(wt, 'third_party/lib/lib.h'))), 2],
144
+ ['another project is not ours', runHook(hook, dir, editOf(join(dirname(dir), 'elsewhere/JUCE/x.h'))), 0],
145
+ ['build/ is the build output by default', e('build/report.md'), 2],
146
+ ]) | selftestBuildOutput(hook);
147
+ }
148
+
149
+ // A layout that names its build output as out/ may write build/ (a report, pictures); out/ and build/_deps stay read-only.
150
+ function selftestBuildOutput(hook) {
151
+ const dir = scratchProject({ 'src/a.cpp': '//\n', 'build/evidence/s1/run.txt': 'measured\n' },
152
+ { folders: {}, concepts: {}, buildOutput: ['out/**'], writeOnce: ['build/evidence/**'] });
153
+ git(dir, 'init', '-q');
154
+ const e = (file) => runHook(hook, dir, editOf(join(dir, file)));
155
+ const w = (file) => runHook(hook, dir, { tool_name: 'Write', tool_input: { file_path: join(dir, file), content: 'x\n' } });
156
+ const b = (command) => runHook(hook, dir, { tool_name: 'Bash', tool_input: { command }, cwd: dir });
157
+ return verdicts('protect-vendor buildOutput', [
158
+ ['build/for-you.md under buildOutput out/**', e('build/for-you.md'), 0],
159
+ ['out/ under buildOutput out/**', e('out/lib/x.o'), 2],
160
+ ['build/_deps stays vendored', e('build/_deps/juce-src/x.h'), 2],
161
+ // KWB-21: evidence is written once
162
+ ['a new evidence file by Write', w('build/evidence/s2/new.txt'), 0],
163
+ ['a new evidence file by cp (KWB-21)', b('cp /tmp/sweep.txt build/evidence/s2/sweep.txt'), 0],
164
+ ['a probe writing new evidence', b('probe --bank > build/evidence/s2/bank.txt'), 0],
165
+ ['an existing evidence file overwritten', w('build/evidence/s1/run.txt'), 2],
166
+ ['an existing evidence file edited', e('build/evidence/s1/run.txt'), 2],
167
+ ['an existing evidence file appended', b('echo x >> build/evidence/s1/run.txt'), 2],
168
+ ['an evidence file deleted', b('rm build/evidence/s1/run.txt'), 2],
169
+ ['an evidence file moved away', b('mv build/evidence/s1/run.txt /tmp/'), 2],
170
+ ]);
171
+ }
172
+
173
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
174
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? ((await selftest()) | selftestRepo()) : await asHook('protect-vendor', main));
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // Spec lock: while a build runs, the spec it builds from cannot change under it.
3
+ // State: .claude/spec-lock.json { "locked": true, "globs": [...], "reason": "...", "since": "..." }
4
+ // node spec-lock.mjs lock "<reason>" [glob ...] lock (Claude may do this when a build starts)
5
+ // node spec-lock.mjs unlock unlock (the USER runs this, e.g. `! node ...`)
6
+ // node spec-lock.mjs status
7
+ // node spec-lock.mjs --selftest proves the hook
8
+ // As a PreToolUse hook (Write|Edit|MultiEdit|NotebookEdit|Bash) it blocks writes to locked files by any route
9
+ // (lib/writes.mjs: a tool, or the shell's >, >>, tee, sed -i, cp, mv, a heredoc), writes to the lock file itself,
10
+ // and Claude running "unlock". Words inside a heredoc body or quotes are text, never the unlock command.
11
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { join, relative, resolve } from 'node:path';
13
+ import { matchesAny } from './lib/glob.mjs';
14
+ import { commandLeavesProject, insideProject, checkoutRoot, relInCheckout } from './lib/project.mjs';
15
+ import { writesOf } from './lib/writes.mjs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { bashOf, runHook, scratchProject, verdicts, writeOf } from './lib/selftest.mjs';
18
+ import { asHook, isMain } from './lib/hook-log.mjs';
19
+
20
+ const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
21
+ const STATE = join(ROOT, '.claude', 'spec-lock.json');
22
+ const DEFAULT_GLOBS = ['spec/**', 'specs/**', 'docs/stellix-*.md', '**/*-spec.md', '**/*.mmd', '**/panel.json', '**/build.md', '**/decisions*.md'];
23
+ const WRITE_CMD = /(>|>>|\btee\b|\bsed\s+-i|\bperl\s+-i|\bmv\b|\bcp\b|\brm\b|\btruncate\b|\bgit\s+(checkout|restore)\b)/;
24
+
25
+ const read = () => (existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : { locked: false });
26
+ const write = (s) => writeFileSync(STATE, JSON.stringify(s, null, 2) + '\n');
27
+
28
+ function cli(args) {
29
+ const [cmd, reason, ...globs] = args;
30
+ if (cmd === 'lock') {
31
+ write({ locked: true, globs: globs.length ? globs : DEFAULT_GLOBS, reason: reason ?? 'build in progress', since: new Date().toISOString() });
32
+ console.log('Spec locked.');
33
+ } else if (cmd === 'unlock') {
34
+ write({ ...read(), locked: false });
35
+ console.log('Spec unlocked.');
36
+ } else console.log(JSON.stringify(read(), null, 2));
37
+ return 0;
38
+ }
39
+
40
+ function block(msg) {
41
+ process.stderr.write(`BLOCKED by spec-lock: ${msg}\n`);
42
+ return 2;
43
+ }
44
+
45
+ function main(text) {
46
+ let payload;
47
+ try {
48
+ payload = JSON.parse(text);
49
+ } catch {
50
+ return 0;
51
+ }
52
+ const state = read();
53
+ const input = payload.tool_input ?? {};
54
+ const howTo = 'To change the spec, stop and ask the user. Only the user unlocks: `! node .claude/hooks/spec-lock.mjs unlock`.';
55
+
56
+ if (payload.tool_name === 'Bash') {
57
+ const cmd = input.command ?? '';
58
+ const bare = cmd.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?:\n|$)/g, '\n').replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "''");
59
+ if (/spec-lock/.test(bare) && /\bunlock\b/.test(bare)) return block(`Claude may not unlock the spec. ${howTo}`);
60
+ if (/spec-lock\.json/.test(bare) && WRITE_CMD.test(bare)) return block(`the lock file can't be edited. ${howTo}`);
61
+ const routed = judgeWrites(payload, state, howTo);
62
+ if (routed) return routed;
63
+ if (!state.locked || commandLeavesProject(cmd)) return 0;
64
+ // Each word as a path in its own checkout, so a worktree's spec/ is the spec too.
65
+ const base = payload.cwd ?? ROOT;
66
+ const toRel = (t) => {
67
+ const abs = resolve(base, t.replace(/^['"]|['"]$/g, ''));
68
+ return insideProject(abs) ? relInCheckout(abs) : null;
69
+ };
70
+ const touched = cmd.split(/\s+/).filter((t) => {
71
+ const rel = t && toRel(t);
72
+ return rel && matchesAny(rel, state.globs);
73
+ });
74
+ if (touched.length && WRITE_CMD.test(cmd)) return block(`${touched.join(', ')} locked (${state.reason}). ${howTo}`);
75
+ return 0;
76
+ }
77
+
78
+ return judgeWrites(payload, state, howTo);
79
+ }
80
+
81
+ // Every file the call writes, whichever tool: the lock file never, a locked spec file not while locked.
82
+ function judgeWrites(payload, state, howTo) {
83
+ for (const w of writesOf(payload)) {
84
+ if (!insideProject(w.path)) continue;
85
+ const rel = relative(checkoutRoot(w.path), w.path);
86
+ if (rel === join('.claude', 'spec-lock.json')) return block(`the lock file can't be edited. ${howTo}`);
87
+ if (state.locked && matchesAny(rel, state.globs)) return block(`${rel} is locked since ${state.since} (${state.reason}). ${howTo}`);
88
+ }
89
+ return 0;
90
+ }
91
+
92
+ function selftest() {
93
+ const hook = fileURLToPath(import.meta.url);
94
+ const state = (locked) => JSON.stringify({ locked, globs: DEFAULT_GLOBS, reason: 'build running', since: 'now' });
95
+ const dir = scratchProject({ '.claude/spec-lock.json': state(true), 'spec/plan.md': 'x\n' });
96
+ const open = scratchProject({ '.claude/spec-lock.json': state(false) });
97
+ const w = (root, file) => runHook(hook, root, writeOf(join(root, file)));
98
+ const sh = (root, cmd) => runHook(hook, root, bashOf(cmd, root));
99
+ return verdicts('spec-lock', [
100
+ ['a locked spec file', w(dir, 'spec/plan.md'), 2],
101
+ ['a panel.json anywhere', w(dir, 'panel/panel.json'), 2],
102
+ ['code while the spec is locked', w(dir, 'src/dsp/Svf.cpp'), 0],
103
+ ['the lock file', w(dir, '.claude/spec-lock.json'), 2],
104
+ ['Claude running unlock', sh(dir, 'node .claude/hooks/spec-lock.mjs unlock'), 2],
105
+ ['a shell edit of the spec', sh(dir, 'sed -i s/a/b/ spec/plan.md'), 2],
106
+ ['reading the spec', sh(dir, 'cat spec/plan.md'), 0],
107
+ ['the word unlock in a heredoc is text', sh(dir, "cat > notes.txt <<'EOF'\nnode .claude/hooks/spec-lock.mjs unlock\nEOF"), 0],
108
+ ['a heredoc into the spec', sh(dir, "cat > spec/plan.md <<'EOF'\nnew\nEOF"), 2],
109
+ ['an unlocked spec', w(open, 'spec/plan.md'), 0],
110
+ ]);
111
+ }
112
+
113
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
114
+ if (isMain(import.meta.url)) {
115
+ const args = process.argv.slice(2);
116
+ process.exit(args.includes('--selftest') ? selftest() : args.length ? cli(args) : await asHook('spec-lock', main));
117
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse (Bash): stage files by name, never everything at once, and never secrets.
3
+ // `git add -A`, `git add .`, `git add --all` and `git commit -a` sweep build output, renders,
4
+ // scratch files and keys into history. Exit 2 refuses the command. `--selftest` proves it.
5
+ import { readFileSync } from 'node:fs';
6
+ import { verdicts } from './lib/selftest.mjs';
7
+ import { asHook, isMain } from './lib/hook-log.mjs';
8
+
9
+ const SECRET = /(^|[\s/])(\.env(\.[\w.-]+)?|[\w.-]+\.(pem|key|p12|pfx)|id_(rsa|ed25519)|credentials\.json)(\s|$)/;
10
+ const SAFE_ENV = /\.env\.(example|sample|template)(\s|$)/;
11
+
12
+ export function problem(command) {
13
+ for (const raw of command.split(/&&|\|\||;|\n/)) {
14
+ const seg = raw.trim();
15
+ const add = seg.match(/^git\s+(?:-C\s+\S+\s+)?add\s+(.*)$/);
16
+ if (add) {
17
+ const args = add[1];
18
+ if (/(^|\s)(-A|--all|-u|--update)(\s|$)/.test(args) || /(^|\s)(\.|\*|:\/)(\s|$)/.test(args)) {
19
+ return 'stage the files you changed by name (git add path/one path/two), never everything at once';
20
+ }
21
+ if (SECRET.test(` ${args} `) && !SAFE_ENV.test(` ${args} `)) return 'that looks like a secret or key file; it must never be committed';
22
+ }
23
+ if (/^git\s+(?:-C\s+\S+\s+)?commit\b/.test(seg) && /(^|\s)(-a|--all|-am|-a\w*m)(\s|$)/.test(seg)) {
24
+ return 'commit -a stages every tracked change; stage by name first, then commit';
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function main(text) {
31
+ let payload;
32
+ try { payload = JSON.parse(text); } catch { return 0; }
33
+ const why = problem(payload.tool_input?.command ?? '');
34
+ if (!why) return 0;
35
+ process.stderr.write(`BLOCKED by stage-guard: ${why}.\nRun \`git status --short\` and add each intended file explicitly.\n`);
36
+ return 2;
37
+ }
38
+
39
+ function selftest() {
40
+ const cases = [
41
+ ['git add -A', true], ['git add .', true], ['cd x && git add --all', true], ['git commit -am "wip"', true],
42
+ ['git add .env', true], ['git add certs/server.key', true],
43
+ ['git add src/dsp/Svf.cpp tests/svf_test.cpp', false], ['git add .env.example', false],
44
+ ['git commit -m "fix: svf"', false], ['git status', false],
45
+ ];
46
+ return verdicts('stage-guard', cases.map(([cmd, block]) => [cmd, problem(cmd) !== null, block]));
47
+ }
48
+
49
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
50
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('stage-guard', main));
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ // time-guard: nothing is slow without someone being told (after a night of two-hour panel exports that
3
+ // every agent, and the session watching them, took for normal).
4
+ //
5
+ // PreToolUse + PostToolUse on every tool, for the session and every agent it starts. After a call it tells the caller
6
+ // (exit 2 on PostToolUse: the call has run, nothing is blocked, the words reach the agent) when:
7
+ // 1. THE CALL ITSELF TOOK LONG: more than SLOW_CALL_MIN minutes (a question to the user never counts);
8
+ // 2. SOMETHING ON THE MACHINE IS SLOW, whoever started it (looked at once a minute at most): a panel export older than
9
+ // EXPORT_MIN, more than CHROME_MAX headless Chromes or one older than CHROME_MIN, a build older than BUILD_MIN, a ctest
10
+ // run older than CTEST_MIN, a probe older than PROBE_MIN (guards.time.probePattern says what a probe is), a wait
11
+ // loop older than WAIT_LOOP_MIN;
12
+ // 3. AN AGENT'S WORKTREE HAS NOT MOVED: work unsaved for SAVE_MIN, no commit for QUIET_MIN, or two agents in one;
13
+ // 4. THE CONTROLLER sees every worktree holding work unsaved for long, every SCAN_MIN minutes;
14
+ // and before a call it REFUSES a wait that can never end: `until ! pgrep -f "<words>"`, whose pgrep finds the loop's own
15
+ // command line (a heredoc body is text, not a loop). A finding is said again only after REPEAT_MIN minutes.
16
+ // Project values: .claude/layout.json "guards": { "time": { "probePattern", "saveCommand" } }.
17
+ // node time-guard.mjs --selftest
18
+ import { execFileSync, spawn } from 'node:child_process';
19
+ import { mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
20
+ import { homedir, tmpdir } from 'node:os';
21
+ import { basename, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { readLayout } from './lib/layout.mjs';
24
+ import { asHook, isMain } from './lib/hook-log.mjs';
25
+
26
+ export const DEFAULTS = {
27
+ probePattern: '\\S*/\\w*(?:Probe|_probe)\\b', // the probe binary itself, never a shell that names one
28
+ saveCommand: 'git add <paths> && git commit -m "<note>" -- <paths>',
29
+ };
30
+ const CFG = { ...DEFAULTS };
31
+ const SLOW_CALL_MIN = 10, EXPORT_MIN = 5, CHROME_MAX = 6, CHROME_MIN = 5, BUILD_MIN = 30, CTEST_MIN = 45, PROBE_MIN = 20;
32
+ const QUIET_MIN = 60, SAVE_MIN = 30, SCAN_MIN = 15, WAIT_LOOP_MIN = 60, REPEAT_MIN = 10, LOOK_EVERY_S = 60;
33
+
34
+ const projectRoot = resolve(process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replace(/\/\.claude\/worktrees\/[^/]+(\/.*)?$/, '');
35
+ const STATE = join(process.env.TMPDIR ?? tmpdir(), `time-guard-${basename(projectRoot).replace(/[^\w.-]/g, '-') || 'project'}`);
36
+ const sh = (cmd, args, cwd) => { try { return execFileSync(cmd, args, { cwd, encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }); } catch { return null; } };
37
+
38
+ export function etimeS(s) {
39
+ const t = s.trim();
40
+ const dash = t.lastIndexOf('-');
41
+ const days = dash >= 0 ? Number(t.slice(0, dash)) : 0;
42
+ const parts = (dash >= 0 ? t.slice(dash + 1) : t).split(':').map(Number);
43
+ while (parts.length < 3) parts.unshift(0);
44
+ return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
45
+ }
46
+
47
+ function processes() {
48
+ const out = sh('ps', ['-axo', 'pid=,etime=,command=']) ?? '';
49
+ return out.split('\n').map((l) => l.match(/^\s*(\d+)\s+(\S+)\s+(.*)$/)).filter(Boolean).map((m) => [Number(m[1]), etimeS(m[2]), m[3]]);
50
+ }
51
+
52
+ export function machineFindings(rows) {
53
+ const f = [];
54
+ const mins = (s) => s / 60;
55
+ const headless = rows.filter((r) => /Google Chrome(?: for Testing)?(?:\.app\/Contents\/MacOS\/\S+)?\s.*--headless/.test(r[2]) && !r[2].includes('Helper'));
56
+ if (headless.length > CHROME_MAX) {
57
+ f.push(['chrome-count', `${headless.length} headless Chromes are running at once (more than ${CHROME_MAX}): something is starting one per picture again. ` +
58
+ 'Find it (ps -axo pid,etime,command | grep -- --headless) and stop it; Chrome is started only by kit/synthpanel-creator/src/onechrome.js, for a whole batch']);
59
+ }
60
+ for (const [pid, age] of headless) {
61
+ if (mins(age) > CHROME_MIN) f.push([`chrome-old-${pid}`, `a headless Chrome (pid ${pid}) has run ${mins(age).toFixed(0)} min: it is stuck or leaked. Find who started it and why it never finished; stop it by its pid`]);
62
+ }
63
+ const probe = new RegExp(`^(?:${CFG.probePattern})`);
64
+ for (const [pid, age, cmd] of rows) {
65
+ const m = mins(age).toFixed(0);
66
+ if (/node \S*synthpanel\S*\.js export-native/.test(cmd) && mins(age) > EXPORT_MIN) {
67
+ f.push([`export-${pid}`, `a panel export (pid ${pid}) has run ${m} min; one takes seconds since B-382 (one Chrome, a cache of every picture). It is stuck: find why before waiting longer`]);
68
+ } else if (/^\S*ctest\b/.test(cmd) && mins(age) > CTEST_MIN) {
69
+ f.push([`ctest-${pid}`, `a ctest run (pid ${pid}) has run ${m} min (more than ${CTEST_MIN}): find the slow test (ctest prints each as it ends) instead of waiting for all of them`]);
70
+ } else if (/^\S*(?:cmake\s+--build|\/make|make|ninja)\b/.test(cmd) && mins(age) > BUILD_MIN) { // the build itself, not a shell naming it
71
+ f.push([`build-${pid}`, `a build (pid ${pid}) has run ${m} min (more than ${BUILD_MIN}): look at the load (uptime) and what else is building`]);
72
+ } else if (probe.test(cmd) && mins(age) > PROBE_MIN) {
73
+ f.push([`probe-${pid}`, `a probe (pid ${pid}: ${cmd.split(/\s+/)[0].split('/').pop()}) has run ${m} min (more than ${PROBE_MIN}): every probe has a time limit; find what it is waiting on`]);
74
+ } else if (/\buntil\b.*;\s*do\s+sleep\b/.test(cmd) && mins(age) > WAIT_LOOP_MIN) {
75
+ f.push([`waitloop-${pid}`, `a wait loop (pid ${pid}) has run ${m} min: whatever it waits for is finished or dead. Stop it (kill ${pid}); wait on a background command's own notification instead of a loop`]);
76
+ }
77
+ }
78
+ return f;
79
+ }
80
+
81
+ function lastCommitMin(cwd) {
82
+ const out = sh('git', ['log', '-1', '--format=%ct'], cwd)?.trim();
83
+ return out ? (Date.now() / 1000 - Number(out)) / 60 : null;
84
+ }
85
+
86
+ // Minutes since the OLDEST change not yet committed in a worktree (build folders left out); null when it is clean.
87
+ function unsavedMin(wt) {
88
+ const out = sh('git', ['--no-optional-locks', '-C', wt, 'status', '--porcelain', '--untracked-files=all']);
89
+ if (out === null) return null;
90
+ let oldest = null;
91
+ for (const line of out.split('\n')) {
92
+ const f = line.slice(3).trim().replace(/^"|"$/g, '');
93
+ if (!f || /^(build|listen\/|screenshots\/)/.test(f) || f.includes('/build')) continue;
94
+ try { const t = statSync(join(wt, f)).mtimeMs / 1000; oldest = oldest === null ? t : Math.min(oldest, t); } catch { /* gone */ }
95
+ }
96
+ return oldest === null ? null : (Date.now() / 1000 - oldest) / 60;
97
+ }
98
+
99
+ function listWorktrees() {
100
+ const root = join(projectRoot, '.claude', 'worktrees');
101
+ try { return readdirSync(root).sort().map((d) => join(root, d)).filter((d) => { try { statSync(join(d, '.git')); return true; } catch { return false; } }); } catch { return []; }
102
+ }
103
+
104
+ // Is this agent still running, by the cap hook's count? Unknown state counts as running (the safe side).
105
+ const CLAIM_LAPSE_MIN = 15;
106
+
107
+ function agentRunning(aid) {
108
+ let st;
109
+ try { st = JSON.parse(readFileSync(join(homedir(), '.claude/agent-cap-state.json'), 'utf8')); } catch { return true; }
110
+ const sessions = Object.values(st).filter((s) => s && typeof s === 'object');
111
+ if (sessions.some((s) => aid in (s.running ?? {}))) return true;
112
+ return !sessions.some((s) => aid in (s.known ?? {}));
113
+ }
114
+
115
+ function sayOnce(key, msg, now, st) {
116
+ if (now - (st[key] ?? 0) < REPEAT_MIN * 60) return null;
117
+ st[key] = now;
118
+ return msg;
119
+ }
120
+
121
+ export function run(event, { now = Date.now() / 1000, rows = null, commitMin = null, state = null, worktrees = null, late = null } = {}) {
122
+ const st = state ?? loadState();
123
+ const said = [];
124
+ const tid = event.tool_use_id ?? '';
125
+ const input = event.tool_input ?? {};
126
+ if (event.hook_event_name === 'PreToolUse') {
127
+ let cmd = event.tool_name === 'Bash' ? String(input.command ?? '') : '';
128
+ cmd = cmd.replace(/<<-?\s*['"]?(\w+)['"]?[^\n]*\n[\s\S]*?\n\s*\1\s*(?:\n|$)/g, '\n'); // a heredoc body is text, not a loop
129
+ if (/until\s+!\s*pgrep\s+-[a-z]*f\s+(['"])(?!\[)/.test(cmd)) {
130
+ return ['REFUSED: `until ! pgrep -f "..."` never ends: pgrep -f finds the loop\'s own command line, which holds the same words. Run the work ' +
131
+ "with run_in_background and wait for its notification; or, if you must poll, make the pattern unable to match itself: pgrep -f '[T]abulaFaceProbe --x'"];
132
+ }
133
+ if (tid) (st.starts ??= {})[tid] = now;
134
+ if (!state) saveState(st);
135
+ return [];
136
+ }
137
+ // 1. the call itself
138
+ const started = tid ? st.starts?.[tid] : undefined;
139
+ if (tid && st.starts) delete st.starts[tid];
140
+ if (started !== undefined && event.tool_name !== 'AskUserQuestion' && (now - started) / 60 > SLOW_CALL_MIN) {
141
+ said.push(`this call took ${((now - started) / 60).toFixed(0)} min (more than ${SLOW_CALL_MIN}): \`${String(input.command ?? event.tool_name ?? '').slice(0, 80)}\`. Find what made it slow before running it again; never wait on it as normal`);
142
+ }
143
+ // 2. the machine, once a minute at most
144
+ if (now - (st.looked ?? 0) >= LOOK_EVERY_S || rows !== null) {
145
+ st.looked = now;
146
+ for (const [key, msg] of machineFindings(rows ?? processes())) { const m = sayOnce(key, msg, now, st); if (m) said.push(m); }
147
+ }
148
+ // 3. each agent's own worktree, learned from the commands it runs (.claude/worktrees/<name>)
149
+ const who = event.agent_id ?? 'main';
150
+ if (who !== 'main') (st.seen ??= {})[who] = now; // when each agent last ran anything: a finished agent's claim lapses (below)
151
+ const text = `${input.command ?? ''} ${input.file_path ?? ''}`;
152
+ for (const m of text.matchAll(/(\/[^\s'"]*?\/\.claude\/worktrees\/([\w.-]+))/g)) {
153
+ if (who === 'main') continue;
154
+ st.owner ??= {};
155
+ let prev = st.owner[m[2]];
156
+ // a claim lapses with its agent: known stopped, or quiet for CLAIM_LAPSE_MIN (the agent-cap state that said "stopped" is no
157
+ // longer written, so without the quiet rule a finished agent kept its worktree for ever and no successor could reuse its build)
158
+ if (prev && prev !== who && (!agentRunning(prev) || now - (st.seen?.[prev] ?? 0) > CLAIM_LAPSE_MIN * 60)) prev = null;
159
+ if (prev && prev !== who) {
160
+ const s = sayOnce(`shared-${m[2]}`, `the worktree .claude/worktrees/${m[2]} is already used by another agent (${prev}). Each agent works only in its own: ` +
161
+ 'make yours (git worktree add --detach .claude/worktrees/<yours> main) and move your files there', now, st);
162
+ if (s) said.push(s);
163
+ } else {
164
+ st.owner[m[2]] = who;
165
+ (st.wt ??= {})[who] = m[1];
166
+ }
167
+ }
168
+ const mine = who !== 'main' ? st.wt?.[who] : null;
169
+ // its git status at most once a minute (KWB-24: two git commands on every call of every agent)
170
+ if (mine && (rows !== null || now - (st.checked?.[mine] ?? 0) >= LOOK_EVERY_S)) {
171
+ if (rows === null) (st.checked ??= {})[mine] = now;
172
+ const u = rows === null ? unsavedMin(mine) : null; // a selftest case: never git on a made-up path
173
+ const cmn = commitMin ?? lastCommitMin(mine);
174
+ if (u !== null && u > SAVE_MIN) {
175
+ const s = sayOnce(`unsaved-${mine}`, `your worktree (${mine.split('/').pop()}) holds work unsaved for ${u.toFixed(0)} min (more than ${SAVE_MIN}). Save it now: ${CFG.saveCommand}`, now, st);
176
+ if (s) said.push(s);
177
+ } else if (cmn !== null && cmn > QUIET_MIN) {
178
+ const s = sayOnce(`quiet-${mine}`, `your worktree's last commit was ${cmn.toFixed(0)} min ago (more than ${QUIET_MIN}). If you are waiting on something slow, find why it is slow; if the work moved, save it`, now, st);
179
+ if (s) said.push(s);
180
+ }
181
+ }
182
+ // 4. the controller sees every worktree: a git status in each runs in its own process, never in the hook (KWB-24: a synth's
183
+ // 125 worktrees took over a minute, the hook was killed at its timeout before it saved `scanned`, so every call scanned again)
184
+ const scan = worktrees ?? (rows !== null ? {} : null);
185
+ const tell = (list) => list.length && said.push(`worktrees holding work unsaved for more than ${SAVE_MIN} min: ${list.join(', ')}. Ask their agents to save (${CFG.saveCommand}), or find why they stall`);
186
+ if (who === 'main' && (now - (st.scanned ?? 0) >= SCAN_MIN * 60 || scan !== null)) {
187
+ st.scanned = now;
188
+ if (scan) tell(lateOf(scan));
189
+ else { saveState(st); scanLater(); }
190
+ }
191
+ const found = late ?? (rows === null ? readLate() : null);
192
+ if (who === 'main' && found && found.at > (st.lateTold ?? 0)) { st.lateTold = found.at; tell(found.list); }
193
+ st.starts = Object.fromEntries(Object.entries(st.starts ?? {}).filter(([, v]) => now - v < 86400));
194
+ if (!state) saveState(st);
195
+ return said;
196
+ }
197
+
198
+ const lateOf = (unsaved) => Object.entries(unsaved).filter(([, u]) => u !== null && u > SAVE_MIN).map(([wt, u]) => `${wt.split('/').pop()} (${u.toFixed(0)} min)`);
199
+ const LATE = join(STATE, 'late.json');
200
+ function readLate() { try { return JSON.parse(readFileSync(LATE, 'utf8')); } catch { return null; } }
201
+ function scanLater() { try { spawn(process.execPath, [fileURLToPath(import.meta.url), '--scan-worktrees'], { cwd: projectRoot, detached: true, stdio: 'ignore', env: process.env }).unref(); } catch { /* next time */ } }
202
+ function scanNow() {
203
+ const list = lateOf(Object.fromEntries(listWorktrees().map((wt) => [wt, unsavedMin(wt)])));
204
+ try { mkdirSync(STATE, { recursive: true }); writeFileSync(`${LATE}.${process.pid}`, JSON.stringify({ at: Date.now() / 1000, list })); renameSync(`${LATE}.${process.pid}`, LATE); } catch { /* next time */ }
205
+ return 0;
206
+ }
207
+
208
+ function loadState() { try { return JSON.parse(readFileSync(join(STATE, 'state.json'), 'utf8')); } catch { return {}; } }
209
+ function saveState(st) {
210
+ try {
211
+ mkdirSync(STATE, { recursive: true });
212
+ const tmp = join(STATE, `state.${process.pid}.json`);
213
+ writeFileSync(tmp, JSON.stringify(st));
214
+ renameSync(tmp, join(STATE, 'state.json'));
215
+ } catch { /* the guard never stops work by failing itself */ }
216
+ }
217
+
218
+ function selftest() {
219
+ let bad = 0;
220
+ const expect = (name, said, want) => { if ((said.length > 0) !== want) { bad += 1; console.error(`FAIL ${name}: ${said.length ? 'said' : 'quiet'}`); } };
221
+ const G = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --headless=new --disable-gpu';
222
+ const post = (extra = {}) => ({ hook_event_name: 'PostToolUse', ...extra });
223
+ const at = (rows, extra = {}) => ({ now: 5000, state: {}, rows, commitMin: 1, ...extra });
224
+ const st = {};
225
+ run({ hook_event_name: 'PreToolUse', tool_use_id: 'a' }, { now: 1000, state: st, rows: [], commitMin: 1 });
226
+ expect('a call of 12 min', run(post({ tool_use_id: 'a', tool_input: { command: 'bash tools/build-panel.sh' } }), { now: 1720, state: st, rows: [], commitMin: 1 }), true);
227
+ run({ hook_event_name: 'PreToolUse', tool_use_id: 'b' }, { now: 2000, state: st, rows: [], commitMin: 1 });
228
+ expect('a call of 2 min', run(post({ tool_use_id: 'b' }), { now: 2120, state: st, rows: [], commitMin: 1 }), false);
229
+ const exp = 'node /x/kit/synthpanel-creator/bin/synthpanel.js export-native design/export-spec.json';
230
+ expect('an export of 7 min', run(post(), at([[1, 420, exp]])), true);
231
+ expect('an export of 30 s', run(post(), at([[1, 30, exp]])), false);
232
+ expect('12 headless Chromes', run(post(), at(Array.from({ length: 12 }, (_, i) => [i, 10, G]))), true);
233
+ expect('2 headless Chromes of 10 s', run(post(), at([[0, 10, G], [1, 10, G]])), false);
234
+ expect('a headless Chrome of a day', run(post(), at([[9, 93600, G]])), true);
235
+ expect('a ctest of an hour', run(post(), at([[3, 3600, 'ctest --test-dir build -j4']])), true);
236
+ expect('a probe of 25 min', run(post(), at([[4, 1500, '/x/build/SynthFaceProbe_artefacts/RelWithDebInfo/SynthFaceProbe --press-all']])), true);
237
+ const A = (command, who = 'a1') => post({ agent_id: who, tool_input: { command } });
238
+ expect("an agent's worktree quiet for 90 min", run(A('cd /x/.claude/worktrees/k2 && make'), at([], { commitMin: 90 })), true);
239
+ const s2 = {};
240
+ run(A('cd /x/.claude/worktrees/k2 && make'), { now: 5000, state: s2, rows: [], commitMin: 90 });
241
+ expect('the same said again a minute later', run(A('ls'), { now: 5060, state: s2, rows: [], commitMin: 90 }), false);
242
+ const s3 = {};
243
+ run(A('cd /x/.claude/worktrees/onechrome && ls', 'main-ish'), { now: 5000, state: s3, rows: [], commitMin: 1 });
244
+ expect('a second agent in the same worktree', run(A('cd /x/.claude/worktrees/onechrome && vi', 'a2'), { now: 5001, state: s3, rows: [], commitMin: 1 }), true);
245
+ const s4 = {};
246
+ run(A('cd /x/.claude/worktrees/handed && ls', 'first'), { now: 5000, state: s4, rows: [], commitMin: 1 });
247
+ expect('a successor after the first agent has been quiet 16 min takes the worktree', run(A('cd /x/.claude/worktrees/handed && ls', 'next'), { now: 5000 + 16 * 60, state: s4, rows: [], commitMin: 1 }), false);
248
+ expect('and the first agent coming back finds it taken', run(A('cd /x/.claude/worktrees/handed && ls', 'first'), { now: 5000 + 17 * 60, state: s4, rows: [], commitMin: 1 }), true);
249
+ expect('the controller told of a worktree unsaved for 45 min', run(post(), at([], { worktrees: { '/x/.claude/worktrees/guide': 45 } })), true);
250
+ expect('the controller, nothing unsaved', run(post(), at([], { worktrees: { '/x/.claude/worktrees/guide': 5 } })), false);
251
+ const told = {};
252
+ const found = { at: 4000, list: ['guide (45 min)'] };
253
+ expect('the background scan\'s finding is told', run(post(), at([], { state: told, late: found })), true);
254
+ expect('and told once', run(post(), { ...at([], { state: told, late: found }), now: 5100 }), false);
255
+ const pre = (command) => run({ hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command } }, { now: 5000, state: {} });
256
+ const loop = ['until', '!', 'pgrep', '-f', '"cmake --build"', '>/dev/null;', 'do', 'sleep', '5;', 'done'].join(' ');
257
+ expect('the endless wait loop inside a heredoc (text for a program) passes', pre(`python3 - <<'EOF'\nprint('${loop}')\nEOF\necho ok`), false);
258
+ expect('the same loop after a heredoc is still refused', pre(`cat > a.txt <<EOF\nx\nEOF\n${loop}`), true);
259
+ expect('REFUSED: the endless wait loop', pre(loop), true);
260
+ expect('a pgrep that cannot match itself passes', pre(loop.replace('"cmake --build"', "'[c]make --build'")), false);
261
+ expect('a shell that names a build is not a build', run(post(), at([[8, 3600, '/bin/zsh -c cmake --build build -j6 && ctest']])), false);
262
+ expect('a build of 40 min', run(post(), at([[8, 2400, 'cmake --build build -j6']])), true);
263
+ expect('a shell that names a probe is not a probe', run(post(), at([[9, 3000, '/bin/zsh -c ./build/SynthFaceProbe_artefacts/RelWithDebInfo/SynthFaceProbe --walk x']])), false);
264
+ expect('a quiet machine', run(post(), at([[5, 4000, '/bin/zsh'], [6, 20, 'cmake --build build -j4']], { commitMin: 5, worktrees: {} })), false);
265
+ const wait = "/bin/zsh -c eval 'until grep -q exit /tmp/x.log; do sleep 15; done'";
266
+ expect('a wait loop of a day', run(post(), at([[7, 90000, wait]])), true);
267
+ expect('a wait loop of 5 min', run(post(), at([[7, 300, wait]])), false);
268
+ expect('a question answered after 12 min is not a slow call (WB-22)', run(post({ tool_name: 'AskUserQuestion', tool_use_id: 'q1' }), { now: 5720, state: { starts: { q1: 5000 } }, rows: [] }), false);
269
+ expect('a Bash call of 12 min still is', run(post({ tool_name: 'Bash', tool_use_id: 'b1', tool_input: { command: 'x' } }), { now: 5720, state: { starts: { b1: 5000 } }, rows: [] }), true);
270
+ expect('a snake-case probe of 25 min', run(post(), at([[4, 1500, '/x/out/probes/engines/synth_engines_probe --only FIRESAW']])), true);
271
+ CFG.probePattern = '\\S*/bench_\\w+'; // a project's own probe pattern (guards.time.probePattern)
272
+ expect("a project's own probe pattern: its bench of 25 min", run(post(), at([[4, 1500, '/x/out/bench_voices --all']])), true);
273
+ expect("a project's own probe pattern: another program is no probe", run(post(), at([[4, 1500, '/x/out/synth_engines_probe']])), false);
274
+ Object.assign(CFG, DEFAULTS);
275
+ if (etimeS('1-02:03:04') !== 93784 || etimeS('05:06') !== 306) { bad += 1; console.error('FAIL ps etime'); }
276
+ console.log(`time-guard selftest: ${bad ? `${bad} wrong` : 'OK'}`);
277
+ return bad ? 1 : 0;
278
+ }
279
+
280
+ function main(text) {
281
+ let event;
282
+ try { event = JSON.parse(text); } catch { return 0; }
283
+ Object.assign(CFG, DEFAULTS, readLayout(projectRoot).guards?.time ?? {});
284
+ let said;
285
+ try { said = run(event); } catch { return 0; } // the guard never stops work by failing itself
286
+ if (!said.length) return 0;
287
+ process.stderr.write(`time-guard: ${said.join('\n time-guard: ')}\n`);
288
+ return 2;
289
+ }
290
+
291
+ export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
292
+ if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : process.argv.includes('--scan-worktrees') ? scanNow() : await asHook('time-guard', main));