@thedecipherist/claude-packs 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +680 -0
- package/LICENSE +21 -0
- package/README.md +316 -2
- package/bin/claude-packs.mjs +204 -0
- package/configurations/base/config.json +85 -0
- package/configurations/cpp/config.json +92 -0
- package/configurations/full-stack/config.json +152 -0
- package/configurations/go/config.json +96 -0
- package/configurations/node/config.json +100 -0
- package/configurations/python/config.json +97 -0
- package/configurations/synth-creator/config.json +148 -0
- package/global/hooks/gnu-shell.mjs +131 -0
- package/global/hooks/lib/program-read.mjs +196 -0
- package/global/hooks/lib/program-writes.mjs +177 -0
- package/global/hooks/lib/shell-writes.mjs +293 -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/claude-md.mjs +58 -0
- package/lib/config-cli.mjs +36 -0
- package/lib/configs.mjs +145 -0
- package/lib/coverage.mjs +86 -0
- package/lib/custom-packs.mjs +93 -0
- package/lib/doctor.mjs +115 -0
- package/lib/fix-plan.mjs +71 -0
- package/lib/git-tokens.mjs +64 -0
- package/lib/global.mjs +125 -0
- package/lib/hook-proof.mjs +28 -0
- package/lib/hook-timing.mjs +42 -0
- package/lib/hover-gate.mjs +29 -0
- package/lib/kit-lock.mjs +149 -0
- package/lib/layout.mjs +75 -0
- package/lib/logs.mjs +69 -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 +107 -0
- package/lib/planner.mjs +59 -0
- package/lib/repeats.mjs +65 -0
- package/lib/repo-weight.mjs +28 -0
- package/lib/rules.mjs +40 -0
- package/lib/scan-detect.mjs +94 -0
- package/lib/scan.mjs +168 -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 +18 -0
- package/lib/switches.mjs +44 -0
- package/lib/symmetry-doctor.mjs +26 -0
- package/lib/sync.mjs +130 -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 +129 -0
- package/packs/audio-core/hooks/realtime-guard.mjs +67 -0
- package/packs/audio-core/pack.json +187 -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 +26 -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 +21 -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 +21 -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 +23 -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 +95 -0
- package/packs/base/hooks/dispatch-once.mjs +87 -0
- package/packs/base/hooks/file-size-guard.mjs +61 -0
- package/packs/base/hooks/folder-law.mjs +74 -0
- package/packs/base/hooks/git-guard.mjs +289 -0
- package/packs/base/hooks/lesson-checks.mjs +174 -0
- package/packs/base/hooks/lib/apply-edit.mjs +20 -0
- package/packs/base/hooks/lib/code-shapes.mjs +137 -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/dup-index.mjs +199 -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 +67 -0
- package/packs/base/hooks/lib/function-shapes.mjs +90 -0
- package/packs/base/hooks/lib/git-adds.mjs +20 -0
- package/packs/base/hooks/lib/git-guard-selftest.mjs +99 -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 +66 -0
- package/packs/base/hooks/lib/layout.mjs +54 -0
- package/packs/base/hooks/lib/making-check-selftest.mjs +109 -0
- package/packs/base/hooks/lib/near-copies.mjs +76 -0
- package/packs/base/hooks/lib/program-read.mjs +196 -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/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.mjs +294 -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 +81 -0
- package/packs/base/hooks/lib/writes.mjs +71 -0
- package/packs/base/hooks/making-check.mjs +243 -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 +132 -0
- package/packs/base/hooks/secrets-guard.mjs +99 -0
- package/packs/base/hooks/shape-law.mjs +133 -0
- package/packs/base/hooks/suite-once.mjs +80 -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 +18 -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 +22 -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/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/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 +21 -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 +25 -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/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 +21 -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 +21 -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/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/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/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 +174 -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 +19 -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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// shell-writes: the files a shell command writes, and what it writes into them where that can be known.
|
|
2
|
+
// Every guard that refuses a write judges the shell's writes through this, so no guard has a route the
|
|
3
|
+
// Write tool lacks (WB-69: a guard covered Write and Edit while `cat > file <<EOF` walked past it).
|
|
4
|
+
//
|
|
5
|
+
// shellWrites(command, cwd) -> [{ path, op, content, from?, sed? }]
|
|
6
|
+
// op: write (>, >|, &>, tee), append (>>, &>>, tee -a), copy (cp, install, rsync), move (mv; its source
|
|
7
|
+
// is a delete), link (ln), edit (sed -i, perl -i, ruby -i), truncate, touch, delete (rm, unlink, shred, mv's source)
|
|
8
|
+
// and an inline program's own writes (python -c / python - <<EOF, node -e, perl -e, ruby -e: program-writes.mjs,
|
|
9
|
+
// KWB-8); a script file run by an interpreter (python3 tools/x.py) is the project's own tool, not a write.
|
|
10
|
+
// content: the text written (an echo or printf, a heredoc or here-string, a cat of known files, what a pipe
|
|
11
|
+
// carries into tee, the source of a cp or mv), or null when it cannot be known.
|
|
12
|
+
// afterText(write) -> { before, after } after is null when it cannot be known; sed -i is run for real on a
|
|
13
|
+
// copy of the text, in GNU sed's --sandbox (no e, r or w commands), so its result is exact.
|
|
14
|
+
// `node shell-writes.mjs --json` reads { command, cwd } on stdin and prints the writes with before and after
|
|
15
|
+
// (the python hooks use this, so there is one reader of the shell). `--selftest`: each route and program form.
|
|
16
|
+
// Text inside quotes, and a heredoc's body, is never read as a command.
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
19
|
+
import { basename, join, resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { programOf, programWrites } from './program-writes.mjs';
|
|
22
|
+
|
|
23
|
+
const HOME = process.env.HOME ?? '';
|
|
24
|
+
// A path that starts with a variable is where that variable points, never a folder of this name in the project (KWB-44). One not set is somewhere unknown.
|
|
25
|
+
const expand = (p) => p.replace(/^(~|\$HOME|\$\{HOME\})(?=\/|$)/, HOME).replace(/^\$\{?([A-Za-z_]\w*)\}?(?=\/|$)/, (_, name) => process.env[name] ?? `/unresolved-${name}`);
|
|
26
|
+
const NOT_FILES = new Set(['/dev/null', '/dev/stdout', '/dev/stderr', '/dev/tty']);
|
|
27
|
+
|
|
28
|
+
export function readText(path) {
|
|
29
|
+
try {
|
|
30
|
+
const st = statSync(path);
|
|
31
|
+
if (!st.isFile() || st.size > 8 * 1024 * 1024) return null;
|
|
32
|
+
return readFileSync(path, 'utf8');
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
38
|
+
|
|
39
|
+
// Heredoc bodies are cut out of the command and kept, in order, for the command that reads them.
|
|
40
|
+
function cutHeredocs(command) {
|
|
41
|
+
const lines = command.split('\n');
|
|
42
|
+
const out = [];
|
|
43
|
+
const bodies = [];
|
|
44
|
+
const MARK = /(?<!<)<<(?!<)(-?)\s*(['"]?)([A-Za-z_][\w-]*)\2/g;
|
|
45
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
46
|
+
const marks = [...lines[i].matchAll(MARK)];
|
|
47
|
+
let n = bodies.length;
|
|
48
|
+
out.push(lines[i].replace(MARK, () => ` <<HEREDOC${n++} `));
|
|
49
|
+
for (const m of marks) {
|
|
50
|
+
const body = [];
|
|
51
|
+
const end = (l) => (m[1] ? l.replace(/^\t+/, '') : l) === m[3];
|
|
52
|
+
while (i + 1 < lines.length && !end(lines[i + 1])) body.push(m[1] ? lines[++i].replace(/^\t+/, '') : lines[++i]);
|
|
53
|
+
i += 1;
|
|
54
|
+
bodies.push(body.length ? `${body.join('\n')}\n` : '');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { text: out.join('\n'), bodies };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Words and operators, quotes removed from words; a heredoc is its marker word.
|
|
61
|
+
function lex(text) {
|
|
62
|
+
const tokens = [];
|
|
63
|
+
let cur = null;
|
|
64
|
+
let i = 0;
|
|
65
|
+
const push = () => { if (cur !== null) tokens.push({ w: cur }); cur = null; };
|
|
66
|
+
while (i < text.length) {
|
|
67
|
+
const c = text[i];
|
|
68
|
+
if (c === "'") {
|
|
69
|
+
const j = text.indexOf("'", i + 1);
|
|
70
|
+
cur = (cur ?? '') + text.slice(i + 1, j < 0 ? text.length : j);
|
|
71
|
+
i = j < 0 ? text.length : j + 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === '"') {
|
|
75
|
+
let j = i + 1;
|
|
76
|
+
let s = '';
|
|
77
|
+
while (j < text.length && text[j] !== '"') {
|
|
78
|
+
if (text[j] === '\\' && j + 1 < text.length && '"\\$`'.includes(text[j + 1])) { s += text[j + 1]; j += 2; } else s += text[j++];
|
|
79
|
+
}
|
|
80
|
+
cur = (cur ?? '') + s;
|
|
81
|
+
i = j + 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (c === '\\' && i + 1 < text.length) {
|
|
85
|
+
if (text[i + 1] !== '\n') cur = (cur ?? '') + text[i + 1];
|
|
86
|
+
i += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (c === '\n') { push(); tokens.push({ op: ';' }); i += 1; continue; }
|
|
90
|
+
if (/\s/.test(c)) { push(); i += 1; continue; }
|
|
91
|
+
if (c === '#' && cur === null) { while (i < text.length && text[i] !== '\n') i += 1; continue; }
|
|
92
|
+
const rest = text.slice(i);
|
|
93
|
+
const redir = rest.match(/^(&>>|&>|>>|>\||>&\d*-?|>|<<<|<<HEREDOC(\d+)|<)/);
|
|
94
|
+
if (redir && (cur === null || /^\d+$/.test(cur))) {
|
|
95
|
+
const fd = cur ?? '';
|
|
96
|
+
cur = null;
|
|
97
|
+
tokens.push({ op: redir[2] !== undefined ? '<<' : redir[1].startsWith('>&') ? '>&' : redir[1], fd, doc: redir[2] !== undefined ? Number(redir[2]) : undefined });
|
|
98
|
+
i += redir[0].length;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const ctl = rest.match(/^(&&|\|\||\|&|\||;|&|\(|\)|`|\$\()/);
|
|
102
|
+
if (ctl) { push(); tokens.push({ op: ctl[1] === '|&' ? '|' : ctl[1] }); i += ctl[1].length; continue; }
|
|
103
|
+
cur = (cur ?? '') + c;
|
|
104
|
+
i += 1;
|
|
105
|
+
}
|
|
106
|
+
push();
|
|
107
|
+
return tokens;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const PREFIX = new Set(['sudo', 'env', 'command', 'builtin', 'nohup', 'time', 'exec', 'nice']);
|
|
111
|
+
|
|
112
|
+
function printf(args) {
|
|
113
|
+
if (!args.length) return '';
|
|
114
|
+
const esc = (s) => s.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\\\/g, '\\');
|
|
115
|
+
const [fmt, ...vals] = args;
|
|
116
|
+
const slots = (fmt.match(/%[sdbq]/g) ?? []).length;
|
|
117
|
+
if (!slots) return esc(fmt);
|
|
118
|
+
let out = '';
|
|
119
|
+
let k = 0;
|
|
120
|
+
do {
|
|
121
|
+
out += esc(fmt).replace(/%[sdbq]/g, () => vals[k++] ?? '');
|
|
122
|
+
} while (k < vals.length);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// One simple command: its words, redirections and stdin, and what it writes.
|
|
127
|
+
function judge(cmd, cwd, stdin, writes, bodies) {
|
|
128
|
+
let words = [...cmd.words];
|
|
129
|
+
while (words.length && (PREFIX.has(words[0]) || /^[A-Za-z_]\w*=/.test(words[0]) || (words[0] === 'timeout' && words.length > 1))) words = words[0] === 'timeout' ? words.slice(2) : words.slice(1);
|
|
130
|
+
const at = (p) => resolve(cwd, expand(p));
|
|
131
|
+
let input = stdin;
|
|
132
|
+
for (const r of cmd.redirs) {
|
|
133
|
+
if (r.op === '<<') input = bodies[r.doc] ?? null;
|
|
134
|
+
else if (r.op === '<<<') input = `${r.target}\n`;
|
|
135
|
+
else if (r.op === '<') input = readText(at(r.target));
|
|
136
|
+
}
|
|
137
|
+
const exe = words.length ? basename(words[0]) : '';
|
|
138
|
+
const args = words.slice(1);
|
|
139
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
140
|
+
const plain = args.filter((a) => !a.startsWith('-'));
|
|
141
|
+
let stdout = null;
|
|
142
|
+
if (exe === 'echo') {
|
|
143
|
+
const n = args[0] === '-n' || args[0] === '-ne' || args[0] === '-en';
|
|
144
|
+
const e = /^-[ne]*e/.test(args[0] ?? '');
|
|
145
|
+
const text = args.filter((a, k) => !(k === 0 && /^-[neE]+$/.test(a))).join(' ');
|
|
146
|
+
stdout = (e ? text.replace(/\\n/g, '\n').replace(/\\t/g, '\t') : text) + (n ? '' : '\n');
|
|
147
|
+
} else if (exe === 'printf') stdout = printf(args);
|
|
148
|
+
else if (exe === 'cat') stdout = plain.length ? plain.map((f) => readText(at(f))).reduce((a, b) => (a === null || b === null ? null : a + b), '') : input;
|
|
149
|
+
else if (exe === 'tee') stdout = input;
|
|
150
|
+
else if (exe === '' && cmd.redirs.some((r) => r.op === '<<' || r.op === '<<<')) stdout = input;
|
|
151
|
+
else if (exe === ':' || exe === 'true') stdout = '';
|
|
152
|
+
|
|
153
|
+
const add = (path, op, content, extra = {}) => { if (!NOT_FILES.has(path)) writes.push({ path, op, content, ...extra }); };
|
|
154
|
+
for (const r of cmd.redirs) {
|
|
155
|
+
if (!['>', '>>', '>|', '&>', '&>>'].includes(r.op) || !r.target) continue;
|
|
156
|
+
const toStdout = r.fd === '' || r.fd === '1' || r.op.startsWith('&');
|
|
157
|
+
add(at(r.target), r.op.includes('>>') ? 'append' : 'write', toStdout ? stdout : null);
|
|
158
|
+
}
|
|
159
|
+
if (exe === 'tee') for (const f of plain) add(at(f), flags.some((x) => /^-\w*a/.test(x) || x === '--append') ? 'append' : 'write', input);
|
|
160
|
+
if (['cp', 'mv', 'install', 'rsync', 'ln'].includes(exe)) {
|
|
161
|
+
const t = args.findIndex((a) => a === '-t' || a === '--target-directory');
|
|
162
|
+
const dirArg = t >= 0 ? args[t + 1] : null;
|
|
163
|
+
const files = plain.filter((a) => a !== dirArg && !(['install'].includes(exe) && /^\d{3,4}$/.test(a)));
|
|
164
|
+
const srcs = dirArg ? files : files.slice(0, -1);
|
|
165
|
+
const dest = dirArg ?? files.at(-1);
|
|
166
|
+
if (dest && srcs.length) {
|
|
167
|
+
for (const s of srcs) {
|
|
168
|
+
const into = dirArg || srcs.length > 1 || dest.endsWith('/') || isDir(at(dest));
|
|
169
|
+
const target = into ? join(at(dest), basename(s)) : at(dest);
|
|
170
|
+
const op = exe === 'mv' ? 'move' : exe === 'ln' ? 'link' : 'copy';
|
|
171
|
+
add(target, op, exe === 'ln' ? null : readText(at(s)), { from: at(s) });
|
|
172
|
+
if (exe === 'mv') add(at(s), 'delete', null);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (exe === 'sed' && args.some((a) => a === '-i' || a.startsWith('-i') || a.startsWith('--in-place'))) {
|
|
177
|
+
const scripts = [];
|
|
178
|
+
const keep = [];
|
|
179
|
+
const files = [];
|
|
180
|
+
for (let k = 0; k < args.length; k += 1) {
|
|
181
|
+
const a = args[k];
|
|
182
|
+
if (a === '-i' && args[k + 1] === '') { k += 1; continue; } // BSD: -i ''
|
|
183
|
+
if (a === '-e' || a === '--expression') { scripts.push(args[++k]); continue; }
|
|
184
|
+
if (a.startsWith('--expression=')) { scripts.push(a.slice(13)); continue; }
|
|
185
|
+
if (a === '-f' || a === '--file') { scripts.push(null); k += 1; continue; }
|
|
186
|
+
if (/^-[^-]/.test(a)) { for (const f of a.slice(1).replace(/i.*$/, '')) if ('Ernsz'.includes(f)) keep.push(`-${f}`); continue; }
|
|
187
|
+
if (a.startsWith('--')) { if (['--regexp-extended', '--quiet', '--silent', '--separate', '--null-data'].includes(a)) keep.push(a); continue; }
|
|
188
|
+
if (!scripts.length) scripts.push(a); else files.push(a);
|
|
189
|
+
}
|
|
190
|
+
for (const f of files) add(at(f), 'edit', null, { sed: scripts.includes(null) ? null : { flags: keep, scripts } });
|
|
191
|
+
}
|
|
192
|
+
if ((exe === 'perl' || exe === 'ruby') && args.some((a) => /^-\w*i/.test(a))) {
|
|
193
|
+
const files = [];
|
|
194
|
+
for (let k = 0; k < args.length; k += 1) {
|
|
195
|
+
if (/^-\w*[eE]$/.test(args[k])) { k += 1; continue; } // -e, -pe, -ne: the next word is the program
|
|
196
|
+
if (!args[k].startsWith('-')) files.push(args[k]);
|
|
197
|
+
}
|
|
198
|
+
for (const f of files) add(at(f), 'edit', null);
|
|
199
|
+
}
|
|
200
|
+
const program = programOf(exe, args, input);
|
|
201
|
+
if (program) for (const w of programWrites(program, at, readText)) add(w.path, w.op, w.content, w.from ? { from: w.from } : {});
|
|
202
|
+
if (exe === 'dd') for (const a of args) if (a.startsWith('of=')) add(at(a.slice(3)), 'write', null);
|
|
203
|
+
if (exe === 'truncate') {
|
|
204
|
+
const k = args.findIndex((a) => a === '-s' || a.startsWith('--size'));
|
|
205
|
+
const size = k >= 0 ? (args[k].includes('=') ? args[k].split('=')[1] : args[k + 1]) : null;
|
|
206
|
+
for (const f of plain.filter((a) => a !== size)) add(at(f), 'truncate', size === '0' ? '' : null);
|
|
207
|
+
}
|
|
208
|
+
if (exe === 'touch') for (const f of plain) add(at(f), 'touch', null);
|
|
209
|
+
if (['rm', 'unlink', 'shred'].includes(exe)) for (const f of plain) add(at(f), 'delete', null);
|
|
210
|
+
return { stdout, cd: exe === 'cd' && plain[0] ? at(plain[0]) : null };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function shellWrites(command = '', cwd = process.cwd()) {
|
|
214
|
+
const { text, bodies } = cutHeredocs(command);
|
|
215
|
+
const writes = [];
|
|
216
|
+
let here = cwd;
|
|
217
|
+
let cmd = { words: [], redirs: [] };
|
|
218
|
+
let pipeIn = null;
|
|
219
|
+
const tokens = lex(text);
|
|
220
|
+
// a variable set earlier in the command ("S=/tmp/x; cat > $S/a.md") names that folder, not "<cwd>/$S" (KWB-18); one never set
|
|
221
|
+
// stays as written
|
|
222
|
+
const vars = {};
|
|
223
|
+
const sub = (w) => w.replace(/\$(?:\{(\w+)\}|(\w+))/g, (m, a, b) => vars[a ?? b] ?? m);
|
|
224
|
+
const flush = () => {
|
|
225
|
+
if (cmd.words.length && !cmd.redirs.length && cmd.words.every((w) => /^[A-Za-z_]\w*=/.test(w))) {
|
|
226
|
+
for (const w of cmd.words) { const i = w.indexOf('='); vars[w.slice(0, i)] = sub(w.slice(i + 1)); }
|
|
227
|
+
cmd = { words: [], redirs: [] };
|
|
228
|
+
pipeIn = null;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
cmd = { words: cmd.words.map(sub), redirs: cmd.redirs.map((r) => (r.target === undefined ? r : { ...r, target: sub(r.target) })) };
|
|
232
|
+
if (cmd.words.length || cmd.redirs.length) {
|
|
233
|
+
const r = judge(cmd, here, pipeIn, writes, bodies);
|
|
234
|
+
if (r.cd) here = r.cd;
|
|
235
|
+
pipeIn = r.stdout;
|
|
236
|
+
} else pipeIn = null;
|
|
237
|
+
cmd = { words: [], redirs: [] };
|
|
238
|
+
};
|
|
239
|
+
for (let k = 0; k < tokens.length; k += 1) {
|
|
240
|
+
const t = tokens[k];
|
|
241
|
+
if (t.w !== undefined) { cmd.words.push(t.w); continue; }
|
|
242
|
+
if (['>', '>>', '>|', '&>', '&>>', '<', '<<<'].includes(t.op)) {
|
|
243
|
+
const next = tokens[k + 1];
|
|
244
|
+
if (next?.w !== undefined) { cmd.redirs.push({ op: t.op, fd: t.fd, target: next.w }); k += 1; }
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (t.op === '<<') { cmd.redirs.push({ op: '<<', doc: t.doc }); continue; }
|
|
248
|
+
if (t.op === '>&') continue;
|
|
249
|
+
if (t.op === '|') { flush(); continue; } // the next command reads this one's output
|
|
250
|
+
flush();
|
|
251
|
+
pipeIn = null;
|
|
252
|
+
}
|
|
253
|
+
flush();
|
|
254
|
+
return writes;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let gnuSed;
|
|
258
|
+
function findGnuSed() {
|
|
259
|
+
if (gnuSed !== undefined) return gnuSed;
|
|
260
|
+
for (const s of ['/opt/homebrew/opt/gnu-sed/libexec/gnubin/sed', '/usr/local/opt/gnu-sed/libexec/gnubin/sed', 'gsed', 'sed']) {
|
|
261
|
+
const r = spawnSync(s, ['--version'], { encoding: 'utf8', timeout: 2000 });
|
|
262
|
+
if (r.status === 0 && /GNU sed/.test(r.stdout)) return (gnuSed = s);
|
|
263
|
+
}
|
|
264
|
+
return (gnuSed = null);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function afterText(w, before = readText(w.path) ?? '') {
|
|
268
|
+
switch (w.op) {
|
|
269
|
+
case 'write': case 'copy': case 'move': case 'truncate': return { before, after: w.content };
|
|
270
|
+
case 'append': return { before, after: w.content === null ? null : before + w.content };
|
|
271
|
+
case 'touch': return { before, after: before };
|
|
272
|
+
case 'edit': {
|
|
273
|
+
const sed = w.sed && findGnuSed();
|
|
274
|
+
if (!sed) return { before, after: null };
|
|
275
|
+
const r = spawnSync(sed, ['--sandbox', ...w.sed.flags, ...w.sed.scripts.flatMap((s) => ['-e', s])], { input: before, encoding: 'utf8', timeout: 3000 });
|
|
276
|
+
return { before, after: r.status === 0 ? r.stdout : null };
|
|
277
|
+
}
|
|
278
|
+
default: return { before, after: null };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const direct = process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
283
|
+
if (direct && process.argv.includes('--selftest')) import('./shell-writes-selftest.mjs').then((m) => { process.exitCode = m.selftest(); });
|
|
284
|
+
if (process.argv[1] && process.argv.includes('--json') && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
285
|
+
let raw = '';
|
|
286
|
+
process.stdin.on('data', (c) => { raw += c; });
|
|
287
|
+
process.stdin.on('end', () => {
|
|
288
|
+
let q = {};
|
|
289
|
+
try { q = JSON.parse(raw || '{}'); } catch { /* nothing to read */ }
|
|
290
|
+
const out = shellWrites(q.command ?? '', q.cwd ?? process.cwd()).map((w) => ({ path: w.path, op: w.op, ...afterText(w) }));
|
|
291
|
+
process.stdout.write(JSON.stringify(out));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// shell-traps (PreToolUse, Bash; global by design): refuses the shell traps that cost time on this Mac, with the right form.
|
|
3
|
+
//
|
|
4
|
+
// "make a skill that triggers the moment you touch bash" (the skill: shell-rules). A skill is read only
|
|
5
|
+
// when remembered; this hook sees every command. It refuses (exit 2):
|
|
6
|
+
// - BSD flags on the GNU tools first on PATH: `date -r <seconds>`, `stat -f`, `sed -i ''` (WB-23);
|
|
7
|
+
// - an in-place perl edit of a code file (apprack B-135: twice it wrote its text at the top of the file);
|
|
8
|
+
// - a `for` loop over an unquoted variable while the session runs zsh (the words are not split there, WB-41);
|
|
9
|
+
// - `;` chaining a destructive step (checkout --detach, rm -rf, kill) after another command (WB-19).
|
|
10
|
+
// Text inside heredoc bodies and quoted strings is ignored.
|
|
11
|
+
// node shell-traps.mjs --selftest
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { basename } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export function noHeredocs(cmd) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const lines = cmd.split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
19
|
+
out.push(lines[i]);
|
|
20
|
+
const m = lines[i].match(/<<-?\s*['"]?(\w+)['"]?/);
|
|
21
|
+
if (m) { i += 1; while (i < lines.length && lines[i].trim() !== m[1]) i += 1; }
|
|
22
|
+
}
|
|
23
|
+
return out.join('\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const bodyOf = (cmd) => noHeredocs(cmd).replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "''");
|
|
27
|
+
const zshSession = () => basename(process.env.SHELL ?? '').includes('zsh') && !(process.env.CLAUDE_CODE_SHELL ?? '').includes('bash');
|
|
28
|
+
|
|
29
|
+
export function verdict(cmd, zsh = zshSession()) {
|
|
30
|
+
const b = bodyOf(cmd);
|
|
31
|
+
if (/(^|[\s;&|(])date\s+(-\w*\s+)*-r\s+\$?\(?[\d@$]/.test(b)) {
|
|
32
|
+
return "`date -r <seconds>` is the BSD form; here date is GNU (-r means a FILE's time). Use: date -d @<seconds> '+%Y-%m-%d %H:%M %Z'";
|
|
33
|
+
}
|
|
34
|
+
if (/(^|[\s;&|(])stat\s+(-\w+\s+)*-f\s/.test(b)) {
|
|
35
|
+
return '`stat -f` is the BSD form; here stat is GNU (-f is file-SYSTEM info). Use `stat -c %Y file`, or better Python os.stat for a measurement (WB-23).';
|
|
36
|
+
}
|
|
37
|
+
if (/(^|[\s;&|(])sed\s+-i\s+(''|"")(\s|$)/.test(noHeredocs(cmd))) return "`sed -i ''` is the BSD form; here sed is GNU: `sed -i 's/a/b/' file`."; // the real quotes
|
|
38
|
+
if (/(^|[\s;&|(])perl\s+(-\w+\s+)*-\w*i\w*\b[^;&|]*\.(ts|tsx|js|mjs|cpp|cc|c|h|hpp|py|cmake)\b/.test(b)) {
|
|
39
|
+
return 'an in-place perl edit of a code file: twice it wrote its text at the top of the file instead of in place (apprack B-135). Change code with the Edit tool; perl -pi stays for data files.';
|
|
40
|
+
}
|
|
41
|
+
if (zsh && /\bfor\s+\w+\s+in\s+\$\w+\s*;/.test(b)) {
|
|
42
|
+
return 'this session runs zsh: `for x in $VAR` does not split words. Use `${=VAR}`, an explicit list, or a script file run with bash. Restart Claude Code to get bash 5 (WB-41).';
|
|
43
|
+
}
|
|
44
|
+
if (/;\s*(GUARD_OK=1\s+)?git\s+checkout\s+-q?\s*--detach|;\s*rm\s+-rf\s|;\s*kill\s/.test(b)) {
|
|
45
|
+
return 'a destructive step chained with `;` runs even when the step before it failed (WB-19). Chain with `&&`, or run it as its own command after checking the first.';
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function selftest() {
|
|
51
|
+
const cases = [
|
|
52
|
+
["TZ=America/New_York date -r 1797000000 '+%F'", false, true], ["date -d @1797000000 '+%F'", false, false],
|
|
53
|
+
["stat -f '%m %N' x", false, true], ['stat -c %Y x', false, false], ["sed -i '' 's/a/b/' f", false, true],
|
|
54
|
+
["sed -i 's/a/b/' f", false, false], ['for t in $LIST; do echo $t; done', true, true],
|
|
55
|
+
['for t in $LIST; do echo $t; done', false, false], ['for t in a b; do echo $t; done', true, false],
|
|
56
|
+
['python3 x.py; GUARD_OK=1 git checkout -q --detach', false, true], ['python3 x.py && git checkout -q --detach', false, false],
|
|
57
|
+
["echo 'date -r 5 and stat -f'", false, false], ["cat <<'EOF'\nsed -i '' x\nEOF", false, false],
|
|
58
|
+
["perl -0pi -e 's|a|b|' src/readonly.ts", false, true], ["cd x && perl -pi -e 's/a/b/' lib/a.cpp", false, true],
|
|
59
|
+
["perl -pi -e 's/a/b/' docs/not-carried.md", false, false], ["perl -0pi -e 's/a/b/' build.json", false, false],
|
|
60
|
+
["perl -ne 'print' src/a.ts", false, false],
|
|
61
|
+
];
|
|
62
|
+
const bad = cases.filter(([cmd, zsh, want]) => (verdict(cmd, zsh) !== null) !== want);
|
|
63
|
+
for (const [cmd, zsh, want] of bad) console.error(`shell-traps selftest FAILED: ${want ? 'not refused' : 'wrongly refused'}: ${cmd.replace(/\n/g, ' / ')}${zsh ? ' (zsh)' : ''}`);
|
|
64
|
+
if (!bad.length) console.log(`shell-traps selftest: OK (${cases.length} cases)`);
|
|
65
|
+
return bad.length ? 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function main() {
|
|
69
|
+
if (process.argv.includes('--selftest')) return selftest();
|
|
70
|
+
let ev;
|
|
71
|
+
try { ev = JSON.parse(readFileSync(0, 'utf8')); } catch { return 0; }
|
|
72
|
+
if (ev.tool_name !== 'Bash') return 0;
|
|
73
|
+
const v = verdict(String(ev.tool_input?.command ?? ''));
|
|
74
|
+
if (!v) return 0;
|
|
75
|
+
process.stderr.write(`shell-traps: ${v}\n`);
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
process.exit(main());
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// version-pin-guard: refuses a new or changed version pin unless docs/versions.md has a row
|
|
3
|
+
// for that dependency checked within MAX_AGE days. Pairs with the latest-versions skill.
|
|
4
|
+
// PreToolUse on Write|Edit|MultiEdit|Bash. Exit 2 blocks; stderr goes back to Claude. Every route that writes a
|
|
5
|
+
// dependency file is judged (WB-69): the Write tool, an Edit or MultiEdit (its new_string taken literally), an install
|
|
6
|
+
// command, and the shell writing the file (>, >>, tee, sed -i, cp, mv, a heredoc; lib/shell-writes.mjs).
|
|
7
|
+
// `node version-pin-guard.mjs --selftest` proves every extractor and the age check.
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
10
|
+
import { afterText, shellWrites } from './lib/shell-writes.mjs';
|
|
11
|
+
|
|
12
|
+
const MAX_AGE = Number(process.env.VERSION_GUARD_DAYS ?? 14);
|
|
13
|
+
const DAY = 86_400_000;
|
|
14
|
+
|
|
15
|
+
// ---------- extractors: text -> Map(name -> version) ----------
|
|
16
|
+
function fromPackageJson(text) {
|
|
17
|
+
const out = new Map();
|
|
18
|
+
let json;
|
|
19
|
+
try { json = JSON.parse(text); } catch { return out; }
|
|
20
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
21
|
+
for (const [n, v] of Object.entries(json[key] ?? {})) out.set(n, String(v));
|
|
22
|
+
}
|
|
23
|
+
if (json.packageManager) out.set(json.packageManager.split('@')[0], json.packageManager);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fromCmake(text) {
|
|
28
|
+
const out = new Map();
|
|
29
|
+
for (const m of text.matchAll(/FetchContent_Declare\s*\(\s*([\w.-]+)([^)]*)\)/gi)) {
|
|
30
|
+
const v = m[2].match(/\b(?:GIT_TAG|URL)\s+("?)([^\s")]+)\1/i);
|
|
31
|
+
if (v) out.set(m[1], v[2]);
|
|
32
|
+
}
|
|
33
|
+
for (const m of text.matchAll(/CPMAddPackage\s*\(\s*"(?:gh:)?([\w./-]+)@([\w.+-]+)"/gi)) out.set(m[1].split('/').pop(), m[2]);
|
|
34
|
+
for (const m of text.matchAll(/ExternalProject_Add\s*\(\s*([\w.-]+)[^)]*?\bGIT_TAG\s+([^\s)]+)/gi)) out.set(m[1], m[2]);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fromRequirements(text) {
|
|
39
|
+
const out = new Map();
|
|
40
|
+
for (const line of text.split('\n')) {
|
|
41
|
+
const m = line.replace(/#.*/, '').trim().match(/^([A-Za-z0-9_.\-\[\]]+)\s*(==|>=|~=|<=|>|<|===)\s*([^\s;,]+)/);
|
|
42
|
+
if (m) out.set(m[1].replace(/\[.*\]/, ''), `${m[2]}${m[3]}`);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fromPyproject(text) {
|
|
48
|
+
const out = new Map();
|
|
49
|
+
for (const m of text.matchAll(/["']([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(==|>=|~=|<=|>|<)\s*([^"',;\s]+)/g)) out.set(m[1], `${m[2]}${m[3]}`);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fromCargo(text) {
|
|
54
|
+
const out = new Map();
|
|
55
|
+
let inDeps = false;
|
|
56
|
+
for (const line of text.split('\n')) {
|
|
57
|
+
const h = line.match(/^\s*\[([^\]]+)\]/);
|
|
58
|
+
if (h) { inDeps = /dependencies/.test(h[1]); continue; }
|
|
59
|
+
if (!inDeps) continue;
|
|
60
|
+
const m = line.match(/^\s*([\w-]+)\s*=\s*(?:"([^"]+)"|\{[^}]*version\s*=\s*"([^"]+)")/);
|
|
61
|
+
if (m) out.set(m[1], m[2] ?? m[3]);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fromGoMod(text) {
|
|
67
|
+
const out = new Map();
|
|
68
|
+
for (const m of text.matchAll(/^\s*(?:require\s+)?([\w.\-/]+\.[\w.\-/]+)\s+(v[\w.+-]+)/gm)) out.set(m[1], m[2]);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fromDockerfile(text) {
|
|
73
|
+
const out = new Map();
|
|
74
|
+
for (const m of text.matchAll(/^\s*FROM\s+(?:--platform=\S+\s+)?([^\s:@]+)[:@](\S+)/gim)) out.set(m[1].split('/').pop(), m[2]);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fromWorkflow(text) {
|
|
79
|
+
const out = new Map();
|
|
80
|
+
for (const m of text.matchAll(/uses:\s*["']?([\w.-]+\/[\w.-]+)(?:\/[\w./-]+)?@([\w.-]+)/g)) out.set(m[1], m[2]);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The parser for each manifest: by exact file name, else by the first name pattern that fits, else a workflow by its path.
|
|
85
|
+
const BY_NAME = { 'package.json': fromPackageJson, 'CMakeLists.txt': fromCmake, 'pyproject.toml': fromPyproject, 'Cargo.toml': fromCargo, 'go.mod': fromGoMod };
|
|
86
|
+
const BY_PATTERN = [
|
|
87
|
+
[/\.cmake$/, fromCmake],
|
|
88
|
+
[/^requirements.*\.(txt|in)$/, fromRequirements],
|
|
89
|
+
[/^(Dockerfile|Containerfile)(\..+)?$|\.dockerfile$/i, fromDockerfile],
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export function extractorFor(file) {
|
|
93
|
+
const name = basename(file);
|
|
94
|
+
if (Object.hasOwn(BY_NAME, name)) return BY_NAME[name];
|
|
95
|
+
const hit = BY_PATTERN.find(([re]) => re.test(name));
|
|
96
|
+
if (hit) return hit[1];
|
|
97
|
+
return /\.github[\\/]workflows[\\/].+\.ya?ml$/.test(file) ? fromWorkflow : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Pins in `after` that are new or different from `before`.
|
|
101
|
+
export function changedPins(extract, before, after) {
|
|
102
|
+
const old = extract(before);
|
|
103
|
+
return [...extract(after)].filter(([n, v]) => old.get(n) !== v).map(([n, v]) => ({ name: n, version: v }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A shell redirection is never a package: `2>&1`, `&>log`, `>/dev/null`, `> out.txt`, `< in`, and a
|
|
107
|
+
// trailing `&` (0.5.x read `2>&1` as a package named "2" at version "&1" and refused the command).
|
|
108
|
+
const REDIRECT = /(?:^|\s)(?:\d*|&)(?:>>?|<)(?:&\d+|&-|\s*[^\s&|;<>]+)?|\s&(?=\s|$)/g;
|
|
109
|
+
|
|
110
|
+
// A heredoc's body is text, never a command (KWB-3: a script fed to python3 <<'EOF' that held the words
|
|
111
|
+
// "npm install x" in a comment was read as forty installs).
|
|
112
|
+
const HEREDOC = /<<-?\s*(['"]?)([A-Za-z_][\w-]*)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(?=\n|$)/g;
|
|
113
|
+
const PREFIX = /^(sudo|env|command|time|nohup|npx|[A-Za-z_]\w*=\S*)$/;
|
|
114
|
+
|
|
115
|
+
// Package names added by an install command (the install is the command itself, never a word an echo prints).
|
|
116
|
+
export function pinsFromCommand(cmd) {
|
|
117
|
+
const found = [];
|
|
118
|
+
for (const seg of cmd.replace(HEREDOC, ' ').split(/&&|\|\||;|\||\n/)) {
|
|
119
|
+
const words = seg.replace(REDIRECT, ' ').trim().split(/\s+/);
|
|
120
|
+
let lead = 0;
|
|
121
|
+
while (lead < words.length && PREFIX.test(words[lead])) lead += 1;
|
|
122
|
+
const w0 = words[lead] ?? '', w1 = words[lead + 1] ?? '';
|
|
123
|
+
const at = (/^(npm|pnpm|yarn|bun)$/.test(w0) && /^(i|install|add)$/.test(w1)) || (/^(pip|pip3|uv|poetry)$/.test(w0) && /^(install|add)$/.test(w1)) || (w0 === 'cargo' && w1 === 'add') || (w0 === 'go' && w1 === 'get') ? lead : -1;
|
|
124
|
+
if (at === -1) continue;
|
|
125
|
+
for (const w of words.slice(at + 2)) {
|
|
126
|
+
if (w.startsWith('-') || w === 'pip' || w.startsWith('.') || w.startsWith('/')) continue;
|
|
127
|
+
const m = w.match(/^(@?[^@=<>~\s]+)(?:[@=<>~]=?(.+))?$/);
|
|
128
|
+
if (m && !/^(-r|requirements)/.test(m[1])) found.push({ name: m[1], version: m[2] ?? '(unpinned)' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return found;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The folder an install in `cmd` runs in: a `cd <dir>` before it moves it (KWB-3: `cd <scratch repo> && npm install x`
|
|
135
|
+
// was judged by the session folder's docs/versions.md, so a pin in a repo that had its row was refused).
|
|
136
|
+
export function installDir(cmd, cwd) {
|
|
137
|
+
let here = cwd;
|
|
138
|
+
for (const seg of cmd.replace(HEREDOC, ' ').split(/&&|\|\||;|\||\n/)) {
|
|
139
|
+
const m = seg.trim().match(/^(?:cd|pushd)\s+(['"]?)([^'"\s]+)\1$/);
|
|
140
|
+
if (m) { here = resolve(here, m[2].replace(/^(~|\$HOME|\$\{HOME\})(?=\/|$)/, process.env.HOME ?? '~')); continue; }
|
|
141
|
+
if (pinsFromCommand(seg).length) return here;
|
|
142
|
+
}
|
|
143
|
+
return here;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------- docs/versions.md ----------
|
|
147
|
+
export function checkedDates(versionsText) {
|
|
148
|
+
const dates = new Map();
|
|
149
|
+
for (const line of versionsText.split('\n')) {
|
|
150
|
+
if (!line.trim().startsWith('|')) continue;
|
|
151
|
+
const cells = line.split('|').map((c) => c.trim().replace(/`/g, '')).filter(Boolean);
|
|
152
|
+
const date = line.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
153
|
+
if (!cells.length || !date) continue;
|
|
154
|
+
const key = cells[0].toLowerCase();
|
|
155
|
+
const t = Date.parse(date[1]);
|
|
156
|
+
if (!dates.has(key) || dates.get(key) < t) dates.set(key, t);
|
|
157
|
+
}
|
|
158
|
+
return dates;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function lookup(dates, name) {
|
|
162
|
+
const n = name.toLowerCase();
|
|
163
|
+
for (const k of [n, n.split('/').pop(), n.replace(/^@[^/]+\//, '')]) if (dates.has(k)) return dates.get(k);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function verdict(pins, dates, now = Date.now()) {
|
|
168
|
+
const problems = [];
|
|
169
|
+
for (const p of pins) {
|
|
170
|
+
const t = lookup(dates, p.name);
|
|
171
|
+
if (t === undefined) problems.push(`${p.name} ${p.version}: no row in docs/versions.md`);
|
|
172
|
+
else if (now - t > MAX_AGE * DAY) problems.push(`${p.name} ${p.version}: last checked ${new Date(t).toISOString().slice(0, 10)}, over ${MAX_AGE} days ago`);
|
|
173
|
+
}
|
|
174
|
+
return problems;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The project the pin is written in: the nearest folder above it with docs/versions.md or .git
|
|
178
|
+
// (a file in a git worktree), so an agent worktree and a sibling project read their own table.
|
|
179
|
+
// CLAUDE_PROJECT_DIR is only the fallback, and for a worktree that carries no versions.md.
|
|
180
|
+
export function projectRoot(start, env = process.env.CLAUDE_PROJECT_DIR) {
|
|
181
|
+
let d = resolve(start);
|
|
182
|
+
let found = null;
|
|
183
|
+
for (;;) {
|
|
184
|
+
if (existsSync(join(d, '.git')) || existsSync(join(d, 'docs', 'versions.md'))) { found = d; break; }
|
|
185
|
+
const up = dirname(d);
|
|
186
|
+
if (up === d) break;
|
|
187
|
+
d = up;
|
|
188
|
+
}
|
|
189
|
+
if (!found) return env ?? resolve(start);
|
|
190
|
+
const has = (root) => existsSync(join(root, 'docs', 'versions.md'));
|
|
191
|
+
if (!has(found) && env && has(env) && !relative(resolve(env), found).startsWith('..')) return env;
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- hook ----------
|
|
196
|
+
function newText(input, current) {
|
|
197
|
+
if (typeof input.content === 'string') return input.content;
|
|
198
|
+
const edits = input.edits ?? (input.old_string !== undefined ? [input] : []);
|
|
199
|
+
let text = current;
|
|
200
|
+
// literally: a new_string holding `$&` or `$1` was read as a replacement pattern (the Edit gap, WB-69)
|
|
201
|
+
for (const e of edits) text = e.replace_all ? text.split(e.old_string).join(e.new_string ?? '') : text.replace(e.old_string, () => e.new_string ?? '');
|
|
202
|
+
return text;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function main(payload) {
|
|
206
|
+
const input = payload.tool_input ?? {};
|
|
207
|
+
let pins = [];
|
|
208
|
+
let where;
|
|
209
|
+
if (payload.tool_name === 'Bash') {
|
|
210
|
+
pins = pinsFromCommand(input.command ?? '');
|
|
211
|
+
where = installDir(input.command ?? '', payload.cwd ?? process.cwd());
|
|
212
|
+
for (const w of shellWrites(input.command ?? '', where)) {
|
|
213
|
+
const extract = extractorFor(w.path);
|
|
214
|
+
if (!extract) continue;
|
|
215
|
+
const { before, after } = afterText(w);
|
|
216
|
+
if (after !== null) pins.push(...changedPins(extract, before, after));
|
|
217
|
+
where = dirname(w.path);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
const file = input.file_path;
|
|
221
|
+
const extract = file && extractorFor(file);
|
|
222
|
+
if (!extract) return 0;
|
|
223
|
+
const before = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
224
|
+
pins = changedPins(extract, before, newText(input, before));
|
|
225
|
+
where = dirname(file);
|
|
226
|
+
}
|
|
227
|
+
if (pins.length === 0) return 0;
|
|
228
|
+
const vfile = join(projectRoot(where), 'docs', 'versions.md');
|
|
229
|
+
const dates = existsSync(vfile) ? checkedDates(readFileSync(vfile, 'utf8')) : new Map();
|
|
230
|
+
const problems = verdict(pins, dates);
|
|
231
|
+
if (problems.length === 0) return 0;
|
|
232
|
+
process.stderr.write(`version-pin-guard: look up the newest release first (latest-versions skill).\n${problems.map((p) => `- ${p}`).join('\n')}\nAdd or refresh each row in ${vfile} (name | latest stable | YYYY-MM-DD | source URL | used | decision), then pin.\n`);
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function selftest() {
|
|
237
|
+
const eq = (a, b, what) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${what}: got ${JSON.stringify(a)}`); };
|
|
238
|
+
const names = (pins) => pins.map((p) => p.name);
|
|
239
|
+
eq(names(changedPins(fromPackageJson, '{"dependencies":{"a":"1"}}', '{"dependencies":{"a":"1","fastify":"^5.6.0"}}')), ['fastify'], 'package.json add');
|
|
240
|
+
eq(names(changedPins(fromPackageJson, '{"dependencies":{"a":"1"}}', '{"dependencies":{"a":"1"},"x":2}')), [], 'package.json unchanged');
|
|
241
|
+
eq([...fromCmake('FetchContent_Declare(JUCE\n GIT_REPOSITORY https://github.com/juce-framework/JUCE\n GIT_TAG 9.0.4)')], [['JUCE', '9.0.4']], 'cmake GIT_TAG');
|
|
242
|
+
eq([...fromCmake('CPMAddPackage("gh:fmtlib/fmt@11.2.0")')], [['fmt', '11.2.0']], 'cmake CPM');
|
|
243
|
+
eq([...fromRequirements('numpy==2.3.1 # x\n-r base.txt\n')], [['numpy', '==2.3.1']], 'requirements');
|
|
244
|
+
eq([...fromPyproject('dependencies = ["httpx>=0.28", "rich"]')], [['httpx', '>=0.28']], 'pyproject');
|
|
245
|
+
eq([...fromCargo('[package]\nversion = "0.1.0"\n[dependencies]\nserde = { version = "1.0.219", features = ["derive"] }\n')], [['serde', '1.0.219']], 'cargo');
|
|
246
|
+
eq([...fromGoMod('module x\nrequire (\n github.com/spf13/cobra v1.9.1\n)\n')], [['github.com/spf13/cobra', 'v1.9.1']], 'go.mod');
|
|
247
|
+
eq([...fromDockerfile('FROM --platform=linux/amd64 node:24-slim AS build\n')], [['node', '24-slim']], 'dockerfile');
|
|
248
|
+
eq([...fromWorkflow('- uses: actions/checkout@v5\n- uses: github/codeql-action/init@v3')], [['actions/checkout', 'v5'], ['github/codeql-action', 'v3']], 'workflow');
|
|
249
|
+
eq(names(pinsFromCommand('cd x && npm install -D typescript@5.9.3 zod')), ['typescript', 'zod'], 'npm install');
|
|
250
|
+
eq(names(pinsFromCommand('pip install --break-system-packages requests==2.32.5')), ['requests'], 'pip install');
|
|
251
|
+
eq(pinsFromCommand('npm install'), [], 'bare npm install');
|
|
252
|
+
eq(names(pinsFromCommand('npm install fastify 2>&1 | tail -5')), ['fastify'], 'install with 2>&1');
|
|
253
|
+
eq(names(pinsFromCommand('npm i zod > /tmp/npm.log 2>&1')), ['zod'], 'install with > log 2>&1');
|
|
254
|
+
eq(names(pinsFromCommand('pip install requests==2.32.5 &>/dev/null &')), ['requests'], 'install with &>/dev/null &');
|
|
255
|
+
eq(pinsFromCommand('npm install 2>&1'), [], 'bare install with 2>&1');
|
|
256
|
+
eq(pinsFromCommand("python3 - <<'EOF'\n# then npm install zod and pip install rich\nEOF"), [], 'a heredoc body is text');
|
|
257
|
+
eq(pinsFromCommand('echo run npm install zod first'), [], 'an echo that names an install');
|
|
258
|
+
eq(names(pinsFromCommand('sudo npm install -g zod@4')), ['zod'], 'install after a prefix');
|
|
259
|
+
eq(installDir('cd /p/other && npm install zod@4', '/p/here'), '/p/other', 'an install after cd runs there');
|
|
260
|
+
eq(installDir('npm install zod@4 && cd /p/other', '/p/here'), '/p/here', 'a cd after the install does not move it');
|
|
261
|
+
const now = Date.parse('2026-10-26');
|
|
262
|
+
const dates = checkedDates('| name | latest |\n|---|---|\n| fastify | 5.6.1 | 2026-10-20 | https://github.com/fastify/fastify/releases | 5.6.1 | use |\n| `JUCE` | 9.0.4 | 2026-09-01 | url | 8.0.4 | F4 |\n');
|
|
263
|
+
eq(verdict([{ name: 'fastify', version: '5.6.1' }], dates, now), [], 'fresh row passes');
|
|
264
|
+
eq(verdict([{ name: 'JUCE', version: '9.0.4' }], dates, now).length, 1, 'stale row blocks');
|
|
265
|
+
eq(verdict([{ name: 'zod', version: '4' }], dates, now).length, 1, 'missing row blocks');
|
|
266
|
+
eq(verdict([{ name: 'github.com/fastify', version: 'v1' }], dates, now), [], 'last path segment matches');
|
|
267
|
+
console.log('version-pin-guard selftest: OK (26 cases)');
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) {
|
|
272
|
+
if (process.argv.includes('--selftest')) process.exit(selftest());
|
|
273
|
+
let raw = '';
|
|
274
|
+
process.stdin.on('data', (c) => { raw += c; });
|
|
275
|
+
process.stdin.on('end', () => {
|
|
276
|
+
let payload;
|
|
277
|
+
try { payload = JSON.parse(raw || '{}'); } catch { process.exit(0); }
|
|
278
|
+
process.exit(main(payload));
|
|
279
|
+
});
|
|
280
|
+
}
|