@thedecipherist/claude-packs 0.0.0-stage → 1.1.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.
- package/CHANGELOG.md +754 -0
- package/LICENSE +21 -0
- package/README.md +573 -2
- package/bin/claude-packs.mjs +212 -0
- package/configurations/base/config.json +85 -0
- package/configurations/cpp/config.json +93 -0
- package/configurations/full-stack/config.json +153 -0
- package/configurations/go/config.json +98 -0
- package/configurations/node/config.json +102 -0
- package/configurations/python/config.json +99 -0
- package/configurations/rust/config.json +106 -0
- package/configurations/synth-creator/config.json +148 -0
- package/global/hooks/gnu-shell.mjs +131 -0
- package/global/hooks/lib/shell-writes.mjs +2 -0
- package/global/hooks/shell-traps.mjs +79 -0
- package/global/hooks/version-pin-guard.mjs +280 -0
- package/global/skills/latest-versions/SKILL.md +49 -0
- package/lib/backup.mjs +34 -0
- package/lib/build-queue.mjs +22 -0
- package/lib/claude-md.mjs +60 -0
- package/lib/config-cli.mjs +36 -0
- package/lib/configs.mjs +145 -0
- package/lib/coverage.mjs +94 -0
- package/lib/custom-packs.mjs +93 -0
- package/lib/doctor.mjs +126 -0
- package/lib/fix-plan.mjs +72 -0
- package/lib/found-folders.mjs +25 -0
- package/lib/git-tokens.mjs +64 -0
- package/lib/global.mjs +125 -0
- package/lib/hook-proof.mjs +45 -0
- package/lib/hook-timing.mjs +42 -0
- package/lib/hover-gate.mjs +29 -0
- package/lib/kit-command.mjs +14 -0
- package/lib/kit-lock.mjs +149 -0
- package/lib/layout.mjs +99 -0
- package/lib/logs.mjs +109 -0
- package/lib/manifest.mjs +24 -0
- package/lib/new.mjs +49 -0
- package/lib/once-only-report.mjs +54 -0
- package/lib/pack-manifest.mjs +20 -0
- package/lib/packs.mjs +109 -0
- package/lib/planner.mjs +59 -0
- package/lib/repeats.mjs +65 -0
- package/lib/repo-weight.mjs +28 -0
- package/lib/rule-tests.mjs +42 -0
- package/lib/rules.mjs +40 -0
- package/lib/scan-detect.mjs +130 -0
- package/lib/scan.mjs +171 -0
- package/lib/selftest-cache.mjs +39 -0
- package/lib/settings.mjs +138 -0
- package/lib/shape-cli.mjs +76 -0
- package/lib/shape.mjs +4 -0
- package/lib/suggest.mjs +31 -0
- package/lib/switches.mjs +44 -0
- package/lib/symmetry-doctor.mjs +26 -0
- package/lib/sync.mjs +133 -0
- package/lib/workorder.mjs +26 -0
- package/package.json +34 -4
- package/packs/ai/checks/boundary-computes-its-own-hash.json +30 -0
- package/packs/ai/checks/model-call-needs-boundary.json +29 -0
- package/packs/ai/checks/model-id-from-config.json +28 -0
- package/packs/ai/checks/model-output-validated.json +26 -0
- package/packs/ai/pack.json +38 -0
- package/packs/ai/rules/ai-input-sha-boundary.md +31 -0
- package/packs/ai/rules/llm-pipeline-security.md +26 -0
- package/packs/audio-core/checks/denormals-not-in-prepare.json +12 -0
- package/packs/audio-core/checks/dsp-without-apvts.json +12 -0
- package/packs/audio-core/checks/float-phase-or-rate.json +12 -0
- package/packs/audio-core/checks/juce-snap-to-zero-off.json +12 -0
- package/packs/audio-core/checks/keep-werror.json +12 -0
- package/packs/audio-core/checks/no-fast-math.json +14 -0
- package/packs/audio-core/checks/no-locale-change.json +12 -0
- package/packs/audio-core/checks/no-march-native.json +14 -0
- package/packs/audio-core/clang-tidy +28 -0
- package/packs/audio-core/hooks/lib/realtime-checks.mjs +56 -0
- package/packs/audio-core/hooks/param-id-lock.mjs +172 -0
- package/packs/audio-core/hooks/realtime-guard.mjs +67 -0
- package/packs/audio-core/pack.json +192 -0
- package/packs/audio-core/rules/a-control-is-proved-through-its-input.md +26 -0
- package/packs/audio-core/rules/audio-thread-realtime-safe.md +27 -0
- package/packs/audio-core/rules/audio-ui-thread-handoff.md +21 -0
- package/packs/audio-core/rules/boundary-tests-cross-the-boundary.md +16 -0
- package/packs/audio-core/rules/build-flags-safe.md +20 -0
- package/packs/audio-core/rules/cached-parameter-pointers.md +28 -0
- package/packs/audio-core/rules/clang-tidy-core-guidelines.md +21 -0
- package/packs/audio-core/rules/control-reaches-every-surface.md +23 -0
- package/packs/audio-core/rules/cpu-worst-case-per-block.md +28 -0
- package/packs/audio-core/rules/dc-block-after-asymmetry.md +22 -0
- package/packs/audio-core/rules/discrete-params-snap.md +19 -0
- package/packs/audio-core/rules/double-where-precision-matters.md +20 -0
- package/packs/audio-core/rules/filters-stable-under-modulation.md +29 -0
- package/packs/audio-core/rules/host-parameters-at-scale.md +24 -0
- package/packs/audio-core/rules/house-control-behaviours.md +34 -0
- package/packs/audio-core/rules/math-domain-guards.md +19 -0
- package/packs/audio-core/rules/modulated-delay-interpolation.md +22 -0
- package/packs/audio-core/rules/multiband-phase-coherent.md +22 -0
- package/packs/audio-core/rules/nonlinear-stages-antialiased.md +28 -0
- package/packs/audio-core/rules/note-ledger-and-pedals.md +22 -0
- package/packs/audio-core/rules/one-formula-per-value.md +20 -0
- package/packs/audio-core/rules/optimisations-are-null-tested.md +23 -0
- package/packs/audio-core/rules/oscillators-bandlimited.md +23 -0
- package/packs/audio-core/rules/output-limiter.md +23 -0
- package/packs/audio-core/rules/parameter-ids-never-change.md +20 -0
- package/packs/audio-core/rules/params-and-state-thread-safe.md +19 -0
- package/packs/audio-core/rules/params-snapshot-per-block.md +21 -0
- package/packs/audio-core/rules/patch-excludes-performance-controls.md +18 -0
- package/packs/audio-core/rules/per-sample-code-inlinable.md +22 -0
- package/packs/audio-core/rules/prepare-is-repeatable.md +23 -0
- package/packs/audio-core/rules/recursive-state-nan-guard.md +20 -0
- package/packs/audio-core/rules/reverb-loop-gain-bounded.md +24 -0
- package/packs/audio-core/rules/sample-accurate-midi.md +22 -0
- package/packs/audio-core/rules/sample-rate-and-block-size-from-host.md +23 -0
- package/packs/audio-core/rules/scoped-no-denormals.md +26 -0
- package/packs/audio-core/rules/smooth-continuous-parameters.md +22 -0
- package/packs/audio-core/rules/state-load-versioned-and-safe.md +22 -0
- package/packs/audio-core/rules/straight-on-and-aligned.md +21 -0
- package/packs/audio-core/rules/ui-matches-reference-look.md +23 -0
- package/packs/audio-core/rules/undo-one-gesture-one-step.md +21 -0
- package/packs/audio-core/rules/voice-steal-fades.md +21 -0
- package/packs/audio-core/rules/voice-sum-headroom.md +20 -0
- package/packs/audio-core/skills/analyse-reference-look/SKILL.md +35 -0
- package/packs/audio-core/skills/copy-and-prove/SKILL.md +98 -0
- package/packs/audio-core/skills/juce-realtime-audit/SKILL.md +22 -0
- package/packs/audio-core/skills/measure-tone/SKILL.md +20 -0
- package/packs/audio-core/skills/new-panel-control/SKILL.md +15 -0
- package/packs/audio-core/skills/prove-control/SKILL.md +25 -0
- package/packs/audio-core/skills/trace-parameter/SKILL.md +29 -0
- package/packs/audio-core/tools/lib/spectrum.mjs +112 -0
- package/packs/audio-core/tools/lib/wav.mjs +41 -0
- package/packs/audio-core/tools/tone-report.mjs +47 -0
- package/packs/base/checks/field-map-over-if-runs.json +25 -0
- package/packs/base/checks/lookup-over-if-chains.json +25 -0
- package/packs/base/checks/lookup-over-ternary-chains.json +20 -0
- package/packs/base/checks/no-code-after-midline-comment.json +12 -0
- package/packs/base/checks/one-walk-per-template.json +23 -0
- package/packs/base/checks/stub-reports-success.json +31 -0
- package/packs/base/checks/test-skip-on-failed-setup.json +17 -0
- package/packs/base/hooks/brief-guard.mjs +96 -0
- package/packs/base/hooks/code-shape.mjs +99 -0
- package/packs/base/hooks/dispatch-once.mjs +124 -0
- package/packs/base/hooks/file-size-guard.mjs +61 -0
- package/packs/base/hooks/folder-law.mjs +77 -0
- package/packs/base/hooks/git-guard.mjs +202 -0
- package/packs/base/hooks/lesson-checks.mjs +182 -0
- package/packs/base/hooks/lib/apply-edit.mjs +20 -0
- package/packs/base/hooks/lib/code-shapes.mjs +142 -0
- package/packs/base/hooks/lib/content.mjs +14 -0
- package/packs/base/hooks/lib/cpp-scan.mjs +94 -0
- package/packs/base/hooks/lib/crash-policy.mjs +47 -0
- package/packs/base/hooks/lib/dup-index.mjs +204 -0
- package/packs/base/hooks/lib/dup.mjs +127 -0
- package/packs/base/hooks/lib/elements.mjs +87 -0
- package/packs/base/hooks/lib/event-log.mjs +68 -0
- package/packs/base/hooks/lib/function-shapes.mjs +90 -0
- package/packs/base/hooks/lib/git-adds.mjs +21 -0
- package/packs/base/hooks/lib/git-auth.mjs +44 -0
- package/packs/base/hooks/lib/git-guard-selftest.mjs +192 -0
- package/packs/base/hooks/lib/glob.mjs +33 -0
- package/packs/base/hooks/lib/hook-log.mjs +100 -0
- package/packs/base/hooks/lib/house.py +77 -0
- package/packs/base/hooks/lib/languages.mjs +69 -0
- package/packs/base/hooks/lib/layout.mjs +58 -0
- package/packs/base/hooks/lib/making-check-selftest.mjs +112 -0
- package/packs/base/hooks/lib/near-copies.mjs +76 -0
- package/packs/base/hooks/lib/program-read.mjs +199 -0
- package/packs/base/hooks/lib/program-writes.mjs +177 -0
- package/packs/base/hooks/lib/project.mjs +81 -0
- package/packs/base/hooks/lib/prose.mjs +11 -0
- package/packs/base/hooks/lib/pushed.mjs +201 -0
- package/packs/base/hooks/lib/repo-index.mjs +52 -0
- package/packs/base/hooks/lib/rust-scan.mjs +139 -0
- package/packs/base/hooks/lib/secret-patterns.mjs +18 -0
- package/packs/base/hooks/lib/selftest.mjs +45 -0
- package/packs/base/hooks/lib/shape-code.mjs +126 -0
- package/packs/base/hooks/lib/shape-lists.mjs +31 -0
- package/packs/base/hooks/lib/shape-prose.mjs +78 -0
- package/packs/base/hooks/lib/shape.mjs +273 -0
- package/packs/base/hooks/lib/shell-writes-selftest.mjs +65 -0
- package/packs/base/hooks/lib/shell-writes.mjs +293 -0
- package/packs/base/hooks/lib/turn.mjs +86 -0
- package/packs/base/hooks/lib/unread-writes.mjs +24 -0
- package/packs/base/hooks/lib/writes.mjs +71 -0
- package/packs/base/hooks/making-check.mjs +248 -0
- package/packs/base/hooks/making.mjs +106 -0
- package/packs/base/hooks/once-only.mjs +150 -0
- package/packs/base/hooks/placement-law.mjs +84 -0
- package/packs/base/hooks/repo-litter.mjs +75 -0
- package/packs/base/hooks/reuse-hint.mjs +75 -0
- package/packs/base/hooks/run-hooks.mjs +140 -0
- package/packs/base/hooks/secrets-guard.mjs +99 -0
- package/packs/base/hooks/shape-law.mjs +140 -0
- package/packs/base/hooks/suite-once.mjs +96 -0
- package/packs/base/hooks/template-law.mjs +148 -0
- package/packs/base/hooks/untrusted-boundary.mjs +53 -0
- package/packs/base/pack.json +214 -0
- package/packs/base/rules/a-control-is-seen-to-fire.md +27 -0
- package/packs/base/rules/behaviour-lives-with-its-kind.md +51 -0
- package/packs/base/rules/bug-fix-starts-red.md +25 -0
- package/packs/base/rules/field-map-over-if-runs.md +47 -0
- package/packs/base/rules/finished-work-lands.md +17 -0
- package/packs/base/rules/git-right-first-time.md +19 -0
- package/packs/base/rules/instances-by-index-identities-by-id.md +54 -0
- package/packs/base/rules/lookup-over-if-chains.md +49 -0
- package/packs/base/rules/making-line-first.md +14 -0
- package/packs/base/rules/open-items-tagged.md +17 -0
- package/packs/base/rules/plan-and-logs.md +22 -0
- package/packs/base/rules/references-use-anchors.md +19 -0
- package/packs/base/rules/refusals-are-judged.md +19 -0
- package/packs/base/rules/reuse-before-writing.md +15 -0
- package/packs/base/rules/secrets-never-committed.md +16 -0
- package/packs/base/rules/skill-writing.md +23 -0
- package/packs/base/rules/split-before-the-limit.md +14 -0
- package/packs/base/rules/subagent-dispatch.md +26 -0
- package/packs/base/rules/test-only-what-changed.md +17 -0
- package/packs/base/rules/test-the-template-once.md +23 -0
- package/packs/base/rules/unattended-runs.md +15 -0
- package/packs/base/rules/untrusted-content-boundary.md +15 -0
- package/packs/base/skills/add-lesson-check/SKILL.md +26 -0
- package/packs/base/skills/fix-from-scan/SKILL.md +25 -0
- package/packs/base/skills/land-branch/SKILL.md +24 -0
- package/packs/base/skills/make-it-a-template/SKILL.md +23 -0
- package/packs/base/skills/new-project/SKILL.md +23 -0
- package/packs/base/skills/pdf-to-md/SKILL.md +25 -0
- package/packs/base/skills/pick-tests/SKILL.md +20 -0
- package/packs/base/skills/pressure-test-skill/SKILL.md +23 -0
- package/packs/base/skills/prove-instances-identical/SKILL.md +37 -0
- package/packs/base/skills/write-a-workorder/SKILL.md +41 -0
- package/packs/base/tools/shape-symmetry-selftest.mjs +66 -0
- package/packs/base/tools/shape-symmetry.mjs +178 -0
- package/packs/base/tools/test-verdict.mjs +75 -0
- package/packs/ci/checks/ci-permissions-stated.json +18 -0
- package/packs/ci/checks/ci-tools-pinned.json +14 -0
- package/packs/ci/pack.json +21 -0
- package/packs/ci/rules/ci-covers-what-ships.md +20 -0
- package/packs/ci/rules/generated-files-have-no-diff.md +15 -0
- package/packs/content-site/checks/captcha-is-not-constant.json +27 -0
- package/packs/content-site/checks/img-has-size.json +20 -0
- package/packs/content-site/checks/page-head-complete.json +26 -0
- package/packs/content-site/pack.json +22 -0
- package/packs/content-site/rules/content-has-one-source.md +23 -0
- package/packs/content-site/rules/media-is-sized.md +24 -0
- package/packs/content-site/rules/page-head-is-complete.md +26 -0
- package/packs/content-site/rules/public-forms-are-defended.md +27 -0
- package/packs/content-site/skills/publish-a-page/SKILL.md +21 -0
- package/packs/cpp/checks/cpp-clamp-before-cast.json +16 -0
- package/packs/cpp/checks/cpp-find-with-the-library.json +12 -0
- package/packs/cpp/checks/cpp-hardened-test-build.json +18 -0
- package/packs/cpp/checks/cpp-m-pi-not-standard.json +23 -0
- package/packs/cpp/checks/cpp-no-atoi.json +16 -0
- package/packs/cpp/checks/cpp-no-const-cast.json +16 -0
- package/packs/cpp/checks/cpp-no-float-loop-counter.json +16 -0
- package/packs/cpp/checks/cpp-no-getenv-in-product.json +22 -0
- package/packs/cpp/checks/cpp-no-mutable-global.json +20 -0
- package/packs/cpp/checks/cpp-no-mutable-local-static.json +23 -0
- package/packs/cpp/checks/cpp-no-unchecked-fwrite.json +17 -0
- package/packs/cpp/checks/cpp-no-using-namespace-in-header.json +19 -0
- package/packs/cpp/checks/cpp-no-zero-size-array.json +23 -0
- package/packs/cpp/checks/cpp-numbers-pi.json +16 -0
- package/packs/cpp/pack.json +22 -0
- package/packs/cpp/rules/cpp-absent-and-failed-are-types.md +24 -0
- package/packs/cpp/rules/cpp-builds-everywhere.md +26 -0
- package/packs/cpp/rules/cpp-faults-only-in-the-test-build.md +21 -0
- package/packs/cpp/rules/cpp-find-with-the-library.md +19 -0
- package/packs/cpp/rules/cpp-gates-share-one-reader.md +22 -0
- package/packs/cpp/rules/cpp-headers-declare.md +22 -0
- package/packs/cpp/rules/cpp-numbers-and-casts.md +22 -0
- package/packs/cpp/rules/cpp-repeats-become-templates.md +33 -0
- package/packs/cpp/rules/cpp-static-analysis-runs.md +22 -0
- package/packs/cpp/rules/cpp-thread-contracts-are-code.md +23 -0
- package/packs/cpp/skills/cpp-extract-template/SKILL.md +38 -0
- package/packs/docker/checks/alpine-with-native-build.json +13 -0
- package/packs/docker/checks/compose-db-needs-auth.json +14 -0
- package/packs/docker/checks/compose-image-pinned.json +14 -0
- package/packs/docker/checks/compose-no-hardcoded-ip.json +12 -0
- package/packs/docker/checks/compose-no-host-bind-for-data.json +18 -0
- package/packs/docker/checks/compose-no-plaintext-secret.json +13 -0
- package/packs/docker/checks/compose-no-swarm-ignored-keys.json +13 -0
- package/packs/docker/checks/compose-overlay-not-bridge.json +13 -0
- package/packs/docker/checks/compose-service-needs-healthcheck.json +11 -0
- package/packs/docker/checks/compose-service-needs-init.json +11 -0
- package/packs/docker/checks/copy-needs-chown-with-user.json +17 -0
- package/packs/docker/checks/copy-source-before-deps.json +13 -0
- package/packs/docker/checks/docker-exec-form.json +16 -0
- package/packs/docker/checks/dockerfile-needs-healthcheck.json +17 -0
- package/packs/docker/checks/dockerfile-needs-user.json +21 -0
- package/packs/docker/checks/dockerignore-has-the-basics.json +21 -0
- package/packs/docker/checks/gitattributes-forces-lf.json +17 -0
- package/packs/docker/checks/health-checks-nothing.json +25 -0
- package/packs/docker/checks/no-crlf-in-scripts.json +15 -0
- package/packs/docker/checks/no-latest-base.json +13 -0
- package/packs/docker/checks/no-secret-in-image.json +13 -0
- package/packs/docker/checks/no-sensitive-file-copy.json +13 -0
- package/packs/docker/checks/node-env-before-install.json +13 -0
- package/packs/docker/checks/npm-ci-not-install.json +13 -0
- package/packs/docker/pack.json +46 -0
- package/packs/docker/rules/compose-pinned-and-closed.md +21 -0
- package/packs/docker/rules/compose-swarm-safe.md +26 -0
- package/packs/docker/rules/docker-exec-form.md +19 -0
- package/packs/docker/rules/dockerfile-multistage.md +13 -0
- package/packs/docker/rules/dockerfile-production.md +25 -0
- package/packs/docker/skills/docker-diagnose/SKILL.md +23 -0
- package/packs/dynamic-data/checks/draft-on-the-server.json +27 -0
- package/packs/dynamic-data/checks/guard-does-its-job.json +28 -0
- package/packs/dynamic-data/checks/key-is-not-a-display-name.json +28 -0
- package/packs/dynamic-data/checks/no-code-from-data.json +29 -0
- package/packs/dynamic-data/checks/no-collection-from-request.json +27 -0
- package/packs/dynamic-data/checks/no-pipeline-from-stored-text.json +27 -0
- package/packs/dynamic-data/checks/no-regexp-from-stored-config.json +27 -0
- package/packs/dynamic-data/checks/no-template-from-stored-text.json +27 -0
- package/packs/dynamic-data/checks/save-takes-named-paths.json +27 -0
- package/packs/dynamic-data/pack.json +23 -0
- package/packs/dynamic-data/rules/a-list-is-a-saved-query.md +23 -0
- package/packs/dynamic-data/rules/definitions-are-validated-data.md +24 -0
- package/packs/dynamic-data/rules/every-dynamic-surface-has-a-boundary.md +29 -0
- package/packs/dynamic-data/rules/fields-have-owners.md +22 -0
- package/packs/dynamic-data/rules/imports-drop-nothing.md +26 -0
- package/packs/dynamic-data/rules/one-definition-many-surfaces.md +24 -0
- package/packs/dynamic-data/rules/one-fact-one-place.md +28 -0
- package/packs/dynamic-data/rules/rules-live-in-the-engine.md +29 -0
- package/packs/dynamic-data/rules/saves-are-patches.md +28 -0
- package/packs/dynamic-data/rules/tenant-costs-one-document.md +22 -0
- package/packs/dynamic-data/skills/design-a-definition/SKILL.md +24 -0
- package/packs/fastify/checks/api-routes-versioned.json +28 -0
- package/packs/fastify/checks/cookie-host-prefix.json +24 -0
- package/packs/fastify/checks/hijack-needs-try.json +28 -0
- package/packs/fastify/checks/no-cast-at-route-boundary.json +24 -0
- package/packs/fastify/checks/no-db-in-route-file.json +28 -0
- package/packs/fastify/checks/no-err-message-to-client.json +24 -0
- package/packs/fastify/checks/no-regexp-from-input.json +24 -0
- package/packs/fastify/checks/no-trust-proxy-true.json +25 -0
- package/packs/fastify/checks/one-error-body.json +26 -0
- package/packs/fastify/checks/origin-from-config-not-host.json +31 -0
- package/packs/fastify/checks/password-needs-slow-hash.json +30 -0
- package/packs/fastify/checks/sec-headers-present.json +28 -0
- package/packs/fastify/checks/secrets-compared-constant-time.json +24 -0
- package/packs/fastify/checks/sse-needs-error-listener.json +28 -0
- package/packs/fastify/checks/user-bytes-need-disposition.json +29 -0
- package/packs/fastify/pack.json +41 -0
- package/packs/fastify/rules/credentials-and-input.md +23 -0
- package/packs/fastify/rules/security-one-switch.md +28 -0
- package/packs/fastify/rules/streams-and-hijacks.md +22 -0
- package/packs/fastify/rules/tests-use-inject.md +13 -0
- package/packs/fastify/rules/three-layers.md +20 -0
- package/packs/fastify/rules/trust-config-not-headers.md +24 -0
- package/packs/full-stack/checks/no-express-no-vite.json +23 -0
- package/packs/full-stack/hooks/stack-guard.mjs +90 -0
- package/packs/full-stack/pack.json +76 -0
- package/packs/full-stack/rules/stack-is-fastify-and-nextjs.md +20 -0
- package/packs/go/checks/go-exec-result-ignored.json +15 -0
- package/packs/go/checks/go-unmarshal-error-ignored.json +15 -0
- package/packs/go/pack.json +26 -0
- package/packs/go/rules/go-errors-are-handled.md +22 -0
- package/packs/gpu-painter/checks/dawn-backends-and-validation.json +9 -0
- package/packs/gpu-painter/checks/dawn-no-spontaneous.json +10 -0
- package/packs/gpu-painter/checks/dawn-present-on-render-thread.json +17 -0
- package/packs/gpu-painter/checks/gpu-no-frame-stalls.json +12 -0
- package/packs/gpu-painter/checks/no-opengl-link.json +11 -0
- package/packs/gpu-painter/checks/no-opengl.json +13 -0
- package/packs/gpu-painter/hooks/browser-guard.py +196 -0
- package/packs/gpu-painter/pack.json +91 -0
- package/packs/gpu-painter/rules/dawn-build-and-shaders.md +27 -0
- package/packs/gpu-painter/rules/dawn-device-and-render-thread.md +26 -0
- package/packs/gpu-painter/rules/gpu-antialiasing.md +20 -0
- package/packs/gpu-painter/rules/gpu-blend-order.md +22 -0
- package/packs/gpu-painter/rules/gpu-bloom.md +20 -0
- package/packs/gpu-painter/rules/gpu-frame-budget.md +25 -0
- package/packs/gpu-painter/rules/gpu-frame-pacing-and-power.md +27 -0
- package/packs/gpu-painter/rules/gpu-frame-work.md +30 -0
- package/packs/gpu-painter/rules/gpu-golden-images.md +21 -0
- package/packs/gpu-painter/rules/gpu-hdr-and-tonemap.md +23 -0
- package/packs/gpu-painter/rules/gpu-picking-on-cpu.md +19 -0
- package/packs/gpu-painter/rules/gpu-render-lifecycle.md +25 -0
- package/packs/gpu-painter/rules/gpu-text-atlas.md +24 -0
- package/packs/gpu-painter/rules/gpu-travellers-from-snapshot.md +24 -0
- package/packs/gpu-painter/rules/hover-every-pressable.md +25 -0
- package/packs/gpu-painter/skills/gpu-capture/SKILL.md +15 -0
- package/packs/gpu-painter/skills/gpu-frame-bench/SKILL.md +14 -0
- package/packs/gpu-painter/skills/gpu-golden-check/SKILL.md +13 -0
- package/packs/gpu-painter/skills/gpu-golden-update/SKILL.md +16 -0
- package/packs/gpu-painter/skills/gpu-lifecycle-stress/SKILL.md +15 -0
- package/packs/gpu-painter/skills/hover-state/SKILL.md +33 -0
- package/packs/hook-proof/checks/hook-catch-must-not-allow.json +16 -0
- package/packs/hook-proof/checks/hook-command-no-pipe.json +16 -0
- package/packs/hook-proof/checks/hook-exit-1-does-not-block.json +17 -0
- package/packs/hook-proof/checks/hook-no-or-true.json +18 -0
- package/packs/hook-proof/checks/hook-refusal-on-stderr.json +17 -0
- package/packs/hook-proof/pack.json +38 -0
- package/packs/hook-proof/rules/a-refusal-is-exit-2.md +28 -0
- package/packs/hook-proof/skills/prove-a-hook/SKILL.md +21 -0
- package/packs/host-agent/checks/file-path-from-param.json +27 -0
- package/packs/host-agent/checks/integrity-check-optional.json +27 -0
- package/packs/host-agent/checks/quoted-value-into-config.json +24 -0
- package/packs/host-agent/checks/secret-on-argv.json +28 -0
- package/packs/host-agent/checks/ssh-verifies-host-key.json +27 -0
- package/packs/host-agent/checks/system-file-written-in-place.json +26 -0
- package/packs/host-agent/checks/tmp-path-predictable.json +27 -0
- package/packs/host-agent/pack.json +22 -0
- package/packs/host-agent/rules/root-on-other-machines.md +27 -0
- package/packs/html-ui/checks/email-html-escapes.json +25 -0
- package/packs/html-ui/checks/external-link-rel.json +21 -0
- package/packs/html-ui/checks/no-duplicate-escape.json +26 -0
- package/packs/html-ui/checks/no-inline-client-script.json +21 -0
- package/packs/html-ui/checks/no-inline-style.json +23 -0
- package/packs/html-ui/pack.json +33 -0
- package/packs/html-ui/rules/email-html-escapes.md +19 -0
- package/packs/html-ui/rules/server-html-escapes-everything.md +22 -0
- package/packs/http-api/checks/api-path-versioned.json +31 -0
- package/packs/http-api/checks/authz-from-request-body.json +26 -0
- package/packs/http-api/checks/client-calls-a-version.json +28 -0
- package/packs/http-api/checks/client-ip-not-leftmost-xff.json +28 -0
- package/packs/http-api/checks/cors-needs-origin.json +26 -0
- package/packs/http-api/checks/csp-not-switched-off.json +28 -0
- package/packs/http-api/checks/debug-route-not-shipped.json +26 -0
- package/packs/http-api/checks/encrypted-name-plaintext-value.json +26 -0
- package/packs/http-api/checks/err-text-stays-server-side.json +27 -0
- package/packs/http-api/checks/failure-is-not-empty.json +28 -0
- package/packs/http-api/checks/gate-is-not-a-todo.json +27 -0
- package/packs/http-api/checks/guard-fails-open.json +28 -0
- package/packs/http-api/checks/hmac-over-reserialised-body.json +26 -0
- package/packs/http-api/checks/jwt-verify-pins-algorithm.json +26 -0
- package/packs/http-api/checks/middleware-skips-without-user.json +26 -0
- package/packs/http-api/checks/next-api-route-versioned.json +47 -0
- package/packs/http-api/checks/no-security-off-switch.json +28 -0
- package/packs/http-api/checks/oauth-state-signed.json +26 -0
- package/packs/http-api/checks/one-time-code-compared-raw.json +26 -0
- package/packs/http-api/checks/query-cast-is-not-validation.json +28 -0
- package/packs/http-api/checks/queue-action-from-client.json +26 -0
- package/packs/http-api/checks/rate-limit-one-module.json +28 -0
- package/packs/http-api/checks/role-checked-in-one-guard.json +27 -0
- package/packs/http-api/checks/route-body-needs-schema.json +31 -0
- package/packs/http-api/checks/route-scopes-to-the-caller.json +31 -0
- package/packs/http-api/checks/secret-compared-constant-time-any.json +28 -0
- package/packs/http-api/checks/secret-in-job-record.json +27 -0
- package/packs/http-api/checks/server-fetches-tenant-url.json +30 -0
- package/packs/http-api/checks/short-code-needs-limit.json +26 -0
- package/packs/http-api/checks/user-url-through-safe-fetch.json +28 -0
- package/packs/http-api/checks/webhook-dedupes-event-id.json +30 -0
- package/packs/http-api/pack.json +45 -0
- package/packs/http-api/rules/adapters-one-db-gateway.md +18 -0
- package/packs/http-api/rules/apis-are-versioned.md +27 -0
- package/packs/http-api/rules/credentials-handled-safely.md +22 -0
- package/packs/http-api/rules/handlers-own-the-work.md +17 -0
- package/packs/http-api/rules/health-and-correlation.md +17 -0
- package/packs/http-api/rules/one-error-contract.md +17 -0
- package/packs/http-api/rules/routes-are-thin.md +17 -0
- package/packs/http-api/rules/routes-scope-to-the-caller.md +26 -0
- package/packs/http-api/rules/routes-validate-and-answer.md +25 -0
- package/packs/http-api/skills/change-an-api/SKILL.md +20 -0
- package/packs/http-api/skills/secure-a-route/SKILL.md +21 -0
- package/packs/instrument-shell/checks/no-raw-struct-blob.json +23 -0
- package/packs/instrument-shell/pack.json +22 -0
- package/packs/instrument-shell/rules/fold-keeps-every-route.md +24 -0
- package/packs/instrument-shell/rules/quiet-announces-itself.md +22 -0
- package/packs/instrument-shell/rules/render-the-extremes.md +25 -0
- package/packs/instrument-shell/rules/seams-are-declared.md +27 -0
- package/packs/instrument-shell/rules/session-blob-append-only.md +25 -0
- package/packs/instrument-shell/skills/gates-from-commit-one/SKILL.md +20 -0
- package/packs/instrument-shell/skills/hit-the-painted-control/SKILL.md +18 -0
- package/packs/js-ts/checks/async-callback-unguarded.json +26 -0
- package/packs/js-ts/checks/background-loop-needs-stop.json +28 -0
- package/packs/js-ts/checks/child-process-error-handler.json +28 -0
- package/packs/js-ts/checks/entry-registers-shutdown.json +31 -0
- package/packs/js-ts/checks/fetch-needs-timeout.json +30 -0
- package/packs/js-ts/checks/job-only-in-memory.json +28 -0
- package/packs/js-ts/checks/json-only-through-helper.json +29 -0
- package/packs/js-ts/checks/line-under-200-chars.json +34 -0
- package/packs/js-ts/checks/localhost-default-in-server.json +29 -0
- package/packs/js-ts/checks/no-any.json +27 -0
- package/packs/js-ts/checks/no-cast-escape-hatches.json +24 -0
- package/packs/js-ts/checks/no-console-in-app.json +31 -0
- package/packs/js-ts/checks/no-dynamic-import-node-builtin.json +25 -0
- package/packs/js-ts/checks/no-inline-binary.json +20 -0
- package/packs/js-ts/checks/no-inline-zod-outside-schemas.json +27 -0
- package/packs/js-ts/checks/no-local-sleep.json +25 -0
- package/packs/js-ts/checks/no-nested-ternary.json +20 -0
- package/packs/js-ts/checks/no-secret-in-log.json +24 -0
- package/packs/js-ts/checks/no-shell-string-interpolation.json +24 -0
- package/packs/js-ts/checks/no-swallowed-errors.json +25 -0
- package/packs/js-ts/checks/no-ts-enum.json +20 -0
- package/packs/js-ts/checks/process-env-only-in-config.json +31 -0
- package/packs/js-ts/checks/race-leaves-work-running.json +23 -0
- package/packs/js-ts/checks/reject-as-catch-all-exists.json +24 -0
- package/packs/js-ts/checks/secret-has-no-fallback.json +24 -0
- package/packs/js-ts/checks/secret-needs-crypto-random.json +24 -0
- package/packs/js-ts/checks/server-close-awaited.json +27 -0
- package/packs/js-ts/checks/session-user-typed-once.json +24 -0
- package/packs/js-ts/checks/switch-is-a-table.json +24 -0
- package/packs/js-ts/checks/test-imports-the-source.json +19 -0
- package/packs/js-ts/checks/test-name-says-behaviour.json +16 -0
- package/packs/js-ts/checks/tsconfig-flags.json +21 -0
- package/packs/js-ts/checks/void-needs-catch.json +24 -0
- package/packs/js-ts/checks/zod-string-needs-max.json +25 -0
- package/packs/js-ts/checks/zod-v4-formats.json +23 -0
- package/packs/js-ts/pack.json +84 -0
- package/packs/js-ts/rules/await-handled-at-a-boundary.md +66 -0
- package/packs/js-ts/rules/env-through-config.md +36 -0
- package/packs/js-ts/rules/errors-never-swallowed.md +33 -0
- package/packs/js-ts/rules/files-under-300-lines.md +18 -0
- package/packs/js-ts/rules/guard-clauses-over-nesting.md +34 -0
- package/packs/js-ts/rules/js-repeats-become-templates.md +26 -0
- package/packs/js-ts/rules/json-only-through-one-helper.md +62 -0
- package/packs/js-ts/rules/named-constants-not-magic-numbers.md +34 -0
- package/packs/js-ts/rules/no-any.md +34 -0
- package/packs/js-ts/rules/no-console.md +30 -0
- package/packs/js-ts/rules/no-floating-promises.md +31 -0
- package/packs/js-ts/rules/no-param-mutation.md +28 -0
- package/packs/js-ts/rules/no-unsafe-assertions.md +30 -0
- package/packs/js-ts/rules/one-logger.md +24 -0
- package/packs/js-ts/rules/outbound-calls-have-a-limit.md +20 -0
- package/packs/js-ts/rules/parallel-independent-awaits.md +29 -0
- package/packs/js-ts/rules/process-lifecycle.md +47 -0
- package/packs/js-ts/rules/schema-single-source-of-truth.md +36 -0
- package/packs/js-ts/rules/secrets-stay-secret.md +22 -0
- package/packs/js-ts/rules/small-functions.md +20 -0
- package/packs/js-ts/rules/test-names-describe-behavior.md +28 -0
- package/packs/js-ts/rules/tests-can-fail.md +19 -0
- package/packs/js-ts/rules/tsconfig-strict.md +32 -0
- package/packs/js-ts/rules/typed-exports.md +27 -0
- package/packs/js-ts/rules/types-once-at-the-edge.md +18 -0
- package/packs/js-ts/rules/unions-over-enums.md +29 -0
- package/packs/js-ts/rules/zod-at-route-boundaries.md +40 -0
- package/packs/midi/checks/active-sensing-no-pulse.json +24 -0
- package/packs/midi/checks/clock-not-at-sample-zero.json +23 -0
- package/packs/midi/checks/midi-realtime-first.json +27 -0
- package/packs/midi/checks/no-tempo-writeback.json +23 -0
- package/packs/midi/checks/position-not-integrated.json +29 -0
- package/packs/midi/checks/stub-playhead-advances.json +27 -0
- package/packs/midi/checks/sysex-length-literal.json +23 -0
- package/packs/midi/checks/sysex-not-on-audio.json +24 -0
- package/packs/midi/checks/sysex-survives-realtime.json +27 -0
- package/packs/midi/pack.json +22 -0
- package/packs/midi/rules/clock-is-a-pulse-stream.md +30 -0
- package/packs/midi/rules/imports-count-every-loss.md +25 -0
- package/packs/midi/rules/measure-the-clock-in-audio.md +30 -0
- package/packs/midi/rules/sync-is-a-mode.md +29 -0
- package/packs/midi/rules/sysex-is-a-record.md +29 -0
- package/packs/midi/skills/clock-probe/SKILL.md +23 -0
- package/packs/midi/skills/clock-probe/clock-law.md +417 -0
- package/packs/midi/skills/does-it-sync/SKILL.md +19 -0
- package/packs/mongo/checks/bulk-over-many.json +28 -0
- package/packs/mongo/checks/created-at-immutable.json +25 -0
- package/packs/mongo/checks/driver-outside-gateway.json +24 -0
- package/packs/mongo/checks/key-lookup-is-hashed.json +24 -0
- package/packs/mongo/checks/mongoclient-cached-and-named.json +34 -0
- package/packs/mongo/checks/mongoclient-outside-gateway.json +24 -0
- package/packs/mongo/checks/no-count-plus-one-ids.json +25 -0
- package/packs/mongo/checks/no-db-write-in-loop.json +25 -0
- package/packs/mongo/checks/no-drop-in-code.json +27 -0
- package/packs/mongo/checks/no-empty-deletemany.json +26 -0
- package/packs/mongo/checks/no-find-reads.json +26 -0
- package/packs/mongo/checks/no-input-spread-into-write.json +24 -0
- package/packs/mongo/checks/no-mongoose.json +23 -0
- package/packs/mongo/checks/no-negation-delete.json +26 -0
- package/packs/mongo/checks/no-non-null-after-findone.json +25 -0
- package/packs/mongo/checks/objectid-equals.json +26 -0
- package/packs/mongo/checks/update-then-reread.json +24 -0
- package/packs/mongo/checks/watch-not-poll.json +25 -0
- package/packs/mongo/pack.json +52 -0
- package/packs/mongo/rules/deletes-name-what-dies.md +21 -0
- package/packs/mongo/rules/driver-only-in-gateway.md +26 -0
- package/packs/mongo/rules/keys-hashed-input-picked.md +21 -0
- package/packs/mongo/rules/one-client-one-pool.md +32 -0
- package/packs/mongo/rules/reads-aggregate-writes-bulk.md +25 -0
- package/packs/mongo/rules/writes-are-atomic.md +27 -0
- package/packs/multi-tenant/checks/role-sees-all-tenants.json +26 -0
- package/packs/multi-tenant/checks/tenant-filter-from-url.json +25 -0
- package/packs/multi-tenant/pack.json +22 -0
- package/packs/multi-tenant/rules/no-role-crosses-a-tenant.md +21 -0
- package/packs/multi-tenant/skills/prove-tenant-isolation/SKILL.md +19 -0
- package/packs/nginx/checks/modsec-no-blanket-off.json +16 -0
- package/packs/nginx/checks/nginx-csp-no-unsafe.json +15 -0
- package/packs/nginx/checks/nginx-headers-always.json +15 -0
- package/packs/nginx/checks/nginx-keepalive-http11.json +20 -0
- package/packs/nginx/checks/nginx-logs-to-stdout.json +15 -0
- package/packs/nginx/checks/nginx-no-crlf.json +17 -0
- package/packs/nginx/checks/nginx-resolver-ttl.json +19 -0
- package/packs/nginx/pack.json +43 -0
- package/packs/nginx/rules/modsecurity-tuning.md +22 -0
- package/packs/nginx/rules/nginx-production.md +26 -0
- package/packs/nginx/skills/waf-rollout/SKILL.md +20 -0
- package/packs/postgres/checks/pg-bind-values.json +33 -0
- package/packs/postgres/checks/pg-concurrently-in-txn.json +18 -0
- package/packs/postgres/checks/pg-fk-not-valid.json +18 -0
- package/packs/postgres/checks/pg-index-concurrently.json +18 -0
- package/packs/postgres/checks/pg-no-float-money.json +19 -0
- package/packs/postgres/checks/pg-no-money.json +18 -0
- package/packs/postgres/checks/pg-no-required-column.json +18 -0
- package/packs/postgres/checks/pg-no-volatile-default.json +18 -0
- package/packs/postgres/checks/pg-not-exists.json +18 -0
- package/packs/postgres/checks/pg-timestamptz.json +18 -0
- package/packs/postgres/hooks/db-wipe-guard.mjs +95 -0
- package/packs/postgres/pack.json +48 -0
- package/packs/postgres/rules/postgres-migrations-never-stop-traffic.md +28 -0
- package/packs/postgres/rules/postgres-one-pool-per-process.md +21 -0
- package/packs/postgres/rules/postgres-queries-are-null-safe-and-bounded.md +26 -0
- package/packs/postgres/rules/postgres-types-say-what-they-hold.md +22 -0
- package/packs/postgres/rules/postgres-values-are-bound.md +130 -0
- package/packs/postgres/skills/postgres-safe-migration/SKILL.md +45 -0
- package/packs/python/checks/py-no-bare-except.json +18 -0
- package/packs/python/checks/py-no-eval.json +18 -0
- package/packs/python/checks/py-no-mutable-default.json +18 -0
- package/packs/python/checks/py-no-shell-true.json +18 -0
- package/packs/python/checks/py-no-star-import.json +18 -0
- package/packs/python/checks/py-no-swallowed-exception.json +18 -0
- package/packs/python/checks/py-none-by-is.json +18 -0
- package/packs/python/checks/py-open-with-with.json +18 -0
- package/packs/python/checks/py-requests-timeout.json +18 -0
- package/packs/python/checks/py-subprocess-output-checked.json +18 -0
- package/packs/python/checks/py-yaml-safe-load.json +18 -0
- package/packs/python/pack.json +26 -0
- package/packs/python/rules/python-errors-and-resources.md +26 -0
- package/packs/python/rules/python-repeats-become-templates.md +27 -0
- package/packs/python/skills/python-extract-template/SKILL.md +35 -0
- package/packs/react/checks/dialog-needs-role.json +28 -0
- package/packs/react/checks/effect-not-for-derived-state.json +24 -0
- package/packs/react/checks/icon-button-needs-name.json +24 -0
- package/packs/react/checks/label-needs-a-field.json +24 -0
- package/packs/react/checks/ui-failed-fetch-says-so.json +23 -0
- package/packs/react/checks/ui-ok-branch-has-an-else.json +23 -0
- package/packs/react/pack.json +20 -0
- package/packs/react/rules/react-accessible-controls.md +20 -0
- package/packs/react/rules/react-state-and-effects.md +23 -0
- package/packs/reverse-engineering/checks/ear-constant-tagged.json +23 -0
- package/packs/reverse-engineering/checks/no-default-on-distrust.json +24 -0
- package/packs/reverse-engineering/checks/no-rom-table-literal.json +29 -0
- package/packs/reverse-engineering/checks/prose-never-decides.json +22 -0
- package/packs/reverse-engineering/hooks/firmware-out-of-git.mjs +49 -0
- package/packs/reverse-engineering/pack.json +35 -0
- package/packs/reverse-engineering/rules/a-negative-names-what-was-scanned.md +19 -0
- package/packs/reverse-engineering/rules/account-every-term.md +20 -0
- package/packs/reverse-engineering/rules/addresses-name-their-space.md +22 -0
- package/packs/reverse-engineering/rules/every-law-has-a-source.md +18 -0
- package/packs/reverse-engineering/rules/findings-are-withdrawn-in-place.md +17 -0
- package/packs/reverse-engineering/rules/firmware-stays-out.md +18 -0
- package/packs/reverse-engineering/rules/names-before-code.md +19 -0
- package/packs/reverse-engineering/rules/reproduction-or-original.md +18 -0
- package/packs/reverse-engineering/rules/two-sources-before-naming.md +21 -0
- package/packs/reverse-engineering/skills/bank-census/SKILL.md +18 -0
- package/packs/reverse-engineering/skills/gather-sources/SKILL.md +22 -0
- package/packs/reverse-engineering/skills/menu-map/SKILL.md +28 -0
- package/packs/reverse-engineering/skills/names-first/SKILL.md +41 -0
- package/packs/reverse-engineering/skills/re-deliverables/SKILL.md +21 -0
- package/packs/reverse-engineering/skills/tripwire-the-law/SKILL.md +19 -0
- package/packs/reverse-engineering/tools/lib/unwrap.mjs +115 -0
- package/packs/reverse-engineering/tools/names-sweep.mjs +264 -0
- package/packs/rust/checks/rust-allow-needs-reason.json +16 -0
- package/packs/rust/checks/rust-blocking-in-async.json +32 -0
- package/packs/rust/checks/rust-if-chain-is-a-match.json +32 -0
- package/packs/rust/checks/rust-static-mut.json +32 -0
- package/packs/rust/checks/rust-string-as-error.json +34 -0
- package/packs/rust/checks/rust-stub-left-in.json +32 -0
- package/packs/rust/checks/rust-unsafe-needs-safety.json +31 -0
- package/packs/rust/checks/rust-unwrap-in-library.json +34 -0
- package/packs/rust/checks/rust-wildcard-dependency.json +16 -0
- package/packs/rust/clippy.toml +5 -0
- package/packs/rust/pack.json +27 -0
- package/packs/rust/rules/rust-async-never-blocks.md +20 -0
- package/packs/rust/rules/rust-borrow-before-you-clone.md +19 -0
- package/packs/rust/rules/rust-errors-are-returned.md +22 -0
- package/packs/rust/rules/rust-lints-are-the-gate.md +23 -0
- package/packs/rust/rules/rust-repeats-become-templates.md +28 -0
- package/packs/rust/rules/rust-unsafe-is-proved.md +21 -0
- package/packs/rust/skills/rust-extract-template/SKILL.md +50 -0
- package/packs/rust/skills/rust-port/SKILL.md +22 -0
- package/packs/rust/skills/rust-review/SKILL.md +24 -0
- package/packs/rust/tools/rust-lints.toml +19 -0
- package/packs/shell/checks/sh-mktemp.json +18 -0
- package/packs/shell/checks/sh-no-backticks.json +18 -0
- package/packs/shell/checks/sh-no-eval.json +18 -0
- package/packs/shell/checks/sh-no-parse-ls.json +18 -0
- package/packs/shell/checks/sh-no-pipe-to-shell.json +18 -0
- package/packs/shell/checks/sh-no-program-in-heredoc.json +18 -0
- package/packs/shell/checks/sh-quote-arguments.json +18 -0
- package/packs/shell/checks/sh-rm-guarded.json +18 -0
- package/packs/shell/checks/sh-strict-mode.json +20 -0
- package/packs/shell/pack.json +28 -0
- package/packs/shell/rules/shell-repeats-become-templates.md +23 -0
- package/packs/shell/rules/shell-strict-and-quoted.md +27 -0
- package/packs/shell/skills/shell-extract-template/SKILL.md +32 -0
- package/packs/shell-hooks/checks/gnu-only-in-shell.json +18 -0
- package/packs/shell-hooks/checks/hook-command-anchored.json +12 -0
- package/packs/shell-hooks/checks/hook-json-by-tool.json +18 -0
- package/packs/shell-hooks/checks/no-bash-c-with-variable.json +20 -0
- package/packs/shell-hooks/pack.json +36 -0
- package/packs/shell-hooks/rules/hooks-portable-anchored-quoted.md +26 -0
- package/packs/sound-design/checks/audio-seed-fixed.json +23 -0
- package/packs/sound-design/checks/mix-is-a-crossfade.json +30 -0
- package/packs/sound-design/checks/no-hash-in-the-signal.json +30 -0
- package/packs/sound-design/checks/no-literal-key-anchor.json +30 -0
- package/packs/sound-design/checks/test-compares-a-function-with-itself.json +22 -0
- package/packs/sound-design/pack.json +22 -0
- package/packs/sound-design/rules/algorithms-are-data.md +26 -0
- package/packs/sound-design/rules/meter-before-engine.md +27 -0
- package/packs/sound-design/rules/neutral-is-exact.md +26 -0
- package/packs/sound-design/rules/test-off-the-easy-point.md +28 -0
- package/packs/sound-design/rules/unreachable-is-said.md +25 -0
- package/packs/sound-design/rules/waveforms-and-octaves.md +24 -0
- package/packs/sound-design/skills/bounce-a-plugin/SKILL.md +40 -0
- package/packs/sound-design/skills/choose-observable/SKILL.md +18 -0
- package/packs/sound-design/skills/measure-a-law/SKILL.md +18 -0
- package/packs/sound-design/skills/measure-reverb/SKILL.md +19 -0
- package/packs/sound-design/skills/spread-probe/SKILL.md +18 -0
- package/packs/sound-design/tools/vst3host/build.sh +73 -0
- package/packs/sound-design/tools/vst3host/host_events.h +101 -0
- package/packs/sound-design/tools/vst3host/options.h +172 -0
- package/packs/sound-design/tools/vst3host/render.cpp +245 -0
- package/packs/sound-design/tools/vst3host/session.h +241 -0
- package/packs/stellix/checks/no-keyboard-focus.json +12 -0
- package/packs/stellix/checks/one-seeded-random.json +16 -0
- package/packs/stellix/checks/world-never-includes-painter.json +12 -0
- package/packs/stellix/checks/world-no-libm.json +15 -0
- package/packs/stellix/checks/world-no-unordered.json +10 -0
- package/packs/stellix/hooks/no-shipped-images.mjs +88 -0
- package/packs/stellix/pack.json +61 -0
- package/packs/stellix/rules/stellix-deterministic-world.md +29 -0
- package/packs/stellix/rules/stellix-reset-generated.md +24 -0
- package/packs/stellix/rules/stellix-world-snapshot.md +27 -0
- package/packs/stellix/rules/world-continuous-collision.md +26 -0
- package/packs/stellix/rules/world-fp-determinism.md +28 -0
- package/packs/stored-text/checks/no-raw-html-from-data.json +31 -0
- package/packs/stored-text/checks/stored-text-libs-only-in-renderer.json +31 -0
- package/packs/stored-text/checks/stored-text-renderer-complete.json +24 -0
- package/packs/stored-text/checks/stored-text-through-renderer.json +25 -0
- package/packs/stored-text/pack.json +41 -0
- package/packs/stored-text/rules/stored-text-renders-once.md +26 -0
- package/packs/stored-text/skills/render-stored-text/SKILL.md +24 -0
- package/packs/synth-creator/checks/timing-from-synthcore.json +24 -0
- package/packs/synth-creator/checks/ui-drawn-in-code.json +23 -0
- package/packs/synth-creator/hooks/build-guard.mjs +232 -0
- package/packs/synth-creator/hooks/build-sweep.mjs +166 -0
- package/packs/synth-creator/hooks/frozen-files.mjs +63 -0
- package/packs/synth-creator/hooks/idle-wake.mjs +90 -0
- package/packs/synth-creator/hooks/protect-vendor.mjs +175 -0
- package/packs/synth-creator/hooks/spec-lock.mjs +117 -0
- package/packs/synth-creator/hooks/stage-guard.mjs +50 -0
- package/packs/synth-creator/hooks/time-guard.mjs +292 -0
- package/packs/synth-creator/hooks/user-asks.mjs +165 -0
- package/packs/synth-creator/pack.json +136 -0
- package/packs/synth-creator/rules/copy-trees-read-only.md +25 -0
- package/packs/synth-creator/rules/debug-by-measurement.md +20 -0
- package/packs/synth-creator/rules/done-means-fresh-evidence.md +18 -0
- package/packs/synth-creator/rules/hooks-and-gates.md +21 -0
- package/packs/synth-creator/rules/long-work-in-the-background.md +16 -0
- package/packs/synth-creator/rules/timing-from-synthcore.md +16 -0
- package/packs/synth-creator/skills/build-step/SKILL.md +125 -0
- package/packs/synth-creator/skills/build-through-the-queue/SKILL.md +21 -0
- package/packs/synth-creator/tools/buildq.mjs +74 -0
- package/packs/synth-creator/tools/freeze.mjs +36 -0
- package/packs/tui/checks/tui-entry-restores-terminal.json +27 -0
- package/packs/tui/checks/tui-gates-on-tty.json +25 -0
- package/packs/tui/checks/tui-no-ris-reset.json +22 -0
- package/packs/tui/checks/tui-resize-listener-removed.json +26 -0
- package/packs/tui/checks/tui-width-is-state.json +33 -0
- package/packs/tui/pack.json +44 -0
- package/packs/tui/rules/tui-resize-and-screen.md +25 -0
- package/packs/tui/skills/build-tui/SKILL.md +21 -0
- package/packs/ui-craft/checks/ui-input-needs-autocomplete.json +23 -0
- package/packs/ui-craft/checks/ui-intl-for-dates.json +21 -0
- package/packs/ui-craft/checks/ui-no-ai-palette.json +22 -0
- package/packs/ui-craft/checks/ui-no-animate-layout.json +22 -0
- package/packs/ui-craft/checks/ui-no-assistant-palette.json +23 -0
- package/packs/ui-craft/checks/ui-no-bounce-easing.json +22 -0
- package/packs/ui-craft/checks/ui-no-custom-cursor.json +22 -0
- package/packs/ui-craft/checks/ui-no-emoji-as-ui.json +22 -0
- package/packs/ui-craft/checks/ui-no-empty-image.json +21 -0
- package/packs/ui-craft/checks/ui-no-eyebrow.json +21 -0
- package/packs/ui-craft/checks/ui-no-furniture-text.json +22 -0
- package/packs/ui-craft/checks/ui-no-gradient-text.json +22 -0
- package/packs/ui-craft/checks/ui-no-index-numbering.json +21 -0
- package/packs/ui-craft/checks/ui-no-lorem.json +22 -0
- package/packs/ui-craft/checks/ui-no-marketing-buzzword.json +22 -0
- package/packs/ui-craft/checks/ui-no-paste-block.json +21 -0
- package/packs/ui-craft/checks/ui-no-placeholder-names.json +21 -0
- package/packs/ui-craft/checks/ui-no-scroll-hijack.json +21 -0
- package/packs/ui-craft/checks/ui-no-side-tab.json +21 -0
- package/packs/ui-craft/checks/ui-no-stock-labels.json +22 -0
- package/packs/ui-craft/checks/ui-no-transition-all.json +22 -0
- package/packs/ui-craft/checks/ui-no-uppercase-table-header.json +21 -0
- package/packs/ui-craft/checks/ui-no-zebra-rows.json +22 -0
- package/packs/ui-craft/checks/ui-no-zoom-lock.json +21 -0
- package/packs/ui-craft/pack.json +43 -0
- package/packs/ui-craft/rules/apps-are-not-pages.md +26 -0
- package/packs/ui-craft/rules/data-tells-the-truth.md +26 -0
- package/packs/ui-craft/rules/dates-and-numbers.md +18 -0
- package/packs/ui-craft/rules/forms-never-fight.md +22 -0
- package/packs/ui-craft/rules/interface-copy.md +26 -0
- package/packs/ui-craft/rules/motion-is-rare.md +31 -0
- package/packs/ui-craft/rules/no-generated-look.md +37 -0
- package/packs/ui-craft/rules/page-holds-its-frame.md +28 -0
- package/packs/ui-craft/rules/page-proved-rendered.md +19 -0
- package/packs/ui-craft/rules/pictures-do-a-job.md +25 -0
- package/packs/ui-craft/rules/pointer-feedback.md +28 -0
- package/packs/ui-craft/rules/screen-answers-one-question.md +24 -0
- package/packs/ui-craft/rules/survives-real-data.md +23 -0
- package/packs/ui-craft/rules/touch-and-small-screens.md +19 -0
- package/packs/ui-craft/rules/type-space-colour.md +26 -0
- package/packs/ui-craft/rules/url-holds-the-state.md +19 -0
- package/packs/ui-craft/skills/app-screen/SKILL.md +33 -0
- package/packs/ui-craft/skills/build-from-reference/SKILL.md +28 -0
- package/packs/ui-craft/skills/chart-choice/SKILL.md +34 -0
- package/packs/ui-craft/skills/hover-state/SKILL.md +28 -0
- package/packs/ui-craft/skills/page-critique/SKILL.md +33 -0
- package/packs/ui-craft/skills/prove-page/SKILL.md +23 -0
- package/packs/ui-craft/skills/redesign-page/SKILL.md +19 -0
- package/packs/ui-craft/skills/ui-code-review/SKILL.md +22 -0
- package/packs/ui-craft/skills/write-the-page/SKILL.md +21 -0
- package/packs/ui-craft/tools/page-measure.mjs +41 -0
- package/packs/web/checks/a11y-img-needs-alt.json +26 -0
- package/packs/web/checks/a11y-no-bare-outline-none.json +21 -0
- package/packs/web/checks/a11y-no-clickable-div.json +26 -0
- package/packs/web/checks/a11y-no-positive-tabindex.json +26 -0
- package/packs/web/checks/client-no-api-key.json +29 -0
- package/packs/web/checks/client-no-browser-global-at-module-scope.json +28 -0
- package/packs/web/checks/client-no-server-import.json +31 -0
- package/packs/web/checks/client-no-window-location-nav.json +28 -0
- package/packs/web/checks/css-no-inline-style.json +30 -0
- package/packs/web/checks/ds-pages-compose-primitives.json +28 -0
- package/packs/web/checks/key-from-index.json +23 -0
- package/packs/web/checks/prodbuild-no-cdn-runtime-deps.json +21 -0
- package/packs/web/checks/prodbuild-no-dev-flags.json +21 -0
- package/packs/web/checks/prodbuild-no-remote-fonts.json +22 -0
- package/packs/web/checks/rcss-button-cursor.json +30 -0
- package/packs/web/checks/rcss-no-100vw.json +21 -0
- package/packs/web/checks/rcss-no-fixed-wide-width.json +21 -0
- package/packs/web/checks/rcss-viewport-meta.json +28 -0
- package/packs/web/checks/secret-not-in-web-storage.json +25 -0
- package/packs/web/checks/tw-no-raw-hex-in-markup.json +34 -0
- package/packs/web/checks/wp-hero-not-lazy.json +27 -0
- package/packs/web/checks/wp-img-needs-dimensions.json +26 -0
- package/packs/web/pack.json +73 -0
- package/packs/web/rules/accessibility.md +26 -0
- package/packs/web/rules/client-boundaries.md +23 -0
- package/packs/web/rules/production-build.md +22 -0
- package/packs/web/rules/secrets-not-in-the-browser.md +18 -0
- package/packs/web/rules/styles-live-in-stylesheets.md +23 -0
- package/packs/web/rules/tailwind-v4.md +23 -0
- package/packs/web/rules/ui-primitives-one-module.md +30 -0
- package/packs/web/rules/web-performance.md +21 -0
- package/packs/web/skills/design-direction/SKILL.md +30 -0
- package/packs/web/skills/design-review/SKILL.md +28 -0
- package/packs/web/skills/pick-rendering-mode/SKILL.md +21 -0
- package/packs/web/skills/preflight-build/SKILL.md +23 -0
- package/packs/web/skills/seo-audit/SKILL.md +22 -0
- package/packs/websocket/checks/no-token-in-query.json +26 -0
- package/packs/websocket/checks/stale-socket-deletes-new-entry.json +25 -0
- package/packs/websocket/checks/ws-identity-from-message.json +26 -0
- package/packs/websocket/pack.json +21 -0
- package/packs/websocket/rules/identity-comes-from-the-connection.md +24 -0
- package/packs/writing/checks/ascii-no-em-dash.json +17 -0
- package/packs/writing/checks/no-copula-dodging.json +18 -0
- package/packs/writing/checks/no-dramatic-colon.json +18 -0
- package/packs/writing/checks/no-not-x-but-y.json +18 -0
- package/packs/writing/checks/no-participial-opener.json +18 -0
- package/packs/writing/checks/no-pseudo-wisdom.json +18 -0
- package/packs/writing/checks/no-slop-words.json +18 -0
- package/packs/writing/checks/no-stock-connectors.json +18 -0
- package/packs/writing/checks/no-time-or-date.json +27 -0
- package/packs/writing/checks/no-trailing-ing.json +18 -0
- package/packs/writing/checks/no-vague-attribution.json +18 -0
- package/packs/writing/pack.json +43 -0
- package/packs/writing/rules/ascii-no-em-dash.md +21 -0
- package/packs/writing/rules/cut-the-padding.md +20 -0
- package/packs/writing/rules/direct-voice.md +22 -0
- package/packs/writing/rules/no-cargo-cult.md +16 -0
- package/packs/writing/rules/no-scaffolding.md +19 -0
- package/packs/writing/rules/no-stock-language.md +27 -0
- package/packs/writing/rules/no-time-or-date.md +18 -0
- package/packs/writing/rules/rhythm-like-talk.md +20 -0
- package/packs/writing/rules/write-from-your-own-corner.md +20 -0
- package/packs/writing/skills/argument-post/SKILL.md +28 -0
- package/packs/writing/skills/humanize/SKILL.md +28 -0
- package/packs/writing/skills/launch-post/SKILL.md +38 -0
- package/packs/writing/skills/pick-subreddit/SKILL.md +69 -0
- package/packs/writing/skills/post-voice/SKILL.md +22 -0
- package/packs/writing/skills/reddit-post/SKILL.md +63 -0
- package/packs/writing/skills/reddit-title/SKILL.md +23 -0
- package/packs/writing/skills/reply-moves/SKILL.md +40 -0
- package/packs/writing/skills/reply-stance/SKILL.md +23 -0
- package/packs/writing/skills/should-i-reply/SKILL.md +21 -0
- package/packs/zod/hooks/schemas-only.mjs +53 -0
- package/packs/zod/pack.json +44 -0
- package/packs/zod/rules/env-shapes-only.md +14 -0
- package/packs/zod/rules/zod-4-schemas.md +14 -0
- package/profiles/stellix.layout.json +315 -0
- package/profiles/sub37.layout.json +247 -0
- package/prompts/scan-review.md +40 -0
- package/prompts/survey.md +82 -0
- package/prompts/workorder/PROMPT.md +53 -0
- package/prompts/workorder/run.sh +36 -0
- package/prompts/workorder/workorder.md +59 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// once-only: code that exists once is called, never copied (claude-packs 0.9.0 item 9; KWB-1). Before a source write
|
|
3
|
+
// lands (C++, Python, JS), the lines it adds are compared with the whole repo through an index kept between calls
|
|
4
|
+
// (lib/dup-index.mjs, KWB-24: jscpd's whole-repo scan on every write was too slow; only files that changed are read again),
|
|
5
|
+
// at jscpd's thresholds (lib/dup.mjs: 50 tokens and 6 lines, a gap of up to 2 lines, so a copy with one line changed is
|
|
6
|
+
// still a copy). doctor's count and `claude-packs dup scan` still run jscpd over the whole repo. A near-duplicate is refused with the
|
|
7
|
+
// existing location and the fix: call it, or extract one function that both call, parameterized over what differs.
|
|
8
|
+
// Generated files, resources, vendored and built code, copy trees and design/shape.json "exclude" are never judged.
|
|
9
|
+
// Every write route (lib/writes.mjs): Write, Edit, MultiEdit, NotebookEdit and the shell's >, >>, tee, sed -i, cp, mv,
|
|
10
|
+
// a heredoc. The file is judged as it would be after the write, its old text left out, so editing a file never
|
|
11
|
+
// matches its own old lines.
|
|
12
|
+
// node once-only.mjs --selftest
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join, relative } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { changedLines, copyGlobs, excludeGlobs, FIX, findJscpd, isGenerated, isSource, real } from './lib/dup.mjs';
|
|
18
|
+
import { judgeWrites } from './lib/dup-index.mjs';
|
|
19
|
+
import { matchesAny } from './lib/glob.mjs';
|
|
20
|
+
import { readLayout } from './lib/layout.mjs';
|
|
21
|
+
import { checkoutRoot, claudeRoot, insideProject } from './lib/project.mjs';
|
|
22
|
+
import { bashOf, editOf, runHook, scratchProject, verdicts, writeOf } from './lib/selftest.mjs';
|
|
23
|
+
import { readShape } from './lib/shape.mjs';
|
|
24
|
+
import { readPayload, writesOf } from './lib/writes.mjs';
|
|
25
|
+
import { isContent } from './lib/content.mjs';
|
|
26
|
+
import { asHook, isMain } from './lib/hook-log.mjs';
|
|
27
|
+
|
|
28
|
+
const HOW = `Code that exists once is called, never copied: ${FIX}. A function written for a many concept takes the instance as a parameter.\n` +
|
|
29
|
+
'Moving or extracting code: take the old copy out in the same write (one Write or MultiEdit), or take it out first.';
|
|
30
|
+
|
|
31
|
+
// [{ root, layout, shape, w, range }] for every source write the call makes that once-only judges.
|
|
32
|
+
function judged(payload) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const gone = [];
|
|
35
|
+
for (const w of writesOf(payload, payload.cwd ?? process.cwd())) {
|
|
36
|
+
if (!insideProject(w.path)) continue;
|
|
37
|
+
if (w.op === 'delete') { gone.push(w.path); continue; }
|
|
38
|
+
if (w.after === null || w.after === undefined || !isSource(w.path) || isGenerated(w.after)) continue;
|
|
39
|
+
const root = checkoutRoot(w.path);
|
|
40
|
+
const rel = relative(root, w.path).replace(/\\/g, '/');
|
|
41
|
+
const layout = readLayout(claudeRoot(w.path));
|
|
42
|
+
const shape = readShape(root);
|
|
43
|
+
if (matchesAny(rel, [...excludeGlobs(layout, shape), ...copyGlobs(layout)]) || isContent(rel, layout)) continue;
|
|
44
|
+
const range = changedLines(w.before, w.after);
|
|
45
|
+
if (range) out.push({ root, layout, shape, w, rel, range });
|
|
46
|
+
}
|
|
47
|
+
return { list: out, gone };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function judge(payload) {
|
|
51
|
+
const { list, gone } = judged(payload);
|
|
52
|
+
if (!list.length) return { code: 0 };
|
|
53
|
+
const found = [];
|
|
54
|
+
// each checkout written to: its index finds the candidates (built once, then only changed files are read again), jscpd
|
|
55
|
+
// confirms on those files alone (lib/dup-index.mjs judgeWrites)
|
|
56
|
+
for (const root of new Set(list.map((j) => j.root))) {
|
|
57
|
+
const mine = list.filter((j) => j.root === root);
|
|
58
|
+
const r = judgeWrites(root, mine.map((j) => ({ path: j.w.path, after: j.w.after, range: j.range })), { layout: mine[0].layout, shape: mine[0].shape, ignore: gone });
|
|
59
|
+
if (r.error) return { code: 0, note: `once-only could not judge: ${r.error}` }; // doctor shows a missing detector
|
|
60
|
+
const base = real(root);
|
|
61
|
+
const show = (s) => `${relative(base, s.file)}:${s.start}-${s.end}`;
|
|
62
|
+
for (const c of r.clones) found.push(`${show(c.at)} repeats ${show(c.other)} (${c.lines} lines, ${c.tokens} tokens)`);
|
|
63
|
+
}
|
|
64
|
+
if (!found.length) return { code: 0 };
|
|
65
|
+
const listed = [...new Set(found)];
|
|
66
|
+
return { code: 2, message: `BLOCKED by once-only: this write repeats code the repo already has.\n${listed.slice(0, 6).map((f) => ` ${f}`).join('\n')}${listed.length > 6 ? `\n ... and ${listed.length - 6} more` : ''}\n${HOW}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const FN = (name, k = '2.0f') => `float ${name}::process (float x) noexcept
|
|
70
|
+
{
|
|
71
|
+
const float g = std::tan (kPi * cutoff / sampleRate);
|
|
72
|
+
const float k = ${k} - 2.0f * resonance;
|
|
73
|
+
const float a1 = 1.0f / (1.0f + g * (g + k));
|
|
74
|
+
const float a2 = g * a1;
|
|
75
|
+
const float a3 = g * a2;
|
|
76
|
+
const float v3 = x - ic2eq;
|
|
77
|
+
const float v1 = a1 * ic1eq + a2 * v3;
|
|
78
|
+
const float v2 = ic2eq + a2 * ic1eq + a3 * v3;
|
|
79
|
+
ic1eq = 2.0f * v1 - ic1eq;
|
|
80
|
+
ic2eq = 2.0f * v2 - ic2eq;
|
|
81
|
+
return mode == 0 ? v2 : mode == 1 ? v1 : x - k * v1 - v2;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
const PY = (name, scale = '0.5') => `def ${name}(frames, rate):
|
|
85
|
+
out = []
|
|
86
|
+
for i, f in enumerate(frames):
|
|
87
|
+
level = sum(abs(s) for s in f) / max(len(f), 1)
|
|
88
|
+
peak = max((abs(s) for s in f), default=0.0)
|
|
89
|
+
crest = peak / level if level > 0 else 0.0
|
|
90
|
+
out.append({"i": i, "t": i / rate, "level": level * ${scale}, "peak": peak, "crest": crest})
|
|
91
|
+
return [o for o in out if o["level"] > 0.001 and o["crest"] < 30.0]
|
|
92
|
+
`;
|
|
93
|
+
const JS = (name, pad = 2) => `export function ${name}(rows, widths) {
|
|
94
|
+
const cells = rows.map((r) => r.map((c, i) => String(c).padEnd(widths[i] ?? 0)));
|
|
95
|
+
const lines = cells.map((r) => r.join(' | ').trimEnd());
|
|
96
|
+
const rule = widths.map((w) => '-'.repeat(Math.max(w, 3))).join('-|-');
|
|
97
|
+
const head = lines.shift() ?? '';
|
|
98
|
+
const body = lines.filter((l) => l.length > 0).map((l) => \` \${l}\`);
|
|
99
|
+
return [head, rule, ...body].join('\\n').padEnd(${pad});
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
function selftest() {
|
|
104
|
+
const hook = fileURLToPath(import.meta.url);
|
|
105
|
+
if (!findJscpd([process.cwd()])) {
|
|
106
|
+
console.error('once-only selftest FAILED: jscpd not found (npm install the kit with its optional dependency, or set CLAUDE_PACKS_JSCPD)');
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const dir = scratchProject({
|
|
110
|
+
'src/core/Filter.cpp': `#include "Filter.h"\n\n${FN('Filter')}`,
|
|
111
|
+
'tools/meter.py': `import math\n\n${PY('meter')}`,
|
|
112
|
+
'tools/table.mjs': `// table\n${JS('table')}`,
|
|
113
|
+
'design/shape.json': JSON.stringify({ concepts: {}, exclude: ['src/gen/**'] }),
|
|
114
|
+
});
|
|
115
|
+
const at = (f) => join(dir, f);
|
|
116
|
+
const w = (f, text) => runHook(hook, dir, writeOf(at(f), text));
|
|
117
|
+
const sh = (cmd) => runHook(hook, dir, bashOf(cmd, dir));
|
|
118
|
+
const filter = readFileSync(at('src/core/Filter.cpp'), 'utf8');
|
|
119
|
+
return verdicts('once-only', [
|
|
120
|
+
['a copy of a function with one line changed, in a new file (Write)', w('src/core/Filter2.cpp', `#include "Filter2.h"\n\n${FN('Filter2', '1.5f')}`), 2],
|
|
121
|
+
['a copy with its middle line changed (the two halves are one near-miss clone)', w('src/core/Mid.cpp', FN('Mid').replace('const float a2 = g * a1;', 'const float a2 = 0.5f * g * a1;')), 2],
|
|
122
|
+
['a copy put into a header (.h and .cpp are one language)', w('src/core/Inline.h', `#pragma once\n\n${FN('Inline', '1.7f')}`), 2],
|
|
123
|
+
['the same code calling the function instead', w('src/core/Mixer.cpp', '#include "Filter.h"\n\nfloat Mixer::render (float x) noexcept { return filter.process (x) * gain; }\n'), 0],
|
|
124
|
+
['a near-copy added to the same file (Edit)', runHook(hook, dir, editOf(at('src/core/Filter.cpp'), '#include "Filter.h"\n', `#include "Filter.h"\n\n${FN('Shelf', '1.0f')}`)), 2],
|
|
125
|
+
['a heredoc that writes a copy (Bash)', sh(`cat > src/core/Copy.cpp <<'EOF'\n${FN('Copy', '0.5f')}EOF`), 2],
|
|
126
|
+
['cp of a source file (Bash)', sh('cp src/core/Filter.cpp src/core/Again.cpp'), 2],
|
|
127
|
+
['mv of a source file: its old place is gone, nothing is repeated', sh('mv src/core/Filter.cpp src/core/Moved.cpp'), 0],
|
|
128
|
+
['a Python copy with one value changed', w('tools/meter2.py', `import math\n\n${PY('meter2', '0.25')}`), 2],
|
|
129
|
+
['a JS copy with one value changed', w('tools/table2.mjs', `// table 2\n${JS('table2', 4)}`), 2],
|
|
130
|
+
['editing a function in place (its own old text is not a match)', w('src/core/Filter.cpp', filter.replace('2.0f - 2.0f * resonance', '1.8f - 2.0f * resonance')), 0],
|
|
131
|
+
['two sed -i edits of one file in one command (KWB-13: each was read against the disk, two versions judged as copies)', sh("sed -i 's/2.0f - 2.0f/1.9f - 2.0f/' src/core/Filter.cpp && sed -i '1a // tuned' src/core/Filter.cpp"), 0],
|
|
132
|
+
['moving a function within its file in one write', w('src/core/Filter.cpp', `${FN('Filter')}\n#include "Filter.h"\n`), 0],
|
|
133
|
+
['a shape.json "exclude" path', w('src/gen/Filter3.cpp', FN('Filter3', '1.1f')), 0],
|
|
134
|
+
['a generated file', w('src/core/Tables.cpp', `// generated by tools/make_tables.py: do not edit\n${FN('Tables', '1.2f')}`), 0],
|
|
135
|
+
['resources', w('src/Resources/Filter4.cpp', FN('Filter4', '1.3f')), 0],
|
|
136
|
+
['a short block of under 6 lines repeated', w('src/core/Small.cpp', 'float Small::process (float x) noexcept\n{\n return x * gain;\n}\n'), 0],
|
|
137
|
+
['a file outside the project', runHook(hook, dir, writeOf(join(tmpdir(), 'other-project-x', 'Filter5.cpp'), FN('Filter5', '1.4f'))), 0],
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function main(text) {
|
|
142
|
+
const payload = readPayload(text);
|
|
143
|
+
if (!payload) return 0;
|
|
144
|
+
const r = judge(payload);
|
|
145
|
+
if (r.code === 2) process.stderr.write(`${r.message}\n`);
|
|
146
|
+
return r.code;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
|
|
150
|
+
if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('once-only', main));
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse (Write|Edit|MultiEdit|NotebookEdit|Bash; every route, lib/writes.mjs): every kind of thing goes in its one place, so the rules for
|
|
3
|
+
// it load. layout.kinds says what goes where; a kind may name file patterns ("**/*.wgsl") and a
|
|
4
|
+
// content pattern (processBlock, wgpu calls, paint(Graphics)). A file of that kind, or an edit that
|
|
5
|
+
// ADDS that content, outside the kind's concept folders is refused. Content is read in C, C++ and shader files, or in the
|
|
6
|
+
// kind's own "extensions" (a TypeScript kind: [".ts", ".tsx"]; KWB-36). `--selftest` proves it.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { extname, relative } from 'node:path';
|
|
9
|
+
import { writesOf } from './lib/writes.mjs';
|
|
10
|
+
import { matchesAny } from './lib/glob.mjs';
|
|
11
|
+
import { conceptGlobs, readLayout } from './lib/layout.mjs';
|
|
12
|
+
import { insideProject, checkoutRoot, claudeRoot } from './lib/project.mjs';
|
|
13
|
+
import { verdicts } from './lib/selftest.mjs';
|
|
14
|
+
import { asHook, isMain } from './lib/hook-log.mjs';
|
|
15
|
+
|
|
16
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
17
|
+
const CODE = new Set(['.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.mm', '.m', '.wgsl', '.metal', '.hlsl']);
|
|
18
|
+
|
|
19
|
+
const home = (layout, kind) => (kind.concepts ?? []).flatMap((c) => conceptGlobs(layout, c) ?? []);
|
|
20
|
+
const where = (layout, kind) => layout.folders?.[kind.in]?.path ?? kind.in;
|
|
21
|
+
|
|
22
|
+
export function misplaced(layout, rel, before, after) {
|
|
23
|
+
for (const [name, kind] of Object.entries(layout.kinds ?? {})) {
|
|
24
|
+
const globs = home(layout, kind);
|
|
25
|
+
if (globs.length === 0 || matchesAny(rel, globs)) continue;
|
|
26
|
+
const byName = kind.files && matchesAny(rel, kind.files) && before === '';
|
|
27
|
+
let byContent = false;
|
|
28
|
+
if (kind.content && (kind.extensions ? kind.extensions.includes(extname(rel)) : CODE.has(extname(rel)))) {
|
|
29
|
+
const re = new RegExp(kind.content);
|
|
30
|
+
byContent = re.test(after) && !re.test(before);
|
|
31
|
+
}
|
|
32
|
+
if (byName || byContent) return { name, kind, where: where(layout, kind), globs };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function main(text) {
|
|
38
|
+
let payload;
|
|
39
|
+
try { payload = JSON.parse(text); } catch { return 0; }
|
|
40
|
+
for (const w of writesOf(payload)) {
|
|
41
|
+
if (w.op === 'delete' || !insideProject(w.path)) continue;
|
|
42
|
+
const rel = relative(checkoutRoot(w.path), w.path).replace(/\\/g, '/');
|
|
43
|
+
// an unknown result (a program's output) is judged by name only
|
|
44
|
+
const hit = misplaced(readLayout(claudeRoot(w.path)), rel, w.before, w.after ?? w.before);
|
|
45
|
+
if (hit) return refuse(rel, hit);
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function refuse(rel, hit) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`BLOCKED by placement-law: ${rel} holds ${hit.kind.what} (kind "${hit.name}").\n` +
|
|
53
|
+
`That goes in ${hit.where}/, where its rules load (${hit.globs.join(', ')}).\n` +
|
|
54
|
+
'Put it there instead. If it truly belongs elsewhere, ask the user to change .claude/layout.json, then `npx --no-install @thedecipherist/claude-packs update`.\n',
|
|
55
|
+
);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function selftest() {
|
|
60
|
+
const layout = {
|
|
61
|
+
folders: { audio: { path: 'src/audio' }, painter: { path: 'src/painter' }, tests: { path: 'tests' }, adapters: { path: 'api/adapters' } },
|
|
62
|
+
concepts: { audio: ['audio'], painter: ['painter'], tests: ['tests'], adapters: ['adapters'] },
|
|
63
|
+
kinds: {
|
|
64
|
+
'audio-entry': { what: 'processBlock', in: 'audio', concepts: ['audio', 'tests'], content: '\\bprocessBlock\\s*\\(' },
|
|
65
|
+
shader: { what: 'WGSL', in: 'painter', concepts: ['painter'], files: ['**/*.wgsl'] },
|
|
66
|
+
'db-gateway': { what: 'the database driver', in: 'adapters', concepts: ['adapters'], content: "from\\s+'mongodb'", extensions: ['.ts'] },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const cases = [
|
|
70
|
+
['processBlock added outside audio', misplaced(layout, 'src/app/Helper.cpp', '', 'void processBlock (A& b) {}') !== null],
|
|
71
|
+
['processBlock in audio is fine', misplaced(layout, 'src/audio/Engine.cpp', '', 'void processBlock (A& b) {}') === null],
|
|
72
|
+
['processBlock already there is not re-judged', misplaced(layout, 'src/app/Old.cpp', 'processBlock (x);', 'processBlock (x); y();') === null],
|
|
73
|
+
['a new shader outside painter', misplaced(layout, 'src/app/glow.wgsl', '', 'fn main() {}') !== null],
|
|
74
|
+
['a shader in painter is fine', misplaced(layout, 'src/painter/glow.wgsl', '', 'fn main() {}') === null],
|
|
75
|
+
['a comment in docs is not code', misplaced(layout, 'docs/notes.md', '', 'processBlock (x)') === null],
|
|
76
|
+
['a kind with its own extensions: the driver in a handler (KWB-36)', misplaced(layout, 'api/handlers/user.ts', '', "import { MongoClient } from 'mongodb';") !== null],
|
|
77
|
+
['the driver in the adapters is fine', misplaced(layout, 'api/adapters/db.ts', '', "import { MongoClient } from 'mongodb';") === null],
|
|
78
|
+
['a kind with extensions leaves other files alone', misplaced(layout, 'api/handlers/notes.md', '', "from 'mongodb'") === null],
|
|
79
|
+
];
|
|
80
|
+
return verdicts('placement-law', cases.map(([label, ok]) => [label, ok, true]));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
|
|
84
|
+
if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('placement-law', main));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse (Bash): what lands in git stays in its history for ever. `git add` of a file over the size limit, a working screenshot in the repo root or a debug
|
|
3
|
+
// dump is refused. One real repo carried 991 MB, with 22 screenshots in its root and 64 MB files. Limits are in layout.json "guards": { "repoLitter": { "maxFileMb": 10,
|
|
4
|
+
// "allow": ["<glob>"] } }. `--selftest` proves it.
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { statSync } from 'node:fs';
|
|
7
|
+
import { join, relative, resolve } from 'node:path';
|
|
8
|
+
import { gitAdds } from './lib/git-adds.mjs';
|
|
9
|
+
import { matchesAny } from './lib/glob.mjs';
|
|
10
|
+
import { readLayout } from './lib/layout.mjs';
|
|
11
|
+
import { checkoutRoot, claudeRoot } from './lib/project.mjs';
|
|
12
|
+
import { bashOf, git, put, runHook, scratchProject, verdicts } from './lib/selftest.mjs';
|
|
13
|
+
import { judgeEvent } from './lib/writes.mjs';
|
|
14
|
+
import { asHook, isMain } from './lib/hook-log.mjs';
|
|
15
|
+
|
|
16
|
+
export const BIG_MB = 10;
|
|
17
|
+
const MB = 1024 * 1024;
|
|
18
|
+
const KEPT_IMAGE = /^(favicon|apple-touch-icon|logo|icon|og-image|social|android-chrome|mstile|safari-pinned)/i;
|
|
19
|
+
const DUMP = /\.(har|heapsnapshot|dmp|core|log)$/i;
|
|
20
|
+
|
|
21
|
+
export const isRootImage = (rel, allow) => /^[^/]+\.(png|jpe?g|gif|webp|bmp|mov|mp4)$/i.test(rel) && !KEPT_IMAGE.test(rel) && !matchesAny(rel, allow);
|
|
22
|
+
|
|
23
|
+
function faultOf(rel, abs, cfg) {
|
|
24
|
+
if (matchesAny(rel, cfg.allow ?? [])) return null;
|
|
25
|
+
if (isRootImage(rel, [])) return `${rel} looks like a working screenshot in the repo root. Keep it out of git, or put an asset in its folder.`;
|
|
26
|
+
if (/:Zone\.Identifier$/.test(rel)) return `${rel} is a browser download marker. It is an invalid path on Windows and breaks checkout. Add *:Zone.Identifier to .gitignore.`;
|
|
27
|
+
if (DUMP.test(rel)) return `${rel} is a log or a debug dump. Add it to .gitignore.`;
|
|
28
|
+
let mb = 0;
|
|
29
|
+
try { mb = statSync(abs).size / MB; } catch { return null; }
|
|
30
|
+
const limit = cfg.maxFileMb ?? BIG_MB;
|
|
31
|
+
return mb > limit ? `${rel} is ${Math.round(mb)} MB (limit ${limit}). Git keeps it for ever: use object storage or a release.` : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A named file, or what a folder would stage.
|
|
35
|
+
function staged(dir, arg) {
|
|
36
|
+
const r = spawnSync('git', ['ls-files', '--others', '--modified', '--exclude-standard', '--', arg], { cwd: dir, encoding: 'utf8' });
|
|
37
|
+
return (r.stdout ?? '').split('\n').filter(Boolean).map((f) => resolve(dir, f));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function judgeAdd(command, cwd) {
|
|
41
|
+
for (const { dir, args } of gitAdds(command, cwd)) {
|
|
42
|
+
const root = checkoutRoot(join(dir, 'x'));
|
|
43
|
+
const cfg = readLayout(claudeRoot(join(dir, 'x'))).guards?.repoLitter ?? {};
|
|
44
|
+
for (const abs of args.filter((a) => !a.startsWith('-')).flatMap((a) => staged(dir, a))) {
|
|
45
|
+
const fault = faultOf(relative(root, abs).replace(/\\/g, '/'), abs, cfg);
|
|
46
|
+
if (fault) return `${fault}\nA real asset can be named in guards.repoLitter.allow.\n`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const main = (text) => judgeEvent(text, { name: 'repo-litter', command: judgeAdd });
|
|
53
|
+
|
|
54
|
+
function selftest() {
|
|
55
|
+
const hook = new URL(import.meta.url).pathname;
|
|
56
|
+
const dir = scratchProject({ 'src/a.pdf:Zone.Identifier': 'x', 'shot.png': 'x', 'favicon.png': 'x', 'src/a.ts': 'x', 'src/hero.png': 'x', 'trace.har': 'x', 'kept.png': 'x', '.claude/layout.json': JSON.stringify({ guards: { repoLitter: { maxFileMb: 1, allow: ['kept.png'] } } }) });
|
|
57
|
+
git(dir, 'init', '-q');
|
|
58
|
+
put(join(dir, 'media/big.bin'), 'x'.repeat(2 * MB));
|
|
59
|
+
const add = (paths) => runHook(hook, dir, bashOf(`git add ${paths}`, dir));
|
|
60
|
+
return verdicts('repo-litter', [
|
|
61
|
+
['a screenshot in the root', add('shot.png'), 2],
|
|
62
|
+
['a big file', add('media/big.bin'), 2],
|
|
63
|
+
['a folder holding a big file', add('media'), 2],
|
|
64
|
+
['a debug dump', add('trace.har'), 2],
|
|
65
|
+
['a download marker', add('src/a.pdf:Zone.Identifier'), 2],
|
|
66
|
+
['source', add('src/a.ts'), 0],
|
|
67
|
+
['an image in its folder', add('src/hero.png'), 0],
|
|
68
|
+
['a favicon in the root', add('favicon.png'), 0],
|
|
69
|
+
['a file the layout allows', add('kept.png'), 0],
|
|
70
|
+
['something else entirely', runHook(hook, dir, bashOf('git status', dir)), 0],
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
|
|
75
|
+
if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('repo-litter', main));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// UserPromptSubmit: repetition is cheapest to stop before a line is drafted. When a prompt's words name something the repo already has, what exists
|
|
3
|
+
// goes in front of the prompt, so it is called and never written again. Only real matches, six at most; a prompt that matches nothing adds nothing.
|
|
4
|
+
// `--selftest` proves it.
|
|
5
|
+
import { TESTS, skippedBy } from './lib/elements.mjs';
|
|
6
|
+
import { readLayout } from './lib/layout.mjs';
|
|
7
|
+
import { checkoutRoot, claudeRoot } from './lib/project.mjs';
|
|
8
|
+
import { repoIndex } from './lib/repo-index.mjs';
|
|
9
|
+
import { scratchProject, verdicts } from './lib/selftest.mjs';
|
|
10
|
+
import { asHook, isMain } from './lib/hook-log.mjs';
|
|
11
|
+
|
|
12
|
+
const SHOWN = 6;
|
|
13
|
+
const COMMON = new Set(`this that with from have make need want when what where which their there then them they will would should could into only also more
|
|
14
|
+
some just like been were does each every code file files function method class value data line test tests work works using used change update write read
|
|
15
|
+
please thing things sure right added adding`.split(/\s+/));
|
|
16
|
+
|
|
17
|
+
const stem = (w) => w.toLowerCase().replace(/(?:ing|ers|er|es|ed|s)$/, '');
|
|
18
|
+
const piecesOf = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[\s_-]+/).filter((w) => w.length >= 3).map(stem);
|
|
19
|
+
const NEAR = 5; // the pieces of a name lie within this many words of each other: scattered over a long prompt they are chance
|
|
20
|
+
const LONG = 150; // words: past this a prompt is a document, and any name can be spelled out of it
|
|
21
|
+
|
|
22
|
+
// { word: [its places in the prompt] }.
|
|
23
|
+
function placesOf(prompt) {
|
|
24
|
+
const places = new Map();
|
|
25
|
+
(prompt.match(/[A-Za-z]+/g) ?? []).forEach((w, i) => {
|
|
26
|
+
if (w.length < 4 || COMMON.has(w.toLowerCase())) return;
|
|
27
|
+
const k = stem(w);
|
|
28
|
+
places.set(k, [...(places.get(k) ?? []), i]);
|
|
29
|
+
});
|
|
30
|
+
return places;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const closeTogether = (pieces, places) => pieces.some((p) => (places.get(p) ?? []).some((at) => pieces.every((q) => (places.get(q) ?? []).some((x) => Math.abs(x - at) <= NEAR))));
|
|
34
|
+
|
|
35
|
+
// A compound name, with all its pieces in the prompt, close together. One word in common says nothing.
|
|
36
|
+
export function matches(prompt, index) {
|
|
37
|
+
if (prompt.split(/\s+/).length > LONG) return [];
|
|
38
|
+
const places = placesOf(prompt);
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const [file, e] of Object.entries(index)) {
|
|
41
|
+
for (const [name, line] of e.defs ?? []) {
|
|
42
|
+
const pieces = piecesOf(name);
|
|
43
|
+
if (pieces.length >= 2 && closeTogether(pieces, places)) out.push({ name, at: `${file}:${line}`, size: pieces.length });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.sort((a, b) => b.size - a.size).slice(0, SHOWN);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main(text) {
|
|
50
|
+
const event = JSON.parse(text || '{}');
|
|
51
|
+
const at = `${event.cwd ?? process.cwd()}/x`;
|
|
52
|
+
const found = matches(event.prompt ?? '', repoIndex(checkoutRoot(at), [...TESTS, ...skippedBy(readLayout(claudeRoot(at)))], readLayout(claudeRoot(at))));
|
|
53
|
+
if (found.length) console.log(`Already in this repo, call it rather than write it again:\n${found.map((f) => `- ${f.name} (${f.at})`).join('\n')}`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function selftest() {
|
|
58
|
+
const dir = scratchProject({ 'src/dates.ts': 'export function formatDateRange(a, b) { return a + b; }\n', 'src/a.py': 'def parse_price_table(text):\n return text\n', 'src/one.ts': 'export function render(a) { return a; }\n' });
|
|
59
|
+
const index = repoIndex(dir, []);
|
|
60
|
+
const named = (prompt) => matches(prompt, index).map((m) => m.name).join();
|
|
61
|
+
return verdicts('reuse-hint', [
|
|
62
|
+
['a prompt naming what exists, camelCase', named('add a page that will format the date range of a booking'), 'formatDateRange'],
|
|
63
|
+
['a prompt naming what exists, snake_case', named('parse the price table from the upload'), 'parse_price_table'],
|
|
64
|
+
['it says where', matches('format the date range', index)[0]?.at, 'src/dates.ts:1'],
|
|
65
|
+
['one word in common says nothing', named('the date of the last deploy'), ''],
|
|
66
|
+
['a one-word name is never matched', named('render the render view'), ''],
|
|
67
|
+
['an empty prompt', named(''), ''],
|
|
68
|
+
['the pieces far apart', named('format the page first, then later on, much further down, show a date and a range'), ''],
|
|
69
|
+
['a long document', named(`format the date range ${'and more words here '.repeat(40)}`), ''],
|
|
70
|
+
['a prompt about nothing here', named('rename the settings page title'), ''],
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
|
|
75
|
+
if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('reuse-hint', main));
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// run-hooks: one node process runs every kit hook of one settings entry, instead of one node process per hook (KWB-24:
|
|
3
|
+
// The user saw 17 hooks take 48 s on one rm; each was its own node start, all at once, on a loaded machine). settings.json
|
|
4
|
+
// names the runner and its hooks in one command: node ".../run-hooks.mjs" .claude/hooks/a.mjs .claude/hooks/b.mjs
|
|
5
|
+
// Each hook module exports hook(text) (the event's JSON text, as stdin was) and returns its exit code; the runner calls
|
|
6
|
+
// them in turn, catches each one's stdout and stderr, writes each one's timing line to .claude/hooks.log (lib/hook-log.mjs)
|
|
7
|
+
// and its own (hook "run-hooks": the whole process, node's start-up included), and exits 2 if any hook refused. A hook that
|
|
8
|
+
// throws, or exits with anything but 0 or 2, crashed, and lib/crash-policy.mjs decides what the call does (KF-18: a crash
|
|
9
|
+
// on a tool call refuses once, and says so). lib/unread-writes.mjs says when no hook could read what a command writes (KF-40).
|
|
10
|
+
// Every hook still runs on its own too (node .claude/hooks/a.mjs, its selftest, a project's hand-made entry).
|
|
11
|
+
// node run-hooks.mjs <hook path or name>... (stdin: the event)
|
|
12
|
+
// node run-hooks.mjs --selftest
|
|
13
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, writeSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
|
+
import { crashVerdict } from './lib/crash-policy.mjs';
|
|
18
|
+
import { logCall } from './lib/event-log.mjs';
|
|
19
|
+
import { appendRotated, isMain, logHook, projectRoot, readHookLog } from './lib/hook-log.mjs';
|
|
20
|
+
import { unreadWrites } from './lib/unread-writes.mjs';
|
|
21
|
+
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
export const RUNNER = 'run-hooks';
|
|
24
|
+
export const nameOf = (arg) => basename(arg).replace(/\.mjs$/, '');
|
|
25
|
+
const fileOf = (arg) => (isAbsolute(arg) ? arg : join(HERE, `${nameOf(arg)}.mjs`));
|
|
26
|
+
|
|
27
|
+
// Several hooks' stdout: one is passed as it is; JSON objects are merged (their messages joined), anything else joined by lines.
|
|
28
|
+
export function mergeOut(outs) {
|
|
29
|
+
const said = outs.filter((o) => o.trim());
|
|
30
|
+
if (said.length < 2) return said[0] ?? '';
|
|
31
|
+
const objs = said.map((o) => { try { const v = JSON.parse(o); return v && typeof v === 'object' && !Array.isArray(v) ? v : null; } catch { return null; } });
|
|
32
|
+
if (objs.some((o) => o === null)) return said.map((o) => o.replace(/\n$/, '')).join('\n') + '\n';
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const o of objs) {
|
|
35
|
+
for (const [k, v] of Object.entries(o)) {
|
|
36
|
+
if (k === 'hookSpecificOutput' && out[k]) {
|
|
37
|
+
const ctx = [out[k].additionalContext, v.additionalContext].filter(Boolean).join('\n');
|
|
38
|
+
out[k] = { ...out[k], ...v, ...(ctx ? { additionalContext: ctx } : {}) };
|
|
39
|
+
} else if (typeof v === 'string' && typeof out[k] === 'string' && ['systemMessage', 'reason', 'stopReason'].includes(k)) out[k] = `${out[k]}\n${v}`;
|
|
40
|
+
else out[k] = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(out);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const capture = (stream, sink) => {
|
|
47
|
+
const write = stream.write;
|
|
48
|
+
stream.write = (chunk, ...rest) => { sink.push(String(chunk)); const cb = rest.find((r) => typeof r === 'function'); if (cb) cb(); return true; };
|
|
49
|
+
return () => { stream.write = write; };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// -> { code, stdout, stderr }. Each hook's own time is logged (its module's first import included).
|
|
53
|
+
export async function runAll(args, text, root = projectRoot()) {
|
|
54
|
+
let ev = {};
|
|
55
|
+
try { ev = JSON.parse(text) ?? {}; } catch { /* the hooks read it themselves */ }
|
|
56
|
+
const outs = [];
|
|
57
|
+
const err = [];
|
|
58
|
+
const judged = [];
|
|
59
|
+
let code = 0;
|
|
60
|
+
for (const arg of args) {
|
|
61
|
+
const name = nameOf(arg);
|
|
62
|
+
const out = [];
|
|
63
|
+
const before = err.length;
|
|
64
|
+
const t0 = performance.now();
|
|
65
|
+
const undo = [capture(process.stdout, out), capture(process.stderr, err)];
|
|
66
|
+
let c;
|
|
67
|
+
try {
|
|
68
|
+
const mod = await import(pathToFileURL(fileOf(arg)).href);
|
|
69
|
+
if (typeof mod.hook !== 'function') throw new Error(`${name} exports no hook(text)`);
|
|
70
|
+
c = await mod.hook(text);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
c = 1;
|
|
73
|
+
err.push(`run-hooks: ${name} failed: ${e?.stack ?? e}\n`);
|
|
74
|
+
} finally {
|
|
75
|
+
for (const u of undo) u();
|
|
76
|
+
}
|
|
77
|
+
c = Number.isInteger(c) ? c : 0;
|
|
78
|
+
outs.push(out.join(''));
|
|
79
|
+
const ms = performance.now() - t0;
|
|
80
|
+
logHook(root, { event: ev.hook_event_name, hook: name, ms, code: c, tool: ev.tool_name ?? ev.source });
|
|
81
|
+
judged.push({ hook: name, ms, code: c, reason: err.slice(before).join(''), out: out.join('') });
|
|
82
|
+
if (c === 2) code = 2;
|
|
83
|
+
}
|
|
84
|
+
logCall(root, text, judged, appendRotated, performance.now());
|
|
85
|
+
const crash = crashVerdict(root, ev, judged);
|
|
86
|
+
if (crash.stderr) err.push(crash.stderr);
|
|
87
|
+
if (crash.code === 2) code = 2;
|
|
88
|
+
outs.push(...[crash.stdout, code === 0 ? unreadWrites(ev, root) : ''].filter(Boolean));
|
|
89
|
+
return { code, stdout: mergeOut(outs), stderr: err.join('') };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function selftest() {
|
|
93
|
+
const dir = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), 'run-hooks-'));
|
|
94
|
+
mkdirSync(join(dir, '.claude'));
|
|
95
|
+
const fake = (name, body) => { const f = join(dir, `${name}.mjs`); writeFileSync(f, `export function hook(text) {\n${body}\n}\n`); return f; };
|
|
96
|
+
const ok = fake('says-ok', "process.stdout.write('{\"systemMessage\":\"a\"}'); return 0;");
|
|
97
|
+
const no = fake('refuses', "process.stderr.write('BLOCKED by refuses\\n'); return JSON.parse(text).tool_name === 'Bash' ? 2 : 0;");
|
|
98
|
+
const also = fake('also-says', "console.log(JSON.stringify({ systemMessage: 'b' })); return 0;");
|
|
99
|
+
const boom = fake('throws', "throw new Error('planted');");
|
|
100
|
+
const bash = JSON.stringify({ hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'rm x' } });
|
|
101
|
+
const cases = [];
|
|
102
|
+
const r1 = await runAll([ok, no, also], bash, dir);
|
|
103
|
+
cases.push(['a refusal is the runner\'s exit 2', r1.code === 2]);
|
|
104
|
+
cases.push(['its message reaches stderr', r1.stderr.includes('BLOCKED by refuses')]);
|
|
105
|
+
cases.push(['two JSON outputs merge into one', JSON.parse(r1.stdout).systemMessage === 'a\nb']);
|
|
106
|
+
const r2 = await runAll([ok, no], JSON.stringify({ hook_event_name: 'PreToolUse', tool_name: 'Write' }), dir);
|
|
107
|
+
cases.push(['no refusal: exit 0', r2.code === 0 && r2.stdout === '{"systemMessage":"a"}']);
|
|
108
|
+
const r3 = await runAll([boom, ok], bash, dir);
|
|
109
|
+
cases.push(['a hook that throws refuses the call, says so, and the next still runs', r3.code === 2 && r3.stderr.includes('throws crashed') && r3.stdout.includes('"a"')]);
|
|
110
|
+
const r4 = await runAll([boom, ok], bash, dir);
|
|
111
|
+
cases.push(['the same call again goes through, and says so', r4.code === 0 && r4.stdout.includes('throws crashed')]);
|
|
112
|
+
const rows = readHookLog(dir);
|
|
113
|
+
cases.push(['one timing line per hook per call', rows.length === 9 && rows.filter((r) => r.hook === 'refuses').map((r) => r.code).join() === '2,0']);
|
|
114
|
+
cases.push(['the line names the event and the tool', rows[0].event === 'PreToolUse' && rows[0].tool === 'Bash']);
|
|
115
|
+
const lines = (f) => readFileSync(join(dir, '.claude/logs', f), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
116
|
+
const refused = lines('refusals.jsonl');
|
|
117
|
+
cases.push(['every call is a line of events.jsonl', lines('events.jsonl').length === 4]);
|
|
118
|
+
cases.push(['a refusal keeps its reason and what was tried', refused[0].hook === 'refuses' && refused[0].reason.includes('BLOCKED by refuses') && refused[0].command === 'rm x']);
|
|
119
|
+
cases.push(['a crash is kept, in its own log', !refused.some((r) => r.hook === 'throws') && lines('crashes.jsonl').some((r) => r.hook === 'throws' && r.code === 1)]);
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
const bad = cases.filter(([, good]) => !good);
|
|
122
|
+
for (const [label] of bad) console.error(`run-hooks selftest FAILED: ${label}`);
|
|
123
|
+
if (!bad.length) console.log(`run-hooks selftest: OK (${cases.length} cases)`);
|
|
124
|
+
return bad.length ? 1 : 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isMain(import.meta.url)) {
|
|
128
|
+
const args = process.argv.slice(2);
|
|
129
|
+
if (args.includes('--selftest')) process.exit(await selftest());
|
|
130
|
+
let text = '';
|
|
131
|
+
try { text = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
|
|
132
|
+
const root = projectRoot();
|
|
133
|
+
const r = await runAll(args, text, root);
|
|
134
|
+
let ev = {};
|
|
135
|
+
try { ev = JSON.parse(text) ?? {}; } catch { /* logged as '-' */ }
|
|
136
|
+
logHook(root, { event: ev.hook_event_name, hook: RUNNER, ms: performance.now(), code: r.code, tool: ev.tool_name ?? ev.source });
|
|
137
|
+
// written at once, then out: a hook may leave a timer or a child behind, and the event never waits on it
|
|
138
|
+
for (const [fd, s] of [[1, r.stdout], [2, r.stderr]]) if (s) try { writeSync(fd, s); } catch { /* a closed pipe */ }
|
|
139
|
+
process.exit(r.code);
|
|
140
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// secrets-guard (PreToolUse, Write|Edit|MultiEdit|NotebookEdit|Bash): a secret never lands in git (KWB-33; STEP 3: ".env
|
|
3
|
+
// never committed"). Refused, by every write route (lib/writes.mjs) and on `git add`:
|
|
4
|
+
// 1. staging a secret file: `git add` (with -f too) of a .env (.env.example, .sample, .template and .dist are fine), a key
|
|
5
|
+
// file (*.pem, *.key, *.p12, *.pfx, *.jks, *.keystore, id_rsa and its kin) or credentials.json; and `git add -A` / `.` /
|
|
6
|
+
// `--all` / a folder while such a file in the checkout is not ignored;
|
|
7
|
+
// 2. a write that ADDS a literal secret to any file but an ignored .env: a GitHub, Anthropic, OpenAI, AWS, Slack or Stripe
|
|
8
|
+
// key, a private key block, a connection string carrying its password (a ${VAR} or $VAR in its place is fine);
|
|
9
|
+
// 3. writing a .env that git does not ignore (add `.env` to .gitignore first).
|
|
10
|
+
// A token is named on the command that uses it ($PERSONAL_GITHUB_TOKEN), never written. Another project's files are not judged.
|
|
11
|
+
// node secrets-guard.mjs --selftest
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
15
|
+
import { judgeEvent } from './lib/writes.mjs';
|
|
16
|
+
import { checkoutRoot } from './lib/project.mjs';
|
|
17
|
+
import { bashOf, git, runHook, scratchProject, verdicts, writeOf } from './lib/selftest.mjs';
|
|
18
|
+
import { gitAdds } from './lib/git-adds.mjs';
|
|
19
|
+
import { SECRETS } from './lib/secret-patterns.mjs';
|
|
20
|
+
import { asHook, isMain } from './lib/hook-log.mjs';
|
|
21
|
+
|
|
22
|
+
const ENV = /^\.env(\..+)?$/;
|
|
23
|
+
const ENV_OK = /\.(example|sample|template|dist)$/;
|
|
24
|
+
const KEY = /\.(pem|key|private|p12|pfx|jks|keystore)$|^id_(rsa|dsa|ecdsa|ed25519)$|^credentials\.json$/;
|
|
25
|
+
export const isSecretFile = (p) => { const b = basename(p); return (ENV.test(b) && !ENV_OK.test(b)) || KEY.test(b); };
|
|
26
|
+
const isEnv = (p) => ENV.test(basename(p)) && !ENV_OK.test(basename(p));
|
|
27
|
+
|
|
28
|
+
export { SECRETS };
|
|
29
|
+
const count = (text, re) => (text.match(re) ?? []).length;
|
|
30
|
+
export const addedSecret = (before, after) => SECRETS.find(([, re]) => count(after, re) > count(before ?? '', re))?.[0] ?? null;
|
|
31
|
+
|
|
32
|
+
function ignored(root, path) {
|
|
33
|
+
const r = spawnSync('git', ['check-ignore', '-q', relative(root, path)], { cwd: root });
|
|
34
|
+
if (r.status === 0 || r.status === 1) return r.status === 0;
|
|
35
|
+
const gi = join(root, '.gitignore'); // not a git repo: read .gitignore
|
|
36
|
+
return existsSync(gi) && readFileSync(gi, 'utf8').split('\n').some((l) => /^\/?(\.env\*?|\.env\.\*|\*\.env|\.env\/?)\s*$/.test(l.trim()) && ENV.test(basename(path)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function judgeWrite(w) {
|
|
40
|
+
const root = checkoutRoot(w.path);
|
|
41
|
+
if (isEnv(w.path)) return ignored(root, w.path) ? null : `${relative(root, w.path)} is a .env file git does not ignore. Add \`.env\` (and \`.env.*\`, with !.env.example) to .gitignore first; a .env is never committed.`;
|
|
42
|
+
if (w.after === null) return null;
|
|
43
|
+
const what = addedSecret(w.before, w.after);
|
|
44
|
+
return what ? `this write puts ${what} into ${relative(root, w.path)}. A secret lives in an ignored .env (or the machine's own store) and is read at run time; code reads it by name (process.env.X), a command names it ($X). Take the value out.` : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `git add` of a secret file by name, or a sweep (-A, ., --all, a folder) while one is not ignored.
|
|
48
|
+
function judgeAdd(command, cwd) {
|
|
49
|
+
for (const { dir, args: raw } of gitAdds(command, cwd)) {
|
|
50
|
+
const args = raw.filter((a) => !a.startsWith('-') || ['-A', '--all'].includes(a));
|
|
51
|
+
const named = args.find((a) => isSecretFile(a));
|
|
52
|
+
if (named) return `\`git add ${named}\`: a secret file is never staged (.env, keys, credentials). Keep it ignored; commit a .env.example with the names and no values.`;
|
|
53
|
+
if (args.some((a) => ['-A', '--all', '.', '*'].includes(a) || a.endsWith('/'))) {
|
|
54
|
+
const r = spawnSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd: dir, encoding: 'utf8' });
|
|
55
|
+
const loose = (r.stdout ?? '').split('\n').filter((f) => f && isSecretFile(f));
|
|
56
|
+
if (loose.length) return `this \`git add\` would stage ${loose.join(', ')}, a secret file git does not ignore. Add it to .gitignore first, then stage by path.`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const main = (text) => judgeEvent(text, {
|
|
63
|
+
name: 'secrets-guard',
|
|
64
|
+
command: judgeAdd,
|
|
65
|
+
write: (w) => (w.op === 'delete' ? null : judgeWrite(w)),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function selftest() {
|
|
69
|
+
const hook = new URL(import.meta.url).pathname;
|
|
70
|
+
const gh = `gh${'p'}_${'a1B2'.repeat(9)}`; // built here, so this file holds no token
|
|
71
|
+
const pem = `-----BEGIN RSA ${'PRIVATE'} KEY-----`;
|
|
72
|
+
const dir = scratchProject({ '.gitignore': '.env\n', 'src/app.ts': 'export const x = 1;\n', 'src/old.ts': `const t = '${gh}';\nconst x = 1;\n` });
|
|
73
|
+
git(dir, 'init', '-q');
|
|
74
|
+
const open = scratchProject({ 'src/app.ts': '\n' });
|
|
75
|
+
git(open, 'init', '-q');
|
|
76
|
+
const run = (d, payload) => runHook(hook, d, payload);
|
|
77
|
+
const cases = [
|
|
78
|
+
['a GitHub token written into source', run(dir, writeOf(join(dir, 'src/app.ts'), `const t = '${gh}';\n`)), 2],
|
|
79
|
+
['a private key written into a doc', run(dir, writeOf(join(dir, 'docs/key.md'), `${pem}\n${'MIIEvQIBADANBgkqhkiG9w0BAQEFAASC'.repeat(3)}\n`)), 2],
|
|
80
|
+
['a bare header in a doc is a placeholder', run(dir, writeOf(join(dir, 'docs/format.md'), `${pem}\n...\n`)), 0],
|
|
81
|
+
['a connection string with its password', run(dir, writeOf(join(dir, 'src/db.ts'), "const u = 'mongodb+srv://user:hunter22@c0.x.net/db';\n")), 2],
|
|
82
|
+
['a connection string that names its password ($VAR)', run(dir, writeOf(join(dir, 'src/db.ts'), 'const u = `mongodb+srv://user:${process.env.PW}@c0.x.net/db`;\n')), 0],
|
|
83
|
+
['code that reads the token by name', run(dir, writeOf(join(dir, 'src/app.ts'), 'const t = process.env.GITHUB_TOKEN;\n')), 0],
|
|
84
|
+
['the token into an ignored .env', run(dir, writeOf(join(dir, '.env'), `GITHUB_TOKEN=${gh}\n`)), 0],
|
|
85
|
+
['a .env git does not ignore', run(open, writeOf(join(open, '.env'), 'A=1\n')), 2],
|
|
86
|
+
['a .env.example with names only', run(open, writeOf(join(open, '.env.example'), 'GITHUB_TOKEN=\n')), 0],
|
|
87
|
+
['git add .env', run(dir, bashOf('git add .env src/app.ts', dir)), 2],
|
|
88
|
+
['git add -f of a key file', run(dir, bashOf('git add -f deploy/server.pem', dir)), 2],
|
|
89
|
+
['git add by path, no secret', run(dir, bashOf('git add src/app.ts', dir)), 0],
|
|
90
|
+
['git add -A while a .env is not ignored', (spawnSync('touch', [join(open, '.env')]), run(open, bashOf('git add -A', open))), 2],
|
|
91
|
+
['git add -A while the .env is ignored', (spawnSync('touch', [join(dir, '.env')]), run(dir, bashOf('git add -A', dir))), 0],
|
|
92
|
+
['an edit beside a token already there adds none', run(dir, { tool_name: 'Edit', tool_input: { file_path: join(dir, 'src/old.ts'), old_string: 'x = 1', new_string: 'x = 2' } }), 0],
|
|
93
|
+
['git -C <dir> add of a .env', run(dir, bashOf(`git -C ${dir} add .env`, dir)), 2],
|
|
94
|
+
];
|
|
95
|
+
return verdicts('secrets-guard', cases);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
|
|
99
|
+
if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : await asHook('secrets-guard', main));
|