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,1700 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deputy-CTO MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Private toolset for the deputy-cto agent to manage CTO questions,
|
|
6
|
+
* commit approvals/rejections, and task spawning.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: This server should only be used by the deputy-cto skill/agent.
|
|
9
|
+
* Other agents should use agent-reports (mcp__agent-reports__report_to_deputy_cto)
|
|
10
|
+
* to submit reports for triage, not this server.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Question queue for CTO decisions/approvals
|
|
14
|
+
* - Commit approval/rejection with automatic question creation on reject
|
|
15
|
+
* - Task spawning for implementing CTO feedback
|
|
16
|
+
* - Commit blocking when rejections are pending
|
|
17
|
+
*
|
|
18
|
+
* Protocol: JSON-RPC 2.0 over stdin/stdout (stdio MCP)
|
|
19
|
+
*
|
|
20
|
+
* @version 1.0.0
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import * as crypto from 'crypto';
|
|
25
|
+
import { spawn, execSync } from 'child_process';
|
|
26
|
+
const { randomUUID } = crypto;
|
|
27
|
+
import Database from 'better-sqlite3';
|
|
28
|
+
import { openReadonlyDb } from '../shared/readonly-db.js';
|
|
29
|
+
import { McpServer } from '../shared/server.js';
|
|
30
|
+
import { AddQuestionArgsSchema, ListQuestionsArgsSchema, ReadQuestionArgsSchema, AnswerQuestionArgsSchema, ClearQuestionArgsSchema, ApproveCommitArgsSchema, RejectCommitArgsSchema, GetCommitDecisionArgsSchema, GetPendingCountArgsSchema, ToggleAutonomousModeArgsSchema, GetAutonomousModeStatusArgsSchema, RecordCtoBriefingArgsSchema, SearchClearedItemsArgsSchema, CleanupOldRecordsArgsSchema, SetAutomationModeArgsSchema, ListAutomationConfigArgsSchema, RequestBypassArgsSchema, ExecuteBypassArgsSchema, ListProtectionsArgsSchema, GetProtectedActionRequestArgsSchema, ApproveProtectedActionArgsSchema, DenyProtectedActionArgsSchema, ListPendingActionRequestsArgsSchema, GetMergeChainStatusArgsSchema, RequestHotfixPromotionArgsSchema, ExecuteHotfixPromotionArgsSchema, } from './types.js';
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Configuration
|
|
33
|
+
// ============================================================================
|
|
34
|
+
const PROJECT_DIR = path.resolve(process.env['CLAUDE_PROJECT_DIR'] || process.cwd());
|
|
35
|
+
const DB_PATH = path.join(PROJECT_DIR, '.claude', 'deputy-cto.db');
|
|
36
|
+
const CTO_REPORTS_DB_PATH = path.join(PROJECT_DIR, '.claude', 'cto-reports.db');
|
|
37
|
+
const AUTONOMOUS_CONFIG_PATH = path.join(PROJECT_DIR, '.claude', 'autonomous-mode.json');
|
|
38
|
+
const AUTOMATION_CONFIG_PATH = path.join(PROJECT_DIR, '.claude', 'state', 'automation-config.json');
|
|
39
|
+
const AUTOMATION_STATE_PATH = path.join(PROJECT_DIR, '.claude', 'hourly-automation-state.json');
|
|
40
|
+
const PROTECTED_ACTIONS_PATH = path.join(PROJECT_DIR, '.claude', 'hooks', 'protected-actions.json');
|
|
41
|
+
const PROTECTED_APPROVALS_PATH = path.join(PROJECT_DIR, '.claude', 'protected-action-approvals.json');
|
|
42
|
+
const PROTECTION_KEY_PATH = path.join(PROJECT_DIR, '.claude', 'protection-key');
|
|
43
|
+
const HOTFIX_APPROVAL_TOKEN_PATH = path.join(PROJECT_DIR, '.claude', 'hotfix-approval-token.json');
|
|
44
|
+
const COOLDOWN_MINUTES = 55;
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Database Schema
|
|
47
|
+
// ============================================================================
|
|
48
|
+
const SCHEMA = `
|
|
49
|
+
CREATE TABLE IF NOT EXISTS questions (
|
|
50
|
+
id TEXT PRIMARY KEY,
|
|
51
|
+
type TEXT NOT NULL,
|
|
52
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
53
|
+
title TEXT NOT NULL,
|
|
54
|
+
description TEXT NOT NULL,
|
|
55
|
+
context TEXT,
|
|
56
|
+
suggested_options TEXT,
|
|
57
|
+
recommendation TEXT,
|
|
58
|
+
answer TEXT,
|
|
59
|
+
created_at TEXT NOT NULL,
|
|
60
|
+
created_timestamp INTEGER NOT NULL,
|
|
61
|
+
answered_at TEXT,
|
|
62
|
+
decided_by TEXT,
|
|
63
|
+
CONSTRAINT valid_type CHECK (type IN ('decision', 'approval', 'rejection', 'question', 'escalation', 'bypass-request', 'protected-action-request')),
|
|
64
|
+
CONSTRAINT valid_status CHECK (status IN ('pending', 'answered')),
|
|
65
|
+
CONSTRAINT valid_decided_by CHECK (decided_by IS NULL OR decided_by IN ('cto', 'deputy-cto'))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS commit_decisions (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
decision TEXT NOT NULL,
|
|
71
|
+
rationale TEXT NOT NULL,
|
|
72
|
+
question_id TEXT,
|
|
73
|
+
created_at TEXT NOT NULL,
|
|
74
|
+
created_timestamp INTEGER NOT NULL,
|
|
75
|
+
CONSTRAINT valid_decision CHECK (decision IN ('approved', 'rejected'))
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS cleared_questions (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
type TEXT NOT NULL,
|
|
81
|
+
title TEXT NOT NULL,
|
|
82
|
+
description TEXT NOT NULL,
|
|
83
|
+
recommendation TEXT,
|
|
84
|
+
answer TEXT,
|
|
85
|
+
answered_at TEXT,
|
|
86
|
+
decided_by TEXT,
|
|
87
|
+
cleared_at TEXT NOT NULL,
|
|
88
|
+
cleared_timestamp INTEGER NOT NULL,
|
|
89
|
+
CONSTRAINT valid_decided_by CHECK (decided_by IS NULL OR decided_by IN ('cto', 'deputy-cto'))
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE TABLE IF NOT EXISTS hotfix_requests (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
code TEXT NOT NULL,
|
|
95
|
+
commits_json TEXT NOT NULL,
|
|
96
|
+
created_at TEXT NOT NULL,
|
|
97
|
+
expires_at TEXT NOT NULL,
|
|
98
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
99
|
+
CONSTRAINT valid_hotfix_status CHECK (status IN ('pending', 'approved', 'executed', 'expired'))
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_cleared_questions_cleared ON cleared_questions(cleared_timestamp DESC);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_questions_type ON questions(type);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_commit_decisions_created ON commit_decisions(created_timestamp DESC);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_hotfix_requests_code ON hotfix_requests(code);
|
|
107
|
+
`;
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Database Management
|
|
110
|
+
// ============================================================================
|
|
111
|
+
let _db = null;
|
|
112
|
+
function initializeDatabase() {
|
|
113
|
+
const dbDir = path.dirname(DB_PATH);
|
|
114
|
+
if (!fs.existsSync(dbDir)) {
|
|
115
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
const db = new Database(DB_PATH);
|
|
118
|
+
db.pragma('journal_mode = WAL');
|
|
119
|
+
db.exec(SCHEMA);
|
|
120
|
+
// Migration: Add decided_by column if it doesn't exist (for existing databases)
|
|
121
|
+
const questionsColumns = db.pragma('table_info(questions)');
|
|
122
|
+
if (!questionsColumns.some(c => c.name === 'decided_by')) {
|
|
123
|
+
db.exec('ALTER TABLE questions ADD COLUMN decided_by TEXT');
|
|
124
|
+
}
|
|
125
|
+
if (!questionsColumns.some(c => c.name === 'recommendation')) {
|
|
126
|
+
db.exec('ALTER TABLE questions ADD COLUMN recommendation TEXT');
|
|
127
|
+
}
|
|
128
|
+
const clearedColumns = db.pragma('table_info(cleared_questions)');
|
|
129
|
+
if (!clearedColumns.some(c => c.name === 'decided_by')) {
|
|
130
|
+
db.exec('ALTER TABLE cleared_questions ADD COLUMN decided_by TEXT');
|
|
131
|
+
}
|
|
132
|
+
if (!clearedColumns.some(c => c.name === 'recommendation')) {
|
|
133
|
+
db.exec('ALTER TABLE cleared_questions ADD COLUMN recommendation TEXT');
|
|
134
|
+
}
|
|
135
|
+
// Run cleanup on startup to prevent unbounded database growth
|
|
136
|
+
// This is safe to call on every startup (idempotent)
|
|
137
|
+
const cleanup = cleanupOldRecordsInternal(db);
|
|
138
|
+
if (cleanup.commit_decisions_deleted > 0 || cleanup.cleared_questions_deleted > 0) {
|
|
139
|
+
console.error(`[deputy-cto] Startup cleanup: ${cleanup.message}`);
|
|
140
|
+
}
|
|
141
|
+
return db;
|
|
142
|
+
}
|
|
143
|
+
function getDb() {
|
|
144
|
+
if (!_db) {
|
|
145
|
+
_db = initializeDatabase();
|
|
146
|
+
}
|
|
147
|
+
return _db;
|
|
148
|
+
}
|
|
149
|
+
function closeDb() {
|
|
150
|
+
if (_db) {
|
|
151
|
+
_db.close();
|
|
152
|
+
_db = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function getPendingRejectionCount() {
|
|
156
|
+
const db = getDb();
|
|
157
|
+
const result = db.prepare("SELECT COUNT(*) as count FROM questions WHERE type = 'rejection' AND status = 'pending'").get();
|
|
158
|
+
return result.count;
|
|
159
|
+
}
|
|
160
|
+
function getPendingCount() {
|
|
161
|
+
const db = getDb();
|
|
162
|
+
const result = db.prepare("SELECT COUNT(*) as count FROM questions WHERE status = 'pending'").get();
|
|
163
|
+
return result.count;
|
|
164
|
+
}
|
|
165
|
+
function getPendingTriageCount() {
|
|
166
|
+
// G020: Pending triage items also block commits
|
|
167
|
+
// G001: If database doesn't exist yet, no triage items to block on (valid startup state)
|
|
168
|
+
if (!fs.existsSync(CTO_REPORTS_DB_PATH)) {
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const reportsDb = openReadonlyDb(CTO_REPORTS_DB_PATH);
|
|
173
|
+
// Check if triage_status column exists
|
|
174
|
+
const columns = reportsDb.pragma('table_info(reports)');
|
|
175
|
+
const hasTriageStatus = columns.some(c => c.name === 'triage_status');
|
|
176
|
+
let count = 0;
|
|
177
|
+
if (hasTriageStatus) {
|
|
178
|
+
const { count: triageCount } = reportsDb.prepare("SELECT COUNT(*) as count FROM reports WHERE triage_status = 'pending'").get();
|
|
179
|
+
count = triageCount;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Fallback for databases without triage_status column
|
|
183
|
+
const { count: triageCount } = reportsDb.prepare("SELECT COUNT(*) as count FROM reports WHERE triaged_at IS NULL").get();
|
|
184
|
+
count = triageCount;
|
|
185
|
+
}
|
|
186
|
+
reportsDb.close();
|
|
187
|
+
return count;
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
// G001: Fail closed - if we can't read triage count, assume there are pending items
|
|
191
|
+
// This blocks commits when the database is corrupted/unreadable (safer default)
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
process.stderr.write(`[deputy-cto] G001: Failed to read triage count, blocking commits: ${message}\n`);
|
|
194
|
+
return 1; // Return 1 to trigger commit blocking
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function getTotalPendingItems() {
|
|
198
|
+
const questions = getPendingCount();
|
|
199
|
+
const triage = getPendingTriageCount();
|
|
200
|
+
return { questions, triage, total: questions + triage };
|
|
201
|
+
}
|
|
202
|
+
function clearLatestCommitDecision() {
|
|
203
|
+
const db = getDb();
|
|
204
|
+
// Clear the most recent commit decision so a new one can be made
|
|
205
|
+
db.prepare(`
|
|
206
|
+
DELETE FROM commit_decisions WHERE id IN (
|
|
207
|
+
SELECT id FROM commit_decisions ORDER BY created_timestamp DESC LIMIT 1
|
|
208
|
+
)
|
|
209
|
+
`).run();
|
|
210
|
+
}
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Tool Implementations
|
|
213
|
+
// ============================================================================
|
|
214
|
+
function addQuestion(args) {
|
|
215
|
+
const db = getDb();
|
|
216
|
+
// Require recommendation for escalations
|
|
217
|
+
if (args.type === 'escalation' && !args.recommendation) {
|
|
218
|
+
return { error: 'Escalations require a recommendation. Provide a concise statement of what you recommend and why.' };
|
|
219
|
+
}
|
|
220
|
+
// Block agents from creating bypass-request or protected-action-request via add_question
|
|
221
|
+
if (args.type === 'bypass-request') {
|
|
222
|
+
return { error: 'Cannot create bypass-request questions via add_question. Use request_bypass instead.' };
|
|
223
|
+
}
|
|
224
|
+
if (args.type === 'protected-action-request') {
|
|
225
|
+
return { error: 'Cannot create protected-action-request questions via add_question. These are created by the protected-action hook.' };
|
|
226
|
+
}
|
|
227
|
+
const id = randomUUID();
|
|
228
|
+
const now = new Date();
|
|
229
|
+
const created_at = now.toISOString();
|
|
230
|
+
const created_timestamp = Math.floor(now.getTime() / 1000);
|
|
231
|
+
db.prepare(`
|
|
232
|
+
INSERT INTO questions (id, type, status, title, description, context, suggested_options, recommendation, created_at, created_timestamp)
|
|
233
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?)
|
|
234
|
+
`).run(id, args.type, args.title, args.description, args.context ?? null, args.suggested_options ? JSON.stringify(args.suggested_options) : null, args.recommendation ?? null, created_at, created_timestamp);
|
|
235
|
+
return {
|
|
236
|
+
id,
|
|
237
|
+
message: `Question added for CTO. ID: ${id}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function listQuestions(args) {
|
|
241
|
+
const db = getDb();
|
|
242
|
+
let sql = 'SELECT id, type, status, title, created_at FROM questions';
|
|
243
|
+
const params = [];
|
|
244
|
+
if (!args.include_answered) {
|
|
245
|
+
sql += " WHERE status = 'pending'";
|
|
246
|
+
}
|
|
247
|
+
sql += ' ORDER BY created_timestamp DESC LIMIT ?';
|
|
248
|
+
params.push(args.limit ?? 20);
|
|
249
|
+
const questions = db.prepare(sql).all(...params);
|
|
250
|
+
const pendingCount = getPendingCount();
|
|
251
|
+
const rejectionCount = getPendingRejectionCount();
|
|
252
|
+
const pendingTriage = getPendingTriageCount();
|
|
253
|
+
const items = questions.map(q => ({
|
|
254
|
+
id: q.id,
|
|
255
|
+
type: q.type,
|
|
256
|
+
status: q.status,
|
|
257
|
+
title: q.title,
|
|
258
|
+
created_at: q.created_at,
|
|
259
|
+
is_rejection: q.type === 'rejection',
|
|
260
|
+
}));
|
|
261
|
+
return {
|
|
262
|
+
questions: items,
|
|
263
|
+
total: items.length,
|
|
264
|
+
pending_count: pendingCount,
|
|
265
|
+
rejection_count: rejectionCount,
|
|
266
|
+
// G020: Block commits when ANY pending items exist (questions OR triage)
|
|
267
|
+
commits_blocked: pendingCount > 0 || pendingTriage > 0,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function readQuestion(args) {
|
|
271
|
+
const db = getDb();
|
|
272
|
+
const question = db.prepare('SELECT * FROM questions WHERE id = ?').get(args.id);
|
|
273
|
+
if (!question) {
|
|
274
|
+
return { error: `Question not found: ${args.id}` };
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
id: question.id,
|
|
278
|
+
type: question.type,
|
|
279
|
+
status: question.status,
|
|
280
|
+
title: question.title,
|
|
281
|
+
description: question.description,
|
|
282
|
+
context: question.context,
|
|
283
|
+
suggested_options: question.suggested_options ? JSON.parse(question.suggested_options) : null,
|
|
284
|
+
recommendation: question.recommendation,
|
|
285
|
+
answer: question.answer,
|
|
286
|
+
created_at: question.created_at,
|
|
287
|
+
answered_at: question.answered_at,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function answerQuestion(args) {
|
|
291
|
+
const db = getDb();
|
|
292
|
+
const question = db.prepare('SELECT * FROM questions WHERE id = ?').get(args.id);
|
|
293
|
+
if (!question) {
|
|
294
|
+
return { error: `Question not found: ${args.id}` };
|
|
295
|
+
}
|
|
296
|
+
// Block answering bypass-request and protected-action-request questions via this tool
|
|
297
|
+
if (question.type === 'bypass-request') {
|
|
298
|
+
return { error: 'Cannot answer bypass-request questions via answer_question. The CTO must type "APPROVE BYPASS <code>" in chat.' };
|
|
299
|
+
}
|
|
300
|
+
if (question.type === 'protected-action-request') {
|
|
301
|
+
return { error: 'Cannot answer protected-action-request questions via answer_question. Use approve_protected_action or deny_protected_action.' };
|
|
302
|
+
}
|
|
303
|
+
if (question.status === 'answered') {
|
|
304
|
+
return {
|
|
305
|
+
id: args.id,
|
|
306
|
+
answered: true,
|
|
307
|
+
message: `Question already answered at ${question.answered_at}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const now = new Date().toISOString();
|
|
311
|
+
const decidedBy = args.decided_by ?? 'cto';
|
|
312
|
+
db.prepare(`
|
|
313
|
+
UPDATE questions SET status = 'answered', answer = ?, answered_at = ?, decided_by = ?
|
|
314
|
+
WHERE id = ?
|
|
315
|
+
`).run(args.answer, now, decidedBy, args.id);
|
|
316
|
+
return {
|
|
317
|
+
id: args.id,
|
|
318
|
+
answered: true,
|
|
319
|
+
message: `Answer recorded by ${decidedBy}. Use clear_question to remove from queue after implementing.`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function clearQuestion(args) {
|
|
323
|
+
const db = getDb();
|
|
324
|
+
const question = db.prepare('SELECT * FROM questions WHERE id = ?').get(args.id);
|
|
325
|
+
if (!question) {
|
|
326
|
+
return { error: `Question not found: ${args.id}` };
|
|
327
|
+
}
|
|
328
|
+
// Block clearing pending bypass-request and protected-action-request questions
|
|
329
|
+
if (question.type === 'bypass-request' && question.status === 'pending') {
|
|
330
|
+
return { error: 'Cannot clear a pending bypass-request. The CTO must type "APPROVE BYPASS <code>". Only answered bypass-requests can be cleared.' };
|
|
331
|
+
}
|
|
332
|
+
if (question.type === 'protected-action-request' && question.status === 'pending') {
|
|
333
|
+
return { error: 'Cannot clear a pending protected-action-request. Use approve_protected_action or deny_protected_action.' };
|
|
334
|
+
}
|
|
335
|
+
const now = new Date();
|
|
336
|
+
const cleared_at = now.toISOString();
|
|
337
|
+
const cleared_timestamp = Math.floor(now.getTime() / 1000);
|
|
338
|
+
// Archive the question before deleting
|
|
339
|
+
db.prepare(`
|
|
340
|
+
INSERT INTO cleared_questions (id, type, title, description, recommendation, answer, answered_at, decided_by, cleared_at, cleared_timestamp)
|
|
341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
342
|
+
`).run(question.id, question.type, question.title, question.description, question.recommendation, question.answer, question.answered_at, question.decided_by, cleared_at, cleared_timestamp);
|
|
343
|
+
db.prepare('DELETE FROM questions WHERE id = ?').run(args.id);
|
|
344
|
+
const remainingCount = getPendingCount();
|
|
345
|
+
// Build message with reminder about plan notes
|
|
346
|
+
let message;
|
|
347
|
+
if (remainingCount === 0) {
|
|
348
|
+
message = 'Question cleared. No more pending questions - CTO session can end.';
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
message = `Question cleared. ${remainingCount} question(s) remaining.`;
|
|
352
|
+
}
|
|
353
|
+
// Add reminder about CTO-PENDING notes in plans
|
|
354
|
+
message += `\n\nREMINDER: If this question was linked to a CTO-PENDING note in PLAN.md or /plans, ` +
|
|
355
|
+
`search for "<!-- CTO-PENDING: ${args.id}" and remove the marker now that the CTO has responded.`;
|
|
356
|
+
return {
|
|
357
|
+
id: args.id,
|
|
358
|
+
cleared: true,
|
|
359
|
+
message,
|
|
360
|
+
remaining_count: remainingCount,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// Token expires after 5 minutes
|
|
364
|
+
const TOKEN_EXPIRY_MS = 5 * 60 * 1000;
|
|
365
|
+
const APPROVAL_TOKEN_PATH = path.join(PROJECT_DIR, '.claude', 'commit-approval-token.json');
|
|
366
|
+
function approveCommit(args) {
|
|
367
|
+
const db = getDb();
|
|
368
|
+
// Reject rationales starting with "EMERGENCY BYPASS" — only execute_bypass may use this prefix
|
|
369
|
+
if (/^EMERGENCY\s+BYPASS/i.test(args.rationale)) {
|
|
370
|
+
return {
|
|
371
|
+
approved: false,
|
|
372
|
+
decision_id: '',
|
|
373
|
+
message: 'Cannot use "EMERGENCY BYPASS" prefix in approve_commit rationale. Use request_bypass for emergency bypass requests.',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// G020: Block commits when ANY pending items exist (questions OR triage)
|
|
377
|
+
const pending = getTotalPendingItems();
|
|
378
|
+
if (pending.total > 0) {
|
|
379
|
+
const blockReasons = [];
|
|
380
|
+
if (pending.questions > 0) {
|
|
381
|
+
blockReasons.push(`${pending.questions} CTO question(s)`);
|
|
382
|
+
}
|
|
383
|
+
if (pending.triage > 0) {
|
|
384
|
+
blockReasons.push(`${pending.triage} untriaged report(s)`);
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
approved: false,
|
|
388
|
+
decision_id: '',
|
|
389
|
+
message: `Cannot approve commit: ${blockReasons.join(' and ')} must be addressed first.`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// Clear any existing decision
|
|
393
|
+
clearLatestCommitDecision();
|
|
394
|
+
const id = randomUUID();
|
|
395
|
+
const now = new Date();
|
|
396
|
+
const created_at = now.toISOString();
|
|
397
|
+
const created_timestamp = Math.floor(now.getTime() / 1000);
|
|
398
|
+
db.prepare(`
|
|
399
|
+
INSERT INTO commit_decisions (id, decision, rationale, created_at, created_timestamp)
|
|
400
|
+
VALUES (?, 'approved', ?, ?, ?)
|
|
401
|
+
`).run(id, args.rationale, created_at, created_timestamp);
|
|
402
|
+
// Write approval token for pre-commit hook
|
|
403
|
+
const diffHash = process.env['DEPUTY_CTO_DIFF_HASH'] || '';
|
|
404
|
+
const token = {
|
|
405
|
+
diffHash,
|
|
406
|
+
expiresAt: Date.now() + TOKEN_EXPIRY_MS,
|
|
407
|
+
approvedAt: created_at,
|
|
408
|
+
approvedBy: 'deputy-cto',
|
|
409
|
+
rationale: args.rationale,
|
|
410
|
+
decisionId: id,
|
|
411
|
+
};
|
|
412
|
+
try {
|
|
413
|
+
fs.writeFileSync(APPROVAL_TOKEN_PATH, JSON.stringify(token, null, 2));
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
// G001: Fail-closed - if token file write fails, the approval is non-functional
|
|
417
|
+
// because the pre-commit hook reads the token file, not the database
|
|
418
|
+
console.error(`[deputy-cto] G001: Approval token write failed: ${err}`);
|
|
419
|
+
// Roll back the database decision so state is consistent
|
|
420
|
+
try {
|
|
421
|
+
db.prepare('DELETE FROM commit_decisions WHERE id = ?').run(id);
|
|
422
|
+
}
|
|
423
|
+
catch (rollbackErr) {
|
|
424
|
+
console.error(`[deputy-cto] G001: Failed to roll back approval decision: ${rollbackErr}`);
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
approved: false,
|
|
428
|
+
decision_id: '',
|
|
429
|
+
message: 'Approval token write failed - check file permissions on commit-approval-token.json. The approval has been rolled back.',
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
approved: true,
|
|
434
|
+
decision_id: id,
|
|
435
|
+
message: `Commit approved. Token written - retry your commit within 5 minutes.${diffHash ? ` (hash: ${diffHash})` : ''}`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function rejectCommit(args) {
|
|
439
|
+
const db = getDb();
|
|
440
|
+
// Clear any existing decision
|
|
441
|
+
clearLatestCommitDecision();
|
|
442
|
+
const decisionId = randomUUID();
|
|
443
|
+
const questionId = randomUUID();
|
|
444
|
+
const now = new Date();
|
|
445
|
+
const created_at = now.toISOString();
|
|
446
|
+
const created_timestamp = Math.floor(now.getTime() / 1000);
|
|
447
|
+
// Create commit decision
|
|
448
|
+
db.prepare(`
|
|
449
|
+
INSERT INTO commit_decisions (id, decision, rationale, question_id, created_at, created_timestamp)
|
|
450
|
+
VALUES (?, 'rejected', ?, ?, ?, ?)
|
|
451
|
+
`).run(decisionId, args.description, questionId, created_at, created_timestamp);
|
|
452
|
+
// Create question entry for CTO to address
|
|
453
|
+
db.prepare(`
|
|
454
|
+
INSERT INTO questions (id, type, status, title, description, created_at, created_timestamp)
|
|
455
|
+
VALUES (?, 'rejection', 'pending', ?, ?, ?, ?)
|
|
456
|
+
`).run(questionId, args.title, args.description, created_at, created_timestamp);
|
|
457
|
+
return {
|
|
458
|
+
rejected: true,
|
|
459
|
+
decision_id: decisionId,
|
|
460
|
+
question_id: questionId,
|
|
461
|
+
message: `Commit rejected. Question created for CTO (ID: ${questionId}). Commits will be blocked until CTO addresses this.`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function getCommitDecision() {
|
|
465
|
+
const db = getDb();
|
|
466
|
+
// Get latest commit decision
|
|
467
|
+
const decision = db.prepare(`
|
|
468
|
+
SELECT * FROM commit_decisions ORDER BY created_timestamp DESC LIMIT 1
|
|
469
|
+
`).get();
|
|
470
|
+
const pendingRejections = getPendingRejectionCount();
|
|
471
|
+
const pending = getTotalPendingItems();
|
|
472
|
+
// G020: Block commits when ANY pending items exist (questions OR triage)
|
|
473
|
+
const commitsBlocked = pending.total > 0;
|
|
474
|
+
// Build informative message about what's blocking
|
|
475
|
+
const blockReasons = [];
|
|
476
|
+
if (pending.questions > 0) {
|
|
477
|
+
blockReasons.push(`${pending.questions} CTO question(s)`);
|
|
478
|
+
}
|
|
479
|
+
if (pending.triage > 0) {
|
|
480
|
+
blockReasons.push(`${pending.triage} untriaged report(s)`);
|
|
481
|
+
}
|
|
482
|
+
const blockMessage = blockReasons.join(' and ');
|
|
483
|
+
if (!decision) {
|
|
484
|
+
return {
|
|
485
|
+
has_decision: false,
|
|
486
|
+
decision: null,
|
|
487
|
+
rationale: null,
|
|
488
|
+
pending_rejections: pendingRejections,
|
|
489
|
+
commits_blocked: commitsBlocked,
|
|
490
|
+
message: commitsBlocked
|
|
491
|
+
? `No decision yet. ${blockMessage} blocking commits.`
|
|
492
|
+
: 'No decision yet. Awaiting deputy-cto review.',
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
has_decision: true,
|
|
497
|
+
decision: decision.decision,
|
|
498
|
+
rationale: decision.rationale,
|
|
499
|
+
pending_rejections: pendingRejections,
|
|
500
|
+
commits_blocked: commitsBlocked,
|
|
501
|
+
message: commitsBlocked
|
|
502
|
+
? `Decision: ${decision.decision}, but ${blockMessage} still blocking commits.`
|
|
503
|
+
: `Decision: ${decision.decision}. Commits may proceed.`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function getPendingCountTool() {
|
|
507
|
+
const pendingCount = getPendingCount();
|
|
508
|
+
const rejectionCount = getPendingRejectionCount();
|
|
509
|
+
const pendingTriage = getPendingTriageCount();
|
|
510
|
+
return {
|
|
511
|
+
pending_count: pendingCount,
|
|
512
|
+
rejection_count: rejectionCount,
|
|
513
|
+
pending_triage_count: pendingTriage,
|
|
514
|
+
// G020: Block commits when ANY pending items exist (questions OR triage)
|
|
515
|
+
commits_blocked: pendingCount > 0 || pendingTriage > 0,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// Autonomous Mode Functions
|
|
520
|
+
// ============================================================================
|
|
521
|
+
function getAutonomousConfig() {
|
|
522
|
+
const defaults = {
|
|
523
|
+
enabled: false,
|
|
524
|
+
claudeMdRefactorEnabled: true,
|
|
525
|
+
lastModified: null,
|
|
526
|
+
modifiedBy: null,
|
|
527
|
+
lastCtoBriefing: null,
|
|
528
|
+
};
|
|
529
|
+
if (!fs.existsSync(AUTONOMOUS_CONFIG_PATH)) {
|
|
530
|
+
return defaults;
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const config = JSON.parse(fs.readFileSync(AUTONOMOUS_CONFIG_PATH, 'utf8'));
|
|
534
|
+
return { ...defaults, ...config };
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
// G001: Config corruption logged but fail-safe to disabled mode
|
|
538
|
+
console.error(`[deputy-cto] Config file corrupted - autonomous mode DISABLED: ${err instanceof Error ? err.message : String(err)}`);
|
|
539
|
+
console.error(`[deputy-cto] Fix: Delete or repair the config file`);
|
|
540
|
+
return defaults;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function getNextRunMinutes() {
|
|
544
|
+
if (!fs.existsSync(AUTOMATION_STATE_PATH)) {
|
|
545
|
+
return 0; // First run would happen immediately
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const state = JSON.parse(fs.readFileSync(AUTOMATION_STATE_PATH, 'utf8'));
|
|
549
|
+
const lastRun = state.lastRun || 0;
|
|
550
|
+
const now = Date.now();
|
|
551
|
+
const timeSinceLastRun = now - lastRun;
|
|
552
|
+
const cooldownMs = COOLDOWN_MINUTES * 60 * 1000;
|
|
553
|
+
if (timeSinceLastRun >= cooldownMs) {
|
|
554
|
+
return 0; // Would run now if service triggers
|
|
555
|
+
}
|
|
556
|
+
return Math.ceil((cooldownMs - timeSinceLastRun) / 60000);
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
// G001: State file corruption - return null to indicate unknown state
|
|
560
|
+
console.error(`[deputy-cto] State file corrupted: ${err instanceof Error ? err.message : String(err)}`);
|
|
561
|
+
console.error(`[deputy-cto] Fix: Delete the state file to reset.`);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function toggleAutonomousMode(args) {
|
|
566
|
+
const config = getAutonomousConfig();
|
|
567
|
+
config.enabled = args.enabled;
|
|
568
|
+
config.lastModified = new Date().toISOString();
|
|
569
|
+
config.modifiedBy = 'deputy-cto';
|
|
570
|
+
try {
|
|
571
|
+
fs.writeFileSync(AUTONOMOUS_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
575
|
+
return {
|
|
576
|
+
enabled: !args.enabled, // Return previous state on failure
|
|
577
|
+
message: `Failed to update config: ${message}`,
|
|
578
|
+
nextRunIn: null,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const nextRunIn = args.enabled ? getNextRunMinutes() : null;
|
|
582
|
+
return {
|
|
583
|
+
enabled: args.enabled,
|
|
584
|
+
message: args.enabled
|
|
585
|
+
? `Autonomous Deputy CTO Mode ENABLED. Automations will run on their configured schedules.`
|
|
586
|
+
: `Autonomous Deputy CTO Mode DISABLED. No hourly automations will run.`,
|
|
587
|
+
nextRunIn,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function getAutonomousModeStatus() {
|
|
591
|
+
const config = getAutonomousConfig();
|
|
592
|
+
const nextRunIn = config.enabled ? getNextRunMinutes() : null;
|
|
593
|
+
// Calculate CTO activity gate status
|
|
594
|
+
let hoursSinceLastBriefing = null;
|
|
595
|
+
let ctoGateOpen = false;
|
|
596
|
+
if (config.lastCtoBriefing) {
|
|
597
|
+
const briefingTime = new Date(config.lastCtoBriefing).getTime();
|
|
598
|
+
if (!isNaN(briefingTime)) {
|
|
599
|
+
hoursSinceLastBriefing = Math.floor((Date.now() - briefingTime) / (1000 * 60 * 60));
|
|
600
|
+
ctoGateOpen = hoursSinceLastBriefing < 24;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
let message;
|
|
604
|
+
if (!config.enabled) {
|
|
605
|
+
message = 'Autonomous Deputy CTO Mode is DISABLED.';
|
|
606
|
+
}
|
|
607
|
+
else if (!ctoGateOpen) {
|
|
608
|
+
const ageStr = hoursSinceLastBriefing !== null ? `${hoursSinceLastBriefing}h ago` : 'never';
|
|
609
|
+
message = `Autonomous Deputy CTO Mode is ENABLED but CTO activity gate is CLOSED (last briefing: ${ageStr}). Run /deputy-cto to reactivate.`;
|
|
610
|
+
}
|
|
611
|
+
else if (nextRunIn === null) {
|
|
612
|
+
message = 'Autonomous Deputy CTO Mode is ENABLED. Status unknown (state file error).';
|
|
613
|
+
}
|
|
614
|
+
else if (nextRunIn === 0) {
|
|
615
|
+
message = 'Autonomous Deputy CTO Mode is ENABLED. Ready to run (waiting for service trigger).';
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
message = `Autonomous Deputy CTO Mode is ENABLED. Next run in ~${nextRunIn} minute(s).`;
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
enabled: config.enabled,
|
|
622
|
+
claudeMdRefactorEnabled: config.claudeMdRefactorEnabled,
|
|
623
|
+
lastModified: config.lastModified,
|
|
624
|
+
nextRunIn,
|
|
625
|
+
lastCtoBriefing: config.lastCtoBriefing,
|
|
626
|
+
ctoGateOpen,
|
|
627
|
+
hoursSinceLastBriefing,
|
|
628
|
+
message,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function recordCtoBriefing() {
|
|
632
|
+
const config = getAutonomousConfig();
|
|
633
|
+
const now = new Date().toISOString();
|
|
634
|
+
config.lastCtoBriefing = now;
|
|
635
|
+
config.lastModified = now;
|
|
636
|
+
config.modifiedBy = 'deputy-cto';
|
|
637
|
+
try {
|
|
638
|
+
fs.writeFileSync(AUTONOMOUS_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
642
|
+
return {
|
|
643
|
+
recorded: false,
|
|
644
|
+
timestamp: now,
|
|
645
|
+
message: `Failed to record CTO briefing timestamp: ${message}`,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
recorded: true,
|
|
650
|
+
timestamp: now,
|
|
651
|
+
message: `CTO briefing activity recorded at ${now}. Automation gate refreshed for 24 hours.`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function searchClearedItems(args) {
|
|
655
|
+
const db = getDb();
|
|
656
|
+
const query = `%${args.query}%`;
|
|
657
|
+
const limit = args.limit ?? 10;
|
|
658
|
+
const items = db.prepare(`
|
|
659
|
+
SELECT id, type, title, answer, answered_at, decided_by
|
|
660
|
+
FROM cleared_questions
|
|
661
|
+
WHERE title LIKE ? OR description LIKE ? OR id LIKE ?
|
|
662
|
+
ORDER BY cleared_timestamp DESC
|
|
663
|
+
LIMIT ?
|
|
664
|
+
`).all(query, query, query, limit);
|
|
665
|
+
return {
|
|
666
|
+
items,
|
|
667
|
+
count: items.length,
|
|
668
|
+
message: items.length === 0
|
|
669
|
+
? `No cleared items found matching "${args.query}".`
|
|
670
|
+
: `Found ${items.length} cleared item(s) matching "${args.query}".`,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// Data Cleanup Functions
|
|
675
|
+
// ============================================================================
|
|
676
|
+
/**
|
|
677
|
+
* Internal cleanup function that accepts a database parameter.
|
|
678
|
+
* Used during initialization when db is not yet stored in _db.
|
|
679
|
+
*
|
|
680
|
+
* Retention Policy:
|
|
681
|
+
* - Keep last 100 commit decisions
|
|
682
|
+
* - Keep cleared questions for 30 days
|
|
683
|
+
* - Keep at least 500 most recent cleared questions (even if < 30 days old)
|
|
684
|
+
*/
|
|
685
|
+
function cleanupOldRecordsInternal(db) {
|
|
686
|
+
// Clean commit_decisions: keep only last 100
|
|
687
|
+
const commitDecisionsResult = db.prepare(`
|
|
688
|
+
DELETE FROM commit_decisions WHERE id NOT IN (
|
|
689
|
+
SELECT id FROM commit_decisions ORDER BY created_timestamp DESC LIMIT 100
|
|
690
|
+
)
|
|
691
|
+
`).run();
|
|
692
|
+
// Clean cleared_questions: keep last 500 OR anything within 30 days
|
|
693
|
+
const thirtyDaysAgo = Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60);
|
|
694
|
+
const clearedQuestionsResult = db.prepare(`
|
|
695
|
+
DELETE FROM cleared_questions
|
|
696
|
+
WHERE cleared_timestamp < ?
|
|
697
|
+
AND id NOT IN (
|
|
698
|
+
SELECT id FROM cleared_questions ORDER BY cleared_timestamp DESC LIMIT 500
|
|
699
|
+
)
|
|
700
|
+
`).run(thirtyDaysAgo);
|
|
701
|
+
const commitDeleted = commitDecisionsResult.changes;
|
|
702
|
+
const clearedDeleted = clearedQuestionsResult.changes;
|
|
703
|
+
const totalDeleted = commitDeleted + clearedDeleted;
|
|
704
|
+
let message;
|
|
705
|
+
if (totalDeleted === 0) {
|
|
706
|
+
message = 'No old records found to clean up. Database is within retention limits.';
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
message = `Cleaned up ${totalDeleted} old record(s): ${commitDeleted} commit decision(s), ${clearedDeleted} cleared question(s).`;
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
commit_decisions_deleted: commitDeleted,
|
|
713
|
+
cleared_questions_deleted: clearedDeleted,
|
|
714
|
+
message,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Public cleanup function for MCP tool.
|
|
719
|
+
* Cleans up old records to prevent unbounded database growth.
|
|
720
|
+
*
|
|
721
|
+
* This function is idempotent and safe to call multiple times.
|
|
722
|
+
* Automatically called on server startup.
|
|
723
|
+
*/
|
|
724
|
+
function cleanupOldRecords() {
|
|
725
|
+
const db = getDb();
|
|
726
|
+
return cleanupOldRecordsInternal(db);
|
|
727
|
+
}
|
|
728
|
+
// ============================================================================
|
|
729
|
+
// Automation Mode Functions
|
|
730
|
+
// ============================================================================
|
|
731
|
+
const AUTOMATION_DEFAULTS = {
|
|
732
|
+
hourly_tasks: 55, triage_check: 5, antipattern_hunter: 360,
|
|
733
|
+
schema_mapper: 1440, lint_checker: 30, todo_maintenance: 15,
|
|
734
|
+
task_runner: 60, triage_per_item: 60, preview_promotion: 360,
|
|
735
|
+
staging_promotion: 1200, staging_health_monitor: 180,
|
|
736
|
+
production_health_monitor: 60, standalone_antipattern_hunter: 180,
|
|
737
|
+
standalone_compliance_checker: 60, user_feedback: 120,
|
|
738
|
+
};
|
|
739
|
+
function readAutomationConfig() {
|
|
740
|
+
const defaults = {
|
|
741
|
+
version: 1,
|
|
742
|
+
defaults: { ...AUTOMATION_DEFAULTS },
|
|
743
|
+
effective: { ...AUTOMATION_DEFAULTS },
|
|
744
|
+
adjustment: { factor: 1.0, last_updated: null },
|
|
745
|
+
};
|
|
746
|
+
if (!fs.existsSync(AUTOMATION_CONFIG_PATH))
|
|
747
|
+
return defaults;
|
|
748
|
+
try {
|
|
749
|
+
const config = JSON.parse(fs.readFileSync(AUTOMATION_CONFIG_PATH, 'utf8'));
|
|
750
|
+
if (!config || config.version !== 1)
|
|
751
|
+
return defaults;
|
|
752
|
+
return config;
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return defaults;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function writeAutomationConfig(config) {
|
|
759
|
+
const dir = path.dirname(AUTOMATION_CONFIG_PATH);
|
|
760
|
+
if (!fs.existsSync(dir)) {
|
|
761
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
762
|
+
}
|
|
763
|
+
fs.writeFileSync(AUTOMATION_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
764
|
+
}
|
|
765
|
+
function setAutomationMode(args) {
|
|
766
|
+
const key = args.automation_name;
|
|
767
|
+
// Validate the automation name exists
|
|
768
|
+
if (!AUTOMATION_DEFAULTS[key]) {
|
|
769
|
+
const validKeys = Object.keys(AUTOMATION_DEFAULTS).join(', ');
|
|
770
|
+
return { error: `Unknown automation: "${key}". Valid names: ${validKeys}` };
|
|
771
|
+
}
|
|
772
|
+
if (args.mode === 'static' && args.static_minutes == null) {
|
|
773
|
+
return { error: 'static_minutes is required when mode is "static".' };
|
|
774
|
+
}
|
|
775
|
+
const config = readAutomationConfig();
|
|
776
|
+
// Initialize modes if not present
|
|
777
|
+
if (!config.modes)
|
|
778
|
+
config.modes = {};
|
|
779
|
+
const entry = {
|
|
780
|
+
mode: args.mode,
|
|
781
|
+
set_at: new Date().toISOString(),
|
|
782
|
+
};
|
|
783
|
+
if (args.mode === 'static' && args.static_minutes != null) {
|
|
784
|
+
entry.static_minutes = args.static_minutes;
|
|
785
|
+
// Also set the effective cooldown immediately
|
|
786
|
+
if (!config.effective)
|
|
787
|
+
config.effective = { ...config.defaults };
|
|
788
|
+
config.effective[key] = args.static_minutes;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
// Switching back to load_balanced: reset effective to what the optimizer would set
|
|
792
|
+
const factor = config.adjustment?.factor ?? 1.0;
|
|
793
|
+
const defaultVal = config.defaults?.[key] ?? AUTOMATION_DEFAULTS[key];
|
|
794
|
+
if (!config.effective)
|
|
795
|
+
config.effective = { ...config.defaults };
|
|
796
|
+
config.effective[key] = Math.max(5, Math.round(defaultVal / factor));
|
|
797
|
+
}
|
|
798
|
+
config.modes[key] = entry;
|
|
799
|
+
try {
|
|
800
|
+
writeAutomationConfig(config);
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
804
|
+
return { error: `Failed to write config: ${message}` };
|
|
805
|
+
}
|
|
806
|
+
const effectiveMinutes = config.effective[key];
|
|
807
|
+
return {
|
|
808
|
+
automation_name: key,
|
|
809
|
+
mode: args.mode,
|
|
810
|
+
effective_minutes: effectiveMinutes,
|
|
811
|
+
message: args.mode === 'static'
|
|
812
|
+
? `Set ${key} to static mode: runs every ${args.static_minutes}m (fixed, optimizer will not adjust).`
|
|
813
|
+
: `Set ${key} to load_balanced mode: currently ${effectiveMinutes}m (optimizer will adjust dynamically).`,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function listAutomationConfig() {
|
|
817
|
+
const config = readAutomationConfig();
|
|
818
|
+
const automations = [];
|
|
819
|
+
const allKeys = new Set([
|
|
820
|
+
...Object.keys(AUTOMATION_DEFAULTS),
|
|
821
|
+
...Object.keys(config.defaults || {}),
|
|
822
|
+
...Object.keys(config.effective || {}),
|
|
823
|
+
]);
|
|
824
|
+
for (const key of allKeys) {
|
|
825
|
+
const defaultMinutes = config.defaults?.[key] ?? AUTOMATION_DEFAULTS[key] ?? 0;
|
|
826
|
+
const effectiveMinutes = config.effective?.[key] ?? defaultMinutes;
|
|
827
|
+
const modeEntry = config.modes?.[key];
|
|
828
|
+
const mode = modeEntry?.mode ?? 'load_balanced';
|
|
829
|
+
const staticMinutes = modeEntry?.static_minutes ?? null;
|
|
830
|
+
automations.push({
|
|
831
|
+
name: key,
|
|
832
|
+
mode,
|
|
833
|
+
default_minutes: defaultMinutes,
|
|
834
|
+
effective_minutes: effectiveMinutes,
|
|
835
|
+
static_minutes: staticMinutes,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
// Sort by name
|
|
839
|
+
automations.sort((a, b) => a.name.localeCompare(b.name));
|
|
840
|
+
return {
|
|
841
|
+
automations,
|
|
842
|
+
factor: config.adjustment?.factor ?? 1.0,
|
|
843
|
+
last_updated: config.adjustment?.last_updated ?? null,
|
|
844
|
+
message: `${automations.length} automation(s) configured. Factor: ${(config.adjustment?.factor ?? 1.0).toFixed(3)}.`,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
// ============================================================================
|
|
848
|
+
// Bypass Governance Functions
|
|
849
|
+
// ============================================================================
|
|
850
|
+
/**
|
|
851
|
+
* Request a bypass from the CTO.
|
|
852
|
+
*
|
|
853
|
+
* This creates a bypass-request question in the CTO queue. The requesting agent
|
|
854
|
+
* should STOP attempting commits and wait for CTO review via /deputy-cto session.
|
|
855
|
+
*
|
|
856
|
+
* IMPORTANT: Agents cannot use SKIP_DEPUTY_CTO_REVIEW directly. They must
|
|
857
|
+
* request approval through this tool, and only the Deputy CTO (in /deputy-cto session)
|
|
858
|
+
* can execute the bypass after CTO approval.
|
|
859
|
+
*/
|
|
860
|
+
/**
|
|
861
|
+
* Generate a 6-character alphanumeric bypass code
|
|
862
|
+
*/
|
|
863
|
+
function generateBypassCode() {
|
|
864
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Exclude confusing chars: 0/O, 1/I
|
|
865
|
+
let code = '';
|
|
866
|
+
for (let i = 0; i < 6; i++) {
|
|
867
|
+
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
868
|
+
}
|
|
869
|
+
return code;
|
|
870
|
+
}
|
|
871
|
+
function requestBypass(args) {
|
|
872
|
+
const db = getDb();
|
|
873
|
+
// Rate limit: max 3 pending bypass requests at a time
|
|
874
|
+
const pendingBypasses = db.prepare("SELECT COUNT(*) as count FROM questions WHERE type = 'bypass-request' AND status = 'pending'").get();
|
|
875
|
+
if (pendingBypasses.count >= 3) {
|
|
876
|
+
return {
|
|
877
|
+
request_id: '',
|
|
878
|
+
bypass_code: '',
|
|
879
|
+
message: 'Too many pending bypass requests (max 3). Wait for existing requests to be addressed before submitting more.',
|
|
880
|
+
instructions: 'Wait for the CTO to address existing bypass requests.',
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const id = randomUUID();
|
|
884
|
+
const bypassCode = generateBypassCode();
|
|
885
|
+
const now = new Date();
|
|
886
|
+
const created_at = now.toISOString();
|
|
887
|
+
const created_timestamp = Math.floor(now.getTime() / 1000);
|
|
888
|
+
const description = `**Bypass requested by:** ${args.reporting_agent}
|
|
889
|
+
|
|
890
|
+
**Reason:** ${args.reason}
|
|
891
|
+
|
|
892
|
+
${args.blocked_by ? `**Blocked by:** ${args.blocked_by}` : ''}
|
|
893
|
+
|
|
894
|
+
---
|
|
895
|
+
|
|
896
|
+
**CTO Action Required:**
|
|
897
|
+
To approve this bypass, type exactly: **APPROVE BYPASS ${bypassCode}**
|
|
898
|
+
|
|
899
|
+
This will create an approval token that allows the agent to execute the bypass.`;
|
|
900
|
+
// Store bypass code in context field for validation
|
|
901
|
+
db.prepare(`
|
|
902
|
+
INSERT INTO questions (id, type, status, title, description, context, created_at, created_timestamp)
|
|
903
|
+
VALUES (?, 'bypass-request', 'pending', ?, ?, ?, ?, ?)
|
|
904
|
+
`).run(id, `Bypass Request: ${args.reason.substring(0, 100)}`, description, bypassCode, created_at, created_timestamp);
|
|
905
|
+
return {
|
|
906
|
+
request_id: id,
|
|
907
|
+
bypass_code: bypassCode,
|
|
908
|
+
message: `Bypass request submitted. To approve, the CTO must type: APPROVE BYPASS ${bypassCode}`,
|
|
909
|
+
instructions: `STOP attempting commits. Ask the CTO to type exactly: APPROVE BYPASS ${bypassCode}`,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Execute an approved bypass.
|
|
914
|
+
*
|
|
915
|
+
* This verifies that the CTO has typed "APPROVE BYPASS <code>" by checking
|
|
916
|
+
* for an approval token created by the UserPromptSubmit hook.
|
|
917
|
+
*
|
|
918
|
+
* The agent cannot forge this token because:
|
|
919
|
+
* 1. UserPromptSubmit hooks only trigger on actual user input
|
|
920
|
+
* 2. The hook validates the code exists in pending bypass requests
|
|
921
|
+
* 3. The token is tied to the specific bypass code
|
|
922
|
+
*/
|
|
923
|
+
function executeBypass(args) {
|
|
924
|
+
const db = getDb();
|
|
925
|
+
const code = args.bypass_code.toUpperCase();
|
|
926
|
+
const approvalTokenPath = path.join(PROJECT_DIR, '.claude', 'bypass-approval-token.json');
|
|
927
|
+
// Step 1: Verify the bypass request exists with this code
|
|
928
|
+
const question = db.prepare(`
|
|
929
|
+
SELECT id, title FROM questions
|
|
930
|
+
WHERE type = 'bypass-request'
|
|
931
|
+
AND status = 'pending'
|
|
932
|
+
AND context = ?
|
|
933
|
+
`).get(code);
|
|
934
|
+
if (!question) {
|
|
935
|
+
return { error: `No pending bypass request found with code: ${code}` };
|
|
936
|
+
}
|
|
937
|
+
// Step 2: Check for approval token (created by UserPromptSubmit hook when CTO types approval)
|
|
938
|
+
if (!fs.existsSync(approvalTokenPath)) {
|
|
939
|
+
return {
|
|
940
|
+
error: `No approval token found. The CTO must type "APPROVE BYPASS ${code}" to create an approval token.`,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// Step 3: Verify the approval token
|
|
944
|
+
let token;
|
|
945
|
+
try {
|
|
946
|
+
token = JSON.parse(fs.readFileSync(approvalTokenPath, 'utf8'));
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
return { error: 'Failed to read approval token. Ask the CTO to type the approval again.' };
|
|
950
|
+
}
|
|
951
|
+
// Empty object means token was consumed (overwrite pattern for sticky-bit compat)
|
|
952
|
+
if (!token.code && !token.request_id && !token.expires_timestamp) {
|
|
953
|
+
return {
|
|
954
|
+
error: `No approval token found. The CTO must type "APPROVE BYPASS ${code}" to create an approval token.`,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
// Verify HMAC signature to prevent agent forgery
|
|
958
|
+
const key = loadProtectionKey();
|
|
959
|
+
if (!key) {
|
|
960
|
+
return { error: 'Protection key missing. Cannot verify bypass approval token. Restore .claude/protection-key.' };
|
|
961
|
+
}
|
|
962
|
+
const expectedHmac = computeHmac(key, token.code, token.request_id, String(token.expires_timestamp), 'bypass-approved');
|
|
963
|
+
if (token.hmac !== expectedHmac) {
|
|
964
|
+
try {
|
|
965
|
+
fs.writeFileSync(approvalTokenPath, '{}');
|
|
966
|
+
}
|
|
967
|
+
catch { /* ignore */ }
|
|
968
|
+
return { error: 'FORGERY DETECTED: Invalid bypass approval token signature. Token deleted.' };
|
|
969
|
+
}
|
|
970
|
+
// Verify code matches
|
|
971
|
+
if (token.code !== code) {
|
|
972
|
+
return {
|
|
973
|
+
error: `Approval token is for a different bypass code (${token.code}). Ask the CTO to type "APPROVE BYPASS ${code}"`,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
// Verify not expired
|
|
977
|
+
if (Date.now() > token.expires_timestamp) {
|
|
978
|
+
// Clean up expired token (overwrite for sticky-bit compat)
|
|
979
|
+
try {
|
|
980
|
+
fs.writeFileSync(approvalTokenPath, '{}');
|
|
981
|
+
}
|
|
982
|
+
catch { /* ignore */ }
|
|
983
|
+
return {
|
|
984
|
+
error: `Approval token has expired. Ask the CTO to type "APPROVE BYPASS ${code}" again.`,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
// Step 4: Approval verified - record the bypass and clean up
|
|
988
|
+
const bypassId = randomUUID();
|
|
989
|
+
const now = new Date();
|
|
990
|
+
const created_at = now.toISOString();
|
|
991
|
+
const created_timestamp = Math.floor(now.getTime() / 1000);
|
|
992
|
+
// Create an approval record that the pre-commit hook can check
|
|
993
|
+
db.prepare(`
|
|
994
|
+
INSERT INTO commit_decisions (id, decision, rationale, question_id, created_at, created_timestamp)
|
|
995
|
+
VALUES (?, 'approved', ?, ?, ?, ?)
|
|
996
|
+
`).run(bypassId, `EMERGENCY BYPASS - CTO typed "APPROVE BYPASS ${code}"`, question.id, created_at, created_timestamp);
|
|
997
|
+
// Clear the bypass request from the queue
|
|
998
|
+
db.prepare('DELETE FROM questions WHERE id = ?').run(question.id);
|
|
999
|
+
// Clear the approval token (one-time use, overwrite for sticky-bit compat)
|
|
1000
|
+
try {
|
|
1001
|
+
fs.writeFileSync(approvalTokenPath, '{}');
|
|
1002
|
+
}
|
|
1003
|
+
catch { /* ignore */ }
|
|
1004
|
+
return {
|
|
1005
|
+
executed: true,
|
|
1006
|
+
message: `Bypass executed (Decision ID: ${bypassId}). The next commit will proceed without deputy-cto review. This is a ONE-TIME bypass.`,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* List all protected MCP actions and their configuration
|
|
1011
|
+
*/
|
|
1012
|
+
function listProtections() {
|
|
1013
|
+
try {
|
|
1014
|
+
if (!fs.existsSync(PROTECTED_ACTIONS_PATH)) {
|
|
1015
|
+
return {
|
|
1016
|
+
protections: [],
|
|
1017
|
+
count: 0,
|
|
1018
|
+
message: 'No protected actions configured. Use setup.sh --protect-mcp to configure.',
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const config = JSON.parse(fs.readFileSync(PROTECTED_ACTIONS_PATH, 'utf8'));
|
|
1022
|
+
if (!config.servers || Object.keys(config.servers).length === 0) {
|
|
1023
|
+
return {
|
|
1024
|
+
protections: [],
|
|
1025
|
+
count: 0,
|
|
1026
|
+
message: 'No protected actions configured.',
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const protections = Object.entries(config.servers).map(([server, cfg]) => ({
|
|
1030
|
+
server,
|
|
1031
|
+
phrase: cfg.phrase,
|
|
1032
|
+
tools: cfg.tools,
|
|
1033
|
+
protection: cfg.protection,
|
|
1034
|
+
description: cfg.description,
|
|
1035
|
+
}));
|
|
1036
|
+
return {
|
|
1037
|
+
protections,
|
|
1038
|
+
count: protections.length,
|
|
1039
|
+
message: `Found ${protections.length} protected server(s).`,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1044
|
+
return {
|
|
1045
|
+
protections: [],
|
|
1046
|
+
count: 0,
|
|
1047
|
+
message: `Error reading protected actions config: ${message}`,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Get details of a pending protected action request by its code
|
|
1053
|
+
*/
|
|
1054
|
+
function getProtectedActionRequest(args) {
|
|
1055
|
+
try {
|
|
1056
|
+
if (!fs.existsSync(PROTECTED_APPROVALS_PATH)) {
|
|
1057
|
+
return {
|
|
1058
|
+
found: false,
|
|
1059
|
+
message: 'No pending approval requests.',
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const data = JSON.parse(fs.readFileSync(PROTECTED_APPROVALS_PATH, 'utf8'));
|
|
1063
|
+
const approvals = data.approvals || {};
|
|
1064
|
+
const code = args.code.toUpperCase();
|
|
1065
|
+
const request = approvals[code];
|
|
1066
|
+
if (!request) {
|
|
1067
|
+
return {
|
|
1068
|
+
found: false,
|
|
1069
|
+
message: `No request found with code: ${code}`,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
// Check if expired
|
|
1073
|
+
if (Date.now() > request.expires_timestamp) {
|
|
1074
|
+
return {
|
|
1075
|
+
found: false,
|
|
1076
|
+
message: `Request with code ${code} has expired.`,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
found: true,
|
|
1081
|
+
request: {
|
|
1082
|
+
code: request.code,
|
|
1083
|
+
server: request.server,
|
|
1084
|
+
tool: request.tool,
|
|
1085
|
+
args: request.args,
|
|
1086
|
+
phrase: request.phrase,
|
|
1087
|
+
status: request.status,
|
|
1088
|
+
created_at: request.created_at,
|
|
1089
|
+
expires_at: request.expires_at,
|
|
1090
|
+
},
|
|
1091
|
+
message: request.status === 'approved'
|
|
1092
|
+
? `Request ${code} is approved and ready to execute.`
|
|
1093
|
+
: `Request ${code} is pending CTO approval. Type: ${request.phrase} ${code}`,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
catch (err) {
|
|
1097
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1098
|
+
return {
|
|
1099
|
+
found: false,
|
|
1100
|
+
message: `Error reading approval requests: ${message}`,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// ============================================================================
|
|
1105
|
+
// Deputy-CTO Protected Action Approval (Fix 8)
|
|
1106
|
+
// ============================================================================
|
|
1107
|
+
/**
|
|
1108
|
+
* Load protection key for HMAC signing.
|
|
1109
|
+
* @returns Base64-encoded key or null if not found
|
|
1110
|
+
*/
|
|
1111
|
+
function loadProtectionKey() {
|
|
1112
|
+
try {
|
|
1113
|
+
if (!fs.existsSync(PROTECTION_KEY_PATH)) {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return fs.readFileSync(PROTECTION_KEY_PATH, 'utf8').trim();
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Compute HMAC-SHA256 over pipe-delimited fields.
|
|
1124
|
+
* Must match the gate hook's computeHmac function exactly.
|
|
1125
|
+
*/
|
|
1126
|
+
function computeHmac(key, ...fields) {
|
|
1127
|
+
const keyBuffer = Buffer.from(key, 'base64');
|
|
1128
|
+
return crypto.createHmac('sha256', keyBuffer)
|
|
1129
|
+
.update(fields.join('|'))
|
|
1130
|
+
.digest('hex');
|
|
1131
|
+
}
|
|
1132
|
+
function loadApprovalsFile() {
|
|
1133
|
+
try {
|
|
1134
|
+
if (!fs.existsSync(PROTECTED_APPROVALS_PATH)) {
|
|
1135
|
+
return { approvals: {} };
|
|
1136
|
+
}
|
|
1137
|
+
return JSON.parse(fs.readFileSync(PROTECTED_APPROVALS_PATH, 'utf8'));
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
return { approvals: {} };
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function saveApprovalsFile(data) {
|
|
1144
|
+
const dir = path.dirname(PROTECTED_APPROVALS_PATH);
|
|
1145
|
+
if (!fs.existsSync(dir)) {
|
|
1146
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1147
|
+
}
|
|
1148
|
+
fs.writeFileSync(PROTECTED_APPROVALS_PATH, JSON.stringify(data, null, 2));
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Approve a protected action request (deputy-cto only).
|
|
1152
|
+
* Computes HMAC-signed approval that the gate hook will verify.
|
|
1153
|
+
*/
|
|
1154
|
+
function approveProtectedAction(args) {
|
|
1155
|
+
const code = args.code.toUpperCase();
|
|
1156
|
+
const data = loadApprovalsFile();
|
|
1157
|
+
const request = data.approvals[code];
|
|
1158
|
+
if (!request) {
|
|
1159
|
+
return { error: `No pending request found with code: ${code}` };
|
|
1160
|
+
}
|
|
1161
|
+
if (request.status === 'approved') {
|
|
1162
|
+
return { error: `Request ${code} has already been approved.` };
|
|
1163
|
+
}
|
|
1164
|
+
if (Date.now() > request.expires_timestamp) {
|
|
1165
|
+
delete data.approvals[code];
|
|
1166
|
+
saveApprovalsFile(data);
|
|
1167
|
+
return { error: `Request ${code} has expired.` };
|
|
1168
|
+
}
|
|
1169
|
+
// Only deputy-cto-approval mode requests can be approved by deputy-cto
|
|
1170
|
+
if (request.approval_mode !== 'deputy-cto') {
|
|
1171
|
+
return {
|
|
1172
|
+
error: `Request ${code} requires CTO approval (mode: ${request.approval_mode || 'cto'}). Deputy-CTO cannot approve this action. Escalate to CTO queue.`,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
// Verify pending_hmac with protection key
|
|
1176
|
+
const key = loadProtectionKey();
|
|
1177
|
+
if (key && request.pending_hmac) {
|
|
1178
|
+
const expectedPendingHmac = computeHmac(key, code, request.server, request.tool, request.argsHash || '', String(request.expires_timestamp));
|
|
1179
|
+
if (request.pending_hmac !== expectedPendingHmac) {
|
|
1180
|
+
// Forged request - delete it
|
|
1181
|
+
delete data.approvals[code];
|
|
1182
|
+
saveApprovalsFile(data);
|
|
1183
|
+
return { error: `FORGERY DETECTED: Invalid pending signature for ${code}. Request deleted.` };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
else if (!key && request.pending_hmac) {
|
|
1187
|
+
// G001 Fail-Closed: Request has HMAC but we can't verify (key missing)
|
|
1188
|
+
return { error: `Cannot verify request signature for ${code} (protection key missing). Restore .claude/protection-key.` };
|
|
1189
|
+
}
|
|
1190
|
+
else if (!key) {
|
|
1191
|
+
// G001 Fail-Closed: No protection key at all — cannot sign approvals
|
|
1192
|
+
return { error: `Protection key missing. Cannot create HMAC-signed approval. Restore .claude/protection-key.` };
|
|
1193
|
+
}
|
|
1194
|
+
// Compute approved_hmac (same algorithm as approval hook)
|
|
1195
|
+
request.status = 'approved';
|
|
1196
|
+
request.approved_at = new Date().toISOString();
|
|
1197
|
+
request.approved_timestamp = Date.now();
|
|
1198
|
+
request.approved_hmac = computeHmac(key, code, request.server, request.tool, 'approved', request.argsHash || '', String(request.expires_timestamp));
|
|
1199
|
+
saveApprovalsFile(data);
|
|
1200
|
+
return {
|
|
1201
|
+
approved: true,
|
|
1202
|
+
code,
|
|
1203
|
+
server: request.server,
|
|
1204
|
+
tool: request.tool,
|
|
1205
|
+
message: `Approved: ${request.server}.${request.tool} (code: ${code}). Agent can now retry the action.`,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Deny a protected action request (deputy-cto only).
|
|
1210
|
+
* Removes the pending entry from the approvals file.
|
|
1211
|
+
*/
|
|
1212
|
+
function denyProtectedAction(args) {
|
|
1213
|
+
const code = args.code.toUpperCase();
|
|
1214
|
+
const data = loadApprovalsFile();
|
|
1215
|
+
const request = data.approvals[code];
|
|
1216
|
+
if (!request) {
|
|
1217
|
+
return { error: `No pending request found with code: ${code}` };
|
|
1218
|
+
}
|
|
1219
|
+
// Remove the request
|
|
1220
|
+
const server = request.server;
|
|
1221
|
+
const tool = request.tool;
|
|
1222
|
+
delete data.approvals[code];
|
|
1223
|
+
saveApprovalsFile(data);
|
|
1224
|
+
return {
|
|
1225
|
+
denied: true,
|
|
1226
|
+
code,
|
|
1227
|
+
reason: args.reason,
|
|
1228
|
+
message: `Denied: ${server}.${tool} (code: ${code}). Reason: ${args.reason}`,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* List all pending (non-expired) protected action requests.
|
|
1233
|
+
* Used by deputy-cto during triage to discover pending requests.
|
|
1234
|
+
*/
|
|
1235
|
+
function listPendingActionRequests() {
|
|
1236
|
+
const data = loadApprovalsFile();
|
|
1237
|
+
const now = Date.now();
|
|
1238
|
+
const requests = [];
|
|
1239
|
+
for (const [code, request] of Object.entries(data.approvals)) {
|
|
1240
|
+
if (request.status !== 'pending')
|
|
1241
|
+
continue;
|
|
1242
|
+
if (request.expires_timestamp < now)
|
|
1243
|
+
continue;
|
|
1244
|
+
requests.push({
|
|
1245
|
+
code,
|
|
1246
|
+
server: request.server,
|
|
1247
|
+
tool: request.tool,
|
|
1248
|
+
args: request.args,
|
|
1249
|
+
approval_mode: request.approval_mode || 'cto',
|
|
1250
|
+
created_at: request.created_at,
|
|
1251
|
+
expires_at: request.expires_at,
|
|
1252
|
+
expires_in_seconds: Math.floor((request.expires_timestamp - now) / 1000),
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
requests,
|
|
1257
|
+
count: requests.length,
|
|
1258
|
+
message: requests.length === 0
|
|
1259
|
+
? 'No pending protected action requests.'
|
|
1260
|
+
: `Found ${requests.length} pending request(s).`,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
// ============================================================================
|
|
1264
|
+
// Hotfix Promotion Functions
|
|
1265
|
+
// ============================================================================
|
|
1266
|
+
function requestHotfixPromotion(_args) {
|
|
1267
|
+
const gitOpts = { cwd: PROJECT_DIR, encoding: 'utf8', timeout: 15000, stdio: 'pipe' };
|
|
1268
|
+
// Fetch latest
|
|
1269
|
+
try {
|
|
1270
|
+
execSync('git fetch origin staging main', gitOpts);
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1274
|
+
return { error: `Failed to fetch origin: ${message}` };
|
|
1275
|
+
}
|
|
1276
|
+
// Get commits on staging ahead of main
|
|
1277
|
+
let commitLines;
|
|
1278
|
+
try {
|
|
1279
|
+
const gitLog = execSync('git log origin/main..origin/staging --oneline', gitOpts).trim();
|
|
1280
|
+
commitLines = gitLog ? gitLog.split('\n') : [];
|
|
1281
|
+
}
|
|
1282
|
+
catch (err) {
|
|
1283
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1284
|
+
return { error: `Failed to compare staging and main: ${message}` };
|
|
1285
|
+
}
|
|
1286
|
+
if (commitLines.length === 0) {
|
|
1287
|
+
return { error: 'No commits on staging ahead of main. Nothing to hotfix.' };
|
|
1288
|
+
}
|
|
1289
|
+
const db = getDb();
|
|
1290
|
+
// Check no pending hotfix already exists
|
|
1291
|
+
const pendingHotfix = db.prepare("SELECT id FROM hotfix_requests WHERE status = 'pending' AND expires_at > datetime('now')").get();
|
|
1292
|
+
if (pendingHotfix) {
|
|
1293
|
+
return { error: `A pending hotfix request already exists (ID: ${pendingHotfix.id}). Wait for it to expire or be executed before requesting another.` };
|
|
1294
|
+
}
|
|
1295
|
+
// Generate 6-char code
|
|
1296
|
+
const code = crypto.randomBytes(3).toString('hex').toUpperCase();
|
|
1297
|
+
const now = new Date();
|
|
1298
|
+
const expiresAt = new Date(now.getTime() + 5 * 60 * 1000);
|
|
1299
|
+
db.prepare(`
|
|
1300
|
+
INSERT INTO hotfix_requests (code, commits_json, created_at, expires_at, status)
|
|
1301
|
+
VALUES (?, ?, ?, ?, 'pending')
|
|
1302
|
+
`).run(code, JSON.stringify(commitLines), now.toISOString(), expiresAt.toISOString());
|
|
1303
|
+
return {
|
|
1304
|
+
code,
|
|
1305
|
+
commits: commitLines,
|
|
1306
|
+
expires_at: expiresAt.toISOString(),
|
|
1307
|
+
message: `Hotfix promotion requested. ${commitLines.length} commit(s) on staging ahead of main. To approve, the CTO must type: APPROVE HOTFIX ${code} (expires in 5 minutes)`,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function executeHotfixPromotion(_args) {
|
|
1311
|
+
// Read approval token
|
|
1312
|
+
if (!fs.existsSync(HOTFIX_APPROVAL_TOKEN_PATH)) {
|
|
1313
|
+
return { error: 'No hotfix approval token found. The CTO must type "APPROVE HOTFIX <code>" first.' };
|
|
1314
|
+
}
|
|
1315
|
+
let token;
|
|
1316
|
+
try {
|
|
1317
|
+
token = JSON.parse(fs.readFileSync(HOTFIX_APPROVAL_TOKEN_PATH, 'utf8'));
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
return { error: 'Failed to read hotfix approval token. Ask the CTO to type the approval again.' };
|
|
1321
|
+
}
|
|
1322
|
+
// Empty object means consumed
|
|
1323
|
+
if (!token.code && !token.hmac) {
|
|
1324
|
+
return { error: 'No hotfix approval token found. The CTO must type "APPROVE HOTFIX <code>" first.' };
|
|
1325
|
+
}
|
|
1326
|
+
// Verify HMAC
|
|
1327
|
+
const key = loadProtectionKey();
|
|
1328
|
+
if (!key) {
|
|
1329
|
+
return { error: 'Protection key missing. Cannot verify hotfix approval token. Restore .claude/protection-key.' };
|
|
1330
|
+
}
|
|
1331
|
+
const expectedHmac = computeHmac(key, token.code, String(token.request_id), token.expires_at, 'hotfix-approved');
|
|
1332
|
+
if (token.hmac !== expectedHmac) {
|
|
1333
|
+
// Consume the forged token
|
|
1334
|
+
try {
|
|
1335
|
+
fs.writeFileSync(HOTFIX_APPROVAL_TOKEN_PATH, '{}');
|
|
1336
|
+
}
|
|
1337
|
+
catch { /* ignore */ }
|
|
1338
|
+
return { error: 'FORGERY DETECTED: Invalid hotfix approval token signature. Token deleted.' };
|
|
1339
|
+
}
|
|
1340
|
+
// Check not expired
|
|
1341
|
+
if (new Date(token.expires_at).getTime() < Date.now()) {
|
|
1342
|
+
try {
|
|
1343
|
+
fs.writeFileSync(HOTFIX_APPROVAL_TOKEN_PATH, '{}');
|
|
1344
|
+
}
|
|
1345
|
+
catch { /* ignore */ }
|
|
1346
|
+
return { error: 'Hotfix approval token has expired. Ask the CTO to approve again.' };
|
|
1347
|
+
}
|
|
1348
|
+
// Consume the token (one-time use)
|
|
1349
|
+
try {
|
|
1350
|
+
fs.writeFileSync(HOTFIX_APPROVAL_TOKEN_PATH, '{}');
|
|
1351
|
+
}
|
|
1352
|
+
catch { /* ignore */ }
|
|
1353
|
+
// Update DB status
|
|
1354
|
+
const db = getDb();
|
|
1355
|
+
db.prepare("UPDATE hotfix_requests SET status = 'executed' WHERE id = ?").run(token.request_id);
|
|
1356
|
+
// Retrieve the commits for the prompt
|
|
1357
|
+
const row = db.prepare('SELECT commits_json FROM hotfix_requests WHERE id = ?').get(token.request_id);
|
|
1358
|
+
const commits = row ? JSON.parse(row.commits_json) : [];
|
|
1359
|
+
// Spawn the hotfix promotion agent
|
|
1360
|
+
const commitList = commits.join('\n');
|
|
1361
|
+
const hotfixPrompt = `[Task][hotfix-promotion] You are the EMERGENCY HOTFIX Promotion Pipeline.
|
|
1362
|
+
|
|
1363
|
+
## Mission
|
|
1364
|
+
|
|
1365
|
+
Immediately merge staging into main. This is a CTO-approved emergency hotfix that bypasses:
|
|
1366
|
+
- The 24-hour stability requirement
|
|
1367
|
+
- The midnight deployment window
|
|
1368
|
+
|
|
1369
|
+
Code review and quality checks still apply.
|
|
1370
|
+
|
|
1371
|
+
## Commits being promoted
|
|
1372
|
+
|
|
1373
|
+
\`\`\`
|
|
1374
|
+
${commitList}
|
|
1375
|
+
\`\`\`
|
|
1376
|
+
|
|
1377
|
+
## Process
|
|
1378
|
+
|
|
1379
|
+
### Step 1: Code Review
|
|
1380
|
+
|
|
1381
|
+
Spawn a code-reviewer sub-agent (Task tool, subagent_type: code-reviewer) to review the commits:
|
|
1382
|
+
- Check for security issues, code quality, spec violations
|
|
1383
|
+
- Look for disabled tests, placeholder code, hardcoded credentials
|
|
1384
|
+
- Verify no spec violations (G001-G019)
|
|
1385
|
+
|
|
1386
|
+
### Step 2: Create and Merge PR
|
|
1387
|
+
|
|
1388
|
+
If code review passes:
|
|
1389
|
+
1. Run: gh pr create --base main --head staging --title "HOTFIX: Emergency promotion staging -> main" --body "CTO-approved emergency hotfix. Bypasses 24h stability and midnight window."
|
|
1390
|
+
2. Wait for CI: gh pr checks <number> --watch
|
|
1391
|
+
3. If CI passes: gh pr merge <number> --merge
|
|
1392
|
+
4. If CI fails: Report failure via mcp__agent-reports__report_to_deputy_cto
|
|
1393
|
+
|
|
1394
|
+
If code review fails:
|
|
1395
|
+
- Report findings via mcp__agent-reports__report_to_deputy_cto with priority "critical"
|
|
1396
|
+
- Do NOT proceed with merge
|
|
1397
|
+
|
|
1398
|
+
## Timeout
|
|
1399
|
+
|
|
1400
|
+
Complete within 25 minutes. If blocked, report and exit.`;
|
|
1401
|
+
try {
|
|
1402
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
1403
|
+
const claude = spawn('claude', [
|
|
1404
|
+
'--dangerously-skip-permissions',
|
|
1405
|
+
'--mcp-config', mcpConfig,
|
|
1406
|
+
'--output-format', 'json',
|
|
1407
|
+
'-p',
|
|
1408
|
+
hotfixPrompt,
|
|
1409
|
+
], {
|
|
1410
|
+
detached: true,
|
|
1411
|
+
stdio: 'ignore',
|
|
1412
|
+
cwd: PROJECT_DIR,
|
|
1413
|
+
env: {
|
|
1414
|
+
...process.env,
|
|
1415
|
+
CLAUDE_PROJECT_DIR: PROJECT_DIR,
|
|
1416
|
+
CLAUDE_SPAWNED_SESSION: 'true',
|
|
1417
|
+
GENTYR_PROMOTION_PIPELINE: 'true',
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
claude.unref();
|
|
1421
|
+
return {
|
|
1422
|
+
success: true,
|
|
1423
|
+
message: `Hotfix promotion agent spawned (PID: ${claude.pid}). Staging -> main promotion is in progress with code review. The 24h stability gate and midnight window are bypassed.`,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
catch (err) {
|
|
1427
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1428
|
+
return { error: `Failed to spawn hotfix promotion agent: ${message}` };
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
// ============================================================================
|
|
1432
|
+
// Merge Chain Status
|
|
1433
|
+
// ============================================================================
|
|
1434
|
+
async function getMergeChainStatus(_args) {
|
|
1435
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
1436
|
+
const gitOpts = { cwd: projectDir, encoding: 'utf8', timeout: 15000, stdio: 'pipe' };
|
|
1437
|
+
const result = {};
|
|
1438
|
+
// Fetch latest
|
|
1439
|
+
try {
|
|
1440
|
+
execSync('git fetch origin --quiet 2>/dev/null || true', gitOpts);
|
|
1441
|
+
}
|
|
1442
|
+
catch { /* non-fatal */ }
|
|
1443
|
+
// Preview ahead of staging
|
|
1444
|
+
try {
|
|
1445
|
+
const log = execSync('git log origin/staging..origin/preview --oneline', gitOpts).trim();
|
|
1446
|
+
const commits = log ? log.split('\n') : [];
|
|
1447
|
+
result.previewAheadOfStaging = commits.length;
|
|
1448
|
+
}
|
|
1449
|
+
catch {
|
|
1450
|
+
result.previewAheadOfStaging = 'unknown (branch may not exist)';
|
|
1451
|
+
}
|
|
1452
|
+
// Staging ahead of main
|
|
1453
|
+
try {
|
|
1454
|
+
const log = execSync('git log origin/main..origin/staging --oneline', gitOpts).trim();
|
|
1455
|
+
const commits = log ? log.split('\n') : [];
|
|
1456
|
+
result.stagingAheadOfMain = commits.length;
|
|
1457
|
+
}
|
|
1458
|
+
catch {
|
|
1459
|
+
result.stagingAheadOfMain = 'unknown (branch may not exist)';
|
|
1460
|
+
}
|
|
1461
|
+
// Active feature branches
|
|
1462
|
+
try {
|
|
1463
|
+
const branches = execSync('git branch -r --list "origin/feature/*"', gitOpts).trim();
|
|
1464
|
+
const branchList = branches ? branches.split('\n').map((b) => b.trim().replace('origin/', '')) : [];
|
|
1465
|
+
result.activeFeatureBranches = branchList.length;
|
|
1466
|
+
result.featureBranchNames = branchList;
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
result.activeFeatureBranches = 0;
|
|
1470
|
+
result.featureBranchNames = [];
|
|
1471
|
+
}
|
|
1472
|
+
// Stale branches (>3 days old with no recent commits)
|
|
1473
|
+
try {
|
|
1474
|
+
const branches = result.featureBranchNames || [];
|
|
1475
|
+
const staleBranches = [];
|
|
1476
|
+
const threeDaysAgo = Math.floor(Date.now() / 1000) - (3 * 86400);
|
|
1477
|
+
for (const branch of branches) {
|
|
1478
|
+
try {
|
|
1479
|
+
const timestamp = parseInt(execSync(`git log -1 --format=%ct origin/${branch}`, gitOpts).trim(), 10);
|
|
1480
|
+
if (timestamp < threeDaysAgo) {
|
|
1481
|
+
staleBranches.push(branch);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
catch { /* skip */ }
|
|
1485
|
+
}
|
|
1486
|
+
result.staleBranches = staleBranches.length;
|
|
1487
|
+
result.staleBranchNames = staleBranches;
|
|
1488
|
+
}
|
|
1489
|
+
catch {
|
|
1490
|
+
result.staleBranches = 0;
|
|
1491
|
+
result.staleBranchNames = [];
|
|
1492
|
+
}
|
|
1493
|
+
// Uncommitted changes
|
|
1494
|
+
try {
|
|
1495
|
+
const status = execSync('git status --porcelain', gitOpts).trim();
|
|
1496
|
+
result.uncommittedChanges = status ? status.split('\n').length : 0;
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
result.uncommittedChanges = 'unknown';
|
|
1500
|
+
}
|
|
1501
|
+
// Last promotion timestamps
|
|
1502
|
+
try {
|
|
1503
|
+
const previewTs = execSync('git log -1 --format=%ct origin/preview', gitOpts).trim();
|
|
1504
|
+
const hoursSince = Math.floor((Date.now() / 1000 - parseInt(previewTs, 10)) / 3600);
|
|
1505
|
+
result.lastPreviewCommitHoursAgo = hoursSince;
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
result.lastPreviewCommitHoursAgo = 'unknown';
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
const stagingTs = execSync('git log -1 --format=%ct origin/staging', gitOpts).trim();
|
|
1512
|
+
const hoursSince = Math.floor((Date.now() / 1000 - parseInt(stagingTs, 10)) / 3600);
|
|
1513
|
+
result.lastStagingCommitHoursAgo = hoursSince;
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
result.lastStagingCommitHoursAgo = 'unknown';
|
|
1517
|
+
}
|
|
1518
|
+
return JSON.stringify(result, null, 2);
|
|
1519
|
+
}
|
|
1520
|
+
// ============================================================================
|
|
1521
|
+
// Server Setup
|
|
1522
|
+
// ============================================================================
|
|
1523
|
+
const tools = [
|
|
1524
|
+
{
|
|
1525
|
+
name: 'add_question',
|
|
1526
|
+
description: 'Add a question/decision request for the CTO. Use for decisions, approvals, or escalations from reports. Escalations REQUIRE a recommendation field.',
|
|
1527
|
+
schema: AddQuestionArgsSchema,
|
|
1528
|
+
handler: addQuestion,
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
name: 'list_questions',
|
|
1532
|
+
description: 'List CTO questions (titles only to preserve tokens). Shows pending count and whether commits are blocked.',
|
|
1533
|
+
schema: ListQuestionsArgsSchema,
|
|
1534
|
+
handler: listQuestions,
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
name: 'read_question',
|
|
1538
|
+
description: 'Read the full content of a question including description and context.',
|
|
1539
|
+
schema: ReadQuestionArgsSchema,
|
|
1540
|
+
handler: readQuestion,
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
name: 'answer_question',
|
|
1544
|
+
description: 'Record the CTO answer to a question. Question remains in queue until cleared.',
|
|
1545
|
+
schema: AnswerQuestionArgsSchema,
|
|
1546
|
+
handler: answerQuestion,
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
name: 'clear_question',
|
|
1550
|
+
description: 'Remove a question from the queue after it has been addressed/implemented.',
|
|
1551
|
+
schema: ClearQuestionArgsSchema,
|
|
1552
|
+
handler: clearQuestion,
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
name: 'approve_commit',
|
|
1556
|
+
description: 'Approve the pending commit. Cannot approve if there are pending rejections.',
|
|
1557
|
+
schema: ApproveCommitArgsSchema,
|
|
1558
|
+
handler: approveCommit,
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
name: 'reject_commit',
|
|
1562
|
+
description: 'Reject the pending commit. Creates a question entry that blocks future commits until addressed.',
|
|
1563
|
+
schema: RejectCommitArgsSchema,
|
|
1564
|
+
handler: rejectCommit,
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
name: 'get_commit_decision',
|
|
1568
|
+
description: 'Get the current commit decision status. Used by pre-commit hook to allow/block commits.',
|
|
1569
|
+
schema: GetCommitDecisionArgsSchema,
|
|
1570
|
+
handler: getCommitDecision,
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
name: 'get_pending_count',
|
|
1574
|
+
description: 'Get count of pending questions and whether commits are blocked. Used by session hooks.',
|
|
1575
|
+
schema: GetPendingCountArgsSchema,
|
|
1576
|
+
handler: getPendingCountTool,
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
name: 'toggle_autonomous_mode',
|
|
1580
|
+
description: 'Enable or disable Autonomous Deputy CTO Mode. When enabled, hourly plan execution and CLAUDE.md refactoring runs.',
|
|
1581
|
+
schema: ToggleAutonomousModeArgsSchema,
|
|
1582
|
+
handler: toggleAutonomousMode,
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
name: 'get_autonomous_mode_status',
|
|
1586
|
+
description: 'Get the current status of Autonomous Deputy CTO Mode, including when next run will occur and CTO activity gate status.',
|
|
1587
|
+
schema: GetAutonomousModeStatusArgsSchema,
|
|
1588
|
+
handler: getAutonomousModeStatus,
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
name: 'record_cto_briefing',
|
|
1592
|
+
description: 'Record that the CTO has started a briefing session. Refreshes the 24-hour automation activity gate. Must be called at the start of every /deputy-cto session.',
|
|
1593
|
+
schema: RecordCtoBriefingArgsSchema,
|
|
1594
|
+
handler: recordCtoBriefing,
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
name: 'search_cleared_items',
|
|
1598
|
+
description: 'Search previously cleared CTO questions by substring. Use to check if a CTO-PENDING note in a plan has been addressed.',
|
|
1599
|
+
schema: SearchClearedItemsArgsSchema,
|
|
1600
|
+
handler: searchClearedItems,
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
name: 'cleanup_old_records',
|
|
1604
|
+
description: 'Clean up old records to prevent unbounded database growth. Retains last 100 commit decisions and cleared questions within 30 days (minimum 500). Automatically runs on startup.',
|
|
1605
|
+
schema: CleanupOldRecordsArgsSchema,
|
|
1606
|
+
handler: cleanupOldRecords,
|
|
1607
|
+
},
|
|
1608
|
+
// Automation mode tools
|
|
1609
|
+
{
|
|
1610
|
+
name: 'set_automation_mode',
|
|
1611
|
+
description: 'ALWAYS use this tool (not manual file edits) to change automation frequency, interval, or schedule. Sets an automation to load_balanced (dynamic) or static (fixed interval) mode. Call list_automation_config first to see current values.',
|
|
1612
|
+
schema: SetAutomationModeArgsSchema,
|
|
1613
|
+
handler: setAutomationMode,
|
|
1614
|
+
},
|
|
1615
|
+
{
|
|
1616
|
+
name: 'list_automation_config',
|
|
1617
|
+
description: 'ALWAYS use this tool (not manual file reads) to view automation frequencies, intervals, schedules, or cooldowns. Lists all automations with their mode, effective intervals, and static overrides.',
|
|
1618
|
+
schema: ListAutomationConfigArgsSchema,
|
|
1619
|
+
handler: listAutomationConfig,
|
|
1620
|
+
},
|
|
1621
|
+
// Bypass governance tools
|
|
1622
|
+
{
|
|
1623
|
+
name: 'request_bypass',
|
|
1624
|
+
description: 'Request an emergency bypass from the CTO. Returns a 6-character code. STOP attempting commits and ask the CTO to type "APPROVE BYPASS <code>" in the chat. Only then call execute_bypass.',
|
|
1625
|
+
schema: RequestBypassArgsSchema,
|
|
1626
|
+
handler: requestBypass,
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
name: 'execute_bypass',
|
|
1630
|
+
description: 'Execute a bypass AFTER the CTO has typed "APPROVE BYPASS <code>" in the chat. The UserPromptSubmit hook creates an approval token when the CTO types the approval phrase. This tool verifies that token exists.',
|
|
1631
|
+
schema: ExecuteBypassArgsSchema,
|
|
1632
|
+
handler: executeBypass,
|
|
1633
|
+
},
|
|
1634
|
+
// Protected action tools
|
|
1635
|
+
{
|
|
1636
|
+
name: 'list_protections',
|
|
1637
|
+
description: 'List all CTO-protected MCP actions. Shows which servers/tools require approval before execution.',
|
|
1638
|
+
schema: ListProtectionsArgsSchema,
|
|
1639
|
+
handler: listProtections,
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
name: 'get_protected_action_request',
|
|
1643
|
+
description: 'Get details of a pending protected action request by its 6-character approval code. Use to check status before retrying a blocked action.',
|
|
1644
|
+
schema: GetProtectedActionRequestArgsSchema,
|
|
1645
|
+
handler: getProtectedActionRequest,
|
|
1646
|
+
},
|
|
1647
|
+
// Deputy-CTO protected action approval tools (Fix 8)
|
|
1648
|
+
{
|
|
1649
|
+
name: 'approve_protected_action',
|
|
1650
|
+
description: 'Approve a pending deputy-cto-approval protected action. Only works for actions with approval_mode "deputy-cto". Creates HMAC-signed approval.',
|
|
1651
|
+
schema: ApproveProtectedActionArgsSchema,
|
|
1652
|
+
handler: approveProtectedAction,
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: 'deny_protected_action',
|
|
1656
|
+
description: 'Deny a pending protected action request. Removes the pending entry. Include a clear reason.',
|
|
1657
|
+
schema: DenyProtectedActionArgsSchema,
|
|
1658
|
+
handler: denyProtectedAction,
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: 'list_pending_action_requests',
|
|
1662
|
+
description: 'List all pending (non-expired) protected action requests. Shows code, server, tool, args, and approval mode for each.',
|
|
1663
|
+
schema: ListPendingActionRequestsArgsSchema,
|
|
1664
|
+
handler: listPendingActionRequests,
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
name: 'get_merge_chain_status',
|
|
1668
|
+
description: 'Get the current merge chain status: branch positions, active/stale feature branches, uncommitted changes. Used for CTO briefing.',
|
|
1669
|
+
schema: GetMergeChainStatusArgsSchema,
|
|
1670
|
+
handler: getMergeChainStatus,
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
name: 'request_hotfix_promotion',
|
|
1674
|
+
description: 'Request an emergency hotfix promotion from staging to main. Returns an approval code the CTO must type to authorize. Validates staging has commits ahead of main.',
|
|
1675
|
+
schema: RequestHotfixPromotionArgsSchema,
|
|
1676
|
+
handler: requestHotfixPromotion,
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
name: 'execute_hotfix_promotion',
|
|
1680
|
+
description: 'Execute a CTO-approved emergency hotfix promotion from staging to main. Requires prior APPROVE HOTFIX approval. Bypasses 24h stability and midnight window.',
|
|
1681
|
+
schema: ExecuteHotfixPromotionArgsSchema,
|
|
1682
|
+
handler: executeHotfixPromotion,
|
|
1683
|
+
},
|
|
1684
|
+
];
|
|
1685
|
+
const server = new McpServer({
|
|
1686
|
+
name: 'deputy-cto',
|
|
1687
|
+
version: '1.0.0',
|
|
1688
|
+
tools,
|
|
1689
|
+
});
|
|
1690
|
+
// Handle cleanup on exit
|
|
1691
|
+
process.on('SIGINT', () => {
|
|
1692
|
+
closeDb();
|
|
1693
|
+
process.exit(0);
|
|
1694
|
+
});
|
|
1695
|
+
process.on('SIGTERM', () => {
|
|
1696
|
+
closeDb();
|
|
1697
|
+
process.exit(0);
|
|
1698
|
+
});
|
|
1699
|
+
server.start();
|
|
1700
|
+
//# sourceMappingURL=server.js.map
|