gentyr 1.3.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/.claude/agents/antipattern-hunter.md +176 -0
- package/.claude/agents/code-reviewer.md +205 -0
- package/.claude/agents/code-writer.md +154 -0
- package/.claude/agents/deputy-cto.md +309 -0
- package/.claude/agents/feedback-agent.md +101 -0
- package/.claude/agents/investigator.md +136 -0
- package/.claude/agents/product-manager.md +97 -0
- package/.claude/agents/project-manager.md +116 -0
- package/.claude/agents/repo-hygiene-expert.md +626 -0
- package/.claude/agents/secret-manager.md +324 -0
- package/.claude/agents/test-writer.md +354 -0
- package/.claude/commands/configure-personas.md +144 -0
- package/.claude/commands/cto-report.md +36 -0
- package/.claude/commands/demo.md +89 -0
- package/.claude/commands/deputy-cto.md +345 -0
- package/.claude/commands/hotfix.md +31 -0
- package/.claude/commands/overdrive-gentyr.md +167 -0
- package/.claude/commands/product-manager.md +32 -0
- package/.claude/commands/push-migrations.md +86 -0
- package/.claude/commands/push-secrets.md +97 -0
- package/.claude/commands/services.json.example +30 -0
- package/.claude/commands/setup-gentyr.md +396 -0
- package/.claude/commands/show.md +42 -0
- package/.claude/commands/spawn-tasks.md +79 -0
- package/.claude/commands/toggle-automation-gentyr.md +75 -0
- package/.claude/commands/toggle-product-manager.md +19 -0
- package/.claude/commands/triage.md +69 -0
- package/.claude/hooks/README.md +686 -0
- package/.claude/hooks/__tests__/README.md +129 -0
- package/.claude/hooks/agent-tracker.js +434 -0
- package/.claude/hooks/antipattern-hunter-hook.js +401 -0
- package/.claude/hooks/api-key-watcher.js +289 -0
- package/.claude/hooks/block-no-verify.js +301 -0
- package/.claude/hooks/bypass-approval-hook.js +313 -0
- package/.claude/hooks/compliance-checker.js +1309 -0
- package/.claude/hooks/config-reader.js +143 -0
- package/.claude/hooks/credential-file-guard.js +1139 -0
- package/.claude/hooks/credential-health-check.js +168 -0
- package/.claude/hooks/credential-sync-hook.js +79 -0
- package/.claude/hooks/cto-notification-hook.js +656 -0
- package/.claude/hooks/feedback-launcher.js +424 -0
- package/.claude/hooks/feedback-orchestrator.js +367 -0
- package/.claude/hooks/gentyr-splash.js +47 -0
- package/.claude/hooks/gentyr-sync.js +389 -0
- package/.claude/hooks/hourly-automation.js +3340 -0
- package/.claude/hooks/key-sync.js +899 -0
- package/.claude/hooks/lib/approval-utils.js +731 -0
- package/.claude/hooks/lib/feature-branch-helper.js +102 -0
- package/.claude/hooks/lib/worktree-manager.js +330 -0
- package/.claude/hooks/mapping-validator.js +285 -0
- package/.claude/hooks/plan-executor.js +398 -0
- package/.claude/hooks/playwright-cli-guard.js +104 -0
- package/.claude/hooks/playwright-health-check.js +71 -0
- package/.claude/hooks/pre-commit-review.js +725 -0
- package/.claude/hooks/prompts/local-spec-enforcement.md +310 -0
- package/.claude/hooks/prompts/mapping-fix.md +92 -0
- package/.claude/hooks/prompts/mapping-review.md +140 -0
- package/.claude/hooks/prompts/schema-mapper.md +185 -0
- package/.claude/hooks/prompts/spec-enforcement.md +233 -0
- package/.claude/hooks/protected-action-approval-hook.js +336 -0
- package/.claude/hooks/protected-action-gate.js +562 -0
- package/.claude/hooks/protected-actions.json +208 -0
- package/.claude/hooks/protected-actions.json.template +122 -0
- package/.claude/hooks/quota-monitor.js +490 -0
- package/.claude/hooks/reporters/jest-failure-reporter.js +401 -0
- package/.claude/hooks/reporters/playwright-failure-reporter.js +446 -0
- package/.claude/hooks/reporters/vitest-failure-reporter.js +443 -0
- package/.claude/hooks/schema-mapper-hook.js +544 -0
- package/.claude/hooks/secret-leak-detector.js +216 -0
- package/.claude/hooks/session-reviver.js +514 -0
- package/.claude/hooks/slash-command-prefetch.js +1145 -0
- package/.claude/hooks/stale-work-detector.js +205 -0
- package/.claude/hooks/stop-continue-hook.js +414 -0
- package/.claude/hooks/todo-maintenance.js +522 -0
- package/.claude/hooks/todo-processing-prompt.md +75 -0
- package/.claude/hooks/usage-optimizer.js +791 -0
- package/.claude/mcp/README.md +246 -0
- package/.claude/settings.json.template +168 -0
- package/.mcp.json.template +207 -0
- package/CLAUDE.md +340 -0
- package/CLAUDE.md.gentyr-section +89 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/cli/commands/init.js +471 -0
- package/cli/commands/migrate.js +132 -0
- package/cli/commands/protect.js +271 -0
- package/cli/commands/scaffold.js +48 -0
- package/cli/commands/status.js +133 -0
- package/cli/commands/sync.js +101 -0
- package/cli/commands/uninstall.js +207 -0
- package/cli/index.js +111 -0
- package/cli/lib/config-gen.js +214 -0
- package/cli/lib/resolve-framework.js +97 -0
- package/cli/lib/state.js +140 -0
- package/cli/lib/symlinks.js +260 -0
- package/docs/AUTOMATION-SYSTEMS.md +484 -0
- package/docs/BINARY-PATCHING.md +212 -0
- package/docs/CHANGELOG.md +2830 -0
- package/docs/CREDENTIAL-DETECTION.md +151 -0
- package/docs/CTO-DASHBOARD.md +476 -0
- package/docs/DEPLOYMENT-FLOW.md +477 -0
- package/docs/DEVELOPER.md +116 -0
- package/docs/Executive.md +372 -0
- package/docs/SECRET-PATHS.md +77 -0
- package/docs/SETUP-GUIDE.md +419 -0
- package/docs/STACK.md +109 -0
- package/docs/TESTING.md +440 -0
- package/docs/assets/claude-logo.svg +3 -0
- package/docs/sessions/2026-01-24-spec-suite-implementation.md +190 -0
- package/docs/sessions/2026-02-15-feedback-e2e-audit.md +484 -0
- package/docs/sessions/2026-02-20-credential-rotation-experiments.md +340 -0
- package/docs/sessions/TEST-COVERAGE-REPORT-2026-02-20.md +168 -0
- package/docs/shared/EPHEMERAL-STATE-FILES.md +115 -0
- package/docs/shared/PROTECTION-SYSTEM.md +341 -0
- package/husky/post-commit +10 -0
- package/husky/pre-commit +40 -0
- package/husky/pre-push +94 -0
- package/package.json +43 -0
- package/packages/cto-dashboard/package-lock.json +3510 -0
- package/packages/cto-dashboard/package.json +41 -0
- package/packages/cto-dashboard/pnpm-lock.yaml +2168 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts +220 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.js +376 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.js.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/index.d.ts +121 -0
- package/packages/mcp-servers/dist/__testUtils__/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/index.js +180 -0
- package/packages/mcp-servers/dist/__testUtils__/index.js.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts +84 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.js +309 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/index.d.ts +7 -0
- package/packages/mcp-servers/dist/agent-reports/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/index.js +8 -0
- package/packages/mcp-servers/dist/agent-reports/index.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/server.d.ts +22 -0
- package/packages/mcp-servers/dist/agent-reports/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/server.js +535 -0
- package/packages/mcp-servers/dist/agent-reports/server.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/types.d.ts +258 -0
- package/packages/mcp-servers/dist/agent-reports/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/types.js +81 -0
- package/packages/mcp-servers/dist/agent-reports/types.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/index.d.ts +5 -0
- package/packages/mcp-servers/dist/agent-tracker/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/index.js +5 -0
- package/packages/mcp-servers/dist/agent-tracker/index.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/server.d.ts +12 -0
- package/packages/mcp-servers/dist/agent-tracker/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/server.js +919 -0
- package/packages/mcp-servers/dist/agent-tracker/server.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/types.d.ts +328 -0
- package/packages/mcp-servers/dist/agent-tracker/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/types.js +128 -0
- package/packages/mcp-servers/dist/agent-tracker/types.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts +27 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js +167 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.d.ts +6 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.js +6 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.d.ts +13 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.js +959 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.d.ts +41 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.js +8 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/index.d.ts +8 -0
- package/packages/mcp-servers/dist/cloudflare/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/index.js +8 -0
- package/packages/mcp-servers/dist/cloudflare/index.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/server.d.ts +16 -0
- package/packages/mcp-servers/dist/cloudflare/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/server.js +253 -0
- package/packages/mcp-servers/dist/cloudflare/server.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/types.d.ts +141 -0
- package/packages/mcp-servers/dist/cloudflare/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/types.js +53 -0
- package/packages/mcp-servers/dist/cloudflare/types.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/index.d.ts +7 -0
- package/packages/mcp-servers/dist/codecov/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/index.js +7 -0
- package/packages/mcp-servers/dist/codecov/index.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/server.d.ts +21 -0
- package/packages/mcp-servers/dist/codecov/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/server.js +376 -0
- package/packages/mcp-servers/dist/codecov/server.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/types.d.ts +269 -0
- package/packages/mcp-servers/dist/codecov/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/types.js +128 -0
- package/packages/mcp-servers/dist/codecov/types.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/index.d.ts +9 -0
- package/packages/mcp-servers/dist/cto-report/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/index.js +9 -0
- package/packages/mcp-servers/dist/cto-report/index.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/server.d.ts +14 -0
- package/packages/mcp-servers/dist/cto-report/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/server.js +859 -0
- package/packages/mcp-servers/dist/cto-report/server.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/types.d.ts +213 -0
- package/packages/mcp-servers/dist/cto-report/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/types.js +29 -0
- package/packages/mcp-servers/dist/cto-report/types.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/index.d.ts +7 -0
- package/packages/mcp-servers/dist/cto-reports/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/index.js +8 -0
- package/packages/mcp-servers/dist/cto-reports/index.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/server.d.ts +20 -0
- package/packages/mcp-servers/dist/cto-reports/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/server.js +538 -0
- package/packages/mcp-servers/dist/cto-reports/server.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/types.d.ts +236 -0
- package/packages/mcp-servers/dist/cto-reports/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/types.js +77 -0
- package/packages/mcp-servers/dist/cto-reports/types.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/index.d.ts +7 -0
- package/packages/mcp-servers/dist/deputy-cto/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/index.js +8 -0
- package/packages/mcp-servers/dist/deputy-cto/index.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/server.d.ts +23 -0
- package/packages/mcp-servers/dist/deputy-cto/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/server.js +1700 -0
- package/packages/mcp-servers/dist/deputy-cto/server.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/types.d.ts +439 -0
- package/packages/mcp-servers/dist/deputy-cto/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/types.js +102 -0
- package/packages/mcp-servers/dist/deputy-cto/types.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/index.d.ts +5 -0
- package/packages/mcp-servers/dist/elastic-logs/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/index.js +5 -0
- package/packages/mcp-servers/dist/elastic-logs/index.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/server.d.ts +18 -0
- package/packages/mcp-servers/dist/elastic-logs/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/server.js +259 -0
- package/packages/mcp-servers/dist/elastic-logs/server.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/types.d.ts +107 -0
- package/packages/mcp-servers/dist/elastic-logs/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/types.js +31 -0
- package/packages/mcp-servers/dist/elastic-logs/types.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.d.ts +2 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.js +2 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.d.ts +21 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.js +580 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.d.ts +331 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.js +40 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.d.ts +9 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.js +9 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.d.ts +36 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.js +392 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.d.ts +152 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.js +67 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.js.map +1 -0
- package/packages/mcp-servers/dist/github/index.d.ts +7 -0
- package/packages/mcp-servers/dist/github/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/index.js +7 -0
- package/packages/mcp-servers/dist/github/index.js.map +1 -0
- package/packages/mcp-servers/dist/github/server.d.ts +15 -0
- package/packages/mcp-servers/dist/github/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/server.js +686 -0
- package/packages/mcp-servers/dist/github/server.js.map +1 -0
- package/packages/mcp-servers/dist/github/types.d.ts +660 -0
- package/packages/mcp-servers/dist/github/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/types.js +209 -0
- package/packages/mcp-servers/dist/github/types.js.map +1 -0
- package/packages/mcp-servers/dist/index.d.ts +30 -0
- package/packages/mcp-servers/dist/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/index.js +32 -0
- package/packages/mcp-servers/dist/index.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.d.ts +5 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.js +5 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.d.ts +15 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.js +252 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.d.ts +74 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.js +20 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/index.d.ts +2 -0
- package/packages/mcp-servers/dist/onepassword/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/index.js +2 -0
- package/packages/mcp-servers/dist/onepassword/index.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/server.d.ts +2 -0
- package/packages/mcp-servers/dist/onepassword/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/server.js +159 -0
- package/packages/mcp-servers/dist/onepassword/server.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/types.d.ts +55 -0
- package/packages/mcp-servers/dist/onepassword/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/types.js +22 -0
- package/packages/mcp-servers/dist/onepassword/types.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/helpers.d.ts +20 -0
- package/packages/mcp-servers/dist/playwright/helpers.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/helpers.js +31 -0
- package/packages/mcp-servers/dist/playwright/helpers.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/index.d.ts +5 -0
- package/packages/mcp-servers/dist/playwright/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/index.js +5 -0
- package/packages/mcp-servers/dist/playwright/index.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/server.d.ts +13 -0
- package/packages/mcp-servers/dist/playwright/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/server.js +1201 -0
- package/packages/mcp-servers/dist/playwright/server.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/types.d.ts +216 -0
- package/packages/mcp-servers/dist/playwright/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/types.js +172 -0
- package/packages/mcp-servers/dist/playwright/types.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts +39 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js +71 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.d.ts +5 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.js +5 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.d.ts +34 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.js +538 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.d.ts +305 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.js +123 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/product-manager/server.d.ts +17 -0
- package/packages/mcp-servers/dist/product-manager/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/product-manager/server.js +690 -0
- package/packages/mcp-servers/dist/product-manager/server.js.map +1 -0
- package/packages/mcp-servers/dist/product-manager/types.d.ts +286 -0
- package/packages/mcp-servers/dist/product-manager/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/product-manager/types.js +99 -0
- package/packages/mcp-servers/dist/product-manager/types.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts +7 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.js +7 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts +19 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js +174 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts +35 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.js +465 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts +127 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.js +80 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/render/index.d.ts +8 -0
- package/packages/mcp-servers/dist/render/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/index.js +8 -0
- package/packages/mcp-servers/dist/render/index.js.map +1 -0
- package/packages/mcp-servers/dist/render/server.d.ts +15 -0
- package/packages/mcp-servers/dist/render/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/server.js +428 -0
- package/packages/mcp-servers/dist/render/server.js.map +1 -0
- package/packages/mcp-servers/dist/render/types.d.ts +273 -0
- package/packages/mcp-servers/dist/render/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/types.js +102 -0
- package/packages/mcp-servers/dist/render/types.js.map +1 -0
- package/packages/mcp-servers/dist/resend/index.d.ts +7 -0
- package/packages/mcp-servers/dist/resend/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/index.js +7 -0
- package/packages/mcp-servers/dist/resend/index.js.map +1 -0
- package/packages/mcp-servers/dist/resend/server.d.ts +15 -0
- package/packages/mcp-servers/dist/resend/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/server.js +298 -0
- package/packages/mcp-servers/dist/resend/server.js.map +1 -0
- package/packages/mcp-servers/dist/resend/types.d.ts +222 -0
- package/packages/mcp-servers/dist/resend/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/types.js +58 -0
- package/packages/mcp-servers/dist/resend/types.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/index.d.ts +6 -0
- package/packages/mcp-servers/dist/review-queue/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/index.js +6 -0
- package/packages/mcp-servers/dist/review-queue/index.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/server.d.ts +17 -0
- package/packages/mcp-servers/dist/review-queue/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/server.js +348 -0
- package/packages/mcp-servers/dist/review-queue/server.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/types.d.ts +162 -0
- package/packages/mcp-servers/dist/review-queue/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/types.js +56 -0
- package/packages/mcp-servers/dist/review-queue/types.js.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/server.d.ts +19 -0
- package/packages/mcp-servers/dist/secret-sync/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/server.js +1139 -0
- package/packages/mcp-servers/dist/secret-sync/server.js.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/types.d.ts +442 -0
- package/packages/mcp-servers/dist/secret-sync/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/types.js +113 -0
- package/packages/mcp-servers/dist/secret-sync/types.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/index.d.ts +5 -0
- package/packages/mcp-servers/dist/session-events/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/index.js +5 -0
- package/packages/mcp-servers/dist/session-events/index.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/server.d.ts +11 -0
- package/packages/mcp-servers/dist/session-events/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/server.js +290 -0
- package/packages/mcp-servers/dist/session-events/server.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/types.d.ts +213 -0
- package/packages/mcp-servers/dist/session-events/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/types.js +69 -0
- package/packages/mcp-servers/dist/session-events/types.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/index.d.ts +9 -0
- package/packages/mcp-servers/dist/session-restart/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/index.js +9 -0
- package/packages/mcp-servers/dist/session-restart/index.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/server.d.ts +20 -0
- package/packages/mcp-servers/dist/session-restart/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/server.js +411 -0
- package/packages/mcp-servers/dist/session-restart/server.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/types.d.ts +26 -0
- package/packages/mcp-servers/dist/session-restart/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/types.js +16 -0
- package/packages/mcp-servers/dist/session-restart/types.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/index.d.ts +5 -0
- package/packages/mcp-servers/dist/setup-helper/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/index.js +5 -0
- package/packages/mcp-servers/dist/setup-helper/index.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/server.d.ts +14 -0
- package/packages/mcp-servers/dist/setup-helper/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/server.js +454 -0
- package/packages/mcp-servers/dist/setup-helper/server.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/types.d.ts +81 -0
- package/packages/mcp-servers/dist/setup-helper/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/types.js +41 -0
- package/packages/mcp-servers/dist/setup-helper/types.js.map +1 -0
- package/packages/mcp-servers/dist/shared/audited-server.d.ts +31 -0
- package/packages/mcp-servers/dist/shared/audited-server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/audited-server.js +126 -0
- package/packages/mcp-servers/dist/shared/audited-server.js.map +1 -0
- package/packages/mcp-servers/dist/shared/constants.d.ts +26 -0
- package/packages/mcp-servers/dist/shared/constants.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/constants.js +41 -0
- package/packages/mcp-servers/dist/shared/constants.js.map +1 -0
- package/packages/mcp-servers/dist/shared/index.d.ts +6 -0
- package/packages/mcp-servers/dist/shared/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/index.js +6 -0
- package/packages/mcp-servers/dist/shared/index.js.map +1 -0
- package/packages/mcp-servers/dist/shared/readonly-db.d.ts +11 -0
- package/packages/mcp-servers/dist/shared/readonly-db.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/readonly-db.js +47 -0
- package/packages/mcp-servers/dist/shared/readonly-db.js.map +1 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.d.ts +20 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.js +65 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.js.map +1 -0
- package/packages/mcp-servers/dist/shared/server.d.ts +86 -0
- package/packages/mcp-servers/dist/shared/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/server.js +291 -0
- package/packages/mcp-servers/dist/shared/server.js.map +1 -0
- package/packages/mcp-servers/dist/shared/types.d.ts +113 -0
- package/packages/mcp-servers/dist/shared/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/types.js +36 -0
- package/packages/mcp-servers/dist/shared/types.js.map +1 -0
- package/packages/mcp-servers/dist/show/server.d.ts +12 -0
- package/packages/mcp-servers/dist/show/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/show/server.js +97 -0
- package/packages/mcp-servers/dist/show/server.js.map +1 -0
- package/packages/mcp-servers/dist/show/types.d.ts +19 -0
- package/packages/mcp-servers/dist/show/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/show/types.js +32 -0
- package/packages/mcp-servers/dist/show/types.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/index.d.ts +5 -0
- package/packages/mcp-servers/dist/specs-browser/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/index.js +5 -0
- package/packages/mcp-servers/dist/specs-browser/index.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/server.d.ts +13 -0
- package/packages/mcp-servers/dist/specs-browser/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/server.js +692 -0
- package/packages/mcp-servers/dist/specs-browser/server.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/types.d.ts +337 -0
- package/packages/mcp-servers/dist/specs-browser/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/types.js +134 -0
- package/packages/mcp-servers/dist/specs-browser/types.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/index.d.ts +10 -0
- package/packages/mcp-servers/dist/supabase/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/index.js +10 -0
- package/packages/mcp-servers/dist/supabase/index.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/server.d.ts +20 -0
- package/packages/mcp-servers/dist/supabase/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/server.js +451 -0
- package/packages/mcp-servers/dist/supabase/server.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/types.d.ts +196 -0
- package/packages/mcp-servers/dist/supabase/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/types.js +76 -0
- package/packages/mcp-servers/dist/supabase/types.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/index.d.ts +5 -0
- package/packages/mcp-servers/dist/todo-db/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/index.js +5 -0
- package/packages/mcp-servers/dist/todo-db/index.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/server.d.ts +13 -0
- package/packages/mcp-servers/dist/todo-db/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/server.js +649 -0
- package/packages/mcp-servers/dist/todo-db/server.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/types.d.ts +225 -0
- package/packages/mcp-servers/dist/todo-db/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/types.js +69 -0
- package/packages/mcp-servers/dist/todo-db/types.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/index.d.ts +7 -0
- package/packages/mcp-servers/dist/user-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/index.js +8 -0
- package/packages/mcp-servers/dist/user-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/server.d.ts +25 -0
- package/packages/mcp-servers/dist/user-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/server.js +914 -0
- package/packages/mcp-servers/dist/user-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/types.d.ts +415 -0
- package/packages/mcp-servers/dist/user-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/types.js +132 -0
- package/packages/mcp-servers/dist/user-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/index.d.ts +9 -0
- package/packages/mcp-servers/dist/vercel/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/index.js +9 -0
- package/packages/mcp-servers/dist/vercel/index.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/server.d.ts +17 -0
- package/packages/mcp-servers/dist/vercel/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/server.js +265 -0
- package/packages/mcp-servers/dist/vercel/server.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/types.d.ts +189 -0
- package/packages/mcp-servers/dist/vercel/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/types.js +65 -0
- package/packages/mcp-servers/dist/vercel/types.js.map +1 -0
- package/packages/mcp-servers/package-lock.json +3765 -0
- package/packages/mcp-servers/package.json +64 -0
- package/packages/mcp-servers/test/reporters/test-failure-reporter.ts +372 -0
- package/packages/mcp-servers/vitest.config.ts +27 -0
- package/scripts/__tests__/README.md +163 -0
- package/scripts/apply-credential-hardening.sh +271 -0
- package/scripts/credential-providers/manual.js +56 -0
- package/scripts/credential-providers/onepassword.js +85 -0
- package/scripts/credential-providers/provider-interface.js +104 -0
- package/scripts/encrypt-credential.js +337 -0
- package/scripts/feedback-launcher.js +338 -0
- package/scripts/feedback-orchestrator.js +373 -0
- package/scripts/fix-mcp-launcher-issues.sh +97 -0
- package/scripts/force-spawn-tasks.js +651 -0
- package/scripts/force-triage-reports.js +560 -0
- package/scripts/generate-protected-actions-spec.js +142 -0
- package/scripts/generate-proxy-certs.sh +158 -0
- package/scripts/grant-chrome-ext-permissions.sh +242 -0
- package/scripts/mcp-launcher.js +125 -0
- package/scripts/merge-settings.cjs +167 -0
- package/scripts/patch-clawd.py +844 -0
- package/scripts/patch-credential-cache.py +313 -0
- package/scripts/patches/credential-file-guard-patched.mjs +573 -0
- package/scripts/patches/credential-file-guard.js.patched +573 -0
- package/scripts/patches/verify-tokenizer.mjs +132 -0
- package/scripts/protect-framework.sh +478 -0
- package/scripts/readme-chrome.template +12 -0
- package/scripts/reap-completed-agents.js +439 -0
- package/scripts/reinstall.sh +86 -0
- package/scripts/resign-node.sh +185 -0
- package/scripts/rotation-proxy.js +656 -0
- package/scripts/rotation-stress-monitor.mjs +862 -0
- package/scripts/setup-automation-service.sh +648 -0
- package/scripts/setup-check.js +251 -0
- package/scripts/watch-claude-version.js +142 -0
- package/specs/framework/CORE-INVARIANTS.md +161 -0
- package/specs/patterns/AGENT-PATTERNS.md +223 -0
- package/specs/patterns/HOOK-PATTERNS.md +242 -0
- package/specs/patterns/MCP-SERVER-PATTERNS.md +144 -0
- package/templates/config/gitignore.template +14 -0
- package/templates/config/merge-chain-check.yml.template +51 -0
- package/templates/config/package.json.template +18 -0
- package/templates/config/pnpm-workspace.yaml +5 -0
- package/templates/config/services.json.template +18 -0
- package/templates/config/tsconfig.base.json +17 -0
- package/templates/scaffold/integrations/_template/.gitkeep +0 -0
- package/templates/scaffold/packages/logger/package.json +17 -0
- package/templates/scaffold/packages/logger/src/logger.ts +44 -0
- package/templates/scaffold/packages/shared/package.json +17 -0
- package/templates/scaffold/packages/shared/src/errors.ts +43 -0
- package/templates/scaffold/products/_product/apps/backend/package.json +21 -0
- package/templates/scaffold/products/_product/apps/backend/src/index.ts +17 -0
- package/templates/scaffold/products/_product/apps/extension/.gitkeep +0 -0
- package/templates/scaffold/products/_product/apps/web/.gitkeep +0 -0
- package/templates/scaffold/specs/global/.gitkeep +0 -0
- package/templates/scaffold/specs/local/.gitkeep +0 -0
- package/templates/scaffold/specs/reference/.gitkeep +0 -0
- package/version.json +15 -0
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compliance Checker System for ODIN
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates batch compliance checking with DUAL ENFORCEMENT MODES:
|
|
7
|
+
*
|
|
8
|
+
* GLOBAL ENFORCEMENT (spec-file-mappings.json):
|
|
9
|
+
* - Validates mapping file
|
|
10
|
+
* - Checks specific files against global specs
|
|
11
|
+
* - Per-file cooldown (7 days default)
|
|
12
|
+
* - Daily agent cap (22 agents default)
|
|
13
|
+
*
|
|
14
|
+
* LOCAL ENFORCEMENT (specs/local/*.md):
|
|
15
|
+
* - No file mappings required
|
|
16
|
+
* - Agent explores codebase freely using Glob/Grep
|
|
17
|
+
* - Per-spec cooldown (7 days default)
|
|
18
|
+
* - Daily agent cap (3 agents default)
|
|
19
|
+
* - One agent per spec file
|
|
20
|
+
*
|
|
21
|
+
* Key Features:
|
|
22
|
+
* - Dual rate limiting (global + local separate budgets)
|
|
23
|
+
* - Mapping validation with auto-fix/review (global only)
|
|
24
|
+
* - Fire-and-forget post-commit integration
|
|
25
|
+
* - Full enforcement history tracking per mode
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* - 0: Success
|
|
29
|
+
* - 1: Error
|
|
30
|
+
* - 2: Validation failed (mapping file issues)
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* node compliance-checker.js [--status] [--dry-run]
|
|
34
|
+
* node compliance-checker.js [--global-only] [--local-only]
|
|
35
|
+
* node compliance-checker.js [--history] [--history-global] [--history-local]
|
|
36
|
+
*
|
|
37
|
+
* @author Claude Code Hooks
|
|
38
|
+
* @version 2.0.0
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import fs from 'fs';
|
|
42
|
+
import path from 'path';
|
|
43
|
+
import { spawn } from 'child_process';
|
|
44
|
+
import { validateMappings, formatValidationResult } from './mapping-validator.js';
|
|
45
|
+
import { registerSpawn, registerHookExecution, AGENT_TYPES, HOOK_TYPES } from './agent-tracker.js';
|
|
46
|
+
import { getCooldown } from './config-reader.js';
|
|
47
|
+
|
|
48
|
+
// Project directory
|
|
49
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
50
|
+
|
|
51
|
+
// Suites config path (optional feature)
|
|
52
|
+
const SUITES_CONFIG_PATH = path.join(projectDir, '.claude/hooks/suites-config.json');
|
|
53
|
+
|
|
54
|
+
// Load configuration
|
|
55
|
+
const configPath = path.join(projectDir, '.claude/hooks/compliance-config.json');
|
|
56
|
+
let CONFIG;
|
|
57
|
+
try {
|
|
58
|
+
const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
59
|
+
// Backwards compatibility: migrate old flat config to nested structure
|
|
60
|
+
if (!rawConfig.global && !rawConfig.local) {
|
|
61
|
+
CONFIG = {
|
|
62
|
+
global: {
|
|
63
|
+
maxAgentsPerDay: rawConfig.maxAgentsPerDay || 22,
|
|
64
|
+
fileVerificationCooldownDays: rawConfig.fileVerificationCooldownDays || 7,
|
|
65
|
+
mappingReviewCooldownDays: rawConfig.mappingReviewCooldownDays || 7,
|
|
66
|
+
mappingFixCooldownHours: rawConfig.mappingFixCooldownHours || 3
|
|
67
|
+
},
|
|
68
|
+
local: {
|
|
69
|
+
maxAgentsPerDay: rawConfig.local?.maxAgentsPerDay || 3,
|
|
70
|
+
specCooldownDays: rawConfig.local?.specCooldownDays || 7
|
|
71
|
+
},
|
|
72
|
+
autoRunIntervalDays: rawConfig.autoRunIntervalDays || 7,
|
|
73
|
+
concurrency: rawConfig.concurrency || 5,
|
|
74
|
+
mappingFile: rawConfig.mappingFile || '.claude/hooks/spec-file-mappings.json',
|
|
75
|
+
stateFile: rawConfig.stateFile || '.claude/hooks/compliance-state.json',
|
|
76
|
+
logFile: rawConfig.logFile || '.claude/hooks/compliance-log.json',
|
|
77
|
+
specsGlobalDir: rawConfig.specsGlobalDir || 'specs/global',
|
|
78
|
+
specsLocalDir: rawConfig.specsLocalDir || 'specs/local'
|
|
79
|
+
};
|
|
80
|
+
} else {
|
|
81
|
+
CONFIG = rawConfig;
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(`Failed to load compliance-config.json: ${err.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Overlay dynamic cooldowns from usage-optimizer (allows overdrive to scale these)
|
|
89
|
+
CONFIG.global.fileVerificationCooldownDays = getCooldown('compliance_checker_file', 10080) / (60 * 24);
|
|
90
|
+
CONFIG.local.specCooldownDays = getCooldown('compliance_checker_spec', 10080) / (60 * 24);
|
|
91
|
+
|
|
92
|
+
// State file paths
|
|
93
|
+
const STATE_FILE = path.join(projectDir, CONFIG.stateFile);
|
|
94
|
+
const LOG_FILE = path.join(projectDir, CONFIG.logFile);
|
|
95
|
+
const MAPPING_FILE = path.join(projectDir, CONFIG.mappingFile);
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Suite Config Support (Optional Feature)
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Load suites config from suites-config.json
|
|
103
|
+
* Returns null if file doesn't exist (feature is optional)
|
|
104
|
+
*/
|
|
105
|
+
function loadSuitesConfig() {
|
|
106
|
+
if (!fs.existsSync(SUITES_CONFIG_PATH)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(fs.readFileSync(SUITES_CONFIG_PATH, 'utf8'));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`Failed to load suites-config.json: ${err.message}`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Simple glob pattern matching (minimatch-like)
|
|
119
|
+
* Supports: *, **, ?
|
|
120
|
+
*/
|
|
121
|
+
function matchesGlob(filePath, pattern) {
|
|
122
|
+
// Convert glob pattern to regex
|
|
123
|
+
let regexStr = pattern
|
|
124
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
|
|
125
|
+
.replace(/\*\*/g, '{{DOUBLESTAR}}') // Placeholder for **
|
|
126
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
127
|
+
.replace(/\?/g, '[^/]') // ? matches single char except /
|
|
128
|
+
.replace(/\{\{DOUBLESTAR\}\}/g, '.*'); // ** matches everything
|
|
129
|
+
|
|
130
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
131
|
+
return regex.test(filePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get all suites that match a file path
|
|
136
|
+
* @param {string} filePath - Relative path to check
|
|
137
|
+
* @param {object} suitesConfig - Loaded suites config (or null)
|
|
138
|
+
* @returns {Array<{id: string, ...suite}>} Array of matching suites
|
|
139
|
+
*/
|
|
140
|
+
function getSuitesForFile(filePath, suitesConfig) {
|
|
141
|
+
if (!suitesConfig) return [];
|
|
142
|
+
|
|
143
|
+
const matches = [];
|
|
144
|
+
for (const [suiteId, suite] of Object.entries(suitesConfig.suites)) {
|
|
145
|
+
if (!suite.enabled) continue;
|
|
146
|
+
if (matchesGlob(filePath, suite.scope)) {
|
|
147
|
+
matches.push({ id: suiteId, ...suite });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return matches;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get ALL specs that apply to a file (main specs + subspecs)
|
|
155
|
+
* Single function used by enforcement - DRY principle
|
|
156
|
+
*
|
|
157
|
+
* @param {string} filePath - Relative path to check
|
|
158
|
+
* @param {object} mappings - Loaded spec-file-mappings.json
|
|
159
|
+
* @param {object} suitesConfig - Loaded suites config (or null)
|
|
160
|
+
* @returns {Array<{name: string, dir: string, priority: string, lastVerified: string|null, suiteId: string|null, suiteScope: string|null}>}
|
|
161
|
+
*/
|
|
162
|
+
function getAllApplicableSpecs(filePath, mappings, suitesConfig) {
|
|
163
|
+
const specs = [];
|
|
164
|
+
|
|
165
|
+
// 1. Main specs from spec-file-mappings.json (existing behavior)
|
|
166
|
+
for (const [specName, specData] of Object.entries(mappings.specs || {})) {
|
|
167
|
+
const fileEntry = specData.files?.find(f => f.path === filePath);
|
|
168
|
+
if (fileEntry) {
|
|
169
|
+
specs.push({
|
|
170
|
+
name: specName,
|
|
171
|
+
dir: CONFIG.specsGlobalDir, // specs/global by default
|
|
172
|
+
priority: specData.priority,
|
|
173
|
+
lastVerified: fileEntry.lastVerified,
|
|
174
|
+
suiteId: null, // null = main spec
|
|
175
|
+
suiteScope: null
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2. Subspecs from matching suites (extension)
|
|
181
|
+
if (suitesConfig) {
|
|
182
|
+
const matchingSuites = getSuitesForFile(filePath, suitesConfig);
|
|
183
|
+
for (const suite of matchingSuites) {
|
|
184
|
+
if (!suite.mappedSpecs) continue;
|
|
185
|
+
const specsDir = path.join(projectDir, suite.mappedSpecs.dir);
|
|
186
|
+
if (!fs.existsSync(specsDir)) continue;
|
|
187
|
+
|
|
188
|
+
const pattern = suite.mappedSpecs.pattern || '*.md';
|
|
189
|
+
const specFiles = fs.readdirSync(specsDir).filter(f => {
|
|
190
|
+
if (!f.endsWith('.md')) return false;
|
|
191
|
+
return matchesGlob(f, pattern);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
for (const specFile of specFiles) {
|
|
195
|
+
specs.push({
|
|
196
|
+
name: specFile,
|
|
197
|
+
dir: suite.mappedSpecs.dir,
|
|
198
|
+
priority: suite.priority || 'medium',
|
|
199
|
+
lastVerified: null, // TODO: track per-suite cooldowns
|
|
200
|
+
suiteId: suite.id,
|
|
201
|
+
suiteScope: suite.scope
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return specs;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get ALL exploratory specs (main local specs + suite exploratory specs)
|
|
212
|
+
* Single function used by local enforcement - DRY principle
|
|
213
|
+
*
|
|
214
|
+
* @param {object} suitesConfig - Loaded suites config (or null)
|
|
215
|
+
* @returns {Array<{name: string, dir: string, suiteId: string|null, suiteScope: string}>}
|
|
216
|
+
*/
|
|
217
|
+
function getAllExploratorySpecs(suitesConfig) {
|
|
218
|
+
const specs = [];
|
|
219
|
+
|
|
220
|
+
// 1. Main local specs (existing behavior)
|
|
221
|
+
const mainLocalDir = path.join(projectDir, CONFIG.specsLocalDir);
|
|
222
|
+
if (fs.existsSync(mainLocalDir)) {
|
|
223
|
+
const files = fs.readdirSync(mainLocalDir).filter(f => f.endsWith('.md'));
|
|
224
|
+
for (const f of files) {
|
|
225
|
+
specs.push({
|
|
226
|
+
name: f,
|
|
227
|
+
dir: CONFIG.specsLocalDir,
|
|
228
|
+
suiteId: null,
|
|
229
|
+
suiteScope: '**/*' // main local specs explore entire codebase
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 2. Suite exploratory specs (extension)
|
|
235
|
+
if (suitesConfig) {
|
|
236
|
+
for (const [suiteId, suite] of Object.entries(suitesConfig.suites)) {
|
|
237
|
+
if (!suite.enabled || !suite.exploratorySpecs) continue;
|
|
238
|
+
const specsDir = path.join(projectDir, suite.exploratorySpecs.dir);
|
|
239
|
+
if (!fs.existsSync(specsDir)) continue;
|
|
240
|
+
|
|
241
|
+
const pattern = suite.exploratorySpecs.pattern || '*.md';
|
|
242
|
+
const specFiles = fs.readdirSync(specsDir).filter(f => {
|
|
243
|
+
if (!f.endsWith('.md')) return false;
|
|
244
|
+
return matchesGlob(f, pattern);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
for (const specFile of specFiles) {
|
|
248
|
+
specs.push({
|
|
249
|
+
name: specFile,
|
|
250
|
+
dir: suite.exploratorySpecs.dir,
|
|
251
|
+
suiteId: suiteId,
|
|
252
|
+
suiteScope: suite.scope // agent explores only within this scope
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return specs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Command Line Parsing
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Parse command line arguments
|
|
267
|
+
* @param {string[]} args
|
|
268
|
+
* @returns {object}
|
|
269
|
+
*/
|
|
270
|
+
function parseArgs(args) {
|
|
271
|
+
return {
|
|
272
|
+
status: args.includes('--status'),
|
|
273
|
+
dryRun: args.includes('--dry-run'),
|
|
274
|
+
globalOnly: args.includes('--global-only'),
|
|
275
|
+
localOnly: args.includes('--local-only'),
|
|
276
|
+
postCommit: args.includes('--post-commit'),
|
|
277
|
+
history: args.includes('--history'),
|
|
278
|
+
historyGlobal: args.includes('--history-global'),
|
|
279
|
+
historyLocal: args.includes('--history-local')
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read state file
|
|
285
|
+
* @returns {object}
|
|
286
|
+
*/
|
|
287
|
+
function readState() {
|
|
288
|
+
try {
|
|
289
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
290
|
+
} catch {
|
|
291
|
+
// Return default state if file doesn't exist
|
|
292
|
+
return {
|
|
293
|
+
version: 1,
|
|
294
|
+
globalSpecs: { lastRun: null, nextEligible: null },
|
|
295
|
+
localSpecs: { lastRun: null, nextEligible: null, perSpecLastRun: {} }
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Write state file
|
|
302
|
+
* @param {object} state
|
|
303
|
+
*/
|
|
304
|
+
function writeState(state) {
|
|
305
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Read daily spawn log
|
|
310
|
+
* @returns {object}
|
|
311
|
+
*/
|
|
312
|
+
function readLog() {
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(fs.readFileSync(LOG_FILE, 'utf8'));
|
|
315
|
+
} catch {
|
|
316
|
+
// Return default log if file doesn't exist
|
|
317
|
+
return {
|
|
318
|
+
version: 1,
|
|
319
|
+
dailySpawns: {},
|
|
320
|
+
history: []
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Write daily spawn log
|
|
327
|
+
* @param {object} log
|
|
328
|
+
*/
|
|
329
|
+
function writeLog(log) {
|
|
330
|
+
fs.writeFileSync(LOG_FILE, JSON.stringify(log, null, 2), 'utf8');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get date string in YYYY-MM-DD format
|
|
335
|
+
* @param {Date} [date] - Optional date to format (defaults to today)
|
|
336
|
+
* @returns {string}
|
|
337
|
+
*/
|
|
338
|
+
function getTodayString(date = null) {
|
|
339
|
+
const now = date || new Date();
|
|
340
|
+
const year = now.getFullYear();
|
|
341
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
342
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
343
|
+
return `${year}-${month}-${day}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check if daily agent cap has been reached for a specific mode
|
|
348
|
+
* @param {string} mode - 'global' or 'local'
|
|
349
|
+
* @returns {{ allowed: boolean, used: number, limit: number, remaining: number }}
|
|
350
|
+
*/
|
|
351
|
+
function checkDailyAgentCap(mode = 'global') {
|
|
352
|
+
const log = readLog();
|
|
353
|
+
const today = getTodayString();
|
|
354
|
+
|
|
355
|
+
// Count spawns for this mode today
|
|
356
|
+
const todaySpawns = log.history.filter(h => {
|
|
357
|
+
const spawnDate = getTodayString(new Date(h.date));
|
|
358
|
+
return spawnDate === today && h.mode === mode;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const used = todaySpawns.reduce((sum, h) => sum + h.count, 0);
|
|
362
|
+
const limit = mode === 'global' ? CONFIG.global.maxAgentsPerDay : CONFIG.local.maxAgentsPerDay;
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
allowed: used < limit,
|
|
366
|
+
used,
|
|
367
|
+
limit,
|
|
368
|
+
remaining: Math.max(0, limit - used)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Record agent spawns for today
|
|
374
|
+
* @param {string} mode - 'global' or 'local' enforcement mode
|
|
375
|
+
* @param {Array<{spec: string, file: string, priority: string}>} agents - Array of agent details
|
|
376
|
+
*/
|
|
377
|
+
function recordAgentSpawns(mode, agents) {
|
|
378
|
+
const log = readLog();
|
|
379
|
+
const today = getTodayString();
|
|
380
|
+
|
|
381
|
+
// Update daily count
|
|
382
|
+
log.dailySpawns[today] = (log.dailySpawns[today] || 0) + agents.length;
|
|
383
|
+
|
|
384
|
+
// Add to history with full details per agent
|
|
385
|
+
log.history.push({
|
|
386
|
+
date: new Date().toISOString(),
|
|
387
|
+
mode,
|
|
388
|
+
count: agents.length,
|
|
389
|
+
agents: agents.map(a => ({
|
|
390
|
+
spec: a.spec,
|
|
391
|
+
file: a.file,
|
|
392
|
+
priority: a.priority
|
|
393
|
+
}))
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Keep only last 30 days of daily spawns
|
|
397
|
+
const thirtyDaysAgo = new Date();
|
|
398
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
399
|
+
const cutoffDate = getTodayString(thirtyDaysAgo);
|
|
400
|
+
|
|
401
|
+
const newDailySpawns = {};
|
|
402
|
+
for (const [date, count] of Object.entries(log.dailySpawns)) {
|
|
403
|
+
if (date >= cutoffDate) {
|
|
404
|
+
newDailySpawns[date] = count;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
log.dailySpawns = newDailySpawns;
|
|
408
|
+
|
|
409
|
+
// Keep only last 1000 history entries
|
|
410
|
+
if (log.history.length > 1000) {
|
|
411
|
+
log.history = log.history.slice(-1000);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
writeLog(log);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Check if we're within the per-file verification cooldown
|
|
419
|
+
* @param {string} lastVerified - ISO timestamp or null
|
|
420
|
+
* @returns {boolean} true if within cooldown (should skip)
|
|
421
|
+
*/
|
|
422
|
+
function isWithinFileCooldown(lastVerified) {
|
|
423
|
+
if (!lastVerified) return false;
|
|
424
|
+
|
|
425
|
+
const lastVerifiedDate = new Date(lastVerified);
|
|
426
|
+
const now = new Date();
|
|
427
|
+
const daysSince = (now - lastVerifiedDate) / (1000 * 60 * 60 * 24);
|
|
428
|
+
|
|
429
|
+
return daysSince < CONFIG.global.fileVerificationCooldownDays;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Check if we're within the per-spec enforcement cooldown (for local specs)
|
|
434
|
+
* @param {string} specName - Name of the spec file (e.g., 'THOR.md')
|
|
435
|
+
* @param {object} state - Current state object
|
|
436
|
+
* @returns {boolean} true if within cooldown (should skip)
|
|
437
|
+
*/
|
|
438
|
+
function isWithinSpecCooldown(specName, state) {
|
|
439
|
+
if (!state.localSpecs.perSpecLastRun) {
|
|
440
|
+
state.localSpecs.perSpecLastRun = {};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const lastRun = state.localSpecs.perSpecLastRun[specName];
|
|
444
|
+
if (!lastRun) return false;
|
|
445
|
+
|
|
446
|
+
const lastRunDate = new Date(lastRun);
|
|
447
|
+
const now = new Date();
|
|
448
|
+
const daysSince = (now - lastRunDate) / (1000 * 60 * 60 * 24);
|
|
449
|
+
|
|
450
|
+
return daysSince < CONFIG.local.specCooldownDays;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Update the last run timestamp for a local spec
|
|
455
|
+
* @param {string} specName - Name of the spec file
|
|
456
|
+
* @param {object} state - Current state object
|
|
457
|
+
*/
|
|
458
|
+
function updateSpecCooldown(specName, state) {
|
|
459
|
+
if (!state.localSpecs.perSpecLastRun) {
|
|
460
|
+
state.localSpecs.perSpecLastRun = {};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
state.localSpecs.perSpecLastRun[specName] = new Date().toISOString();
|
|
464
|
+
writeState(state);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Update lastVerified timestamp for a file in the mapping
|
|
469
|
+
* @param {string} specName
|
|
470
|
+
* @param {string} filePath
|
|
471
|
+
*/
|
|
472
|
+
function updateFileVerificationTimestamp(specName, filePath) {
|
|
473
|
+
try {
|
|
474
|
+
const mappings = JSON.parse(fs.readFileSync(MAPPING_FILE, 'utf8'));
|
|
475
|
+
|
|
476
|
+
if (mappings.specs[specName]) {
|
|
477
|
+
const fileEntry = mappings.specs[specName].files.find(f => f.path === filePath);
|
|
478
|
+
if (fileEntry) {
|
|
479
|
+
fileEntry.lastVerified = new Date().toISOString();
|
|
480
|
+
fs.writeFileSync(MAPPING_FILE, JSON.stringify(mappings, null, 2), 'utf8');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.error(`Warning: Failed to update lastVerified for ${filePath}: ${err.message}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Spawn Claude instance (fire-and-forget)
|
|
490
|
+
* @param {string} prompt - Prompt to send
|
|
491
|
+
* @param {object} env - Additional environment variables
|
|
492
|
+
* @param {object} trackingInfo - Agent tracking info (type, description, metadata)
|
|
493
|
+
* @returns {number} PID of spawned process
|
|
494
|
+
*/
|
|
495
|
+
function spawnClaudeInstance(prompt, env = {}, trackingInfo = null) {
|
|
496
|
+
// Use type from trackingInfo for [Task][type] format, fallback to 'compliance' for untyped spawns
|
|
497
|
+
const taskType = trackingInfo?.type || 'compliance';
|
|
498
|
+
const taggedPrompt = `[Task][${taskType}] ${prompt}`;
|
|
499
|
+
|
|
500
|
+
// Register spawn with agent tracker if tracking info provided
|
|
501
|
+
if (trackingInfo) {
|
|
502
|
+
registerSpawn({
|
|
503
|
+
type: trackingInfo.type,
|
|
504
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
505
|
+
description: trackingInfo.description,
|
|
506
|
+
prompt: taggedPrompt,
|
|
507
|
+
metadata: trackingInfo.metadata || {},
|
|
508
|
+
projectDir
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const claude = spawn('claude', [
|
|
513
|
+
'--dangerously-skip-permissions',
|
|
514
|
+
'-p',
|
|
515
|
+
taggedPrompt
|
|
516
|
+
], {
|
|
517
|
+
detached: true,
|
|
518
|
+
stdio: 'ignore',
|
|
519
|
+
cwd: projectDir,
|
|
520
|
+
env: {
|
|
521
|
+
...process.env,
|
|
522
|
+
CLAUDE_PROJECT_DIR: projectDir,
|
|
523
|
+
CLAUDE_SPAWNED_SESSION: 'true',
|
|
524
|
+
...env
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
claude.unref();
|
|
529
|
+
return claude.pid;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Build prompt from template with variable substitution
|
|
534
|
+
* @param {string} templatePath
|
|
535
|
+
* @param {object} variables
|
|
536
|
+
* @returns {string}
|
|
537
|
+
*/
|
|
538
|
+
function buildPrompt(templatePath, variables) {
|
|
539
|
+
let template = fs.readFileSync(templatePath, 'utf8');
|
|
540
|
+
|
|
541
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
542
|
+
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
543
|
+
template = template.replace(regex, value);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return template;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Check if mapping fix spawn is rate limited
|
|
551
|
+
* @returns {{ allowed: boolean, nextEligible: string|null }}
|
|
552
|
+
*/
|
|
553
|
+
function checkMappingFixRateLimit() {
|
|
554
|
+
const state = readState();
|
|
555
|
+
const cooldownHours = CONFIG.global.mappingFixCooldownHours || 3;
|
|
556
|
+
|
|
557
|
+
if (!state.mappingFix?.lastSpawn) {
|
|
558
|
+
return { allowed: true, nextEligible: null };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const lastSpawn = new Date(state.mappingFix.lastSpawn);
|
|
562
|
+
const now = new Date();
|
|
563
|
+
const hoursSinceSpawn = (now - lastSpawn) / (1000 * 60 * 60);
|
|
564
|
+
|
|
565
|
+
if (hoursSinceSpawn >= cooldownHours) {
|
|
566
|
+
return { allowed: true, nextEligible: null };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const nextEligible = new Date(lastSpawn.getTime() + cooldownHours * 60 * 60 * 1000);
|
|
570
|
+
return {
|
|
571
|
+
allowed: false,
|
|
572
|
+
nextEligible: nextEligible.toISOString(),
|
|
573
|
+
hoursRemaining: Math.ceil(cooldownHours - hoursSinceSpawn)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Record mapping fix spawn
|
|
579
|
+
*/
|
|
580
|
+
function recordMappingFixSpawn() {
|
|
581
|
+
const state = readState();
|
|
582
|
+
const now = new Date();
|
|
583
|
+
const cooldownHours = CONFIG.global.mappingFixCooldownHours || 3;
|
|
584
|
+
const nextEligible = new Date(now.getTime() + cooldownHours * 60 * 60 * 1000);
|
|
585
|
+
|
|
586
|
+
state.mappingFix = {
|
|
587
|
+
lastSpawn: now.toISOString(),
|
|
588
|
+
nextEligible: nextEligible.toISOString()
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
writeState(state);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Handle mapping validation failure - spawn Claude to fix it (rate limited)
|
|
596
|
+
* @param {ValidationResult} result
|
|
597
|
+
*/
|
|
598
|
+
function handleMappingValidationFailure(result) {
|
|
599
|
+
console.error('\n' + formatValidationResult(result));
|
|
600
|
+
|
|
601
|
+
// Check rate limit before spawning
|
|
602
|
+
const rateLimit = checkMappingFixRateLimit();
|
|
603
|
+
if (!rateLimit.allowed) {
|
|
604
|
+
console.error('\n╔═══════════════════════════════════════════════════════════════╗');
|
|
605
|
+
console.error('║ MAPPING FIX RATE LIMITED ║');
|
|
606
|
+
console.error('╠═══════════════════════════════════════════════════════════════╣');
|
|
607
|
+
console.error(`║ Cannot spawn Claude to fix mapping (cooldown: ${CONFIG.global.mappingFixCooldownHours || 3}h) ║`);
|
|
608
|
+
console.error(`║ Next eligible: ${rateLimit.nextEligible.substring(0, 19).replace('T', ' ')} ║`);
|
|
609
|
+
console.error(`║ Hours remaining: ${rateLimit.hoursRemaining} ║`);
|
|
610
|
+
console.error('╠═══════════════════════════════════════════════════════════════╣');
|
|
611
|
+
console.error('║ Fix the mapping file manually or wait for cooldown to expire. ║');
|
|
612
|
+
console.error('╚═══════════════════════════════════════════════════════════════╝');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.error('\nSpawning Claude to fix mapping file...\n');
|
|
617
|
+
|
|
618
|
+
const promptPath = path.join(projectDir, '.claude/hooks/prompts/mapping-fix.md');
|
|
619
|
+
|
|
620
|
+
if (!fs.existsSync(promptPath)) {
|
|
621
|
+
console.error(`Error: Prompt template not found at ${promptPath}`);
|
|
622
|
+
process.exit(2);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Read current mappings
|
|
626
|
+
let currentMappings = '{}';
|
|
627
|
+
try {
|
|
628
|
+
currentMappings = fs.readFileSync(MAPPING_FILE, 'utf8');
|
|
629
|
+
} catch {
|
|
630
|
+
currentMappings = '{}';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Read schema
|
|
634
|
+
const schemaPath = path.join(projectDir, '.claude/hooks/spec-file-mappings-schema.json');
|
|
635
|
+
const schema = fs.readFileSync(schemaPath, 'utf8');
|
|
636
|
+
|
|
637
|
+
// Format errors for prompt
|
|
638
|
+
const errorsOutput = result.errors.map(e =>
|
|
639
|
+
`[${e.severity.toUpperCase()}] ${e.code}\n ${e.message}\n Suggestion: ${e.suggestion}`
|
|
640
|
+
).join('\n\n');
|
|
641
|
+
|
|
642
|
+
const prompt = buildPrompt(promptPath, {
|
|
643
|
+
VALIDATION_ERRORS: errorsOutput,
|
|
644
|
+
CURRENT_MAPPINGS: currentMappings,
|
|
645
|
+
SCHEMA_CONTENT: schema,
|
|
646
|
+
REQUIRED_SPECS_LIST: '- ' + [
|
|
647
|
+
'BARDE.md',
|
|
648
|
+
'HEIMDALL.md',
|
|
649
|
+
'HUGINN.md',
|
|
650
|
+
'OVERSEER.md',
|
|
651
|
+
'UNDERSEER.md',
|
|
652
|
+
'THOR.md',
|
|
653
|
+
'SIGNALS.md',
|
|
654
|
+
'CORE-INVARIANTS.md'
|
|
655
|
+
].join('\n- '),
|
|
656
|
+
MAX_AGENTS: CONFIG.global.maxAgentsPerDay.toString(),
|
|
657
|
+
CURRENT_AGENT_COUNT: result.agentCount.toString(),
|
|
658
|
+
EXCESS_COUNT: result.errors.find(e => e.code === 'AGENT_LIMIT_EXCEEDED')?.details?.excess?.toString() || '0'
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
spawnClaudeInstance(prompt, { COMPLIANCE_MODE: 'mapping-fix' }, {
|
|
662
|
+
type: AGENT_TYPES.COMPLIANCE_MAPPING_FIX,
|
|
663
|
+
description: `Fixing mapping validation errors (${result.errors.length} errors)`,
|
|
664
|
+
metadata: { errorCount: result.errors.length, agentCount: result.agentCount }
|
|
665
|
+
});
|
|
666
|
+
recordMappingFixSpawn();
|
|
667
|
+
console.log('Claude spawned to fix mapping file. Run this script again after fixes are applied.');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Handle mapping validation success - optionally spawn Claude to review
|
|
672
|
+
* @param {ValidationResult} result
|
|
673
|
+
*/
|
|
674
|
+
function handleMappingValidationSuccess(result) {
|
|
675
|
+
console.log('\n' + formatValidationResult(result));
|
|
676
|
+
|
|
677
|
+
// Check if we should review the mappings (weekly cooldown)
|
|
678
|
+
const mappings = JSON.parse(fs.readFileSync(MAPPING_FILE, 'utf8'));
|
|
679
|
+
const lastReviewed = mappings.lastReviewed ? new Date(mappings.lastReviewed) : null;
|
|
680
|
+
const now = new Date();
|
|
681
|
+
|
|
682
|
+
if (lastReviewed) {
|
|
683
|
+
const daysSinceReview = (now - lastReviewed) / (1000 * 60 * 60 * 24);
|
|
684
|
+
if (daysSinceReview < CONFIG.global.mappingReviewCooldownDays) {
|
|
685
|
+
console.log(`\nMapping review skipped (last reviewed ${Math.round(daysSinceReview)} days ago, cooldown: ${CONFIG.global.mappingReviewCooldownDays} days)\n`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
console.log('\nSpawning Claude to review mappings...\n');
|
|
691
|
+
|
|
692
|
+
const promptPath = path.join(projectDir, '.claude/hooks/prompts/mapping-review.md');
|
|
693
|
+
|
|
694
|
+
if (!fs.existsSync(promptPath)) {
|
|
695
|
+
console.log('Note: mapping-review.md prompt not found, skipping review');
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Build spec breakdown table
|
|
700
|
+
const breakdownLines = Object.entries(result.specBreakdown)
|
|
701
|
+
.map(([spec, data]) => `| ${spec.padEnd(25)} | ${String(data.fileCount).padStart(3)} | ${data.priority.padEnd(8)} |`)
|
|
702
|
+
.join('\n');
|
|
703
|
+
|
|
704
|
+
const breakdownTable = `| Spec | Files | Priority |\n|------|-------|----------|\n${breakdownLines}`;
|
|
705
|
+
|
|
706
|
+
const prompt = buildPrompt(promptPath, {
|
|
707
|
+
CURRENT_MAPPINGS: fs.readFileSync(MAPPING_FILE, 'utf8'),
|
|
708
|
+
CURRENT_AGENT_COUNT: result.agentCount.toString(),
|
|
709
|
+
MAX_AGENTS: CONFIG.global.maxAgentsPerDay.toString(),
|
|
710
|
+
UTILIZATION_PERCENT: result.utilizationPercent.toString(),
|
|
711
|
+
REMAINING_BUDGET: (result.limit - result.agentCount).toString(),
|
|
712
|
+
SPEC_BREAKDOWN_TABLE: breakdownTable
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
spawnClaudeInstance(prompt, { COMPLIANCE_MODE: 'mapping-review' }, {
|
|
716
|
+
type: AGENT_TYPES.COMPLIANCE_MAPPING_REVIEW,
|
|
717
|
+
description: `Reviewing spec-file-mappings.json (${result.agentCount} agents, ${result.utilizationPercent}% utilization)`,
|
|
718
|
+
metadata: { agentCount: result.agentCount, utilizationPercent: result.utilizationPercent }
|
|
719
|
+
});
|
|
720
|
+
console.log('Claude spawned to review mappings.');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Run global spec enforcement for files that need checking (uses spec-file-mappings.json + suites)
|
|
725
|
+
* @param {object} args - Command line arguments
|
|
726
|
+
*/
|
|
727
|
+
function runGlobalEnforcement(args) {
|
|
728
|
+
// Check daily agent cap first
|
|
729
|
+
const agentCap = checkDailyAgentCap('global');
|
|
730
|
+
|
|
731
|
+
if (!agentCap.allowed) {
|
|
732
|
+
console.log(`[GLOBAL] Daily agent cap reached: ${agentCap.used}/${agentCap.limit} agents used today`);
|
|
733
|
+
console.log('Adjust global.maxAgentsPerDay in compliance-config.json to increase limit');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
console.log(`[GLOBAL] Daily agent budget: ${agentCap.used}/${agentCap.limit} used, ${agentCap.remaining} remaining\n`);
|
|
738
|
+
|
|
739
|
+
// Load mappings and suites config
|
|
740
|
+
const mappings = JSON.parse(fs.readFileSync(MAPPING_FILE, 'utf8'));
|
|
741
|
+
const suitesConfig = loadSuitesConfig(); // null if not configured
|
|
742
|
+
|
|
743
|
+
if (suitesConfig) {
|
|
744
|
+
const suiteCount = Object.keys(suitesConfig.suites).length;
|
|
745
|
+
console.log(`[GLOBAL] Loaded ${suiteCount} suite(s) from suites-config.json\n`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Collect ALL files from mappings
|
|
749
|
+
const allFiles = new Set();
|
|
750
|
+
for (const specData of Object.values(mappings.specs || {})) {
|
|
751
|
+
for (const fileEntry of specData.files || []) {
|
|
752
|
+
allFiles.add(fileEntry.path);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Collect files that need checking with their applicable specs
|
|
757
|
+
const filesToCheck = [];
|
|
758
|
+
|
|
759
|
+
for (const filePath of allFiles) {
|
|
760
|
+
// Check if file exists
|
|
761
|
+
const fullPath = path.join(projectDir, filePath);
|
|
762
|
+
if (!fs.existsSync(fullPath)) {
|
|
763
|
+
console.warn(`Warning: File ${filePath} not found, skipping`);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Get ALL specs for this file (main + subspecs) - UNIFIED
|
|
768
|
+
const applicableSpecs = getAllApplicableSpecs(filePath, mappings, suitesConfig);
|
|
769
|
+
|
|
770
|
+
for (const spec of applicableSpecs) {
|
|
771
|
+
// Skip if within cooldown
|
|
772
|
+
if (isWithinFileCooldown(spec.lastVerified)) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
filesToCheck.push({
|
|
777
|
+
spec: spec.name,
|
|
778
|
+
specDir: spec.dir,
|
|
779
|
+
file: filePath,
|
|
780
|
+
priority: spec.priority,
|
|
781
|
+
suiteId: spec.suiteId,
|
|
782
|
+
suiteScope: spec.suiteScope
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (filesToCheck.length === 0) {
|
|
788
|
+
console.log('[GLOBAL] No files need checking (all within cooldown period)');
|
|
789
|
+
console.log('Adjust global.fileVerificationCooldownDays in compliance-config.json to change cooldown');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Check if we have enough budget
|
|
794
|
+
const needed = filesToCheck.length;
|
|
795
|
+
if (needed > agentCap.remaining) {
|
|
796
|
+
console.log(`Cannot spawn ${needed} agents (only ${agentCap.remaining} remaining in daily budget)`);
|
|
797
|
+
console.log(`Files needing check: ${needed}`);
|
|
798
|
+
console.log(`Files that will be checked today: ${agentCap.remaining}`);
|
|
799
|
+
console.log('\nPrioritizing by spec priority and checking what we can...\n');
|
|
800
|
+
|
|
801
|
+
// Sort by priority (critical > high > medium > low)
|
|
802
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
803
|
+
filesToCheck.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
804
|
+
|
|
805
|
+
// Take only what we can check
|
|
806
|
+
filesToCheck.splice(agentCap.remaining);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (args.dryRun) {
|
|
810
|
+
console.log('DRY RUN - would check these files:\n');
|
|
811
|
+
for (const item of filesToCheck) {
|
|
812
|
+
const suiteInfo = item.suiteId ? ` [suite: ${item.suiteId}]` : '';
|
|
813
|
+
console.log(` [${item.priority.toUpperCase()}] ${item.spec}: ${item.file}${suiteInfo}`);
|
|
814
|
+
}
|
|
815
|
+
console.log(`\nTotal agents: ${filesToCheck.length}`);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Spawn enforcement agents
|
|
820
|
+
console.log(`Spawning ${filesToCheck.length} compliance enforcement agents...\n`);
|
|
821
|
+
|
|
822
|
+
const promptPath = path.join(projectDir, '.claude/hooks/prompts/spec-enforcement.md');
|
|
823
|
+
|
|
824
|
+
if (!fs.existsSync(promptPath)) {
|
|
825
|
+
console.error(`Error: spec-enforcement.md prompt not found at ${promptPath}`);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Read spec files and store their paths (keyed by dir+name for uniqueness)
|
|
830
|
+
const specContents = {};
|
|
831
|
+
const specPaths = {};
|
|
832
|
+
for (const item of filesToCheck) {
|
|
833
|
+
const specKey = `${item.specDir}/${item.spec}`;
|
|
834
|
+
if (!specContents[specKey]) {
|
|
835
|
+
// Use the spec's directory (from suite or default)
|
|
836
|
+
let specPath = path.join(projectDir, item.specDir, item.spec);
|
|
837
|
+
|
|
838
|
+
// Fallback search order for main specs only (not suite specs)
|
|
839
|
+
if (!item.suiteId && !fs.existsSync(specPath)) {
|
|
840
|
+
specPath = path.join(projectDir, 'specs/local', item.spec);
|
|
841
|
+
if (!fs.existsSync(specPath)) {
|
|
842
|
+
specPath = path.join(projectDir, 'specs/global', item.spec);
|
|
843
|
+
}
|
|
844
|
+
if (!fs.existsSync(specPath)) {
|
|
845
|
+
specPath = path.join(projectDir, 'specs/reference', item.spec);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Fail hard if spec file cannot be read (per CLAUDE.md - no graceful fallbacks)
|
|
850
|
+
if (!fs.existsSync(specPath)) {
|
|
851
|
+
throw new Error(`CRITICAL: Spec file '${item.spec}' not found in ${item.specDir}. Cannot proceed with compliance checking without spec definition.`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
specContents[specKey] = fs.readFileSync(specPath, 'utf8');
|
|
856
|
+
// Store relative path for the agent to use when updating specs
|
|
857
|
+
specPaths[specKey] = specPath.replace(projectDir + '/', '');
|
|
858
|
+
} catch (err) {
|
|
859
|
+
throw new Error(`CRITICAL: Failed to read spec file '${item.spec}' at ${specPath}: ${err.message}. Per CLAUDE.md, no graceful fallbacks allowed.`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Spawn agents
|
|
865
|
+
for (const item of filesToCheck) {
|
|
866
|
+
const specKey = `${item.specDir}/${item.spec}`;
|
|
867
|
+
|
|
868
|
+
// Build prompt with optional suite context
|
|
869
|
+
const promptVars = {
|
|
870
|
+
FILE_PATH: item.file,
|
|
871
|
+
SPEC_NAME: item.spec,
|
|
872
|
+
SPEC_PATH: specPaths[specKey],
|
|
873
|
+
SPEC_CONTENT: specContents[specKey]
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// Add suite context if this is a subspec
|
|
877
|
+
if (item.suiteId) {
|
|
878
|
+
promptVars.SUITE_ID = item.suiteId;
|
|
879
|
+
promptVars.SUITE_SCOPE = item.suiteScope;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const prompt = buildPrompt(promptPath, promptVars);
|
|
883
|
+
|
|
884
|
+
const suiteInfo = item.suiteId ? ` [suite: ${item.suiteId}]` : '';
|
|
885
|
+
spawnClaudeInstance(prompt, {
|
|
886
|
+
COMPLIANCE_MODE: 'enforcement',
|
|
887
|
+
COMPLIANCE_SPEC: item.spec,
|
|
888
|
+
COMPLIANCE_FILE: item.file,
|
|
889
|
+
COMPLIANCE_SUITE: item.suiteId || ''
|
|
890
|
+
}, {
|
|
891
|
+
type: AGENT_TYPES.COMPLIANCE_GLOBAL,
|
|
892
|
+
description: `Global enforcement: ${item.file} against ${item.spec}${suiteInfo}`,
|
|
893
|
+
metadata: { spec: item.spec, file: item.file, priority: item.priority, suiteId: item.suiteId }
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
console.log(` ✓ Spawned for ${item.file} (${item.spec})${suiteInfo}`);
|
|
897
|
+
|
|
898
|
+
// Update lastVerified timestamp (only for main specs)
|
|
899
|
+
if (!item.suiteId) {
|
|
900
|
+
updateFileVerificationTimestamp(item.spec, item.file);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Record spawns in log (global specs enforcement)
|
|
905
|
+
recordAgentSpawns('global', filesToCheck);
|
|
906
|
+
|
|
907
|
+
console.log(`\n[GLOBAL] Spawned ${filesToCheck.length} enforcement agents`);
|
|
908
|
+
console.log('Agents will run in background and report results when complete');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Run local spec enforcement (no file mappings, agent explores codebase + suites)
|
|
913
|
+
* @param {object} args - Command line arguments
|
|
914
|
+
*/
|
|
915
|
+
async function runLocalEnforcement(args) {
|
|
916
|
+
const state = readState();
|
|
917
|
+
|
|
918
|
+
// Check daily agent cap first
|
|
919
|
+
const agentCap = checkDailyAgentCap('local');
|
|
920
|
+
|
|
921
|
+
if (!agentCap.allowed) {
|
|
922
|
+
console.log(`[LOCAL] Daily agent cap reached: ${agentCap.used}/${agentCap.limit} agents used today`);
|
|
923
|
+
console.log('Adjust local.maxAgentsPerDay in compliance-config.json to increase limit');
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
console.log(`[LOCAL] Daily agent budget: ${agentCap.used}/${agentCap.limit} used, ${agentCap.remaining} remaining\n`);
|
|
928
|
+
|
|
929
|
+
// Load suites config (optional)
|
|
930
|
+
const suitesConfig = loadSuitesConfig();
|
|
931
|
+
|
|
932
|
+
// Get ALL exploratory specs (main + suites) - UNIFIED
|
|
933
|
+
const allSpecs = getAllExploratorySpecs(suitesConfig);
|
|
934
|
+
|
|
935
|
+
if (allSpecs.length === 0) {
|
|
936
|
+
console.log('[LOCAL] No exploratory specs found');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (suitesConfig) {
|
|
941
|
+
const suiteSpecs = allSpecs.filter(s => s.suiteId);
|
|
942
|
+
if (suiteSpecs.length > 0) {
|
|
943
|
+
console.log(`[LOCAL] Found ${allSpecs.length} specs (${allSpecs.length - suiteSpecs.length} main + ${suiteSpecs.length} from suites)\n`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Filter out specs within cooldown
|
|
948
|
+
const specsToRun = [];
|
|
949
|
+
for (const spec of allSpecs) {
|
|
950
|
+
// Use suite:specName as cooldown key for suite specs
|
|
951
|
+
const cooldownKey = spec.suiteId ? `${spec.suiteId}:${spec.name}` : spec.name;
|
|
952
|
+
if (isWithinSpecCooldown(cooldownKey, state)) {
|
|
953
|
+
const lastRun = state.localSpecs.perSpecLastRun[cooldownKey];
|
|
954
|
+
const daysSince = Math.round((Date.now() - new Date(lastRun)) / (1000 * 60 * 60 * 24));
|
|
955
|
+
const suiteInfo = spec.suiteId ? ` [suite: ${spec.suiteId}]` : '';
|
|
956
|
+
console.log(`[LOCAL] Skipping ${spec.name}${suiteInfo} (last run ${daysSince} days ago, cooldown: ${CONFIG.local.specCooldownDays} days)`);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
specsToRun.push(spec);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (specsToRun.length === 0) {
|
|
963
|
+
console.log('[LOCAL] No specs need enforcement (all within cooldown period)');
|
|
964
|
+
console.log('Adjust local.specCooldownDays in compliance-config.json to change cooldown');
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Respect daily agent cap (one agent per spec)
|
|
969
|
+
const specsToRunToday = specsToRun.slice(0, agentCap.remaining);
|
|
970
|
+
|
|
971
|
+
if (specsToRun.length > agentCap.remaining) {
|
|
972
|
+
console.log(`[LOCAL] Cannot spawn agents for ${specsToRun.length} specs (only ${agentCap.remaining} remaining in daily budget)`);
|
|
973
|
+
console.log(`[LOCAL] Will enforce first ${agentCap.remaining} specs today\n`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (args.dryRun) {
|
|
977
|
+
console.log('[LOCAL] DRY RUN - would enforce these specs:\n');
|
|
978
|
+
for (const spec of specsToRunToday) {
|
|
979
|
+
const suiteInfo = spec.suiteId ? ` [suite: ${spec.suiteId}, scope: ${spec.suiteScope}]` : '';
|
|
980
|
+
console.log(` - ${spec.name}${suiteInfo}`);
|
|
981
|
+
}
|
|
982
|
+
console.log(`\nTotal agents: ${specsToRunToday.length}`);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Spawn enforcement agents
|
|
987
|
+
console.log(`[LOCAL] Spawning ${specsToRunToday.length} local spec enforcement agents...\n`);
|
|
988
|
+
|
|
989
|
+
const promptPath = path.join(projectDir, '.claude/hooks/prompts/local-spec-enforcement.md');
|
|
990
|
+
|
|
991
|
+
if (!fs.existsSync(promptPath)) {
|
|
992
|
+
console.error(`Error: local-spec-enforcement.md prompt not found at ${promptPath}`);
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const agentsSpawned = [];
|
|
997
|
+
|
|
998
|
+
for (const spec of specsToRunToday) {
|
|
999
|
+
const specPath = path.join(projectDir, spec.dir, spec.name);
|
|
1000
|
+
const specContent = fs.readFileSync(specPath, 'utf8');
|
|
1001
|
+
const specRelativePath = path.join(spec.dir, spec.name);
|
|
1002
|
+
|
|
1003
|
+
// Build prompt with optional suite context
|
|
1004
|
+
const promptVars = {
|
|
1005
|
+
SPEC_NAME: spec.name,
|
|
1006
|
+
SPEC_PATH: specRelativePath,
|
|
1007
|
+
SPEC_CONTENT: specContent
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// Add suite context if this is a suite exploratory spec
|
|
1011
|
+
if (spec.suiteId) {
|
|
1012
|
+
promptVars.SUITE_ID = spec.suiteId;
|
|
1013
|
+
promptVars.SUITE_SCOPE = spec.suiteScope;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const prompt = buildPrompt(promptPath, promptVars);
|
|
1017
|
+
|
|
1018
|
+
const suiteInfo = spec.suiteId ? ` [suite: ${spec.suiteId}]` : '';
|
|
1019
|
+
spawnClaudeInstance(prompt, {
|
|
1020
|
+
COMPLIANCE_MODE: 'local-enforcement',
|
|
1021
|
+
COMPLIANCE_SPEC: spec.name,
|
|
1022
|
+
COMPLIANCE_SUITE: spec.suiteId || ''
|
|
1023
|
+
}, {
|
|
1024
|
+
type: AGENT_TYPES.COMPLIANCE_LOCAL,
|
|
1025
|
+
description: `Local enforcement: exploring for ${spec.name}${suiteInfo}`,
|
|
1026
|
+
metadata: { spec: spec.name, specPath: specRelativePath, suiteId: spec.suiteId, suiteScope: spec.suiteScope }
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
console.log(` ✓ Spawned for ${spec.name}${suiteInfo}`);
|
|
1030
|
+
|
|
1031
|
+
// Update last run timestamp (use suite:specName for suite specs)
|
|
1032
|
+
const cooldownKey = spec.suiteId ? `${spec.suiteId}:${spec.name}` : spec.name;
|
|
1033
|
+
updateSpecCooldown(cooldownKey, state);
|
|
1034
|
+
|
|
1035
|
+
agentsSpawned.push({
|
|
1036
|
+
spec: spec.name,
|
|
1037
|
+
file: spec.suiteScope || '**/* (agent explores)',
|
|
1038
|
+
priority: 'N/A',
|
|
1039
|
+
suiteId: spec.suiteId
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Record spawns in log (local specs enforcement)
|
|
1044
|
+
recordAgentSpawns('local', agentsSpawned);
|
|
1045
|
+
|
|
1046
|
+
console.log(`\n[LOCAL] Spawned ${specsToRunToday.length} enforcement agents`);
|
|
1047
|
+
console.log('Agents will explore codebase and report results when complete');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Show status
|
|
1052
|
+
*/
|
|
1053
|
+
function showStatus() {
|
|
1054
|
+
const state = readState();
|
|
1055
|
+
const globalAgentCap = checkDailyAgentCap('global');
|
|
1056
|
+
const localAgentCap = checkDailyAgentCap('local');
|
|
1057
|
+
const mappings = JSON.parse(fs.readFileSync(MAPPING_FILE, 'utf8'));
|
|
1058
|
+
|
|
1059
|
+
console.log('╔═══════════════════════════════════════════════════════════════╗');
|
|
1060
|
+
console.log('║ COMPLIANCE CHECKER STATUS ║');
|
|
1061
|
+
console.log('╠═══════════════════════════════════════════════════════════════╣');
|
|
1062
|
+
console.log('║ Daily Agent Budget (GLOBAL - mapped files) ║');
|
|
1063
|
+
console.log(`║ Used Today: ${String(globalAgentCap.used).padStart(3)} / ${String(globalAgentCap.limit).padStart(3)} ║`);
|
|
1064
|
+
console.log(`║ Remaining: ${String(globalAgentCap.remaining).padStart(3)} ║`);
|
|
1065
|
+
console.log('╠═══════════════════════════════════════════════════════════════╣');
|
|
1066
|
+
console.log('║ Daily Agent Budget (LOCAL - explore codebase) ║');
|
|
1067
|
+
console.log(`║ Used Today: ${String(localAgentCap.used).padStart(3)} / ${String(localAgentCap.limit).padStart(3)} ║`);
|
|
1068
|
+
console.log(`║ Remaining: ${String(localAgentCap.remaining).padStart(3)} ║`);
|
|
1069
|
+
console.log('╠═══════════════════════════════════════════════════════════════╣');
|
|
1070
|
+
console.log('║ Mapped Files ║');
|
|
1071
|
+
console.log(`║ Total Files: ${String(mappings.totalMappedFiles).padStart(3)} ║`);
|
|
1072
|
+
console.log(`║ Last Reviewed: ${mappings.lastReviewed ? new Date(mappings.lastReviewed).toISOString().substring(0, 16).replace('T', ' ') : 'Never'.padEnd(16)} ║`);
|
|
1073
|
+
console.log('╠═══════════════════════════════════════════════════════════════╣');
|
|
1074
|
+
console.log('║ Global Spec Breakdown (mapped files) ║');
|
|
1075
|
+
|
|
1076
|
+
for (const [spec, data] of Object.entries(mappings.specs)) {
|
|
1077
|
+
const filesNeedingCheck = data.files.filter(f => !isWithinFileCooldown(f.lastVerified)).length;
|
|
1078
|
+
const specStr = spec.substring(0, 20).padEnd(20);
|
|
1079
|
+
const filesStr = String(data.files.length).padStart(2);
|
|
1080
|
+
const needsStr = String(filesNeedingCheck).padStart(2);
|
|
1081
|
+
|
|
1082
|
+
console.log(`║ ${specStr} ${filesStr} files (${needsStr} need check) ║`);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
console.log('╠═══════════════════════════════════════════════════════════════╣');
|
|
1086
|
+
console.log('║ Local Spec Breakdown (explore codebase) ║');
|
|
1087
|
+
|
|
1088
|
+
const specsLocalDir = path.join(projectDir, CONFIG.specsLocalDir);
|
|
1089
|
+
if (fs.existsSync(specsLocalDir)) {
|
|
1090
|
+
const localSpecFiles = fs.readdirSync(specsLocalDir).filter(f => f.endsWith('.md'));
|
|
1091
|
+
|
|
1092
|
+
if (localSpecFiles.length === 0) {
|
|
1093
|
+
console.log('║ (no local specs found) ║');
|
|
1094
|
+
} else {
|
|
1095
|
+
for (const specFile of localSpecFiles) {
|
|
1096
|
+
const needsEnforcement = !isWithinSpecCooldown(specFile, state);
|
|
1097
|
+
const specStr = specFile.substring(0, 20).padEnd(20);
|
|
1098
|
+
const statusStr = needsEnforcement ? 'needs check' : 'in cooldown';
|
|
1099
|
+
|
|
1100
|
+
console.log(`║ ${specStr} ${statusStr.padEnd(20)} ║`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} else {
|
|
1104
|
+
console.log('║ (specs/local/ directory not found) ║');
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
console.log('╚═══════════════════════════════════════════════════════════════╝');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Show enforcement history
|
|
1112
|
+
* @param {string} filter - 'all', 'global', or 'local'
|
|
1113
|
+
*/
|
|
1114
|
+
function showHistory(filter = 'all') {
|
|
1115
|
+
const log = readLog();
|
|
1116
|
+
|
|
1117
|
+
if (!log.history || log.history.length === 0) {
|
|
1118
|
+
console.log('No enforcement history found.');
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Filter history by mode
|
|
1123
|
+
let filteredHistory = log.history;
|
|
1124
|
+
if (filter === 'global') {
|
|
1125
|
+
filteredHistory = log.history.filter(h => h.mode === 'global');
|
|
1126
|
+
} else if (filter === 'local') {
|
|
1127
|
+
filteredHistory = log.history.filter(h => h.mode === 'local');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (filteredHistory.length === 0) {
|
|
1131
|
+
console.log(`No ${filter} enforcement history found.`);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const title = filter === 'all' ? 'ALL ENFORCEMENT' : `${filter.toUpperCase()} SPEC ENFORCEMENT`;
|
|
1136
|
+
console.log('╔═══════════════════════════════════════════════════════════════════════════════╗');
|
|
1137
|
+
console.log(`║ ${title} HISTORY ║`);
|
|
1138
|
+
console.log('╚═══════════════════════════════════════════════════════════════════════════════╝');
|
|
1139
|
+
console.log('');
|
|
1140
|
+
|
|
1141
|
+
// Group by date and show sessions
|
|
1142
|
+
for (const session of filteredHistory.slice().reverse()) {
|
|
1143
|
+
const date = new Date(session.date);
|
|
1144
|
+
const dateStr = date.toISOString().substring(0, 19).replace('T', ' ');
|
|
1145
|
+
const modeLabel = session.mode === 'global' ? '[GLOBAL]' : '[LOCAL ]';
|
|
1146
|
+
|
|
1147
|
+
console.log(`┌─────────────────────────────────────────────────────────────────────────────┐`);
|
|
1148
|
+
console.log(`│ ${modeLabel} ${dateStr} (${session.count} agents spawned)`);
|
|
1149
|
+
console.log(`├─────────────────────────────────────────────────────────────────────────────┤`);
|
|
1150
|
+
|
|
1151
|
+
if (session.agents && session.agents.length > 0) {
|
|
1152
|
+
// Group agents by spec
|
|
1153
|
+
const bySpec = {};
|
|
1154
|
+
for (const agent of session.agents) {
|
|
1155
|
+
if (!bySpec[agent.spec]) {
|
|
1156
|
+
bySpec[agent.spec] = [];
|
|
1157
|
+
}
|
|
1158
|
+
bySpec[agent.spec].push(agent.file);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
for (const [spec, files] of Object.entries(bySpec)) {
|
|
1162
|
+
console.log(`│ ${spec}`);
|
|
1163
|
+
for (const file of files) {
|
|
1164
|
+
console.log(`│ └─ ${file}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
} else if (session.files) {
|
|
1168
|
+
// Legacy format support
|
|
1169
|
+
console.log(`│ ${session.spec || 'enforcement'}`);
|
|
1170
|
+
for (const file of session.files) {
|
|
1171
|
+
console.log(`│ └─ ${file}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
console.log(`└─────────────────────────────────────────────────────────────────────────────┘`);
|
|
1176
|
+
console.log('');
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
console.log(`Total sessions shown: ${filteredHistory.length}`);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Main entry point
|
|
1184
|
+
*/
|
|
1185
|
+
async function main() {
|
|
1186
|
+
const startTime = Date.now();
|
|
1187
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1188
|
+
|
|
1189
|
+
// Prevent chain reactions
|
|
1190
|
+
if (process.env.CLAUDE_SPAWNED_SESSION === 'true') {
|
|
1191
|
+
console.log('Spawned session detected - skipping compliance check');
|
|
1192
|
+
registerHookExecution({
|
|
1193
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1194
|
+
status: 'skipped',
|
|
1195
|
+
durationMs: Date.now() - startTime,
|
|
1196
|
+
metadata: { reason: 'spawned_session' }
|
|
1197
|
+
});
|
|
1198
|
+
process.exit(0);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Show status
|
|
1202
|
+
if (args.status) {
|
|
1203
|
+
showStatus();
|
|
1204
|
+
registerHookExecution({
|
|
1205
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1206
|
+
status: 'success',
|
|
1207
|
+
durationMs: Date.now() - startTime,
|
|
1208
|
+
metadata: { mode: 'status' }
|
|
1209
|
+
});
|
|
1210
|
+
process.exit(0);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Show history
|
|
1214
|
+
if (args.history) {
|
|
1215
|
+
showHistory('all');
|
|
1216
|
+
registerHookExecution({
|
|
1217
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1218
|
+
status: 'success',
|
|
1219
|
+
durationMs: Date.now() - startTime,
|
|
1220
|
+
metadata: { mode: 'history' }
|
|
1221
|
+
});
|
|
1222
|
+
process.exit(0);
|
|
1223
|
+
}
|
|
1224
|
+
if (args.historyGlobal) {
|
|
1225
|
+
showHistory('global');
|
|
1226
|
+
registerHookExecution({
|
|
1227
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1228
|
+
status: 'success',
|
|
1229
|
+
durationMs: Date.now() - startTime,
|
|
1230
|
+
metadata: { mode: 'history-global' }
|
|
1231
|
+
});
|
|
1232
|
+
process.exit(0);
|
|
1233
|
+
}
|
|
1234
|
+
if (args.historyLocal) {
|
|
1235
|
+
showHistory('local');
|
|
1236
|
+
registerHookExecution({
|
|
1237
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1238
|
+
status: 'success',
|
|
1239
|
+
durationMs: Date.now() - startTime,
|
|
1240
|
+
metadata: { mode: 'history-local' }
|
|
1241
|
+
});
|
|
1242
|
+
process.exit(0);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Step 1: Validate mapping file (only for global enforcement)
|
|
1246
|
+
if (!args.localOnly) {
|
|
1247
|
+
console.log('Validating spec-file-mappings.json...\n');
|
|
1248
|
+
const validationResult = validateMappings(projectDir, CONFIG);
|
|
1249
|
+
|
|
1250
|
+
if (!validationResult.valid) {
|
|
1251
|
+
handleMappingValidationFailure(validationResult);
|
|
1252
|
+
registerHookExecution({
|
|
1253
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1254
|
+
status: 'failure',
|
|
1255
|
+
durationMs: Date.now() - startTime,
|
|
1256
|
+
metadata: { reason: 'mapping_validation_failed', errorCount: validationResult.errors?.length }
|
|
1257
|
+
});
|
|
1258
|
+
process.exit(2);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
handleMappingValidationSuccess(validationResult);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Step 2: Run enforcement (unless only showing status)
|
|
1265
|
+
if (!args.status) {
|
|
1266
|
+
console.log('\n═══════════════════════════════════════════════════════════════\n');
|
|
1267
|
+
console.log('Running compliance enforcement...\n');
|
|
1268
|
+
|
|
1269
|
+
// Run global enforcement (mapped files)
|
|
1270
|
+
if (!args.localOnly) {
|
|
1271
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
1272
|
+
console.log(' GLOBAL ENFORCEMENT');
|
|
1273
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
1274
|
+
runGlobalEnforcement(args);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Run local enforcement (explore codebase)
|
|
1278
|
+
if (!args.globalOnly) {
|
|
1279
|
+
console.log('\n═══════════════════════════════════════════════════════════════');
|
|
1280
|
+
console.log(' LOCAL ENFORCEMENT');
|
|
1281
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
1282
|
+
await runLocalEnforcement(args);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
console.log('\n═══════════════════════════════════════════════════════════════');
|
|
1286
|
+
console.log('Compliance enforcement complete');
|
|
1287
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
registerHookExecution({
|
|
1291
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1292
|
+
status: 'success',
|
|
1293
|
+
durationMs: Date.now() - startTime,
|
|
1294
|
+
metadata: { globalOnly: args.globalOnly, localOnly: args.localOnly, dryRun: args.dryRun }
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
process.exit(0);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
main().catch(err => {
|
|
1301
|
+
console.error(`Error: ${err.message}`);
|
|
1302
|
+
registerHookExecution({
|
|
1303
|
+
hookType: HOOK_TYPES.COMPLIANCE_CHECKER,
|
|
1304
|
+
status: 'failure',
|
|
1305
|
+
durationMs: 0,
|
|
1306
|
+
metadata: { error: err.message }
|
|
1307
|
+
});
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
});
|