voidmachine 0.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -4
- package/bin/void-harness.mjs +12 -0
- package/bin/void-machine.mjs +12 -0
- package/core-assets/.claude-plugin/plugin.json +253 -0
- package/core-assets/.gitkeep +0 -0
- package/core-assets/PHILOSOPHY.md +218 -0
- package/core-assets/PROJECT-DOCTRINE.template.md +14 -0
- package/core-assets/adapters/security/manifest.yaml +96 -0
- package/core-assets/agents/.gitkeep +0 -0
- package/core-assets/agents/accessibility-specialist.md +59 -0
- package/core-assets/agents/api-integration-engineer.md +59 -0
- package/core-assets/agents/code-explorer.md +94 -0
- package/core-assets/agents/code-explorer.source +7 -0
- package/core-assets/agents/data-migration-engineer.md +59 -0
- package/core-assets/agents/devex-docs-engineer.md +57 -0
- package/core-assets/agents/doctrine-critic.md +91 -0
- package/core-assets/agents/doctrine-critic.source +11 -0
- package/core-assets/agents/domain-architect.md +56 -0
- package/core-assets/agents/experience-designer.md +59 -0
- package/core-assets/agents/frontend-engineer.md +60 -0
- package/core-assets/agents/independent-code-reviewer.md +61 -0
- package/core-assets/agents/migration-planner.md +97 -0
- package/core-assets/agents/migration-planner.source +7 -0
- package/core-assets/agents/observability-sre-engineer.md +59 -0
- package/core-assets/agents/pdf-specialist.md +58 -0
- package/core-assets/agents/performance-engineer.md +58 -0
- package/core-assets/agents/product-challenger.md +55 -0
- package/core-assets/agents/security-engineer.md +66 -0
- package/core-assets/agents/silent-failure-hunter.md +94 -0
- package/core-assets/agents/silent-failure-hunter.source +8 -0
- package/core-assets/agents/solution-architect.md +57 -0
- package/core-assets/agents/test-qa-engineer.md +60 -0
- package/core-assets/agents/type-design-analyzer.md +93 -0
- package/core-assets/agents/type-design-analyzer.source +8 -0
- package/core-assets/agents/visual-craft-director.md +62 -0
- package/core-assets/codex/hooks.json +157 -0
- package/core-assets/contract-history/visual-craft-director/v2.yaml +43 -0
- package/core-assets/data/catalog.v3.json +10479 -0
- package/core-assets/data/certification.json +1800 -0
- package/core-assets/data/identity.json +8 -0
- package/core-assets/data/model.json +4226 -0
- package/core-assets/enforce/ci-enforce.sh +183 -0
- package/core-assets/hooks/_checks.sh +145 -0
- package/core-assets/hooks/_hooklib.sh +163 -0
- package/core-assets/hooks/_syntax-worker.cjs +503 -0
- package/core-assets/hooks/_void-hook.mjs +5459 -0
- package/core-assets/hooks/activation-meter.sh +13 -0
- package/core-assets/hooks/anti-ai-slop-grep.sh +38 -0
- package/core-assets/hooks/auto-format.sh +9 -0
- package/core-assets/hooks/axe-precommit.sh +35 -0
- package/core-assets/hooks/block-dangerous-bash.sh +10 -0
- package/core-assets/hooks/block-protected-push.sh +81 -0
- package/core-assets/hooks/boundary-direction-check.sh +10 -0
- package/core-assets/hooks/control-character-grep.sh +10 -0
- package/core-assets/hooks/fowler-refactors.txt +71 -0
- package/core-assets/hooks/large-cl-grep.sh +9 -0
- package/core-assets/hooks/llm-cost-precommit.sh +55 -0
- package/core-assets/hooks/migration-lint.sh +58 -0
- package/core-assets/hooks/migration-pr-template.sh +55 -0
- package/core-assets/hooks/no-ai-design-slop.sh +10 -0
- package/core-assets/hooks/no-any-grep.sh +10 -0
- package/core-assets/hooks/no-as-cast-grep.sh +10 -0
- package/core-assets/hooks/no-console-log-grep.sh +10 -0
- package/core-assets/hooks/no-null-grep.sh +10 -0
- package/core-assets/hooks/no-only-no-skip.sh +10 -0
- package/core-assets/hooks/outcome-meter.sh +13 -0
- package/core-assets/hooks/pre-PR-review-evidence.sh +47 -0
- package/core-assets/hooks/protect-sensitive-files.sh +10 -0
- package/core-assets/hooks/refactor-named-grep.sh +49 -0
- package/core-assets/hooks/secret-in-content.sh +10 -0
- package/core-assets/hooks/sessionstart-context.sh +9 -0
- package/core-assets/hooks/stop-typecheck.sh +9 -0
- package/core-assets/hooks/tdd-guard.md +90 -0
- package/core-assets/hooks/tdd-guard.sh +10 -0
- package/core-assets/hooks/test-name-lint.sh +10 -0
- package/core-assets/hooks/tidying-commit-prefix.sh +37 -0
- package/core-assets/hooks/trim-large-output.sh +9 -0
- package/core-assets/hooks/tsc-noemit-precommit.sh +22 -0
- package/core-assets/hooks/ubiquitous-language-lint.sh +58 -0
- package/core-assets/hooks/viewport-screenshot-gate.sh +48 -0
- package/core-assets/modules/.gitkeep +0 -0
- package/core-assets/packs/pack-mobile/skills/void-eas-build-profile/.source +7 -0
- package/core-assets/packs/pack-mobile/skills/void-eas-build-profile/SKILL.md +164 -0
- package/core-assets/packs/pack-mobile/skills/void-eas-build-profile/harness.yaml +14 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-config-plugins/.source +7 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-config-plugins/SKILL.md +178 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-config-plugins/harness.yaml +14 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-router-pattern/.source +8 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-router-pattern/SKILL.md +209 -0
- package/core-assets/packs/pack-mobile/skills/void-expo-router-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-mobile/skills/void-ota-update-strategy/.source +7 -0
- package/core-assets/packs/pack-mobile/skills/void-ota-update-strategy/SKILL.md +194 -0
- package/core-assets/packs/pack-mobile/skills/void-ota-update-strategy/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-dependency-direction/.source +5 -0
- package/core-assets/packs/pack-monorepo/skills/void-dependency-direction/SKILL.md +160 -0
- package/core-assets/packs/pack-monorepo/skills/void-dependency-direction/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-package-extraction/.source +5 -0
- package/core-assets/packs/pack-monorepo/skills/void-package-extraction/SKILL.md +70 -0
- package/core-assets/packs/pack-monorepo/skills/void-package-extraction/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-service-package/.source +5 -0
- package/core-assets/packs/pack-monorepo/skills/void-service-package/SKILL.md +122 -0
- package/core-assets/packs/pack-monorepo/skills/void-service-package/harness.yaml +14 -0
- package/core-assets/packs/pack-monorepo/skills/void-turbo-pipeline-tuning/.source +7 -0
- package/core-assets/packs/pack-monorepo/skills/void-turbo-pipeline-tuning/SKILL.md +116 -0
- package/core-assets/packs/pack-monorepo/skills/void-turbo-pipeline-tuning/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-cache-component-pattern/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-cache-component-pattern/SKILL.md +162 -0
- package/core-assets/packs/pack-nextjs/skills/void-cache-component-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-instrumentation-setup/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-instrumentation-setup/SKILL.md +161 -0
- package/core-assets/packs/pack-nextjs/skills/void-instrumentation-setup/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-loading-error-boundaries/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-loading-error-boundaries/SKILL.md +176 -0
- package/core-assets/packs/pack-nextjs/skills/void-loading-error-boundaries/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-parallel-routes-slots/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-parallel-routes-slots/SKILL.md +129 -0
- package/core-assets/packs/pack-nextjs/skills/void-parallel-routes-slots/harness.yaml +14 -0
- package/core-assets/packs/pack-nextjs/skills/void-route-group-decision/.source +5 -0
- package/core-assets/packs/pack-nextjs/skills/void-route-group-decision/SKILL.md +107 -0
- package/core-assets/packs/pack-nextjs/skills/void-route-group-decision/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-install-prompt-ux/.source +5 -0
- package/core-assets/packs/pack-pwa/skills/void-install-prompt-ux/SKILL.md +153 -0
- package/core-assets/packs/pack-pwa/skills/void-install-prompt-ux/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-manifest-checklist/.source +5 -0
- package/core-assets/packs/pack-pwa/skills/void-manifest-checklist/SKILL.md +204 -0
- package/core-assets/packs/pack-pwa/skills/void-manifest-checklist/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-offline-first-mutation/.source +8 -0
- package/core-assets/packs/pack-pwa/skills/void-offline-first-mutation/SKILL.md +266 -0
- package/core-assets/packs/pack-pwa/skills/void-offline-first-mutation/harness.yaml +14 -0
- package/core-assets/packs/pack-pwa/skills/void-service-worker-strategy/.source +5 -0
- package/core-assets/packs/pack-pwa/skills/void-service-worker-strategy/SKILL.md +178 -0
- package/core-assets/packs/pack-pwa/skills/void-service-worker-strategy/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-accessibility-check/.source +8 -0
- package/core-assets/packs/pack-react/skills/void-accessibility-check/SKILL.md +136 -0
- package/core-assets/packs/pack-react/skills/void-accessibility-check/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-client-vs-server-component/.source +5 -0
- package/core-assets/packs/pack-react/skills/void-client-vs-server-component/SKILL.md +116 -0
- package/core-assets/packs/pack-react/skills/void-client-vs-server-component/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-form-pattern/.source +7 -0
- package/core-assets/packs/pack-react/skills/void-form-pattern/SKILL.md +157 -0
- package/core-assets/packs/pack-react/skills/void-form-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-react/skills/void-state-architecture/.source +5 -0
- package/core-assets/packs/pack-react/skills/void-state-architecture/SKILL.md +160 -0
- package/core-assets/packs/pack-react/skills/void-state-architecture/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-background-job-pattern/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-background-job-pattern/SKILL.md +214 -0
- package/core-assets/packs/pack-server/skills/void-background-job-pattern/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-drizzle-migration-safe/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-drizzle-migration-safe/SKILL.md +168 -0
- package/core-assets/packs/pack-server/skills/void-drizzle-migration-safe/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-env-validation/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-env-validation/SKILL.md +131 -0
- package/core-assets/packs/pack-server/skills/void-env-validation/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-rate-limit-strategy/.source +6 -0
- package/core-assets/packs/pack-server/skills/void-rate-limit-strategy/SKILL.md +149 -0
- package/core-assets/packs/pack-server/skills/void-rate-limit-strategy/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-server-action/.source +7 -0
- package/core-assets/packs/pack-server/skills/void-server-action/SKILL.md +181 -0
- package/core-assets/packs/pack-server/skills/void-server-action/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-testing-server-modules/.source +7 -0
- package/core-assets/packs/pack-server/skills/void-testing-server-modules/SKILL.md +73 -0
- package/core-assets/packs/pack-server/skills/void-testing-server-modules/harness.yaml +14 -0
- package/core-assets/packs/pack-server/skills/void-webhook-handler-pattern/.source +7 -0
- package/core-assets/packs/pack-server/skills/void-webhook-handler-pattern/SKILL.md +205 -0
- package/core-assets/packs/pack-server/skills/void-webhook-handler-pattern/harness.yaml +14 -0
- package/core-assets/policies/core.yaml +95 -0
- package/core-assets/policies/ui.yaml +23 -0
- package/core-assets/profiles/base.yaml +29 -0
- package/core-assets/profiles/expo-config.yaml +36 -0
- package/core-assets/profiles/expo.yaml +35 -0
- package/core-assets/profiles/monorepo.yaml +33 -0
- package/core-assets/profiles/nextjs-config.yaml +31 -0
- package/core-assets/profiles/nextjs.yaml +31 -0
- package/core-assets/profiles/node-server.yaml +31 -0
- package/core-assets/profiles/pwa.yaml +33 -0
- package/core-assets/profiles/react.yaml +39 -0
- package/core-assets/profiles/sql.yaml +39 -0
- package/core-assets/profiles/typescript.yaml +39 -0
- package/core-assets/skills/.gitkeep +0 -0
- package/core-assets/skills/void-accessibility/.source +15 -0
- package/core-assets/skills/void-accessibility/SKILL.md +275 -0
- package/core-assets/skills/void-accessibility/harness.yaml +17 -0
- package/core-assets/skills/void-api-and-interface-design/.source +13 -0
- package/core-assets/skills/void-api-and-interface-design/SKILL.md +231 -0
- package/core-assets/skills/void-api-and-interface-design/harness.yaml +16 -0
- package/core-assets/skills/void-async-safety/.source +13 -0
- package/core-assets/skills/void-async-safety/SKILL.md +287 -0
- package/core-assets/skills/void-async-safety/harness.yaml +16 -0
- package/core-assets/skills/void-audit/.source +15 -0
- package/core-assets/skills/void-audit/SKILL.md +83 -0
- package/core-assets/skills/void-audit/harness.yaml +14 -0
- package/core-assets/skills/void-autopilot/.source +21 -0
- package/core-assets/skills/void-autopilot/SKILL.md +358 -0
- package/core-assets/skills/void-autopilot/harness.yaml +14 -0
- package/core-assets/skills/void-brainstorm/.source +14 -0
- package/core-assets/skills/void-brainstorm/SKILL.md +271 -0
- package/core-assets/skills/void-brainstorm/harness.yaml +14 -0
- package/core-assets/skills/void-checkpoint/.source +39 -0
- package/core-assets/skills/void-checkpoint/SKILL.md +227 -0
- package/core-assets/skills/void-checkpoint/harness.yaml +14 -0
- package/core-assets/skills/void-claude-md/.source +15 -0
- package/core-assets/skills/void-claude-md/SKILL.md +180 -0
- package/core-assets/skills/void-claude-md/harness.yaml +14 -0
- package/core-assets/skills/void-code-review/.source +14 -0
- package/core-assets/skills/void-code-review/SKILL.md +232 -0
- package/core-assets/skills/void-code-review/harness.yaml +15 -0
- package/core-assets/skills/void-commit-discipline/.source +9 -0
- package/core-assets/skills/void-commit-discipline/SKILL.md +211 -0
- package/core-assets/skills/void-commit-discipline/harness.yaml +15 -0
- package/core-assets/skills/void-context/.source +15 -0
- package/core-assets/skills/void-context/SKILL.md +167 -0
- package/core-assets/skills/void-context/harness.yaml +14 -0
- package/core-assets/skills/void-debug/.source +11 -0
- package/core-assets/skills/void-debug/SKILL.md +211 -0
- package/core-assets/skills/void-debug/harness.yaml +14 -0
- package/core-assets/skills/void-decide/.source +9 -0
- package/core-assets/skills/void-decide/SKILL.md +147 -0
- package/core-assets/skills/void-decide/harness.yaml +14 -0
- package/core-assets/skills/void-devex-audit/.source +8 -0
- package/core-assets/skills/void-devex-audit/SKILL.md +113 -0
- package/core-assets/skills/void-devex-audit/harness.yaml +15 -0
- package/core-assets/skills/void-doctor/.source +15 -0
- package/core-assets/skills/void-doctor/SKILL.md +83 -0
- package/core-assets/skills/void-doctor/harness.yaml +14 -0
- package/core-assets/skills/void-domain-driven-design/.source +13 -0
- package/core-assets/skills/void-domain-driven-design/SKILL.md +292 -0
- package/core-assets/skills/void-domain-driven-design/harness.yaml +15 -0
- package/core-assets/skills/void-frontend-design/.source +19 -0
- package/core-assets/skills/void-frontend-design/SKILL.md +274 -0
- package/core-assets/skills/void-frontend-design/harness.yaml +17 -0
- package/core-assets/skills/void-functional/.source +14 -0
- package/core-assets/skills/void-functional/SKILL.md +312 -0
- package/core-assets/skills/void-functional/harness.yaml +15 -0
- package/core-assets/skills/void-graph/.source +15 -0
- package/core-assets/skills/void-graph/SKILL.md +80 -0
- package/core-assets/skills/void-graph/harness.yaml +14 -0
- package/core-assets/skills/void-hexagonal-architecture/.source +12 -0
- package/core-assets/skills/void-hexagonal-architecture/SKILL.md +276 -0
- package/core-assets/skills/void-hexagonal-architecture/harness.yaml +15 -0
- package/core-assets/skills/void-implement/.source +39 -0
- package/core-assets/skills/void-implement/SKILL.md +183 -0
- package/core-assets/skills/void-implement/harness.yaml +14 -0
- package/core-assets/skills/void-learn/.source +13 -0
- package/core-assets/skills/void-learn/SKILL.md +226 -0
- package/core-assets/skills/void-learn/harness.yaml +14 -0
- package/core-assets/skills/void-llm-cost-discipline/.source +13 -0
- package/core-assets/skills/void-llm-cost-discipline/SKILL.md +299 -0
- package/core-assets/skills/void-llm-cost-discipline/harness.yaml +15 -0
- package/core-assets/skills/void-make-pdf/.source +9 -0
- package/core-assets/skills/void-make-pdf/SKILL.md +52 -0
- package/core-assets/skills/void-make-pdf/harness.yaml +15 -0
- package/core-assets/skills/void-merge/.source +8 -0
- package/core-assets/skills/void-merge/SKILL.md +111 -0
- package/core-assets/skills/void-merge/harness.yaml +14 -0
- package/core-assets/skills/void-migrations/.source +13 -0
- package/core-assets/skills/void-migrations/SKILL.md +264 -0
- package/core-assets/skills/void-migrations/harness.yaml +17 -0
- package/core-assets/skills/void-observability/.source +12 -0
- package/core-assets/skills/void-observability/SKILL.md +245 -0
- package/core-assets/skills/void-observability/harness.yaml +15 -0
- package/core-assets/skills/void-plan/.source +15 -0
- package/core-assets/skills/void-plan/SKILL.md +239 -0
- package/core-assets/skills/void-plan/harness.yaml +14 -0
- package/core-assets/skills/void-plan-review/.source +12 -0
- package/core-assets/skills/void-plan-review/SKILL.md +128 -0
- package/core-assets/skills/void-plan-review/harness.yaml +15 -0
- package/core-assets/skills/void-qa/.source +15 -0
- package/core-assets/skills/void-qa/SKILL.md +103 -0
- package/core-assets/skills/void-qa/harness.yaml +15 -0
- package/core-assets/skills/void-refactor/.source +9 -0
- package/core-assets/skills/void-refactor/SKILL.md +220 -0
- package/core-assets/skills/void-refactor/harness.yaml +15 -0
- package/core-assets/skills/void-retrospective/.source +9 -0
- package/core-assets/skills/void-retrospective/SKILL.md +71 -0
- package/core-assets/skills/void-retrospective/harness.yaml +15 -0
- package/core-assets/skills/void-security-audit/.source +12 -0
- package/core-assets/skills/void-security-audit/SKILL.md +138 -0
- package/core-assets/skills/void-security-audit/harness.yaml +15 -0
- package/core-assets/skills/void-security-guidance/.source +12 -0
- package/core-assets/skills/void-security-guidance/SKILL.md +255 -0
- package/core-assets/skills/void-security-guidance/harness.yaml +15 -0
- package/core-assets/skills/void-source-driven-development/.source +12 -0
- package/core-assets/skills/void-source-driven-development/SKILL.md +158 -0
- package/core-assets/skills/void-source-driven-development/harness.yaml +15 -0
- package/core-assets/skills/void-tdd/.source +15 -0
- package/core-assets/skills/void-tdd/SKILL.md +394 -0
- package/core-assets/skills/void-tdd/harness.yaml +15 -0
- package/core-assets/skills/void-testing/.source +19 -0
- package/core-assets/skills/void-testing/SKILL.md +301 -0
- package/core-assets/skills/void-testing/harness.yaml +17 -0
- package/core-assets/skills/void-ticket/.source +18 -0
- package/core-assets/skills/void-ticket/SKILL.md +239 -0
- package/core-assets/skills/void-ticket/harness.yaml +14 -0
- package/core-assets/skills/void-typescript-strict/.source +10 -0
- package/core-assets/skills/void-typescript-strict/SKILL.md +294 -0
- package/core-assets/skills/void-typescript-strict/harness.yaml +17 -0
- package/core-assets/skills/void-ui-review/.source +12 -0
- package/core-assets/skills/void-ui-review/SKILL.md +97 -0
- package/core-assets/skills/void-ui-review/harness.yaml +15 -0
- package/core-assets/skills/void-verify/.source +12 -0
- package/core-assets/skills/void-verify/SKILL.md +184 -0
- package/core-assets/skills/void-verify/harness.yaml +15 -0
- package/core-assets/specialists/accessibility-specialist.source +11 -0
- package/core-assets/specialists/accessibility-specialist.yaml +30 -0
- package/core-assets/specialists/api-integration-engineer.source +11 -0
- package/core-assets/specialists/api-integration-engineer.yaml +30 -0
- package/core-assets/specialists/data-migration-engineer.source +11 -0
- package/core-assets/specialists/data-migration-engineer.yaml +30 -0
- package/core-assets/specialists/devex-docs-engineer.source +11 -0
- package/core-assets/specialists/devex-docs-engineer.yaml +29 -0
- package/core-assets/specialists/domain-architect.source +11 -0
- package/core-assets/specialists/domain-architect.yaml +28 -0
- package/core-assets/specialists/experience-designer.source +15 -0
- package/core-assets/specialists/experience-designer.yaml +38 -0
- package/core-assets/specialists/frontend-engineer.source +11 -0
- package/core-assets/specialists/frontend-engineer.yaml +32 -0
- package/core-assets/specialists/independent-code-reviewer.source +10 -0
- package/core-assets/specialists/independent-code-reviewer.yaml +37 -0
- package/core-assets/specialists/migrations.json +15 -0
- package/core-assets/specialists/observability-sre-engineer.source +11 -0
- package/core-assets/specialists/observability-sre-engineer.yaml +30 -0
- package/core-assets/specialists/pdf-specialist.source +11 -0
- package/core-assets/specialists/pdf-specialist.yaml +29 -0
- package/core-assets/specialists/performance-engineer.source +11 -0
- package/core-assets/specialists/performance-engineer.yaml +29 -0
- package/core-assets/specialists/product-challenger.source +10 -0
- package/core-assets/specialists/product-challenger.yaml +27 -0
- package/core-assets/specialists/security-engineer.source +13 -0
- package/core-assets/specialists/security-engineer.yaml +41 -0
- package/core-assets/specialists/solution-architect.source +13 -0
- package/core-assets/specialists/solution-architect.yaml +32 -0
- package/core-assets/specialists/test-qa-engineer.source +14 -0
- package/core-assets/specialists/test-qa-engineer.yaml +35 -0
- package/core-assets/specialists/visual-craft-director.source +17 -0
- package/core-assets/specialists/visual-craft-director.yaml +60 -0
- package/core-assets/templates/github/dependency-audit.mjs +54 -0
- package/core-assets/templates/github/void-dependency-audit.yml +35 -0
- package/core-assets/templates/github/void-security.yml +85 -0
- package/dist/main.js +57001 -0
- package/package.json +66 -4
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ci-enforce — replay the void-machine enforcement floor over a PR diff.
|
|
3
|
+
#
|
|
4
|
+
# Critical path, secret-content, TDD and boundary rules execute through the SAME
|
|
5
|
+
# portable Node bundle as local PreToolUse hooks.
|
|
6
|
+
#
|
|
7
|
+
# FAIL-CLOSED (DEV-393, the #62-64 class): a missing prerequisite or an
|
|
8
|
+
# unresolvable base ref is an explicit RED check, never a silent green.
|
|
9
|
+
#
|
|
10
|
+
# Usage: ci-enforce.sh --base <ref> # ref = the PR base, e.g. origin/main
|
|
11
|
+
# Exit: 0 = clean, 1 = violations found OR fail-closed error.
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
RUNNER="${BASH_SOURCE[0]%/*}/../hooks/_void-hook.mjs"
|
|
15
|
+
|
|
16
|
+
BASE=""
|
|
17
|
+
while [[ $# -gt 0 ]]; do
|
|
18
|
+
case "$1" in
|
|
19
|
+
--base) BASE="${2:-}"; shift 2 ;;
|
|
20
|
+
-h | --help) printf 'Usage: ci-enforce.sh --base <ref>\n'; exit 0 ;;
|
|
21
|
+
*) printf '::error::void-enforce: unknown argument %s\n' "$1" >&2; exit 1 ;;
|
|
22
|
+
esac
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
fail_closed() {
|
|
26
|
+
printf '::error::void-enforce: %s\n' "$1"
|
|
27
|
+
printf 'void-enforce (fail-closed): %s\n' "$1" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
command -v git >/dev/null 2>&1 || fail_closed "git not found on runner (cannot compute the diff)"
|
|
32
|
+
NODE_BIN=$(command -v node 2>/dev/null) || fail_closed "Node.js not found on runner (cannot execute the shared floor)"
|
|
33
|
+
[[ -f "$RUNNER" ]] || fail_closed "shared Node enforcement bundle missing at '$RUNNER'"
|
|
34
|
+
[[ -n "$BASE" ]] || fail_closed "no base ref given (pass --base <ref>)"
|
|
35
|
+
git rev-parse --verify "${BASE}^{commit}" >/dev/null 2>&1 \
|
|
36
|
+
|| fail_closed "base ref '${BASE}' is unresolvable (checkout with fetch-depth: 0 and fetch the base)"
|
|
37
|
+
# A three-dot diff needs a merge-base. A shallow clone or a rewritten/disjoint
|
|
38
|
+
# base HAS the commit object but shares NO common ancestor: `git diff base...HEAD`
|
|
39
|
+
# then exits 128 and prints nothing — indistinguishable from a clean diff, which
|
|
40
|
+
# would fail OPEN. Verify the merge-base up front so this fails CLOSED instead.
|
|
41
|
+
git merge-base "${BASE}" HEAD >/dev/null 2>&1 \
|
|
42
|
+
|| fail_closed "no merge-base between '${BASE}' and HEAD (shallow clone or disjoint history — checkout with fetch-depth: 0)"
|
|
43
|
+
|
|
44
|
+
FAIL=0
|
|
45
|
+
|
|
46
|
+
# Committed, reviewable exemptions: `.github/void-enforce-allow` lists path globs
|
|
47
|
+
# (one per line, # comments) skipped entirely. This is the Action equivalent of
|
|
48
|
+
# the local VOID_MACHINE_ALLOW_SECRET_EDIT override — e.g. a file legitimately
|
|
49
|
+
# NAMED for secrets (the harness's own secret-in-content.sh) that sensitive-path
|
|
50
|
+
# would otherwise flag. A skip is LOGGED, never silent. Loaded once.
|
|
51
|
+
ALLOW_GLOBS=()
|
|
52
|
+
if [[ -f .github/void-enforce-allow ]]; then
|
|
53
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
54
|
+
line="${line%%#*}" # strip trailing comment
|
|
55
|
+
read -r g <<<"$line" || true # trim surrounding whitespace, take the glob
|
|
56
|
+
[[ -n "${g:-}" ]] && ALLOW_GLOBS+=("$g")
|
|
57
|
+
done <.github/void-enforce-allow
|
|
58
|
+
fi
|
|
59
|
+
_allowlisted() {
|
|
60
|
+
local p="$1" g
|
|
61
|
+
for g in "${ALLOW_GLOBS[@]:-}"; do
|
|
62
|
+
[[ -n "$g" ]] || continue
|
|
63
|
+
# shellcheck disable=SC2053 -- intentional glob match, $g is a pattern
|
|
64
|
+
[[ "$p" == $g ]] && return 0
|
|
65
|
+
done
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Escape a workflow-command MESSAGE value: % -> %25, CR/LF -> %0D/%0A.
|
|
70
|
+
esc_msg() { printf '%s' "$1" | sed -e 's/%/%25/g' -e 's/\r/%0D/g' | awk 'NR>1{printf "%%0A"} {printf "%s",$0} END{print ""}'; }
|
|
71
|
+
# Escape a PROPERTY value (file/title): also , -> %2C and : -> %3A.
|
|
72
|
+
esc_prop() { printf '%s' "$1" | sed -e 's/%/%25/g' -e 's/:/%3A/g' -e 's/,/%2C/g'; }
|
|
73
|
+
|
|
74
|
+
annotate() { # <level> <file> <line> <message>
|
|
75
|
+
printf '::%s file=%s,line=%s::%s\n' "$1" "$(esc_prop "$2")" "$3" "$(esc_msg "$4")"
|
|
76
|
+
if [[ "$1" == error ]]; then FAIL=1; fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Added lines of <file> in base...HEAD as `<new-line-number>\t<text>`, one per
|
|
80
|
+
# added line, so a content match maps back to a real line for the annotation.
|
|
81
|
+
# Returns 2 (not empty output) if the per-file git diff itself fails, so the
|
|
82
|
+
# caller can fail CLOSED rather than mistake a git error for "no added lines".
|
|
83
|
+
added_lines() {
|
|
84
|
+
local out
|
|
85
|
+
out=$(git diff "${BASE}"...HEAD -- "$1") || return 2
|
|
86
|
+
printf '%s' "$out" | awk '
|
|
87
|
+
/^@@/ { if (match($0, /\+[0-9]+/)) n = substr($0, RSTART+1, RLENGTH-1) + 0; next }
|
|
88
|
+
/^\+\+\+/ { next }
|
|
89
|
+
/^\+/ { print n "\t" substr($0, 2); n++; next }
|
|
90
|
+
/^-/ { next }
|
|
91
|
+
/^ / { n++; next }
|
|
92
|
+
'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Walk the changed files. Enumerate with -z (NUL-delimited) and
|
|
96
|
+
# core.quotepath=false so paths with non-ASCII bytes, spaces, tabs or newlines
|
|
97
|
+
# arrive RAW: under the default quotepath git octal-escapes and quotes such a
|
|
98
|
+
# path, and the escaped string then matches no file — silently skipping every
|
|
99
|
+
# content check (a leaked secret in `café.ts` would pass GREEN). Captured to a
|
|
100
|
+
# file (a bash variable cannot hold the NUL delimiters) with an exit check so a
|
|
101
|
+
# git error fails CLOSED, not silently clean.
|
|
102
|
+
NAMES=$(mktemp) || fail_closed "cannot create a temp file"
|
|
103
|
+
trap 'rm -f "$NAMES"' EXIT
|
|
104
|
+
git -c core.quotepath=false diff --name-status -z "${BASE}"...HEAD >"$NAMES" \
|
|
105
|
+
|| fail_closed "git diff --name-status failed for base '${BASE}'"
|
|
106
|
+
|
|
107
|
+
# -z records: `status\0path\0`, or `R###\0old\0new\0` for renames/copies.
|
|
108
|
+
while IFS= read -r -d '' status; do
|
|
109
|
+
case "$status" in
|
|
110
|
+
R* | C*) IFS= read -r -d '' _src || break; IFS= read -r -d '' path || break ;;
|
|
111
|
+
D*) IFS= read -r -d '' _gone || break; continue ;; # a deletion cannot leak
|
|
112
|
+
*) IFS= read -r -d '' path || break ;;
|
|
113
|
+
esac
|
|
114
|
+
[[ -z "${path:-}" ]] && continue
|
|
115
|
+
|
|
116
|
+
if _allowlisted "$path"; then
|
|
117
|
+
printf 'void-enforce: %s skipped (allowlisted in .github/void-enforce-allow)\n' "$path"
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# 1) Never-edit file (path only): lockfile / key / secret filename / .git.
|
|
122
|
+
if protected=$("$NODE_BIN" "$RUNNER" enforce-ci protected-file "$path" </dev/null 2>&1); then :; else
|
|
123
|
+
# A committed diff cannot prove whether the package manager generated it.
|
|
124
|
+
# Dependency refreshes may leave manifests unchanged; keep content scanning.
|
|
125
|
+
if [[ "$protected" == *": lockfile (regenerate via the package manager, do not hand-edit)" ]]; then
|
|
126
|
+
annotate warning "$path" 1 "lockfile diff requires dependency validation: frozen install, vulnerability audit, tests and review; generation is not verified by this check"
|
|
127
|
+
else
|
|
128
|
+
protected_reason="${protected##*: }"
|
|
129
|
+
annotate error "$path" 1 "protected file: ${protected_reason//$'\n'/ }"
|
|
130
|
+
continue
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# TDD judges final source even when the diff only removes lines.
|
|
135
|
+
if tdd=$("$NODE_BIN" "$RUNNER" enforce-ci tdd-order "$path" </dev/null 2>&1); then
|
|
136
|
+
[[ -z "$tdd" ]] || printf 'void-enforce: %s\n' "${tdd//$'\n'/ }"
|
|
137
|
+
else
|
|
138
|
+
annotate error "$path" 1 "${tdd//$'\n'/ }"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# 2+3) Content checks over the ADDED lines, with real line numbers.
|
|
142
|
+
diff_added=$(added_lines "$path") || fail_closed "git diff failed for '${path}' (cannot scan its content)"
|
|
143
|
+
[[ -z "$diff_added" ]] && continue
|
|
144
|
+
LNOS=()
|
|
145
|
+
while IFS= read -r line_number; do
|
|
146
|
+
LNOS+=("$line_number")
|
|
147
|
+
done < <(printf '%s\n' "$diff_added" | cut -f1)
|
|
148
|
+
added_text=$(printf '%s\n' "$diff_added" | cut -f2-)
|
|
149
|
+
|
|
150
|
+
if hits=$(printf '%s' "$added_text" | "$NODE_BIN" "$RUNNER" enforce-ci secret-content "$path" 2>&1); then :; else
|
|
151
|
+
FOUND=0
|
|
152
|
+
while IFS= read -r hit; do
|
|
153
|
+
[[ "$hit" =~ ^-\ .+:([0-9]+)$ ]] || continue
|
|
154
|
+
FOUND=1
|
|
155
|
+
rel="${BASH_REMATCH[1]}"
|
|
156
|
+
annotate error "$path" "${LNOS[rel - 1]:-1}" "leaked secret (see harness:security-guidance)"
|
|
157
|
+
done <<<"$hits"
|
|
158
|
+
[[ "$FOUND" -eq 1 ]] || annotate error "$path" 1 "secret scan failed closed: ${hits//$'\n'/ }"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if hits=$(printf '%s' "$added_text" | "$NODE_BIN" "$RUNNER" enforce-ci boundary-direction "$path" 2>&1); then :; else
|
|
162
|
+
FOUND=0
|
|
163
|
+
while IFS= read -r hit; do
|
|
164
|
+
[[ "$hit" =~ ^-\ .+:([0-9]+)\ \-\>\ (.+)$ ]] || continue
|
|
165
|
+
FOUND=1
|
|
166
|
+
rel="${BASH_REMATCH[1]}"
|
|
167
|
+
annotate error "$path" "${LNOS[rel - 1]:-1}" "forbidden @repo/* import: ${BASH_REMATCH[2]}"
|
|
168
|
+
done <<<"$hits"
|
|
169
|
+
[[ "$FOUND" -eq 1 ]] || annotate error "$path" 1 "boundary scan failed closed: ${hits//$'\n'/ }"
|
|
170
|
+
fi
|
|
171
|
+
# NB: checks_dangerous_command (the local Bash runtime guard) is deliberately
|
|
172
|
+
# NOT replayed over the diff. A destructive PATTERN committed into a file is a
|
|
173
|
+
# weak signal that self-matches the harness's own detector, security docs, and
|
|
174
|
+
# test fixtures — net-negative false positives for a floor check. Deferred to a
|
|
175
|
+
# follow-up with a proper per-line allow tag (see docs/DECISIONS.md).
|
|
176
|
+
done <"$NAMES"
|
|
177
|
+
|
|
178
|
+
if [[ "$FAIL" -ne 0 ]]; then
|
|
179
|
+
printf 'void-enforce: floor violations found (see annotations above).\n' >&2
|
|
180
|
+
exit 1
|
|
181
|
+
fi
|
|
182
|
+
printf 'void-enforce: clean — no floor violations in the diff.\n'
|
|
183
|
+
exit 0
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# _checks.sh — pure detection functions for Void Machine enforcement floor.
|
|
3
|
+
# SOURCED, never executed (leading underscore = library; exempt from the hook
|
|
4
|
+
# size cap, still syntax-checked — see scripts/anti-bloat-check.sh).
|
|
5
|
+
#
|
|
6
|
+
# WHY this file exists (DEV-393): the floor (never-edit files, leaked secrets,
|
|
7
|
+
# @repo/* import direction, catastrophic shell) must be enforced identically by
|
|
8
|
+
# the PreToolUse hooks (Claude runtime, per-edit) AND the CI diff driver
|
|
9
|
+
# (enforce/ci-enforce.sh, per-PR). Extracting the detection here makes it ONE
|
|
10
|
+
# body of logic — the local hook and the server-side Action can never diverge.
|
|
11
|
+
#
|
|
12
|
+
# Contract: every function returns 1 and prints a payload on STDOUT when it
|
|
13
|
+
# detects a violation; returns 0 and prints nothing when the input is clean.
|
|
14
|
+
# Functions never `exit` (they are sourced) and never depend on the caller's
|
|
15
|
+
# stdin plumbing (_hooklib.sh) — path comes as $1, edited content on stdin.
|
|
16
|
+
# Content checks emit `<n>:<line>` (grep -n numbering over the scanned content)
|
|
17
|
+
# so a caller can map a hit back to a real file line for an annotation.
|
|
18
|
+
|
|
19
|
+
# checks_sensitive_path <path>: 1 + reason if the path is a never-edit file
|
|
20
|
+
# (env/secret/key/credential/lockfile/.git), else 0. Case-insensitive: on a
|
|
21
|
+
# case-insensitive FS .ENV and .env are the same file.
|
|
22
|
+
checks_sensitive_path() {
|
|
23
|
+
local path="$1" base lpath
|
|
24
|
+
[[ -z "$path" ]] && return 0
|
|
25
|
+
base=$(basename "$path" | tr '[:upper:]' '[:lower:]')
|
|
26
|
+
lpath=$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')
|
|
27
|
+
if [[ "$base" =~ ^\.env(\..+)?$ ]] && [[ ! "$base" =~ \.(example|sample|template|dist)$ ]]; then
|
|
28
|
+
printf 'environment file with secrets\n'; return 1; fi
|
|
29
|
+
if [[ "$base" =~ \.(pem|key|p12|pfx|keystore|jks|asc)$ || "$base" =~ ^id_(rsa|ed25519|ecdsa|dsa)$ ]]; then
|
|
30
|
+
printf 'private key / certificate\n'; return 1; fi
|
|
31
|
+
if [[ "$base" =~ (\.npmrc$|\.netrc$|\.pgpass$) ]]; then
|
|
32
|
+
printf 'credential file\n'; return 1; fi
|
|
33
|
+
# The loose secret|credential NAME match is a heuristic; exempt markdown docs.
|
|
34
|
+
# A real credential file is never .md, and content secret-scanning still covers
|
|
35
|
+
# .md, so this only removes false positives (e.g. a decision note whose slug
|
|
36
|
+
# contains "…-byo-credentials.md" is documentation, not a credential file).
|
|
37
|
+
if [[ ! "$base" =~ \.md$ ]] && [[ "$base" =~ (secret|credential) ]]; then
|
|
38
|
+
printf 'credential file\n'; return 1; fi
|
|
39
|
+
case "$base" in
|
|
40
|
+
package-lock.json|pnpm-lock.yaml|yarn.lock|bun.lock|bun.lockb|cargo.lock|poetry.lock|composer.lock)
|
|
41
|
+
printf 'lockfile (regenerate via the package manager, do not hand-edit)\n'; return 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
[[ "$lpath" =~ (^|/)\.git/ ]] && { printf 'internal git metadata\n'; return 1; }
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# checks_boundary_imports <path> (content on stdin): 1 + `<n>:<line>` for each
|
|
48
|
+
# forbidden @repo/* import from packages/<X>/ (X may only import @repo/core or
|
|
49
|
+
# itself). No-ops for apps/, packages/core/, test/generated files — those
|
|
50
|
+
# exemptions are check semantics, so they live here, not in each caller.
|
|
51
|
+
checks_boundary_imports() {
|
|
52
|
+
local file="$1" content pkg re_import violations="" line import target
|
|
53
|
+
content=$(cat)
|
|
54
|
+
[[ "$file" =~ ^packages/[^/]+/ ]] || return 0
|
|
55
|
+
[[ "$file" =~ ^packages/core/ ]] && return 0
|
|
56
|
+
[[ "$file" =~ \.(test|spec)\.(ts|tsx)$|\.d\.ts$|/__generated__/ ]] && return 0
|
|
57
|
+
# The @repo/* direction is a TypeScript-source rule. Docs (.md — e.g. skill
|
|
58
|
+
# examples that show a forbidden `import ... from '@repo/db'` marked ✗) carry
|
|
59
|
+
# such imports as illustrations, never as real edges — only .ts/.tsx count.
|
|
60
|
+
[[ "$file" =~ \.(ts|tsx)$ ]] || return 0
|
|
61
|
+
pkg=$(printf '%s' "$file" | sed -E 's|^packages/([^/]+)/.*|\1|')
|
|
62
|
+
re_import="from[[:space:]]+['\"]@repo/([a-zA-Z0-9-]+)"
|
|
63
|
+
while IFS= read -r line; do
|
|
64
|
+
import=$(printf '%s' "$line" | grep -oE '@repo/[a-zA-Z0-9-]+' | head -1)
|
|
65
|
+
target=$(printf '%s' "$import" | sed 's|@repo/||')
|
|
66
|
+
if [[ -n "$target" && "$target" != "core" && "$target" != "$pkg" ]]; then
|
|
67
|
+
violations="${violations}${line}"$'\n'
|
|
68
|
+
fi
|
|
69
|
+
done < <(printf '%s' "$content" | grep -nE "$re_import" | grep -vE '// *allow-boundary:' || true)
|
|
70
|
+
[[ -n "$violations" ]] && { printf '%s' "$violations"; return 1; }
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# checks_secret_content <path> (content on stdin): 1 + `<n>:<line>` for each line
|
|
75
|
+
# leaking a HIGH-confidence secret. Test-fixture paths are exempt (fake secrets
|
|
76
|
+
# are legitimate there). POSIX ERE only (no grep -P, #64).
|
|
77
|
+
checks_secret_content() {
|
|
78
|
+
local file="$1" content re_hi re_assign n=0 line val hits=""
|
|
79
|
+
content=$(cat)
|
|
80
|
+
[[ "$file" =~ \.(test|spec)\.|/__tests__/|/__fixtures__/|/fixtures/|/__generated__/ ]] && return 0
|
|
81
|
+
|
|
82
|
+
re_hi='AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}' # AWS
|
|
83
|
+
re_hi="$re_hi"'|gh[posru]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{40,}' # GitHub
|
|
84
|
+
re_hi="$re_hi"'|sk_live_[A-Za-z0-9]{20,}|rk_live_[A-Za-z0-9]{20,}' # Stripe
|
|
85
|
+
re_hi="$re_hi"'|sk-ant-[A-Za-z0-9_-]{40,}|sk-proj-[A-Za-z0-9_-]{40,}' # Anthropic/OpenAI
|
|
86
|
+
re_hi="$re_hi"'|sk-[A-Za-z0-9]{40,}|xox[baprs]-[A-Za-z0-9-]{10,}' # OpenAI/Slack
|
|
87
|
+
re_hi="$re_hi"'|AIza[0-9A-Za-z_-]{35}' # Google API
|
|
88
|
+
re_hi="$re_hi"'|-----BEGIN [A-Z ]*PRIVATE KEY-----' # PEM
|
|
89
|
+
re_assign='(_KEY|_SECRET|_TOKEN|_PASSWORD|_PASSWD|_APIKEY)["'\'' ]*[:=][ ]*["'\'']([A-Za-z0-9+/=_-]{24,})["'\'']'
|
|
90
|
+
|
|
91
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
92
|
+
n=$((n + 1))
|
|
93
|
+
printf '%s' "$line" | grep -qE 'allow-secret-pattern:' && continue
|
|
94
|
+
if printf '%s' "$line" | grep -qE "$re_hi"; then hits="${hits}${n}:${line}"$'\n'; continue; fi
|
|
95
|
+
printf '%s' "$line" | grep -qiE "$re_assign" || continue
|
|
96
|
+
val=$(printf '%s' "$line" | sed -E "s/.*[:=][ ]*[\"']([A-Za-z0-9+/=_-]{24,})[\"'].*/\1/")
|
|
97
|
+
printf '%s' "$line" | grep -qiE 'process\.env|import\.meta\.env|xxx|changeme|example|redacted|your[-_]|<[a-z]|placeholder|todo' && continue
|
|
98
|
+
[[ "$val" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]] && continue
|
|
99
|
+
[[ "$val" =~ ^[0-9a-fA-F]+$ ]] && continue
|
|
100
|
+
printf '%s' "$val" | grep -qE '[A-Za-z]' && printf '%s' "$val" | grep -qE '[0-9]' || continue
|
|
101
|
+
hits="${hits}${n}:${line}"$'\n'
|
|
102
|
+
done < <(printf '%s' "$content")
|
|
103
|
+
|
|
104
|
+
[[ -n "$hits" ]] && { printf '%s' "$hits"; return 1; }
|
|
105
|
+
return 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# checks_dangerous_command (command text on stdin): 1 + a short label if the
|
|
109
|
+
# text contains a catastrophic shell pattern (recursive root delete, fork bomb,
|
|
110
|
+
# raw-device write, root chmod/chown, force-push, unsafe git flags, DROP/
|
|
111
|
+
# TRUNCATE). Best-effort blocklist, not a complete boundary.
|
|
112
|
+
checks_dangerous_command() {
|
|
113
|
+
local cmd norm
|
|
114
|
+
cmd=$(cat)
|
|
115
|
+
[[ -z "$cmd" ]] && return 0
|
|
116
|
+
norm=$(printf '%s' "$cmd" | tr -d "\"'")
|
|
117
|
+
local HOME_ROOT='(/\*?|~/?\*?|\$HOME/?\*?|\$\{HOME\}/?\*?)'
|
|
118
|
+
local RM_TARGET='('"$HOME_ROOT"'|\./?\*?|\*)'
|
|
119
|
+
|
|
120
|
+
if printf '%s' "$norm" | grep -qE '(^|[;&|[:space:]])rm([[:space:]]+(-[a-zA-Z]+|--[a-z-]+))*[[:space:]]+(-[a-zA-Z]*[rR]|--recursive)' \
|
|
121
|
+
&& printf '%s' "$norm" | grep -qE "(^|[;&|[:space:]])rm([[:space:]]+[a-zA-Z-]+)*[[:space:]]+(--[[:space:]]+)?${RM_TARGET}([[:space:]]|$)"; then
|
|
122
|
+
printf 'recursive delete of a root path\n'; return 1; fi
|
|
123
|
+
|
|
124
|
+
printf '%s' "$cmd" | grep -qE ':\(\)[[:space:]]*\{[[:space:]]*:[[:space:]]*\|[[:space:]]*:' \
|
|
125
|
+
&& { printf 'fork bomb\n'; return 1; }
|
|
126
|
+
|
|
127
|
+
printf '%s' "$cmd" | grep -qE '\bmkfs(\.[a-z0-9]+)?\b|\bdd\b[^|]*\bof=/dev/|>[[:space:]]*/dev/(sd|nvme|hd|disk)' \
|
|
128
|
+
&& { printf 'filesystem / raw-device write\n'; return 1; }
|
|
129
|
+
|
|
130
|
+
if printf '%s' "$norm" | grep -qE 'ch(mod|own)([[:space:]]+[a-zA-Z0-9-]+)*[[:space:]]+(-[a-zA-Z]*R|--recursive)' \
|
|
131
|
+
&& printf '%s' "$norm" | grep -qE "[[:space:]](--[[:space:]]+)?${HOME_ROOT}([[:space:]]|$)"; then
|
|
132
|
+
printf 'recursive permission/ownership change on a root path\n'; return 1; fi
|
|
133
|
+
|
|
134
|
+
printf '%s' "$cmd" | grep -qE 'git[[:space:]]+push\b[^&|;]*[[:space:]](--force([^-]|$)|-f([[:space:]]|$))' \
|
|
135
|
+
&& { printf 'git push --force (use --force-with-lease)\n'; return 1; }
|
|
136
|
+
|
|
137
|
+
if printf '%s' "$cmd" | grep -qE '\bgit\b([[:space:]]+-[^[:space:]]+)*[[:space:]]+(rebase|am|apply|cherry-pick)\b' \
|
|
138
|
+
&& printf '%s' "$cmd" | grep -qE '(--exec([[:space:]=]|$)|--rebase-merges|--strategy-option|--unsafe-paths)'; then
|
|
139
|
+
printf 'git command-execution / unsafe-path flag\n'; return 1; fi
|
|
140
|
+
|
|
141
|
+
printf '%s' "$cmd" | grep -qiE '\b(drop[[:space:]]+(database|table|schema)|truncate[[:space:]]+table)\b' \
|
|
142
|
+
&& { printf 'destructive SQL (DROP / TRUNCATE)\n'; return 1; }
|
|
143
|
+
|
|
144
|
+
return 0
|
|
145
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# _hooklib.sh — shared helpers for the PreToolUse Edit|Write enforcement hooks.
|
|
3
|
+
# SOURCED, never executed (leading underscore = library; excluded from the
|
|
4
|
+
# hook-size anti-bloat rule, see scripts/anti-bloat-check.sh). Goals (#63):
|
|
5
|
+
# - parse the Claude Code stdin JSON exactly once per hook;
|
|
6
|
+
# - never fail OPEN when jq is missing: scalar fields fall back to pure-bash
|
|
7
|
+
# extraction, and content-scanning hooks fail CLOSED with one explicit
|
|
8
|
+
# message rather than silently skipping enforcement (the audited bug: an
|
|
9
|
+
# unguarded jq call exited 127 under `set -e`, which Claude treats as a
|
|
10
|
+
# non-blocking error, so the whole hook layer went dark without jq);
|
|
11
|
+
# - share the physical-path root normalization introduced for #62.
|
|
12
|
+
# Every symbol is prefixed hooklib_ / _hooklib_.
|
|
13
|
+
|
|
14
|
+
# hooklib_read: consume stdin once. Sets HOOK_INPUT and HOOK_JQ (1 iff jq usable).
|
|
15
|
+
hooklib_read() {
|
|
16
|
+
HOOK_INPUT=$(cat)
|
|
17
|
+
if command -v jq >/dev/null 2>&1; then HOOK_JQ=1; else HOOK_JQ=0; fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# hooklib_str <jq-filter> <bash-key>: extract a JSON scalar string from
|
|
21
|
+
# HOOK_INPUT. jq when available; else a pure-bash regex on the (unescaped)
|
|
22
|
+
# "<bash-key>": "<value>" pair — exact for the flat scalar fields we read.
|
|
23
|
+
hooklib_str() {
|
|
24
|
+
if [[ "${HOOK_JQ:-0}" == 1 ]]; then
|
|
25
|
+
printf '%s' "$HOOK_INPUT" | jq -r "$1 // empty" 2>/dev/null || true
|
|
26
|
+
else
|
|
27
|
+
local re="\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\""
|
|
28
|
+
[[ "$HOOK_INPUT" =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
hooklib_tool() { hooklib_str '.tool_name' 'tool_name'; }
|
|
34
|
+
hooklib_file() { hooklib_str '.tool_input.file_path' 'file_path'; }
|
|
35
|
+
|
|
36
|
+
# hooklib_content: the edited text (.content for Write, .new_string for Edit).
|
|
37
|
+
# jq gives the exact, unescaped text. Returns 1 (prints nothing) when jq is
|
|
38
|
+
# absent — a pure-bash JSON string-unescape is not reliable enough to scan for
|
|
39
|
+
# forbidden patterns, so the caller must gate on hooklib_require_jq first.
|
|
40
|
+
hooklib_content() {
|
|
41
|
+
[[ "${HOOK_JQ:-0}" == 1 ]] || return 1
|
|
42
|
+
printf '%s' "$HOOK_INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty' 2>/dev/null || true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# hooklib_command: shell command (.command string, or Codex argv array joined).
|
|
46
|
+
hooklib_command() {
|
|
47
|
+
if [[ "${HOOK_JQ:-0}" == 1 ]]; then
|
|
48
|
+
printf '%s' "$HOOK_INPUT" \
|
|
49
|
+
| jq -r 'if (.tool_input.command? | type) == "array" then (.tool_input.command | join(" ")) else (.tool_input.command // empty) end' 2>/dev/null || true
|
|
50
|
+
else
|
|
51
|
+
hooklib_str '.tool_input.command' 'command'
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Framing for the hooklib_edits stream (ASCII RS/US). Edited content routinely
|
|
56
|
+
# contains newlines and tabs, so neither can frame the records.
|
|
57
|
+
_HOOKLIB_RS=$'\036'
|
|
58
|
+
_HOOKLIB_US=$'\037'
|
|
59
|
+
|
|
60
|
+
# _hooklib_patch_text: the raw apply_patch envelope, whichever field a given
|
|
61
|
+
# Codex build carries it in. Same candidate list as the floor's
|
|
62
|
+
# protect-sensitive-files, so the two can never disagree on where a patch lives.
|
|
63
|
+
_hooklib_patch_text() {
|
|
64
|
+
printf '%s' "$HOOK_INPUT" \
|
|
65
|
+
| jq -r '[.tool_input.input, .tool_input.patch, .tool_input.content, .tool_input.command]
|
|
66
|
+
| map(select(. != null))
|
|
67
|
+
| map(if type == "array" then join("\n") else . end)
|
|
68
|
+
| .[]' 2>/dev/null || true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# _hooklib_parse_patch: stdin = an apply_patch envelope; stdout = one
|
|
72
|
+
# <path>US<added-content>RS record per Add/Update section. Only ADDED (`+`)
|
|
73
|
+
# lines are collected, so a REMOVED or untouched context line can never trip a
|
|
74
|
+
# forbidden-pattern scan — the Codex equivalent of Claude's `.new_string`. A
|
|
75
|
+
# Delete section yields no record: there is nothing left to scan.
|
|
76
|
+
_hooklib_parse_patch() {
|
|
77
|
+
awk -v USEP="$_HOOKLIB_US" -v RSEP="$_HOOKLIB_RS" '
|
|
78
|
+
function emit() { if (path != "") printf "%s%s%s%s", path, USEP, buf, RSEP }
|
|
79
|
+
/^\*\*\* (Add|Update) File: / { emit(); path = substr($0, index($0, "File: ") + 6); buf = ""; next }
|
|
80
|
+
/^\*\*\* Delete File: / { emit(); path = ""; buf = ""; next }
|
|
81
|
+
/^\*\*\* (Begin|End) Patch/ { next }
|
|
82
|
+
{ if (path != "" && substr($0, 1, 1) == "+" && substr($0, 1, 3) != "+++") buf = buf substr($0, 2) "\n" }
|
|
83
|
+
END { emit() }
|
|
84
|
+
'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# hooklib_edits: the edits of THIS tool call as a NORMALIZED, runtime-agnostic
|
|
88
|
+
# stream of <path>US<new-content>RS records — one per edited file. It exists so a
|
|
89
|
+
# content-scanning hook iterates instead of assuming Claude's single-file shape:
|
|
90
|
+
# - Claude: Edit|Write -> one record (.tool_input.file_path + .content/.new_string)
|
|
91
|
+
# - Codex: apply_patch -> one record per patched file, carrying its added lines
|
|
92
|
+
# Wiring those hooks for Codex WITHOUT this would fire them against an empty
|
|
93
|
+
# payload: a wired-but-dead hook, which is a silent enforcement hole rather than
|
|
94
|
+
# an honest absence. Requires jq (exact content extraction) — callers gate on
|
|
95
|
+
# hooklib_require_jq first. Read it with:
|
|
96
|
+
# while IFS= read -r -d "$_HOOKLIB_RS" rec; do
|
|
97
|
+
# file="${rec%%"$_HOOKLIB_US"*}"; new="${rec#*"$_HOOKLIB_US"}"
|
|
98
|
+
# done < <(hooklib_edits)
|
|
99
|
+
hooklib_edits() {
|
|
100
|
+
# Without jq: degrade to the pure-bash file_path, emitting one CONTENTLESS
|
|
101
|
+
# record. That keeps the PATH-ONLY hooks (tdd-guard, auto-format) enforcing on
|
|
102
|
+
# the Claude shape exactly as they did before this stream existed. It does not
|
|
103
|
+
# weaken the content-scanning hooks: those call hooklib_require_jq BEFORE the
|
|
104
|
+
# loop and have already exited 2 by the time they would read a record (#63).
|
|
105
|
+
# A Codex patch simply cannot be parsed without jq, so it yields nothing.
|
|
106
|
+
if [[ "${HOOK_JQ:-0}" != 1 ]]; then
|
|
107
|
+
local bash_file
|
|
108
|
+
bash_file=$(hooklib_file)
|
|
109
|
+
[[ -n "$bash_file" ]] && printf '%s%s%s' "$bash_file" "$_HOOKLIB_US" "$_HOOKLIB_RS"
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
# A file_path means the single-file runtime shape. The patch parse is then
|
|
113
|
+
# deliberately skipped: file CONTENT that happens to document the apply_patch
|
|
114
|
+
# format (this repo's own docs do) would otherwise forge phantom records.
|
|
115
|
+
if [[ -n "$(hooklib_file)" ]]; then
|
|
116
|
+
printf '%s' "$HOOK_INPUT" \
|
|
117
|
+
| jq -j --arg us "$_HOOKLIB_US" --arg rs "$_HOOKLIB_RS" \
|
|
118
|
+
'.tool_input.file_path + $us + ((.tool_input.content // .tool_input.new_string) // "") + $rs' \
|
|
119
|
+
2>/dev/null || true
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
_hooklib_patch_text | _hooklib_parse_patch
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# hooklib_require_jq [name]: a content-scanning hook cannot verify an edit
|
|
127
|
+
# without jq. Rather than fail OPEN (the #63 bug), block once with an explicit,
|
|
128
|
+
# actionable message. jq is a documented prerequisite (void-machine doctor).
|
|
129
|
+
hooklib_require_jq() {
|
|
130
|
+
[[ "${HOOK_JQ:-0}" == 1 ]] && return 0
|
|
131
|
+
printf '%s: jq is required to inspect edit content but is not installed.\n' "${1:-hook}" >&2
|
|
132
|
+
printf 'Blocking rather than silently skipping enforcement. Install jq\n' >&2
|
|
133
|
+
printf '(e.g. brew install jq) or run: void-machine doctor.\n' >&2
|
|
134
|
+
exit 2
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# hooklib_root: the project root in physical form (CLAUDE_PROJECT_DIR, else the
|
|
138
|
+
# git toplevel, else pwd). Trailing slash stripped.
|
|
139
|
+
hooklib_root() {
|
|
140
|
+
local r
|
|
141
|
+
r=$(_hooklib_phys "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}")
|
|
142
|
+
printf '%s' "${r%/}"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# hooklib_relpath <path>: strip the project-root prefix so root-anchored globs
|
|
146
|
+
# match the ABSOLUTE paths Claude Code passes (#62). Physical-path compare
|
|
147
|
+
# (logical/physical roots differ under symlinks, e.g. macOS /var -> /private/var).
|
|
148
|
+
hooklib_relpath() {
|
|
149
|
+
local file="$1" root
|
|
150
|
+
[[ "$file" == /* ]] || { printf '%s' "$file"; return 0; }
|
|
151
|
+
root=$(hooklib_root)
|
|
152
|
+
file=$(_hooklib_phys "$file")
|
|
153
|
+
case "$file" in "$root"/*) file="${file#"$root"/}" ;; esac
|
|
154
|
+
printf '%s' "$file"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# _hooklib_phys: resolve the longest existing directory prefix to its physical
|
|
158
|
+
# path, then re-append the trailing (possibly non-existent) components.
|
|
159
|
+
_hooklib_phys() {
|
|
160
|
+
local p="$1" t=""
|
|
161
|
+
while [[ ! -d "$p" && "$p" == */* ]]; do t="/${p##*/}$t"; p="${p%/*}"; done
|
|
162
|
+
if [[ -d "$p" ]]; then printf '%s%s' "$(cd "$p" && pwd -P)" "$t"; else printf '%s' "$1"; fi
|
|
163
|
+
}
|