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,3340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hourly Automation Runner
|
|
4
|
+
*
|
|
5
|
+
* Wrapper script called by systemd/launchd hourly service.
|
|
6
|
+
* Delegates to individual automation scripts based on config.
|
|
7
|
+
*
|
|
8
|
+
* This design allows changing behavior without re-installing the service.
|
|
9
|
+
*
|
|
10
|
+
* Automations:
|
|
11
|
+
* 1. Plan Executor - Execute pending project plans
|
|
12
|
+
* 2. CLAUDE.md Refactor - Compact CLAUDE.md when it exceeds size threshold
|
|
13
|
+
*
|
|
14
|
+
* @version 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { spawn, execSync, execFileSync } from 'child_process';
|
|
21
|
+
import { registerSpawn, updateAgent, registerHookExecution, AGENT_TYPES, HOOK_TYPES } from './agent-tracker.js';
|
|
22
|
+
import { getCooldown } from './config-reader.js';
|
|
23
|
+
import { runUsageOptimizer } from './usage-optimizer.js';
|
|
24
|
+
import { syncKeys } from './key-sync.js';
|
|
25
|
+
import { runFeedbackPipeline } from './feedback-orchestrator.js';
|
|
26
|
+
import { createWorktree, cleanupMergedWorktrees } from './lib/worktree-manager.js';
|
|
27
|
+
import { getFeatureBranchName } from './lib/feature-branch-helper.js';
|
|
28
|
+
import { detectStaleWork, formatReport } from './stale-work-detector.js';
|
|
29
|
+
|
|
30
|
+
// Try to import better-sqlite3 for task runner
|
|
31
|
+
let Database = null;
|
|
32
|
+
try {
|
|
33
|
+
Database = (await import('better-sqlite3')).default;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// Non-fatal: task runner will be skipped if unavailable
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = path.dirname(__filename);
|
|
40
|
+
|
|
41
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
42
|
+
const CONFIG_FILE = path.join(PROJECT_DIR, '.claude', 'autonomous-mode.json');
|
|
43
|
+
const LOG_FILE = path.join(PROJECT_DIR, '.claude', 'hourly-automation.log');
|
|
44
|
+
const STATE_FILE = path.join(PROJECT_DIR, '.claude', 'hourly-automation-state.json');
|
|
45
|
+
const CTO_REPORTS_DB = path.join(PROJECT_DIR, '.claude', 'cto-reports.db');
|
|
46
|
+
|
|
47
|
+
// Rotation proxy
|
|
48
|
+
const PROXY_PORT = process.env.GENTYR_PROXY_PORT || 18080;
|
|
49
|
+
const PROXY_HEALTH_URL = `http://localhost:${PROXY_PORT}/__health`;
|
|
50
|
+
|
|
51
|
+
// Thresholds
|
|
52
|
+
const CLAUDE_MD_SIZE_THRESHOLD = 25000; // 25K characters
|
|
53
|
+
// Note: Per-item cooldown (1 hour) is now handled by the agent-reports MCP server
|
|
54
|
+
|
|
55
|
+
// Task Runner: section-to-agent mapping
|
|
56
|
+
const SECTION_AGENT_MAP = {
|
|
57
|
+
'CODE-REVIEWER': { agent: 'code-reviewer', agentType: AGENT_TYPES.TASK_RUNNER_CODE_REVIEWER },
|
|
58
|
+
'INVESTIGATOR & PLANNER': { agent: 'investigator', agentType: AGENT_TYPES.TASK_RUNNER_INVESTIGATOR },
|
|
59
|
+
'TEST-WRITER': { agent: 'test-writer', agentType: AGENT_TYPES.TASK_RUNNER_TEST_WRITER },
|
|
60
|
+
'PROJECT-MANAGER': { agent: 'project-manager', agentType: AGENT_TYPES.TASK_RUNNER_PROJECT_MANAGER },
|
|
61
|
+
'DEPUTY-CTO': { agent: 'deputy-cto', agentType: AGENT_TYPES.TASK_RUNNER_DEPUTY_CTO },
|
|
62
|
+
'PRODUCT-MANAGER': { agent: 'product-manager', agentType: AGENT_TYPES.TASK_RUNNER_PRODUCT_MANAGER },
|
|
63
|
+
};
|
|
64
|
+
const TODO_DB_PATH = path.join(PROJECT_DIR, '.claude', 'todo.db');
|
|
65
|
+
|
|
66
|
+
// Concurrency guard: max simultaneous automation agents
|
|
67
|
+
const MAX_CONCURRENT_AGENTS = 5;
|
|
68
|
+
const MAX_TASKS_PER_CYCLE = 3;
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// CREDENTIAL CACHE: Lazily resolve 1Password credentials on first agent spawn.
|
|
72
|
+
// Credentials exist only in this process's memory and are passed to child
|
|
73
|
+
// processes via environment variables. MCP servers (started by Claude CLI
|
|
74
|
+
// from .mcp.json env blocks) skip `op read` when the credential is already
|
|
75
|
+
// in process.env.
|
|
76
|
+
//
|
|
77
|
+
// Lazy resolution means credentials are NOT resolved on cycles where all
|
|
78
|
+
// tasks hit cooldowns and no agents are spawned (~90% of cycles). This
|
|
79
|
+
// eliminates unnecessary `op` CLI calls that trigger macOS TCC prompts
|
|
80
|
+
// ("node would like to access data from other apps") and 1Password Touch ID
|
|
81
|
+
// prompts in background/launchd contexts.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
let resolvedCredentials = {};
|
|
84
|
+
let credentialsResolved = false;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ensure credentials have been resolved (lazy, called only when spawning).
|
|
88
|
+
* Wraps preResolveCredentials() with a guard flag so it runs at most once
|
|
89
|
+
* per automation cycle.
|
|
90
|
+
*/
|
|
91
|
+
function ensureCredentials() {
|
|
92
|
+
if (credentialsResolved) return;
|
|
93
|
+
credentialsResolved = true;
|
|
94
|
+
preResolveCredentials();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pre-resolve all 1Password credentials needed by infrastructure MCP servers.
|
|
99
|
+
* Reads vault-mappings.json for op:// references and protected-actions.json
|
|
100
|
+
* for which keys each server needs. Calls `op read` once per unique reference.
|
|
101
|
+
* Results are cached in `resolvedCredentials` (in-memory only, never on disk).
|
|
102
|
+
*
|
|
103
|
+
* In headless contexts (launchd/systemd), skips `op read` calls unless
|
|
104
|
+
* OP_SERVICE_ACCOUNT_TOKEN is set, to prevent macOS permission prompts.
|
|
105
|
+
*/
|
|
106
|
+
function preResolveCredentials() {
|
|
107
|
+
// Headless guard: In launchd/systemd contexts, `op` communicates with the
|
|
108
|
+
// 1Password desktop app via IPC, triggering macOS TCC and Touch ID prompts.
|
|
109
|
+
// OP_SERVICE_ACCOUNT_TOKEN uses the 1Password API directly (no desktop app).
|
|
110
|
+
const hasServiceAccount = !!process.env.OP_SERVICE_ACCOUNT_TOKEN;
|
|
111
|
+
const isLaunchdService = process.env.GENTYR_LAUNCHD_SERVICE === 'true';
|
|
112
|
+
|
|
113
|
+
if (isLaunchdService && !hasServiceAccount) {
|
|
114
|
+
log('Credential cache: headless mode without OP_SERVICE_ACCOUNT_TOKEN — skipping op read to prevent macOS prompts.');
|
|
115
|
+
log('Credential cache: spawned agents will start MCP servers without pre-resolved credentials.');
|
|
116
|
+
log('Credential cache: for full headless credentials, reinstall with: setup-automation-service.sh setup --op-token <TOKEN>');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const mappingsPath = path.join(PROJECT_DIR, '.claude', 'vault-mappings.json');
|
|
121
|
+
const actionsPath = path.join(PROJECT_DIR, '.claude', 'hooks', 'protected-actions.json');
|
|
122
|
+
|
|
123
|
+
let mappings = {};
|
|
124
|
+
let servers = {};
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(fs.readFileSync(mappingsPath, 'utf8'));
|
|
128
|
+
mappings = data.mappings || {};
|
|
129
|
+
} catch {
|
|
130
|
+
log('Credential cache: no vault-mappings.json, skipping pre-resolution.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const actions = JSON.parse(fs.readFileSync(actionsPath, 'utf8'));
|
|
136
|
+
servers = actions.servers || {};
|
|
137
|
+
} catch {
|
|
138
|
+
log('Credential cache: no protected-actions.json, skipping pre-resolution.');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Collect all unique credential keys across all servers
|
|
143
|
+
const allKeys = new Set();
|
|
144
|
+
for (const server of Object.values(servers)) {
|
|
145
|
+
if (server.credentialKeys) {
|
|
146
|
+
for (const key of server.credentialKeys) {
|
|
147
|
+
allKeys.add(key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let resolved = 0;
|
|
153
|
+
let skipped = 0;
|
|
154
|
+
let failed = 0;
|
|
155
|
+
|
|
156
|
+
for (const key of allKeys) {
|
|
157
|
+
// Skip if already in environment
|
|
158
|
+
if (process.env[key]) {
|
|
159
|
+
skipped++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const ref = mappings[key];
|
|
164
|
+
if (!ref) continue;
|
|
165
|
+
|
|
166
|
+
if (ref.startsWith('op://')) {
|
|
167
|
+
try {
|
|
168
|
+
const value = execFileSync('op', ['read', ref], {
|
|
169
|
+
encoding: 'utf-8',
|
|
170
|
+
timeout: 15000,
|
|
171
|
+
stdio: 'pipe',
|
|
172
|
+
}).trim();
|
|
173
|
+
|
|
174
|
+
if (value) {
|
|
175
|
+
resolvedCredentials[key] = value;
|
|
176
|
+
resolved++;
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
failed++;
|
|
180
|
+
log(`Credential cache: failed to resolve ${key} from ${ref}: ${err.message || err}`);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Direct value (non-secret like URL, zone ID)
|
|
184
|
+
resolvedCredentials[key] = ref;
|
|
185
|
+
resolved++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (allKeys.size > 0) {
|
|
190
|
+
log(`Credential cache: resolved ${resolved}/${allKeys.size} credentials (${skipped} from env, ${failed} failed).`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build the env object for spawning claude processes.
|
|
196
|
+
* Lazily resolves credentials on first call, then includes them so
|
|
197
|
+
* MCP servers skip `op read`.
|
|
198
|
+
*/
|
|
199
|
+
function buildSpawnEnv(agentId) {
|
|
200
|
+
ensureCredentials();
|
|
201
|
+
return {
|
|
202
|
+
...process.env,
|
|
203
|
+
...resolvedCredentials,
|
|
204
|
+
CLAUDE_PROJECT_DIR: PROJECT_DIR,
|
|
205
|
+
CLAUDE_SPAWNED_SESSION: 'true',
|
|
206
|
+
CLAUDE_AGENT_ID: agentId,
|
|
207
|
+
HTTPS_PROXY: 'http://localhost:18080',
|
|
208
|
+
HTTP_PROXY: 'http://localhost:18080',
|
|
209
|
+
NO_PROXY: 'localhost,127.0.0.1',
|
|
210
|
+
NODE_EXTRA_CA_CERTS: path.join(process.env.HOME || '/tmp', '.claude', 'proxy-certs', 'ca.pem'),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if the rotation proxy is running and healthy.
|
|
216
|
+
* Non-blocking, returns status for logging only. Agents still spawn if proxy is down.
|
|
217
|
+
*/
|
|
218
|
+
async function checkProxyHealth() {
|
|
219
|
+
const http = await import('http');
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const req = http.request(PROXY_HEALTH_URL, { timeout: 2000 }, (res) => {
|
|
222
|
+
let data = '';
|
|
223
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
224
|
+
res.on('end', () => {
|
|
225
|
+
try {
|
|
226
|
+
const health = JSON.parse(data);
|
|
227
|
+
resolve({ running: true, ...health });
|
|
228
|
+
} catch {
|
|
229
|
+
resolve({ running: true, raw: data });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
req.on('error', () => resolve({ running: false }));
|
|
234
|
+
req.on('timeout', () => { req.destroy(); resolve({ running: false }); });
|
|
235
|
+
req.end();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Count running automation agents to prevent process accumulation
|
|
241
|
+
*/
|
|
242
|
+
function countRunningAgents() {
|
|
243
|
+
try {
|
|
244
|
+
const result = execSync(
|
|
245
|
+
"pgrep -cf 'claude.*--dangerously-skip-permissions'",
|
|
246
|
+
{ encoding: 'utf8', timeout: 5000, stdio: 'pipe' }
|
|
247
|
+
).trim();
|
|
248
|
+
return parseInt(result, 10) || 0;
|
|
249
|
+
} catch {
|
|
250
|
+
// pgrep returns exit code 1 when no processes match
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Append to log file
|
|
257
|
+
*/
|
|
258
|
+
function log(message) {
|
|
259
|
+
const timestamp = new Date().toISOString();
|
|
260
|
+
const logLine = `[${timestamp}] ${message}\n`;
|
|
261
|
+
fs.appendFileSync(LOG_FILE, logLine);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get config (autonomous mode settings)
|
|
266
|
+
*
|
|
267
|
+
* G001 Note: If config is corrupted, we use safe defaults (enabled: false).
|
|
268
|
+
* This is intentional fail-safe behavior - corrupt config should NOT enable automation.
|
|
269
|
+
* The corruption is logged prominently for CTO awareness.
|
|
270
|
+
*/
|
|
271
|
+
function getConfig() {
|
|
272
|
+
const defaults = {
|
|
273
|
+
enabled: false,
|
|
274
|
+
claudeMdRefactorEnabled: true,
|
|
275
|
+
lintCheckerEnabled: true,
|
|
276
|
+
taskRunnerEnabled: true,
|
|
277
|
+
standaloneAntipatternHunterEnabled: true,
|
|
278
|
+
standaloneComplianceCheckerEnabled: true,
|
|
279
|
+
productManagerEnabled: false,
|
|
280
|
+
lastModified: null,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
284
|
+
return defaults;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
289
|
+
return { ...defaults, ...config };
|
|
290
|
+
} catch (err) {
|
|
291
|
+
// G001: Config corruption is logged but we fail-safe to disabled mode
|
|
292
|
+
// This is intentional - corrupt config should never enable automation
|
|
293
|
+
log(`ERROR: Config file corrupted - automation DISABLED for safety: ${err.message}`);
|
|
294
|
+
log(`Fix: Delete or repair ${CONFIG_FILE}`);
|
|
295
|
+
return defaults;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check CTO activity gate.
|
|
301
|
+
* G001: Fail-closed - if lastCtoBriefing is missing or older than 24h, automation is gated.
|
|
302
|
+
*
|
|
303
|
+
* @returns {{ open: boolean, reason: string, hoursSinceLastBriefing: number | null }}
|
|
304
|
+
*/
|
|
305
|
+
function checkCtoActivityGate(config) {
|
|
306
|
+
const lastCtoBriefing = config.lastCtoBriefing;
|
|
307
|
+
|
|
308
|
+
if (!lastCtoBriefing) {
|
|
309
|
+
return {
|
|
310
|
+
open: false,
|
|
311
|
+
reason: 'No CTO briefing recorded. Run /deputy-cto to activate automation.',
|
|
312
|
+
hoursSinceLastBriefing: null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const briefingTime = new Date(lastCtoBriefing).getTime();
|
|
318
|
+
if (isNaN(briefingTime)) {
|
|
319
|
+
return {
|
|
320
|
+
open: false,
|
|
321
|
+
reason: 'CTO briefing timestamp is invalid. Run /deputy-cto to reset.',
|
|
322
|
+
hoursSinceLastBriefing: null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const hoursSince = (Date.now() - briefingTime) / (1000 * 60 * 60);
|
|
327
|
+
if (hoursSince >= 24) {
|
|
328
|
+
return {
|
|
329
|
+
open: false,
|
|
330
|
+
reason: `CTO briefing was ${Math.floor(hoursSince)}h ago (>24h). Run /deputy-cto to reactivate.`,
|
|
331
|
+
hoursSinceLastBriefing: Math.floor(hoursSince),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
open: true,
|
|
337
|
+
reason: `CTO briefing was ${Math.floor(hoursSince)}h ago. Gate is open.`,
|
|
338
|
+
hoursSinceLastBriefing: Math.floor(hoursSince),
|
|
339
|
+
};
|
|
340
|
+
} catch (err) {
|
|
341
|
+
// G001: Parse error = fail closed
|
|
342
|
+
return {
|
|
343
|
+
open: false,
|
|
344
|
+
reason: `Failed to parse CTO briefing timestamp: ${err.message}`,
|
|
345
|
+
hoursSinceLastBriefing: null,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// GAP 2: PERSISTENT ALERTS SYSTEM
|
|
352
|
+
// Tracks recurring issues (production errors, CI failures, merge chain gaps)
|
|
353
|
+
// with automatic re-escalation when issues persist beyond thresholds.
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
const PERSISTENT_ALERTS_PATH = path.join(PROJECT_DIR, '.claude', 'state', 'persistent_alerts.json');
|
|
356
|
+
const ALERT_RE_ESCALATION_HOURS = { critical: 4, high: 12, medium: 24 };
|
|
357
|
+
const ALERT_RESOLVED_GC_DAYS = 7;
|
|
358
|
+
const MERGE_CHAIN_GAP_THRESHOLD = 50;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Read persistent alerts state file. Returns default structure if missing/corrupt.
|
|
362
|
+
*/
|
|
363
|
+
function readPersistentAlerts() {
|
|
364
|
+
try {
|
|
365
|
+
if (fs.existsSync(PERSISTENT_ALERTS_PATH)) {
|
|
366
|
+
const raw = JSON.parse(fs.readFileSync(PERSISTENT_ALERTS_PATH, 'utf8'));
|
|
367
|
+
// Validate structure
|
|
368
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw) ||
|
|
369
|
+
typeof raw.alerts !== 'object' || raw.alerts === null || Array.isArray(raw.alerts)) {
|
|
370
|
+
log('Persistent alerts: invalid structure, using defaults.');
|
|
371
|
+
return { version: 1, alerts: {} };
|
|
372
|
+
}
|
|
373
|
+
// Validate individual alerts — drop malformed entries
|
|
374
|
+
for (const [key, alert] of Object.entries(raw.alerts)) {
|
|
375
|
+
if (typeof alert !== 'object' || alert === null ||
|
|
376
|
+
typeof alert.severity !== 'string' ||
|
|
377
|
+
typeof alert.resolved !== 'boolean') {
|
|
378
|
+
log(`Persistent alerts: dropping malformed alert '${key}'.`);
|
|
379
|
+
delete raw.alerts[key];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return raw;
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
log(`Persistent alerts: failed to read state (${err.message}), using defaults.`);
|
|
386
|
+
}
|
|
387
|
+
return { version: 1, alerts: {} };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Write persistent alerts state file.
|
|
392
|
+
*/
|
|
393
|
+
function writePersistentAlerts(data) {
|
|
394
|
+
try {
|
|
395
|
+
const dir = path.dirname(PERSISTENT_ALERTS_PATH);
|
|
396
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
397
|
+
fs.writeFileSync(PERSISTENT_ALERTS_PATH, JSON.stringify(data, null, 2));
|
|
398
|
+
} catch (err) {
|
|
399
|
+
log(`Persistent alerts: failed to write state: ${err.message}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Record or update a persistent alert.
|
|
405
|
+
* @param {string} key - Alert key (e.g., 'production_error', 'ci_main_failure')
|
|
406
|
+
* @param {object} opts - { title, severity, source }
|
|
407
|
+
*/
|
|
408
|
+
function recordAlert(key, { title, severity, source }) {
|
|
409
|
+
const data = readPersistentAlerts();
|
|
410
|
+
const now = new Date().toISOString();
|
|
411
|
+
|
|
412
|
+
if (data.alerts[key] && !data.alerts[key].resolved) {
|
|
413
|
+
// Update existing unresolved alert
|
|
414
|
+
data.alerts[key].last_detected_at = now;
|
|
415
|
+
data.alerts[key].detection_count += 1;
|
|
416
|
+
data.alerts[key].title = title;
|
|
417
|
+
} else {
|
|
418
|
+
// Create new alert (or replace resolved one)
|
|
419
|
+
data.alerts[key] = {
|
|
420
|
+
key,
|
|
421
|
+
title,
|
|
422
|
+
severity,
|
|
423
|
+
first_detected_at: now,
|
|
424
|
+
last_detected_at: now,
|
|
425
|
+
last_escalated_at: null,
|
|
426
|
+
detection_count: 1,
|
|
427
|
+
escalation_count: 0,
|
|
428
|
+
resolved: false,
|
|
429
|
+
resolved_at: null,
|
|
430
|
+
source,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
writePersistentAlerts(data);
|
|
435
|
+
return data.alerts[key];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Resolve a persistent alert if it exists and is unresolved.
|
|
440
|
+
*/
|
|
441
|
+
function resolveAlert(key) {
|
|
442
|
+
const data = readPersistentAlerts();
|
|
443
|
+
if (data.alerts[key] && !data.alerts[key].resolved) {
|
|
444
|
+
data.alerts[key].resolved = true;
|
|
445
|
+
data.alerts[key].resolved_at = new Date().toISOString();
|
|
446
|
+
writePersistentAlerts(data);
|
|
447
|
+
log(`Persistent alerts: resolved '${key}'.`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Sanitize an alert field for safe prompt interpolation.
|
|
453
|
+
* Strips backticks, newlines, and template-like syntax to prevent prompt injection.
|
|
454
|
+
*/
|
|
455
|
+
function sanitizeAlertField(val) {
|
|
456
|
+
if (typeof val !== 'string') return String(val ?? '');
|
|
457
|
+
return val.replace(/[`\n\r]/g, '').replace(/\$\{/g, '$ {').slice(0, 200);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Spawn a minimal re-escalation agent that posts to deputy-CTO.
|
|
462
|
+
*/
|
|
463
|
+
function spawnAlertEscalation(alert) {
|
|
464
|
+
// Sanitize all alert fields before any interpolation to prevent prompt injection
|
|
465
|
+
const safeTitle = sanitizeAlertField(alert.title);
|
|
466
|
+
const safeKey = sanitizeAlertField(alert.key);
|
|
467
|
+
const safeSeverity = sanitizeAlertField(alert.severity);
|
|
468
|
+
const safeSource = sanitizeAlertField(alert.source);
|
|
469
|
+
const safeFirstDetected = sanitizeAlertField(alert.first_detected_at);
|
|
470
|
+
const safeDetectionCount = Number(alert.detection_count) || 0;
|
|
471
|
+
const safeEscalationCount = Number(alert.escalation_count) || 0;
|
|
472
|
+
|
|
473
|
+
const agentId = registerSpawn({
|
|
474
|
+
type: AGENT_TYPES.PRODUCTION_HEALTH_MONITOR,
|
|
475
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
476
|
+
description: `Alert re-escalation: ${safeKey}`,
|
|
477
|
+
prompt: '',
|
|
478
|
+
metadata: { alertKey: safeKey, escalationCount: safeEscalationCount },
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const firstDetectedTs = new Date(alert.first_detected_at).getTime();
|
|
482
|
+
const ageHours = Number.isFinite(firstDetectedTs) ? Math.round((Date.now() - firstDetectedTs) / 3600000) : 0;
|
|
483
|
+
|
|
484
|
+
const prompt = `[Task][alert-escalation][AGENT:${agentId}] ALERT RE-ESCALATION
|
|
485
|
+
|
|
486
|
+
A persistent issue has NOT been resolved and requires CTO attention.
|
|
487
|
+
|
|
488
|
+
**Alert:** ${safeTitle}
|
|
489
|
+
**Key:** ${safeKey}
|
|
490
|
+
**Severity:** ${safeSeverity}
|
|
491
|
+
**First detected:** ${safeFirstDetected} (${ageHours}h ago)
|
|
492
|
+
**Detection count:** ${safeDetectionCount} times
|
|
493
|
+
**Previous escalations:** ${safeEscalationCount}
|
|
494
|
+
|
|
495
|
+
Call \`mcp__deputy-cto__add_question\` with:
|
|
496
|
+
- type: "escalation"
|
|
497
|
+
- title: "PERSISTENT: ${safeTitle} (${ageHours}h, ${safeDetectionCount} detections)"
|
|
498
|
+
- description: "This issue was first detected ${ageHours}h ago and has been detected ${safeDetectionCount} times. It has been escalated ${safeEscalationCount} time(s) previously but remains unresolved. Source: ${safeSource}."
|
|
499
|
+
- recommendation: "Investigate and resolve the ${safeKey} issue. Previous escalations were cleared but the underlying problem persists."
|
|
500
|
+
|
|
501
|
+
Then exit immediately.`;
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
505
|
+
const claude = spawn('claude', [
|
|
506
|
+
'--dangerously-skip-permissions',
|
|
507
|
+
'--mcp-config', mcpConfig,
|
|
508
|
+
'--output-format', 'json',
|
|
509
|
+
'-p', prompt,
|
|
510
|
+
], {
|
|
511
|
+
detached: true,
|
|
512
|
+
stdio: 'ignore',
|
|
513
|
+
cwd: PROJECT_DIR,
|
|
514
|
+
env: buildSpawnEnv(agentId),
|
|
515
|
+
});
|
|
516
|
+
claude.unref();
|
|
517
|
+
updateAgent(agentId, { pid: claude.pid, status: 'running', prompt });
|
|
518
|
+
return true;
|
|
519
|
+
} catch (err) {
|
|
520
|
+
log(`Alert escalation spawn error: ${err.message}`);
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* GAP 2: Check persistent alerts for re-escalation needs.
|
|
527
|
+
* Runs every cycle (gate-exempt). Spawns re-escalation agents for
|
|
528
|
+
* unresolved alerts past their re-escalation threshold. Garbage-collects
|
|
529
|
+
* resolved alerts older than 7 days.
|
|
530
|
+
*/
|
|
531
|
+
function checkPersistentAlerts() {
|
|
532
|
+
const data = readPersistentAlerts();
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
let escalated = 0;
|
|
535
|
+
let gcCount = 0;
|
|
536
|
+
|
|
537
|
+
for (const [key, alert] of Object.entries(data.alerts)) {
|
|
538
|
+
if (!alert.resolved) {
|
|
539
|
+
// Check re-escalation threshold
|
|
540
|
+
const thresholdHours = ALERT_RE_ESCALATION_HOURS[alert.severity] || 24;
|
|
541
|
+
const lastEscalated = alert.last_escalated_at ? new Date(alert.last_escalated_at).getTime() : 0;
|
|
542
|
+
const hoursSinceEscalation = (now - lastEscalated) / 3600000;
|
|
543
|
+
|
|
544
|
+
if (hoursSinceEscalation >= thresholdHours) {
|
|
545
|
+
log(`Persistent alerts: re-escalating '${key}' (${alert.severity}, ${Math.round(hoursSinceEscalation)}h since last escalation).`);
|
|
546
|
+
spawnAlertEscalation(alert);
|
|
547
|
+
alert.last_escalated_at = new Date().toISOString();
|
|
548
|
+
alert.escalation_count += 1;
|
|
549
|
+
escalated++;
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
// Garbage-collect resolved alerts older than 7 days
|
|
553
|
+
const resolvedAt = alert.resolved_at ? new Date(alert.resolved_at).getTime() : 0;
|
|
554
|
+
if (resolvedAt > 0 && (now - resolvedAt) > ALERT_RESOLVED_GC_DAYS * 86400000) {
|
|
555
|
+
delete data.alerts[key];
|
|
556
|
+
gcCount++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (escalated > 0 || gcCount > 0) {
|
|
562
|
+
writePersistentAlerts(data);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (escalated > 0) log(`Persistent alerts: ${escalated} alert(s) re-escalated.`);
|
|
566
|
+
if (gcCount > 0) log(`Persistent alerts: ${gcCount} resolved alert(s) garbage-collected.`);
|
|
567
|
+
return { escalated, gcCount };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* GAP 3: Check CI status for main and staging branches via GitHub Actions API.
|
|
572
|
+
* Creates/resolves persistent alerts for CI failures.
|
|
573
|
+
*/
|
|
574
|
+
function checkCiStatus() {
|
|
575
|
+
let owner, repo;
|
|
576
|
+
try {
|
|
577
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
578
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 10000, stdio: 'pipe',
|
|
579
|
+
}).trim();
|
|
580
|
+
// Parse owner/repo from git URL (handles SSH and HTTPS)
|
|
581
|
+
const match = remoteUrl.match(/[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
582
|
+
if (!match) {
|
|
583
|
+
log('CI monitoring: could not parse owner/repo from remote URL.');
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
[, owner, repo] = match;
|
|
587
|
+
} catch {
|
|
588
|
+
log('CI monitoring: failed to get git remote URL.');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const branches = ['main', 'staging'];
|
|
593
|
+
for (const branch of branches) {
|
|
594
|
+
const alertKey = `ci_${branch}_failure`;
|
|
595
|
+
try {
|
|
596
|
+
const result = execFileSync('gh', [
|
|
597
|
+
'api',
|
|
598
|
+
`repos/${owner}/${repo}/actions/runs?branch=${branch}&per_page=5&status=completed`,
|
|
599
|
+
'--jq',
|
|
600
|
+
'.workflow_runs | map({conclusion, name, html_url, created_at})',
|
|
601
|
+
], {
|
|
602
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 15000, stdio: 'pipe',
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
const runs = JSON.parse(result || '[]');
|
|
606
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
607
|
+
log(`CI monitoring (${branch}): no completed runs found.`);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const latestRun = runs[0];
|
|
612
|
+
if (typeof latestRun.conclusion !== 'string') {
|
|
613
|
+
log(`CI monitoring (${branch}): unexpected API response shape. Skipping.`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (latestRun.conclusion === 'failure') {
|
|
617
|
+
log(`CI monitoring (${branch}): latest run FAILED — ${latestRun.name}`);
|
|
618
|
+
recordAlert(alertKey, {
|
|
619
|
+
title: `CI failure on ${branch}: ${latestRun.name}`,
|
|
620
|
+
severity: branch === 'main' ? 'critical' : 'high',
|
|
621
|
+
source: 'ci-monitoring',
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
if (latestRun.conclusion === 'success') {
|
|
625
|
+
resolveAlert(alertKey);
|
|
626
|
+
}
|
|
627
|
+
log(`CI monitoring (${branch}): latest run ${latestRun.conclusion}.`);
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
log(`CI monitoring (${branch}): gh api call failed (${err.message}). Skipping.`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get state
|
|
637
|
+
* G001: Fail-closed if state file is corrupted
|
|
638
|
+
*/
|
|
639
|
+
function getState() {
|
|
640
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
641
|
+
return {
|
|
642
|
+
lastRun: 0, lastClaudeMdRefactor: 0, lastTriageCheck: 0, lastTaskRunnerCheck: 0,
|
|
643
|
+
lastPreviewPromotionCheck: 0, lastStagingPromotionCheck: 0,
|
|
644
|
+
lastStagingHealthCheck: 0, lastProductionHealthCheck: 0,
|
|
645
|
+
lastStandaloneAntipatternHunt: 0, lastStandaloneComplianceCheck: 0,
|
|
646
|
+
lastFeedbackCheck: 0, lastFeedbackSha: null,
|
|
647
|
+
lastPreviewToStagingMergeAt: 0,
|
|
648
|
+
stagingFreezeActive: false,
|
|
649
|
+
stagingFreezeActivatedAt: 0,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
655
|
+
// Migration for existing state files
|
|
656
|
+
if (state.lastTriageCheck === undefined) {
|
|
657
|
+
state.lastTriageCheck = state.lastTriage || 0;
|
|
658
|
+
delete state.lastTriage;
|
|
659
|
+
}
|
|
660
|
+
// Remove legacy triageAttempts if present (now handled by MCP server)
|
|
661
|
+
delete state.triageAttempts;
|
|
662
|
+
// Migration for staging freeze fields
|
|
663
|
+
if (state.lastPreviewToStagingMergeAt === undefined) state.lastPreviewToStagingMergeAt = 0;
|
|
664
|
+
if (state.stagingFreezeActive === undefined) state.stagingFreezeActive = false;
|
|
665
|
+
if (state.stagingFreezeActivatedAt === undefined) state.stagingFreezeActivatedAt = 0;
|
|
666
|
+
return state;
|
|
667
|
+
} catch (err) {
|
|
668
|
+
log(`FATAL: State file corrupted: ${err.message}`);
|
|
669
|
+
log(`Delete ${STATE_FILE} to reset.`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Save state
|
|
676
|
+
* G001: Fail-closed if state can't be saved
|
|
677
|
+
*/
|
|
678
|
+
function saveState(state) {
|
|
679
|
+
try {
|
|
680
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
681
|
+
} catch (err) {
|
|
682
|
+
log(`FATAL: Cannot save state: ${err.message}`);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Check CLAUDE.md size
|
|
689
|
+
*/
|
|
690
|
+
function getClaudeMdSize() {
|
|
691
|
+
const claudeMdPath = path.join(PROJECT_DIR, 'CLAUDE.md');
|
|
692
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
const stats = fs.statSync(claudeMdPath);
|
|
698
|
+
return stats.size;
|
|
699
|
+
} catch {
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Check if there are any reports ready for triage
|
|
706
|
+
* Uses simple sqlite3 query - MCP server handles cooldown filtering
|
|
707
|
+
*/
|
|
708
|
+
function hasReportsReadyForTriage() {
|
|
709
|
+
if (!Database || !fs.existsSync(CTO_REPORTS_DB)) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
const db = new Database(CTO_REPORTS_DB, { readonly: true });
|
|
715
|
+
const row = db.prepare("SELECT COUNT(*) as cnt FROM reports WHERE triage_status = 'pending'").get();
|
|
716
|
+
db.close();
|
|
717
|
+
return (row?.cnt || 0) > 0;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
log(`WARN: Failed to check for pending reports: ${err.message}`);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Spawn deputy-cto to triage pending reports
|
|
726
|
+
* The agent will discover reports via MCP tools (which handle cooldown filtering)
|
|
727
|
+
*/
|
|
728
|
+
function spawnReportTriage() {
|
|
729
|
+
// Register spawn first to get agentId for prompt embedding
|
|
730
|
+
const agentId = registerSpawn({
|
|
731
|
+
type: AGENT_TYPES.DEPUTY_CTO_REVIEW,
|
|
732
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
733
|
+
description: 'Triaging pending CTO reports',
|
|
734
|
+
prompt: '',
|
|
735
|
+
metadata: {},
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const prompt = `[Task][report-triage][AGENT:${agentId}] You are an orchestrator performing REPORT TRIAGE.
|
|
739
|
+
|
|
740
|
+
## IMMEDIATE ACTION
|
|
741
|
+
|
|
742
|
+
Your first action MUST be to spawn the deputy-cto sub-agent:
|
|
743
|
+
\`\`\`
|
|
744
|
+
Task(subagent_type='deputy-cto', prompt='Triage all pending agent reports. Use mcp__agent-reports__get_reports_for_triage to get reports, then investigate and decide on each.')
|
|
745
|
+
\`\`\`
|
|
746
|
+
|
|
747
|
+
The deputy-cto sub-agent has specialized instructions loaded from .claude/agents/deputy-cto.md.
|
|
748
|
+
|
|
749
|
+
## Mission
|
|
750
|
+
|
|
751
|
+
Triage all pending agent reports that are ready (past cooldown). For each report:
|
|
752
|
+
1. Investigate to understand the context
|
|
753
|
+
2. Decide whether to handle it yourself, escalate to CTO, or dismiss
|
|
754
|
+
3. Take appropriate action
|
|
755
|
+
|
|
756
|
+
## Step 1: Get Reports Ready for Triage
|
|
757
|
+
|
|
758
|
+
\`\`\`
|
|
759
|
+
mcp__agent-reports__get_reports_for_triage({ limit: 10 })
|
|
760
|
+
\`\`\`
|
|
761
|
+
|
|
762
|
+
This returns reports that are:
|
|
763
|
+
- Status = pending
|
|
764
|
+
- Past the 1-hour per-item cooldown (if previously attempted)
|
|
765
|
+
|
|
766
|
+
If no reports are returned, output "No reports ready for triage" and exit.
|
|
767
|
+
|
|
768
|
+
## Step 2: Triage Each Report
|
|
769
|
+
|
|
770
|
+
For each report from the list above:
|
|
771
|
+
|
|
772
|
+
### 2a: Start Triage
|
|
773
|
+
\`\`\`
|
|
774
|
+
mcp__agent-reports__start_triage({ id: "<report-id>" })
|
|
775
|
+
\`\`\`
|
|
776
|
+
|
|
777
|
+
### 2b: Read the Report
|
|
778
|
+
\`\`\`
|
|
779
|
+
mcp__agent-reports__read_report({ id: "<report-id>" })
|
|
780
|
+
\`\`\`
|
|
781
|
+
|
|
782
|
+
### 2c: Investigate
|
|
783
|
+
|
|
784
|
+
**Search for related work:**
|
|
785
|
+
\`\`\`
|
|
786
|
+
mcp__todo-db__list_tasks({ limit: 50 }) // Check current tasks
|
|
787
|
+
mcp__deputy-cto__search_cleared_items({ query: "<keywords from report>" }) // Check past CTO items
|
|
788
|
+
mcp__agent-tracker__search_sessions({ query: "<keywords>", limit: 10 }) // Search session history
|
|
789
|
+
\`\`\`
|
|
790
|
+
|
|
791
|
+
**If needed, search the codebase:**
|
|
792
|
+
- Use Grep to find related code
|
|
793
|
+
- Use Read to examine specific files mentioned in the report
|
|
794
|
+
|
|
795
|
+
### 2d: Check Auto-Escalation Rules
|
|
796
|
+
|
|
797
|
+
**ALWAYS ESCALATE (no exceptions):**
|
|
798
|
+
- **G002 Violations**: Any report mentioning stub code, placeholder, TODO, FIXME, or "not implemented"
|
|
799
|
+
- **Security vulnerabilities**: Any report with category "security" or mentioning vulnerabilities
|
|
800
|
+
- **Bypass requests**: Any bypass-request type (these require CTO approval)
|
|
801
|
+
|
|
802
|
+
If the report matches ANY auto-escalation rule, skip to "If ESCALATING" - do not self-handle or dismiss.
|
|
803
|
+
|
|
804
|
+
### 2e: Apply Decision Framework (if no auto-escalation)
|
|
805
|
+
|
|
806
|
+
| ESCALATE to CTO | SELF-HANDLE | DISMISS |
|
|
807
|
+
|-----------------|-------------|---------|
|
|
808
|
+
| Breaking change to users | Issue already in todos | Already resolved |
|
|
809
|
+
| Architectural decision needed | Similar issue recently fixed | Not a real problem |
|
|
810
|
+
| Resource/budget implications | Clear fix, low risk | False positive |
|
|
811
|
+
| Cross-team coordination | Obvious code quality fix | Duplicate report |
|
|
812
|
+
| Uncertain about approach | Documentation/test gap | Informational only |
|
|
813
|
+
| High priority + ambiguity | Performance fix clear path | Outdated concern |
|
|
814
|
+
| Policy/process change | Routine maintenance | |
|
|
815
|
+
| | Isolated bug fix | |
|
|
816
|
+
|
|
817
|
+
**Decision Rules:**
|
|
818
|
+
- **>80% confident** you know the right action → self-handle
|
|
819
|
+
- **<80% confident** OR sensitive → escalate
|
|
820
|
+
- **Not actionable** (already fixed, false positive, duplicate) → dismiss
|
|
821
|
+
|
|
822
|
+
### 2f: Take Action
|
|
823
|
+
|
|
824
|
+
**If SELF-HANDLING:**
|
|
825
|
+
\`\`\`
|
|
826
|
+
// Create an urgent task — dispatched immediately by the urgent dispatcher
|
|
827
|
+
mcp__todo-db__create_task({
|
|
828
|
+
section: "CODE-REVIEWER", // Choose based on task type (see section mapping below)
|
|
829
|
+
title: "Brief actionable title",
|
|
830
|
+
description: "Full context: what to fix, where, why, and acceptance criteria",
|
|
831
|
+
assigned_by: "deputy-cto",
|
|
832
|
+
priority: "urgent"
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
// Complete the triage
|
|
836
|
+
mcp__agent-reports__complete_triage({
|
|
837
|
+
id: "<report-id>",
|
|
838
|
+
status: "self_handled",
|
|
839
|
+
outcome: "Created urgent task to [brief description of fix]"
|
|
840
|
+
})
|
|
841
|
+
\`\`\`
|
|
842
|
+
|
|
843
|
+
Section mapping for self-handled tasks:
|
|
844
|
+
- Code changes (full agent sequence) → "CODE-REVIEWER"
|
|
845
|
+
- Research/analysis only → "INVESTIGATOR & PLANNER"
|
|
846
|
+
- Test creation/updates → "TEST-WRITER"
|
|
847
|
+
- Documentation/cleanup → "PROJECT-MANAGER"
|
|
848
|
+
- Orchestration/delegation → "DEPUTY-CTO"
|
|
849
|
+
|
|
850
|
+
**If ESCALATING:**
|
|
851
|
+
\`\`\`
|
|
852
|
+
// Add to CTO queue with context
|
|
853
|
+
mcp__deputy-cto__add_question({
|
|
854
|
+
type: "escalation", // or "decision" if CTO needs to choose
|
|
855
|
+
title: "Brief title of the issue",
|
|
856
|
+
description: "Context from investigation + why CTO input needed",
|
|
857
|
+
suggested_options: ["Option A", "Option B"], // if applicable
|
|
858
|
+
recommendation: "Your recommended course of action and why" // REQUIRED for escalations
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
// Complete the triage
|
|
862
|
+
mcp__agent-reports__complete_triage({
|
|
863
|
+
id: "<report-id>",
|
|
864
|
+
status: "escalated",
|
|
865
|
+
outcome: "Escalated: [reason CTO input is needed]"
|
|
866
|
+
})
|
|
867
|
+
\`\`\`
|
|
868
|
+
|
|
869
|
+
**If DISMISSING:**
|
|
870
|
+
\`\`\`
|
|
871
|
+
// Complete the triage - no further action needed
|
|
872
|
+
mcp__agent-reports__complete_triage({
|
|
873
|
+
id: "<report-id>",
|
|
874
|
+
status: "dismissed",
|
|
875
|
+
outcome: "Dismissed: [reason - e.g., already resolved, not actionable, duplicate]"
|
|
876
|
+
})
|
|
877
|
+
\`\`\`
|
|
878
|
+
|
|
879
|
+
**IMPORTANT: Only dismiss when you have clear evidence** the issue is not actionable.
|
|
880
|
+
If in doubt, escalate instead.
|
|
881
|
+
|
|
882
|
+
## Question Types for Escalation
|
|
883
|
+
|
|
884
|
+
Use the appropriate type when calling \`add_question\`:
|
|
885
|
+
- \`decision\` - CTO needs to choose between options
|
|
886
|
+
- \`approval\` - CTO needs to approve a proposed action
|
|
887
|
+
- \`question\` - Seeking CTO guidance/input
|
|
888
|
+
- \`escalation\` - Raising awareness of an issue
|
|
889
|
+
|
|
890
|
+
## IMPORTANT
|
|
891
|
+
|
|
892
|
+
- Process ALL reports returned by get_reports_for_triage
|
|
893
|
+
- Always call \`start_triage\` before investigating
|
|
894
|
+
- Always call \`complete_triage\` when done
|
|
895
|
+
- Be thorough in investigation but efficient in execution
|
|
896
|
+
- When self-handling, the spawned task prompt should be detailed enough to succeed independently
|
|
897
|
+
|
|
898
|
+
## Output
|
|
899
|
+
|
|
900
|
+
After processing all reports, output a summary:
|
|
901
|
+
- How many self-handled vs escalated vs dismissed
|
|
902
|
+
- Brief description of each action taken`;
|
|
903
|
+
|
|
904
|
+
// Store prompt now that it's built
|
|
905
|
+
updateAgent(agentId, { prompt });
|
|
906
|
+
|
|
907
|
+
return new Promise((resolve, reject) => {
|
|
908
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
909
|
+
const spawnArgs = [
|
|
910
|
+
'--dangerously-skip-permissions',
|
|
911
|
+
'--mcp-config', mcpConfig,
|
|
912
|
+
'-p',
|
|
913
|
+
prompt,
|
|
914
|
+
];
|
|
915
|
+
|
|
916
|
+
// Use stdio: 'inherit' - Claude CLI requires TTY-like environment
|
|
917
|
+
// Output goes directly to parent process stdout/stderr
|
|
918
|
+
const claude = spawn('claude', [...spawnArgs, '--output-format', 'json'], {
|
|
919
|
+
cwd: PROJECT_DIR,
|
|
920
|
+
stdio: 'inherit',
|
|
921
|
+
env: buildSpawnEnv(agentId),
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
claude.on('close', (code) => {
|
|
925
|
+
resolve({ code, output: '(output sent to inherit stdio)' });
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
claude.on('error', (err) => {
|
|
929
|
+
reject(err);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// 15 minute timeout for triage
|
|
933
|
+
setTimeout(() => {
|
|
934
|
+
claude.kill();
|
|
935
|
+
reject(new Error('Report triage timed out after 15 minutes'));
|
|
936
|
+
}, 15 * 60 * 1000);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Spawn Claude for CLAUDE.md refactoring
|
|
942
|
+
*/
|
|
943
|
+
function spawnClaudeMdRefactor() {
|
|
944
|
+
// Register spawn first to get agentId for prompt embedding
|
|
945
|
+
const agentId = registerSpawn({
|
|
946
|
+
type: AGENT_TYPES.CLAUDEMD_REFACTOR,
|
|
947
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
948
|
+
description: 'Refactoring oversized CLAUDE.md',
|
|
949
|
+
prompt: '',
|
|
950
|
+
metadata: {},
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
const prompt = `[Task][claudemd-refactor][AGENT:${agentId}] You are an orchestrator performing CLAUDE.md REFACTORING.
|
|
954
|
+
|
|
955
|
+
## IMMEDIATE ACTION
|
|
956
|
+
|
|
957
|
+
Your first action MUST be to spawn the project-manager sub-agent:
|
|
958
|
+
\`\`\`
|
|
959
|
+
Task(subagent_type='project-manager', prompt='CLAUDE.md has grown beyond 25,000 characters. Refactor it by moving detailed content to sub-files in docs/ or specs/, replacing moved content with brief summaries and links. Preserve ALL information. NEVER modify anything below the CTO-PROTECTED divider.')
|
|
960
|
+
\`\`\`
|
|
961
|
+
|
|
962
|
+
The project-manager sub-agent has specialized instructions loaded from .claude/agents/project-manager.md.
|
|
963
|
+
|
|
964
|
+
## Mission
|
|
965
|
+
|
|
966
|
+
CLAUDE.md has grown beyond 25,000 characters. Your job is to carefully refactor it by:
|
|
967
|
+
1. Moving detailed content to sub-files in \`docs/\` or \`specs/\`
|
|
968
|
+
2. Replacing moved content with brief summaries and links
|
|
969
|
+
3. Preserving ALL information (nothing lost, just reorganized)
|
|
970
|
+
|
|
971
|
+
## CRITICAL RULE
|
|
972
|
+
|
|
973
|
+
There is a divider line "---" near the bottom of CLAUDE.md followed by:
|
|
974
|
+
\`\`\`
|
|
975
|
+
<!-- CTO-PROTECTED: Changes below this line require CTO approval -->
|
|
976
|
+
\`\`\`
|
|
977
|
+
|
|
978
|
+
**NEVER modify anything below that divider.** That section contains critical instructions that must remain in CLAUDE.md.
|
|
979
|
+
|
|
980
|
+
## Refactoring Strategy
|
|
981
|
+
|
|
982
|
+
1. **Read CLAUDE.md carefully** - Understand the full content
|
|
983
|
+
2. **Identify movable sections** - Look for:
|
|
984
|
+
- Detailed code examples (move to specs/reference/)
|
|
985
|
+
- Long tables (summarize, link to full version)
|
|
986
|
+
- Verbose explanations (condense, link to details)
|
|
987
|
+
3. **Create sub-files** - Use existing directories:
|
|
988
|
+
- \`specs/reference/\` for development guides
|
|
989
|
+
- \`specs/local/\` for component details
|
|
990
|
+
- \`docs/\` for general documentation
|
|
991
|
+
4. **Update CLAUDE.md** - Replace with concise summary + link
|
|
992
|
+
5. **Verify nothing lost** - All information must be preserved
|
|
993
|
+
|
|
994
|
+
## Example Refactor
|
|
995
|
+
|
|
996
|
+
Before:
|
|
997
|
+
\`\`\`markdown
|
|
998
|
+
## MCP Tools Reference
|
|
999
|
+
|
|
1000
|
+
### Core Tools
|
|
1001
|
+
- \`page_get_snapshot\` - Get page structure
|
|
1002
|
+
- \`page_click\` - Click element
|
|
1003
|
+
[... 50 more lines ...]
|
|
1004
|
+
\`\`\`
|
|
1005
|
+
|
|
1006
|
+
After:
|
|
1007
|
+
\`\`\`markdown
|
|
1008
|
+
## MCP Tools Reference
|
|
1009
|
+
|
|
1010
|
+
See [specs/reference/MCP-TOOLS.md](specs/reference/MCP-TOOLS.md) for complete tool reference.
|
|
1011
|
+
|
|
1012
|
+
Key tools: \`page_get_snapshot\`, \`page_click\`, \`mcp__todo-db__*\`, \`mcp__specs-browser__*\`
|
|
1013
|
+
\`\`\`
|
|
1014
|
+
|
|
1015
|
+
## Rate Limiting
|
|
1016
|
+
|
|
1017
|
+
- Make at most 5 file edits per run
|
|
1018
|
+
- If more refactoring needed, it will continue next hour
|
|
1019
|
+
|
|
1020
|
+
## Start Now
|
|
1021
|
+
|
|
1022
|
+
1. Read CLAUDE.md
|
|
1023
|
+
2. Identify the largest movable sections
|
|
1024
|
+
3. Create sub-files and update CLAUDE.md
|
|
1025
|
+
4. Report what you refactored via mcp__agent-reports__report_to_deputy_cto`;
|
|
1026
|
+
|
|
1027
|
+
// Store prompt now that it's built
|
|
1028
|
+
updateAgent(agentId, { prompt });
|
|
1029
|
+
|
|
1030
|
+
return new Promise((resolve, reject) => {
|
|
1031
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
1032
|
+
const spawnArgs = [
|
|
1033
|
+
'--dangerously-skip-permissions',
|
|
1034
|
+
'--mcp-config', mcpConfig,
|
|
1035
|
+
'-p',
|
|
1036
|
+
prompt,
|
|
1037
|
+
];
|
|
1038
|
+
|
|
1039
|
+
// Use stdio: 'inherit' - Claude CLI requires TTY-like environment
|
|
1040
|
+
// Output goes directly to parent process stdout/stderr
|
|
1041
|
+
const claude = spawn('claude', [...spawnArgs, '--output-format', 'json'], {
|
|
1042
|
+
cwd: PROJECT_DIR,
|
|
1043
|
+
stdio: 'inherit',
|
|
1044
|
+
env: buildSpawnEnv(agentId),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
claude.on('close', (code) => {
|
|
1048
|
+
resolve({ code, output: '(output sent to inherit stdio)' });
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
claude.on('error', (err) => {
|
|
1052
|
+
reject(err);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// 30 minute timeout
|
|
1056
|
+
setTimeout(() => {
|
|
1057
|
+
claude.kill();
|
|
1058
|
+
reject(new Error('CLAUDE.md refactor timed out after 30 minutes'));
|
|
1059
|
+
}, 30 * 60 * 1000);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Run linter and return errors if any
|
|
1065
|
+
* Returns { hasErrors: boolean, output: string }
|
|
1066
|
+
*/
|
|
1067
|
+
function runLintCheck() {
|
|
1068
|
+
try {
|
|
1069
|
+
// Run ESLint and capture output
|
|
1070
|
+
const result = execSync('npm run lint 2>&1', {
|
|
1071
|
+
cwd: PROJECT_DIR,
|
|
1072
|
+
encoding: 'utf8',
|
|
1073
|
+
timeout: 60000, // 1 minute timeout
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// If we got here without throwing, there were no errors
|
|
1077
|
+
return { hasErrors: false, output: result };
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
// ESLint exits with non-zero code when there are errors
|
|
1080
|
+
// The output is in err.stdout or err.message
|
|
1081
|
+
const output = err.stdout || err.message || 'Unknown error';
|
|
1082
|
+
|
|
1083
|
+
// Check if it's actually lint errors (not a command failure)
|
|
1084
|
+
if (output.includes('error') && !output.includes('Command failed')) {
|
|
1085
|
+
return { hasErrors: true, output: output };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Actual command failure
|
|
1089
|
+
log(`WARN: Lint check failed unexpectedly: ${output.substring(0, 200)}`);
|
|
1090
|
+
return { hasErrors: false, output: '' };
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Spawn Claude to fix lint errors
|
|
1096
|
+
*/
|
|
1097
|
+
function spawnLintFixer(lintOutput) {
|
|
1098
|
+
// Extract just the errors, not warnings
|
|
1099
|
+
const errorLines = lintOutput.split('\n')
|
|
1100
|
+
.filter(line => line.includes('error'))
|
|
1101
|
+
.slice(0, 50) // Limit to first 50 error lines
|
|
1102
|
+
.join('\n');
|
|
1103
|
+
|
|
1104
|
+
// Register spawn first to get agentId for prompt embedding
|
|
1105
|
+
const agentId = registerSpawn({
|
|
1106
|
+
type: AGENT_TYPES.LINT_FIXER,
|
|
1107
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
1108
|
+
description: 'Fixing lint errors',
|
|
1109
|
+
prompt: '',
|
|
1110
|
+
metadata: { errorCount: errorLines.split('\n').length },
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const prompt = `[Task][lint-fixer][AGENT:${agentId}] You are an orchestrator fixing LINT ERRORS.
|
|
1114
|
+
|
|
1115
|
+
## IMMEDIATE ACTION
|
|
1116
|
+
|
|
1117
|
+
Your first action MUST be to spawn the code-writer sub-agent to fix the lint errors:
|
|
1118
|
+
\`\`\`
|
|
1119
|
+
Task(subagent_type='code-writer', prompt='Fix the following ESLint lint errors. Read each file, understand the context, fix the error, then verify by re-running the linter.\\n\\nLint Errors:\\n${errorLines.replace(/`/g, '\\`').replace(/\n/g, '\\n')}')
|
|
1120
|
+
\`\`\`
|
|
1121
|
+
|
|
1122
|
+
Then after code-writer completes, spawn code-reviewer to review and commit the fixes:
|
|
1123
|
+
\`\`\`
|
|
1124
|
+
Task(subagent_type='code-reviewer', prompt='Review the lint fix changes and commit them if they look correct.')
|
|
1125
|
+
\`\`\`
|
|
1126
|
+
|
|
1127
|
+
Each sub-agent has specialized instructions loaded from .claude/agents/ configs.
|
|
1128
|
+
|
|
1129
|
+
## Mission
|
|
1130
|
+
|
|
1131
|
+
The project's ESLint linter has detected errors that need to be fixed.
|
|
1132
|
+
|
|
1133
|
+
## Lint Errors Found
|
|
1134
|
+
|
|
1135
|
+
\`\`\`
|
|
1136
|
+
${errorLines}
|
|
1137
|
+
\`\`\`
|
|
1138
|
+
|
|
1139
|
+
## Process
|
|
1140
|
+
|
|
1141
|
+
1. **Spawn code-writer** to fix the lint errors
|
|
1142
|
+
2. **Spawn code-reviewer** to review and commit the fixes
|
|
1143
|
+
|
|
1144
|
+
## Constraints
|
|
1145
|
+
|
|
1146
|
+
- Make at most 20 file edits per run
|
|
1147
|
+
- If more fixes are needed, they will continue next hour
|
|
1148
|
+
- Focus on errors only - warnings can be ignored
|
|
1149
|
+
|
|
1150
|
+
## When Done
|
|
1151
|
+
|
|
1152
|
+
Report completion via mcp__agent-reports__report_to_deputy_cto with a summary of what was fixed.`;
|
|
1153
|
+
|
|
1154
|
+
// Store prompt now that it's built
|
|
1155
|
+
updateAgent(agentId, { prompt });
|
|
1156
|
+
|
|
1157
|
+
return new Promise((resolve, reject) => {
|
|
1158
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
1159
|
+
const spawnArgs = [
|
|
1160
|
+
'--dangerously-skip-permissions',
|
|
1161
|
+
'--mcp-config', mcpConfig,
|
|
1162
|
+
'-p',
|
|
1163
|
+
prompt,
|
|
1164
|
+
];
|
|
1165
|
+
|
|
1166
|
+
// Use stdio: 'inherit' - Claude CLI requires TTY-like environment
|
|
1167
|
+
const claude = spawn('claude', [...spawnArgs, '--output-format', 'json'], {
|
|
1168
|
+
cwd: PROJECT_DIR,
|
|
1169
|
+
stdio: 'inherit',
|
|
1170
|
+
env: buildSpawnEnv(agentId),
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
claude.on('close', (code) => {
|
|
1174
|
+
resolve({ code, output: '(output sent to inherit stdio)' });
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
claude.on('error', (err) => {
|
|
1178
|
+
reject(err);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// 20 minute timeout for lint fixing
|
|
1182
|
+
setTimeout(() => {
|
|
1183
|
+
claude.kill();
|
|
1184
|
+
reject(new Error('Lint fixer timed out after 20 minutes'));
|
|
1185
|
+
}, 20 * 60 * 1000);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// =========================================================================
|
|
1190
|
+
// TASK RUNNER HELPERS
|
|
1191
|
+
// =========================================================================
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Query todo.db for ALL pending tasks older than 1 hour.
|
|
1195
|
+
* Each task gets its own Claude session. No section limits.
|
|
1196
|
+
*/
|
|
1197
|
+
function getPendingTasksForRunner() {
|
|
1198
|
+
if (!Database || !fs.existsSync(TODO_DB_PATH)) {
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
try {
|
|
1203
|
+
const db = new Database(TODO_DB_PATH, { readonly: true });
|
|
1204
|
+
const nowTimestamp = Math.floor(Date.now() / 1000);
|
|
1205
|
+
const oneHourAgo = nowTimestamp - 3600;
|
|
1206
|
+
|
|
1207
|
+
const candidates = db.prepare(`
|
|
1208
|
+
SELECT id, section, title, description
|
|
1209
|
+
FROM tasks
|
|
1210
|
+
WHERE status = 'pending'
|
|
1211
|
+
AND section IN (${Object.keys(SECTION_AGENT_MAP).map(() => '?').join(',')})
|
|
1212
|
+
AND created_timestamp <= ?
|
|
1213
|
+
ORDER BY created_timestamp ASC
|
|
1214
|
+
`).all(...Object.keys(SECTION_AGENT_MAP), oneHourAgo);
|
|
1215
|
+
|
|
1216
|
+
db.close();
|
|
1217
|
+
return candidates;
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
log(`Task runner: DB query error: ${err.message}`);
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Query todo.db for pending tasks with priority = 'urgent'.
|
|
1226
|
+
* No age filter, no batch limit — urgent tasks are dispatched immediately.
|
|
1227
|
+
*/
|
|
1228
|
+
function getUrgentPendingTasks() {
|
|
1229
|
+
if (!Database || !fs.existsSync(TODO_DB_PATH)) return [];
|
|
1230
|
+
|
|
1231
|
+
try {
|
|
1232
|
+
const db = new Database(TODO_DB_PATH, { readonly: true });
|
|
1233
|
+
const candidates = db.prepare(`
|
|
1234
|
+
SELECT id, section, title, description
|
|
1235
|
+
FROM tasks
|
|
1236
|
+
WHERE status = 'pending'
|
|
1237
|
+
AND priority = 'urgent'
|
|
1238
|
+
AND section IN (${Object.keys(SECTION_AGENT_MAP).map(() => '?').join(',')})
|
|
1239
|
+
ORDER BY created_timestamp ASC
|
|
1240
|
+
`).all(...Object.keys(SECTION_AGENT_MAP));
|
|
1241
|
+
db.close();
|
|
1242
|
+
return candidates;
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
log(`Urgent dispatcher: DB query error: ${err.message}`);
|
|
1245
|
+
return [];
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Mark a task as in_progress before spawning the agent
|
|
1251
|
+
*/
|
|
1252
|
+
function markTaskInProgress(taskId) {
|
|
1253
|
+
if (!Database || !fs.existsSync(TODO_DB_PATH)) return false;
|
|
1254
|
+
|
|
1255
|
+
try {
|
|
1256
|
+
const db = new Database(TODO_DB_PATH);
|
|
1257
|
+
const now = new Date().toISOString();
|
|
1258
|
+
db.prepare(
|
|
1259
|
+
"UPDATE tasks SET status = 'in_progress', started_at = ? WHERE id = ?"
|
|
1260
|
+
).run(now, taskId);
|
|
1261
|
+
db.close();
|
|
1262
|
+
return true;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
log(`Task runner: Failed to mark task ${taskId} in_progress: ${err.message}`);
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Reset a task back to pending on spawn failure
|
|
1271
|
+
*/
|
|
1272
|
+
function resetTaskToPending(taskId) {
|
|
1273
|
+
if (!Database || !fs.existsSync(TODO_DB_PATH)) return;
|
|
1274
|
+
|
|
1275
|
+
try {
|
|
1276
|
+
const db = new Database(TODO_DB_PATH);
|
|
1277
|
+
db.prepare(
|
|
1278
|
+
"UPDATE tasks SET status = 'pending', started_at = NULL WHERE id = ?"
|
|
1279
|
+
).run(taskId);
|
|
1280
|
+
db.close();
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
log(`Task runner: Failed to reset task ${taskId}: ${err.message}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Build the prompt for a deputy-cto task orchestrator agent
|
|
1288
|
+
*/
|
|
1289
|
+
function buildDeputyCtoTaskPrompt(task, agentId) {
|
|
1290
|
+
return `[Task][task-runner-deputy-cto][AGENT:${agentId}] You are the Deputy-CTO processing a high-level task assignment.
|
|
1291
|
+
|
|
1292
|
+
## Task Details
|
|
1293
|
+
|
|
1294
|
+
- **Task ID**: ${task.id}
|
|
1295
|
+
- **Section**: ${task.section}
|
|
1296
|
+
- **Title**: ${task.title}
|
|
1297
|
+
${task.description ? `- **Description**: ${task.description}` : ''}
|
|
1298
|
+
|
|
1299
|
+
## Your Mission
|
|
1300
|
+
|
|
1301
|
+
You are an ORCHESTRATOR. You do NOT implement tasks yourself — you evaluate, decompose, and delegate.
|
|
1302
|
+
|
|
1303
|
+
## Process (FOLLOW THIS ORDER)
|
|
1304
|
+
|
|
1305
|
+
### Step 1: Evaluate Alignment
|
|
1306
|
+
Before doing anything, evaluate whether this task aligns with:
|
|
1307
|
+
- The project's specs (read specs/global/ and specs/local/ as needed)
|
|
1308
|
+
- Existing plans (check plans/ directory)
|
|
1309
|
+
- CTO directives (check mcp__deputy-cto__list_questions for relevant decisions)
|
|
1310
|
+
|
|
1311
|
+
If the task does NOT align with specs, plans, or CTO requests:
|
|
1312
|
+
- Report the misalignment via mcp__agent-reports__report_to_deputy_cto
|
|
1313
|
+
- Mark this task complete WITHOUT creating sub-tasks
|
|
1314
|
+
- Explain in the completion why you declined
|
|
1315
|
+
|
|
1316
|
+
### Step 2: Create Investigator Task FIRST
|
|
1317
|
+
Always start by creating an urgent investigator task:
|
|
1318
|
+
\`\`\`
|
|
1319
|
+
mcp__todo-db__create_task({
|
|
1320
|
+
section: "INVESTIGATOR & PLANNER",
|
|
1321
|
+
title: "Investigate: ${task.title}",
|
|
1322
|
+
description: "You are the INVESTIGATOR. Analyze the following task and create a detailed implementation plan with specific sub-tasks:\\n\\nTask: ${task.title}\\n${task.description || ''}\\n\\nInvestigate the codebase, read relevant specs, and create TODO items in the appropriate sections via mcp__todo-db__create_task for each sub-task you identify.",
|
|
1323
|
+
assigned_by: "deputy-cto",
|
|
1324
|
+
priority: "urgent"
|
|
1325
|
+
})
|
|
1326
|
+
\`\`\`
|
|
1327
|
+
|
|
1328
|
+
### Step 3: Create Implementation Sub-Tasks
|
|
1329
|
+
Based on your own analysis (don't wait for the investigator — it runs async), create concrete sub-tasks:
|
|
1330
|
+
|
|
1331
|
+
For non-urgent work (picked up by hourly automation):
|
|
1332
|
+
\`\`\`
|
|
1333
|
+
mcp__todo-db__create_task({
|
|
1334
|
+
section: "INVESTIGATOR & PLANNER", // or CODE-REVIEWER, TEST-WRITER, PROJECT-MANAGER
|
|
1335
|
+
title: "Specific actionable task title",
|
|
1336
|
+
description: "Detailed context and acceptance criteria",
|
|
1337
|
+
assigned_by: "deputy-cto"
|
|
1338
|
+
})
|
|
1339
|
+
\`\`\`
|
|
1340
|
+
|
|
1341
|
+
Section mapping:
|
|
1342
|
+
- Code changes (triggers full agent sequence: investigator → code-writer → test-writer → code-reviewer → project-manager) → CODE-REVIEWER
|
|
1343
|
+
- Research, analysis, planning only → INVESTIGATOR & PLANNER
|
|
1344
|
+
- Test creation/updates only → TEST-WRITER
|
|
1345
|
+
- Documentation, cleanup only → PROJECT-MANAGER
|
|
1346
|
+
|
|
1347
|
+
### Step 4: Mark Complete
|
|
1348
|
+
After all sub-tasks are created:
|
|
1349
|
+
\`\`\`
|
|
1350
|
+
mcp__todo-db__complete_task({ id: "${task.id}" })
|
|
1351
|
+
\`\`\`
|
|
1352
|
+
This will automatically create a follow-up verification task.
|
|
1353
|
+
|
|
1354
|
+
## Constraints
|
|
1355
|
+
|
|
1356
|
+
- Do NOT write code yourself (you have no Edit/Write/Bash tools)
|
|
1357
|
+
- Create 3-8 specific sub-tasks per high-level task
|
|
1358
|
+
- Each sub-task must be self-contained with enough context to execute independently
|
|
1359
|
+
- Only delegate tasks that align with project specs and plans
|
|
1360
|
+
- Report blockers via mcp__agent-reports__report_to_deputy_cto
|
|
1361
|
+
- If the task needs CTO input, create a question via mcp__deputy-cto__add_question`;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Build the prompt for a task runner agent
|
|
1366
|
+
*/
|
|
1367
|
+
function buildTaskRunnerPrompt(task, agentName, agentId, worktreePath = null) {
|
|
1368
|
+
const taskDetails = `[Task][task-runner-${agentName}][AGENT:${agentId}] You are an orchestrator processing a TODO task.
|
|
1369
|
+
|
|
1370
|
+
## Task Details
|
|
1371
|
+
|
|
1372
|
+
- **Task ID**: ${task.id}
|
|
1373
|
+
- **Section**: ${task.section}
|
|
1374
|
+
- **Title**: ${task.title}
|
|
1375
|
+
${task.description ? `- **Description**: ${task.description}` : ''}`;
|
|
1376
|
+
|
|
1377
|
+
// Git workflow block for worktree-based agents
|
|
1378
|
+
const gitWorkflowBlock = worktreePath ? `
|
|
1379
|
+
## Git Workflow
|
|
1380
|
+
|
|
1381
|
+
You are working in a git worktree on a feature branch.
|
|
1382
|
+
Your working directory: ${worktreePath}
|
|
1383
|
+
MCP tools access shared state in the main project directory.
|
|
1384
|
+
|
|
1385
|
+
When your work is complete:
|
|
1386
|
+
1. \`git add <specific files>\` (never \`git add .\` or \`git add -A\`)
|
|
1387
|
+
2. \`git commit -m "descriptive message"\`
|
|
1388
|
+
3. \`git push -u origin HEAD\`
|
|
1389
|
+
4. Create a PR to preview:
|
|
1390
|
+
\`\`\`
|
|
1391
|
+
gh pr create --base preview --head "$(git branch --show-current)" --title "${task.title}" --body "Automated: ${task.section} task"
|
|
1392
|
+
\`\`\`
|
|
1393
|
+
5. After CI passes: \`gh pr merge --merge --delete-branch\`
|
|
1394
|
+
` : '';
|
|
1395
|
+
|
|
1396
|
+
const completionBlock = `## When Done
|
|
1397
|
+
|
|
1398
|
+
You MUST call this MCP tool to mark the task as completed:
|
|
1399
|
+
|
|
1400
|
+
\`\`\`
|
|
1401
|
+
mcp__todo-db__complete_task({ id: "${task.id}" })
|
|
1402
|
+
\`\`\`
|
|
1403
|
+
${gitWorkflowBlock}
|
|
1404
|
+
## Constraints
|
|
1405
|
+
|
|
1406
|
+
- Focus only on this specific task
|
|
1407
|
+
- Do not create new tasks unless absolutely necessary
|
|
1408
|
+
- Report any issues via mcp__agent-reports__report_to_deputy_cto`;
|
|
1409
|
+
|
|
1410
|
+
// Section-specific workflow instructions
|
|
1411
|
+
if (task.section === 'CODE-REVIEWER') {
|
|
1412
|
+
return `${taskDetails}
|
|
1413
|
+
|
|
1414
|
+
## MANDATORY SUB-AGENT WORKFLOW
|
|
1415
|
+
|
|
1416
|
+
You are an ORCHESTRATOR. Do NOT edit files directly. Follow this sequence using the Task tool:
|
|
1417
|
+
|
|
1418
|
+
1. \`Task(subagent_type='investigator')\` - Research the task, understand the codebase
|
|
1419
|
+
2. \`Task(subagent_type='code-writer')\` - Implement the changes
|
|
1420
|
+
3. \`Task(subagent_type='test-writer')\` - Add/update tests
|
|
1421
|
+
4. \`Task(subagent_type='code-reviewer')\` - Review changes, commit
|
|
1422
|
+
5. \`Task(subagent_type='project-manager')\` - Sync documentation (ALWAYS LAST)
|
|
1423
|
+
|
|
1424
|
+
Pass the full task context to each sub-agent. Each sub-agent has specialized
|
|
1425
|
+
instructions loaded from .claude/agents/ configs.
|
|
1426
|
+
|
|
1427
|
+
**YOU ARE PROHIBITED FROM:**
|
|
1428
|
+
- Directly editing ANY files using Edit, Write, or NotebookEdit tools
|
|
1429
|
+
- Making code changes without the code-writer sub-agent
|
|
1430
|
+
- Making test changes without the test-writer sub-agent
|
|
1431
|
+
- Skipping investigation before implementation
|
|
1432
|
+
- Skipping code-reviewer after any code/test changes
|
|
1433
|
+
- Skipping project-manager at the end
|
|
1434
|
+
|
|
1435
|
+
${completionBlock}`;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (task.section === 'INVESTIGATOR & PLANNER') {
|
|
1439
|
+
return `${taskDetails}
|
|
1440
|
+
|
|
1441
|
+
## IMMEDIATE ACTION
|
|
1442
|
+
|
|
1443
|
+
Your first action MUST be:
|
|
1444
|
+
\`\`\`
|
|
1445
|
+
Task(subagent_type='investigator', prompt='${task.title}. ${task.description || ''}')
|
|
1446
|
+
\`\`\`
|
|
1447
|
+
|
|
1448
|
+
The investigator sub-agent has specialized instructions loaded from .claude/agents/investigator.md.
|
|
1449
|
+
Pass the full task context including title and description.
|
|
1450
|
+
|
|
1451
|
+
${completionBlock}`;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (task.section === 'TEST-WRITER') {
|
|
1455
|
+
return `${taskDetails}
|
|
1456
|
+
|
|
1457
|
+
## IMMEDIATE ACTION
|
|
1458
|
+
|
|
1459
|
+
Your first action MUST be:
|
|
1460
|
+
\`\`\`
|
|
1461
|
+
Task(subagent_type='test-writer', prompt='${task.title}. ${task.description || ''}')
|
|
1462
|
+
\`\`\`
|
|
1463
|
+
|
|
1464
|
+
Then after test-writer completes:
|
|
1465
|
+
\`\`\`
|
|
1466
|
+
Task(subagent_type='code-reviewer', prompt='Review the test changes from the previous step')
|
|
1467
|
+
\`\`\`
|
|
1468
|
+
|
|
1469
|
+
Each sub-agent has specialized instructions loaded from .claude/agents/ configs.
|
|
1470
|
+
|
|
1471
|
+
${completionBlock}`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (task.section === 'PROJECT-MANAGER') {
|
|
1475
|
+
return `${taskDetails}
|
|
1476
|
+
|
|
1477
|
+
## IMMEDIATE ACTION
|
|
1478
|
+
|
|
1479
|
+
Your first action MUST be:
|
|
1480
|
+
\`\`\`
|
|
1481
|
+
Task(subagent_type='project-manager', prompt='${task.title}. ${task.description || ''}')
|
|
1482
|
+
\`\`\`
|
|
1483
|
+
|
|
1484
|
+
The project-manager sub-agent has specialized instructions loaded from .claude/agents/project-manager.md.
|
|
1485
|
+
Pass the full task context including title and description.
|
|
1486
|
+
|
|
1487
|
+
${completionBlock}`;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Fallback for any other section
|
|
1491
|
+
return `${taskDetails}
|
|
1492
|
+
|
|
1493
|
+
## Your Role
|
|
1494
|
+
|
|
1495
|
+
You are the \`${agentName}\` agent. Complete the task described above using your expertise.
|
|
1496
|
+
Use the Task tool to spawn the appropriate sub-agent: \`Task(subagent_type='${agentName}')\`
|
|
1497
|
+
|
|
1498
|
+
${completionBlock}`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Spawn a fire-and-forget Claude agent for a task.
|
|
1503
|
+
* When worktrees are available (preview branch exists), each agent gets its
|
|
1504
|
+
* own isolated worktree on a feature branch. Falls back to PROJECT_DIR if
|
|
1505
|
+
* worktree creation fails.
|
|
1506
|
+
*/
|
|
1507
|
+
function spawnTaskAgent(task) {
|
|
1508
|
+
const mapping = SECTION_AGENT_MAP[task.section];
|
|
1509
|
+
if (!mapping) return false;
|
|
1510
|
+
|
|
1511
|
+
// --- Worktree setup (best-effort) ---
|
|
1512
|
+
let agentCwd = PROJECT_DIR;
|
|
1513
|
+
let agentMcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
1514
|
+
let worktreePath = null;
|
|
1515
|
+
|
|
1516
|
+
try {
|
|
1517
|
+
const branchName = getFeatureBranchName(task.title, task.id);
|
|
1518
|
+
const worktree = createWorktree(branchName);
|
|
1519
|
+
worktreePath = worktree.path;
|
|
1520
|
+
agentCwd = worktree.path;
|
|
1521
|
+
agentMcpConfig = path.join(worktree.path, '.mcp.json');
|
|
1522
|
+
log(`Task runner: worktree ready at ${worktree.path} (branch ${branchName}, created=${worktree.created})`);
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
log(`Task runner: worktree creation failed, falling back to PROJECT_DIR: ${err.message}`);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Register first to get agentId for prompt embedding
|
|
1528
|
+
const agentId = registerSpawn({
|
|
1529
|
+
type: mapping.agentType,
|
|
1530
|
+
hookType: HOOK_TYPES.TASK_RUNNER,
|
|
1531
|
+
description: `Task runner: ${mapping.agent} - ${task.title}`,
|
|
1532
|
+
prompt: '',
|
|
1533
|
+
metadata: { taskId: task.id, section: task.section, worktreePath },
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
const prompt = mapping.agent === 'deputy-cto'
|
|
1537
|
+
? buildDeputyCtoTaskPrompt(task, agentId)
|
|
1538
|
+
: buildTaskRunnerPrompt(task, mapping.agent, agentId, worktreePath);
|
|
1539
|
+
|
|
1540
|
+
// Store prompt now that it's built
|
|
1541
|
+
updateAgent(agentId, { prompt });
|
|
1542
|
+
|
|
1543
|
+
try {
|
|
1544
|
+
const claude = spawn('claude', [
|
|
1545
|
+
'--dangerously-skip-permissions',
|
|
1546
|
+
'--mcp-config', agentMcpConfig,
|
|
1547
|
+
'--output-format', 'json',
|
|
1548
|
+
'-p',
|
|
1549
|
+
prompt,
|
|
1550
|
+
], {
|
|
1551
|
+
detached: true,
|
|
1552
|
+
stdio: 'ignore',
|
|
1553
|
+
cwd: agentCwd,
|
|
1554
|
+
env: {
|
|
1555
|
+
...buildSpawnEnv(agentId),
|
|
1556
|
+
CLAUDE_PROJECT_DIR: PROJECT_DIR, // State files always in main project
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
claude.unref();
|
|
1561
|
+
|
|
1562
|
+
// Store PID for reaper tracking
|
|
1563
|
+
updateAgent(agentId, { pid: claude.pid, status: 'running' });
|
|
1564
|
+
|
|
1565
|
+
return true;
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
log(`Task runner: Failed to spawn ${mapping.agent} for task ${task.id}: ${err.message}`);
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// =========================================================================
|
|
1573
|
+
// PROMOTION & HEALTH MONITOR SPAWN FUNCTIONS
|
|
1574
|
+
// =========================================================================
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Check if a git branch exists on the remote
|
|
1578
|
+
*/
|
|
1579
|
+
function remoteBranchExists(branch) {
|
|
1580
|
+
try {
|
|
1581
|
+
execSync(`git rev-parse --verify origin/${branch}`, {
|
|
1582
|
+
cwd: PROJECT_DIR,
|
|
1583
|
+
encoding: 'utf8',
|
|
1584
|
+
timeout: 10000,
|
|
1585
|
+
stdio: 'pipe',
|
|
1586
|
+
});
|
|
1587
|
+
return true;
|
|
1588
|
+
} catch {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Get commits on source not yet in target
|
|
1595
|
+
*/
|
|
1596
|
+
function getNewCommits(source, target) {
|
|
1597
|
+
try {
|
|
1598
|
+
const result = execSync(`git log origin/${target}..origin/${source} --oneline`, {
|
|
1599
|
+
cwd: PROJECT_DIR,
|
|
1600
|
+
encoding: 'utf8',
|
|
1601
|
+
timeout: 10000,
|
|
1602
|
+
stdio: 'pipe',
|
|
1603
|
+
}).trim();
|
|
1604
|
+
return result ? result.split('\n') : [];
|
|
1605
|
+
} catch {
|
|
1606
|
+
return [];
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Get Unix timestamp of last commit on a branch
|
|
1612
|
+
*/
|
|
1613
|
+
function getLastCommitTimestamp(branch) {
|
|
1614
|
+
try {
|
|
1615
|
+
const result = execSync(`git log origin/${branch} -1 --format=%ct`, {
|
|
1616
|
+
cwd: PROJECT_DIR,
|
|
1617
|
+
encoding: 'utf8',
|
|
1618
|
+
timeout: 10000,
|
|
1619
|
+
stdio: 'pipe',
|
|
1620
|
+
}).trim();
|
|
1621
|
+
return parseInt(result, 10) || 0;
|
|
1622
|
+
} catch {
|
|
1623
|
+
return 0;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Check if any commit messages contain bug-fix keywords
|
|
1629
|
+
*/
|
|
1630
|
+
function hasBugFixCommits(commits) {
|
|
1631
|
+
const bugFixPattern = /\b(fix|bug|hotfix|patch|critical)\b/i;
|
|
1632
|
+
return commits.some(line => bugFixPattern.test(line));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Create or reuse a worktree for promotion agents.
|
|
1637
|
+
* Uses deterministic branch names so worktrees persist across cycles.
|
|
1638
|
+
* Falls back to PROJECT_DIR on failure (matches task runner pattern).
|
|
1639
|
+
*/
|
|
1640
|
+
function getPromotionWorktree(promotionType) {
|
|
1641
|
+
const branchName = `automation/${promotionType}`;
|
|
1642
|
+
const baseBranch = promotionType === 'preview-promotion' ? 'preview' : 'staging';
|
|
1643
|
+
try {
|
|
1644
|
+
const worktree = createWorktree(branchName, baseBranch);
|
|
1645
|
+
if (!worktree.created) {
|
|
1646
|
+
// Worktree exists, pull latest
|
|
1647
|
+
try {
|
|
1648
|
+
execSync('git pull --ff-only', { cwd: worktree.path, encoding: 'utf8', timeout: 30000, stdio: 'pipe' });
|
|
1649
|
+
} catch { /* non-fatal */ }
|
|
1650
|
+
}
|
|
1651
|
+
return { cwd: worktree.path, mcpConfig: path.join(worktree.path, '.mcp.json') };
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
log(`Promotion worktree creation failed for ${promotionType}, falling back to PROJECT_DIR: ${err.message}`);
|
|
1654
|
+
return { cwd: PROJECT_DIR, mcpConfig: path.join(PROJECT_DIR, '.mcp.json') };
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Spawn Preview -> Staging promotion orchestrator
|
|
1660
|
+
*/
|
|
1661
|
+
function spawnPreviewPromotion(newCommits, hoursSinceLastStagingMerge, hasBugFix) {
|
|
1662
|
+
const commitList = newCommits.join('\n');
|
|
1663
|
+
|
|
1664
|
+
const agentId = registerSpawn({
|
|
1665
|
+
type: AGENT_TYPES.PREVIEW_PROMOTION,
|
|
1666
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
1667
|
+
description: 'Preview -> Staging promotion pipeline',
|
|
1668
|
+
prompt: '',
|
|
1669
|
+
metadata: { commitCount: newCommits.length, hoursSinceLastStagingMerge, hasBugFix },
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
const prompt = `[Task][preview-promotion][AGENT:${agentId}] You are the PREVIEW -> STAGING Promotion Pipeline orchestrator.
|
|
1673
|
+
|
|
1674
|
+
## Mission
|
|
1675
|
+
|
|
1676
|
+
Evaluate whether commits on the \`preview\` branch are ready to be promoted to \`staging\`.
|
|
1677
|
+
|
|
1678
|
+
## Context
|
|
1679
|
+
|
|
1680
|
+
**New commits on preview (not in staging):**
|
|
1681
|
+
\`\`\`
|
|
1682
|
+
${commitList}
|
|
1683
|
+
\`\`\`
|
|
1684
|
+
|
|
1685
|
+
**Hours since last staging merge:** ${hoursSinceLastStagingMerge}
|
|
1686
|
+
**Bug-fix commits detected:** ${hasBugFix ? 'YES (24h waiting period bypassed)' : 'No'}
|
|
1687
|
+
|
|
1688
|
+
## Process
|
|
1689
|
+
|
|
1690
|
+
### Step 1: Code Review
|
|
1691
|
+
|
|
1692
|
+
Spawn a code-reviewer sub-agent (Task tool, subagent_type: code-reviewer) to review the commits:
|
|
1693
|
+
- Check for security issues, code quality, spec violations
|
|
1694
|
+
- Look for disabled tests, placeholder code, hardcoded credentials
|
|
1695
|
+
- Verify no spec violations (G001-G019)
|
|
1696
|
+
|
|
1697
|
+
### Step 2: Test Assessment
|
|
1698
|
+
|
|
1699
|
+
Spawn a test-writer sub-agent (Task tool, subagent_type: test-writer) to assess test quality:
|
|
1700
|
+
- Check if new code has adequate test coverage
|
|
1701
|
+
- Verify no tests were disabled or weakened
|
|
1702
|
+
|
|
1703
|
+
### Step 3: Evaluate Results
|
|
1704
|
+
|
|
1705
|
+
If EITHER agent reports issues:
|
|
1706
|
+
- Report findings via mcp__cto-reports__report_to_cto with category "decision", priority "normal"
|
|
1707
|
+
- Create TODO tasks for fixes
|
|
1708
|
+
- Do NOT proceed with promotion
|
|
1709
|
+
- Output: "Promotion blocked: [reasons]"
|
|
1710
|
+
|
|
1711
|
+
### Step 4: Deputy-CTO Decision
|
|
1712
|
+
|
|
1713
|
+
If both agents pass, spawn a deputy-cto sub-agent (Task tool, subagent_type: deputy-cto) with:
|
|
1714
|
+
- The review results from both agents
|
|
1715
|
+
- The commit list
|
|
1716
|
+
- Request: Evaluate stability and decide whether to promote
|
|
1717
|
+
|
|
1718
|
+
The deputy-cto should:
|
|
1719
|
+
- **If approving**: Report approval via \`mcp__cto-reports__report_to_cto\` with category "decision", summary "Preview promotion approved"
|
|
1720
|
+
- **If rejecting**: Report issues via \`mcp__cto-reports__report_to_cto\`, create TODO tasks for fixes
|
|
1721
|
+
|
|
1722
|
+
### Step 5: Execute Promotion (after deputy-cto approves)
|
|
1723
|
+
|
|
1724
|
+
If the deputy-cto approved, execute the promotion yourself:
|
|
1725
|
+
1. Run: \`gh pr create --base staging --head preview --title "Promote preview to staging" --body "Automated promotion. Commits: ${newCommits.length} new commits. Reviewed by code-reviewer and test-writer agents."\`
|
|
1726
|
+
2. Wait for CI: \`gh pr checks <number> --watch\`
|
|
1727
|
+
3. If CI passes: \`gh pr merge <number> --merge\`
|
|
1728
|
+
4. If CI fails: Report failure via \`mcp__cto-reports__report_to_cto\`
|
|
1729
|
+
|
|
1730
|
+
## Timeout
|
|
1731
|
+
|
|
1732
|
+
Complete within 25 minutes. If blocked, report and exit.
|
|
1733
|
+
|
|
1734
|
+
## Output
|
|
1735
|
+
|
|
1736
|
+
Summarize the promotion decision and actions taken.`;
|
|
1737
|
+
|
|
1738
|
+
// Store prompt now that it's built
|
|
1739
|
+
updateAgent(agentId, { prompt });
|
|
1740
|
+
|
|
1741
|
+
try {
|
|
1742
|
+
const wt = getPromotionWorktree('preview-promotion');
|
|
1743
|
+
const claude = spawn('claude', [
|
|
1744
|
+
'--dangerously-skip-permissions',
|
|
1745
|
+
'--mcp-config', wt.mcpConfig,
|
|
1746
|
+
'--output-format', 'json',
|
|
1747
|
+
'-p',
|
|
1748
|
+
prompt,
|
|
1749
|
+
], {
|
|
1750
|
+
cwd: wt.cwd,
|
|
1751
|
+
stdio: 'inherit',
|
|
1752
|
+
env: {
|
|
1753
|
+
...buildSpawnEnv(agentId),
|
|
1754
|
+
CLAUDE_PROJECT_DIR: PROJECT_DIR,
|
|
1755
|
+
GENTYR_PROMOTION_PIPELINE: 'true',
|
|
1756
|
+
},
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
return new Promise((resolve, reject) => {
|
|
1760
|
+
claude.on('close', (code) => {
|
|
1761
|
+
resolve({ code, output: '(output sent to inherit stdio)' });
|
|
1762
|
+
});
|
|
1763
|
+
claude.on('error', (err) => reject(err));
|
|
1764
|
+
setTimeout(() => {
|
|
1765
|
+
claude.kill();
|
|
1766
|
+
reject(new Error('Preview promotion timed out after 30 minutes'));
|
|
1767
|
+
}, 30 * 60 * 1000);
|
|
1768
|
+
});
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
log(`Preview promotion spawn error: ${err.message}`);
|
|
1771
|
+
return Promise.resolve({ code: 1, output: err.message });
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* Spawn Staging -> Production promotion orchestrator
|
|
1777
|
+
*/
|
|
1778
|
+
function spawnStagingPromotion(newCommits, hoursSinceLastStagingCommit) {
|
|
1779
|
+
const commitList = newCommits.join('\n');
|
|
1780
|
+
|
|
1781
|
+
const agentId = registerSpawn({
|
|
1782
|
+
type: AGENT_TYPES.STAGING_PROMOTION,
|
|
1783
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
1784
|
+
description: 'Staging -> Production promotion pipeline',
|
|
1785
|
+
prompt: '',
|
|
1786
|
+
metadata: { commitCount: newCommits.length, hoursSinceLastStagingCommit },
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
const prompt = `[Task][staging-promotion][AGENT:${agentId}] You are the STAGING -> PRODUCTION Promotion Pipeline orchestrator.
|
|
1790
|
+
|
|
1791
|
+
## Mission
|
|
1792
|
+
|
|
1793
|
+
Evaluate whether commits on the \`staging\` branch are ready to be promoted to \`main\` (production).
|
|
1794
|
+
|
|
1795
|
+
## Context
|
|
1796
|
+
|
|
1797
|
+
**New commits on staging (not in main):**
|
|
1798
|
+
\`\`\`
|
|
1799
|
+
${commitList}
|
|
1800
|
+
\`\`\`
|
|
1801
|
+
|
|
1802
|
+
**Hours since last staging commit:** ${hoursSinceLastStagingCommit} (must be >= 24 for stability)
|
|
1803
|
+
|
|
1804
|
+
## Process
|
|
1805
|
+
|
|
1806
|
+
### Step 1: Code Review
|
|
1807
|
+
|
|
1808
|
+
Spawn a code-reviewer sub-agent (Task tool, subagent_type: code-reviewer) to review all staging commits:
|
|
1809
|
+
- Full security audit
|
|
1810
|
+
- Spec compliance check (G001-G019)
|
|
1811
|
+
- No placeholder code, disabled tests, or hardcoded credentials
|
|
1812
|
+
|
|
1813
|
+
### Step 2: Test Assessment
|
|
1814
|
+
|
|
1815
|
+
Spawn a test-writer sub-agent (Task tool, subagent_type: test-writer) to assess:
|
|
1816
|
+
- Test coverage meets thresholds (80% global, 100% critical paths)
|
|
1817
|
+
- No tests disabled or weakened
|
|
1818
|
+
|
|
1819
|
+
### Step 3: Evaluate Results
|
|
1820
|
+
|
|
1821
|
+
If EITHER agent reports issues:
|
|
1822
|
+
- Report via mcp__cto-reports__report_to_cto with priority "high"
|
|
1823
|
+
- Create TODO tasks for fixes
|
|
1824
|
+
- Do NOT proceed with promotion
|
|
1825
|
+
- Output: "Production promotion blocked: [reasons]"
|
|
1826
|
+
|
|
1827
|
+
### Step 4: Deputy-CTO Decision
|
|
1828
|
+
|
|
1829
|
+
If both agents pass, spawn a deputy-cto sub-agent (Task tool, subagent_type: deputy-cto) with:
|
|
1830
|
+
- The review results from both agents
|
|
1831
|
+
- The commit list
|
|
1832
|
+
- Request: Create the production release PR and CTO decision task
|
|
1833
|
+
|
|
1834
|
+
The deputy-cto should:
|
|
1835
|
+
1. Call \`mcp__deputy-cto__add_question\` with:
|
|
1836
|
+
- type: "approval"
|
|
1837
|
+
- title: "Production Release: Merge staging -> main (${newCommits.length} commits)"
|
|
1838
|
+
- description: Include review results, commit list, stability assessment
|
|
1839
|
+
- suggested_options: ["Approve merge to production", "Reject - needs more work"]
|
|
1840
|
+
|
|
1841
|
+
2. Report via mcp__cto-reports__report_to_cto
|
|
1842
|
+
|
|
1843
|
+
### Step 5: Create Production PR (after deputy-cto approves)
|
|
1844
|
+
|
|
1845
|
+
If the deputy-cto approved, create the PR yourself:
|
|
1846
|
+
1. Run: \`gh pr create --base main --head staging --title "Production Release: ${newCommits.length} commits" --body "Automated production promotion. Staging stable for ${hoursSinceLastStagingCommit}h. Reviewed by code-reviewer and test-writer."\`
|
|
1847
|
+
Do NOT merge — CTO approval required via /deputy-cto.
|
|
1848
|
+
|
|
1849
|
+
**CTO approval**: When CTO approves via /deputy-cto, an urgent merge task is created:
|
|
1850
|
+
\`\`\`
|
|
1851
|
+
mcp__todo-db__create_task({
|
|
1852
|
+
section: "CODE-REVIEWER",
|
|
1853
|
+
title: "Merge production release PR #<number>",
|
|
1854
|
+
description: "CTO approved. Run: gh pr merge <number> --merge",
|
|
1855
|
+
assigned_by: "deputy-cto",
|
|
1856
|
+
priority: "urgent"
|
|
1857
|
+
})
|
|
1858
|
+
\`\`\`
|
|
1859
|
+
|
|
1860
|
+
## Timeout
|
|
1861
|
+
|
|
1862
|
+
Complete within 25 minutes. If blocked, report and exit.
|
|
1863
|
+
|
|
1864
|
+
## Output
|
|
1865
|
+
|
|
1866
|
+
Summarize the promotion decision and actions taken.`;
|
|
1867
|
+
|
|
1868
|
+
// Store prompt now that it's built
|
|
1869
|
+
updateAgent(agentId, { prompt });
|
|
1870
|
+
|
|
1871
|
+
try {
|
|
1872
|
+
const wt = getPromotionWorktree('staging-promotion');
|
|
1873
|
+
const claude = spawn('claude', [
|
|
1874
|
+
'--dangerously-skip-permissions',
|
|
1875
|
+
'--mcp-config', wt.mcpConfig,
|
|
1876
|
+
'--output-format', 'json',
|
|
1877
|
+
'-p',
|
|
1878
|
+
prompt,
|
|
1879
|
+
], {
|
|
1880
|
+
cwd: wt.cwd,
|
|
1881
|
+
stdio: 'inherit',
|
|
1882
|
+
env: {
|
|
1883
|
+
...buildSpawnEnv(agentId),
|
|
1884
|
+
CLAUDE_PROJECT_DIR: PROJECT_DIR,
|
|
1885
|
+
GENTYR_PROMOTION_PIPELINE: 'true',
|
|
1886
|
+
},
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
return new Promise((resolve, reject) => {
|
|
1890
|
+
claude.on('close', (code) => {
|
|
1891
|
+
resolve({ code, output: '(output sent to inherit stdio)' });
|
|
1892
|
+
});
|
|
1893
|
+
claude.on('error', (err) => reject(err));
|
|
1894
|
+
setTimeout(() => {
|
|
1895
|
+
claude.kill();
|
|
1896
|
+
reject(new Error('Staging promotion timed out after 30 minutes'));
|
|
1897
|
+
}, 30 * 60 * 1000);
|
|
1898
|
+
});
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
log(`Staging promotion spawn error: ${err.message}`);
|
|
1901
|
+
return Promise.resolve({ code: 1, output: err.message });
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Spawn Emergency Hotfix Promotion (staging -> main, bypasses 24h + midnight)
|
|
1907
|
+
*
|
|
1908
|
+
* Called by the deputy-cto MCP server's execute_hotfix_promotion tool.
|
|
1909
|
+
* Uses the staging-promotion worktree for isolation, sets GENTYR_PROMOTION_PIPELINE=true.
|
|
1910
|
+
*
|
|
1911
|
+
* @param {string[]} commits - Commit oneline summaries being promoted
|
|
1912
|
+
* @returns {Promise<{code: number, output: string}>}
|
|
1913
|
+
*/
|
|
1914
|
+
export function spawnHotfixPromotion(commits) {
|
|
1915
|
+
const commitList = commits.join('\n');
|
|
1916
|
+
|
|
1917
|
+
const agentId = registerSpawn({
|
|
1918
|
+
type: AGENT_TYPES.HOTFIX_PROMOTION,
|
|
1919
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
1920
|
+
description: 'Emergency hotfix: staging -> main promotion',
|
|
1921
|
+
prompt: '',
|
|
1922
|
+
metadata: { commitCount: commits.length, isHotfix: true },
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
const prompt = `[Task][hotfix-promotion][AGENT:${agentId}] You are the EMERGENCY HOTFIX Promotion Pipeline.
|
|
1926
|
+
|
|
1927
|
+
## Mission
|
|
1928
|
+
|
|
1929
|
+
Immediately merge staging into main. This is a CTO-approved emergency hotfix that bypasses:
|
|
1930
|
+
- The 24-hour stability requirement
|
|
1931
|
+
- The midnight deployment window
|
|
1932
|
+
|
|
1933
|
+
Code review and quality checks still apply.
|
|
1934
|
+
|
|
1935
|
+
## Commits being promoted
|
|
1936
|
+
|
|
1937
|
+
\`\`\`
|
|
1938
|
+
${commitList}
|
|
1939
|
+
\`\`\`
|
|
1940
|
+
|
|
1941
|
+
## Process
|
|
1942
|
+
|
|
1943
|
+
### Step 1: Code Review
|
|
1944
|
+
|
|
1945
|
+
Spawn a code-reviewer sub-agent (Task tool, subagent_type: code-reviewer) to review the commits:
|
|
1946
|
+
- Check for security issues, code quality, spec violations
|
|
1947
|
+
- Look for disabled tests, placeholder code, hardcoded credentials
|
|
1948
|
+
- Verify no spec violations (G001-G019)
|
|
1949
|
+
|
|
1950
|
+
### Step 2: Create and Merge PR
|
|
1951
|
+
|
|
1952
|
+
If code review passes:
|
|
1953
|
+
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."
|
|
1954
|
+
2. Wait for CI: gh pr checks <number> --watch
|
|
1955
|
+
3. If CI passes: gh pr merge <number> --merge
|
|
1956
|
+
4. If CI fails: Report failure via mcp__agent-reports__report_to_deputy_cto
|
|
1957
|
+
|
|
1958
|
+
If code review fails:
|
|
1959
|
+
- Report findings via mcp__agent-reports__report_to_deputy_cto with priority "critical"
|
|
1960
|
+
- Do NOT proceed with merge
|
|
1961
|
+
|
|
1962
|
+
## Timeout
|
|
1963
|
+
|
|
1964
|
+
Complete within 25 minutes. If blocked, report and exit.`;
|
|
1965
|
+
|
|
1966
|
+
updateAgent(agentId, { prompt });
|
|
1967
|
+
|
|
1968
|
+
try {
|
|
1969
|
+
const wt = getPromotionWorktree('staging-promotion');
|
|
1970
|
+
const claude = spawn('claude', [
|
|
1971
|
+
'--dangerously-skip-permissions',
|
|
1972
|
+
'--mcp-config', wt.mcpConfig,
|
|
1973
|
+
'--output-format', 'json',
|
|
1974
|
+
'-p',
|
|
1975
|
+
prompt,
|
|
1976
|
+
], {
|
|
1977
|
+
cwd: wt.cwd,
|
|
1978
|
+
stdio: 'inherit',
|
|
1979
|
+
env: {
|
|
1980
|
+
...buildSpawnEnv(agentId),
|
|
1981
|
+
CLAUDE_PROJECT_DIR: PROJECT_DIR,
|
|
1982
|
+
GENTYR_PROMOTION_PIPELINE: 'true',
|
|
1983
|
+
},
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
return new Promise((resolve, reject) => {
|
|
1987
|
+
claude.on('close', (code) => {
|
|
1988
|
+
resolve({ code, output: '(output sent to inherit stdio)' });
|
|
1989
|
+
});
|
|
1990
|
+
claude.on('error', (err) => reject(err));
|
|
1991
|
+
setTimeout(() => {
|
|
1992
|
+
claude.kill();
|
|
1993
|
+
reject(new Error('Hotfix promotion timed out after 30 minutes'));
|
|
1994
|
+
}, 30 * 60 * 1000);
|
|
1995
|
+
});
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
log(`Hotfix promotion spawn error: ${err.message}`);
|
|
1998
|
+
return Promise.resolve({ code: 1, output: err.message });
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* GAP 4: Verify a spawned process is still alive after a short delay.
|
|
2004
|
+
* Returns true if the PID responds to signal 0, false otherwise.
|
|
2005
|
+
* Prevents cooldown consumption when spawn() succeeds but the process dies immediately.
|
|
2006
|
+
*/
|
|
2007
|
+
async function verifySpawnAlive(pid, label) {
|
|
2008
|
+
if (!pid) return false;
|
|
2009
|
+
return new Promise(resolve => {
|
|
2010
|
+
setTimeout(() => {
|
|
2011
|
+
try {
|
|
2012
|
+
process.kill(pid, 0);
|
|
2013
|
+
resolve(true);
|
|
2014
|
+
} catch {
|
|
2015
|
+
log(`${label}: PID ${pid} not alive after 2s. Cooldown NOT consumed.`);
|
|
2016
|
+
resolve(false);
|
|
2017
|
+
}
|
|
2018
|
+
}, 2000);
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* Spawn Staging Health Monitor (fire-and-forget)
|
|
2024
|
+
* GAP 4: Returns { success, pid } instead of boolean for deferred cooldown stamps.
|
|
2025
|
+
*/
|
|
2026
|
+
function spawnStagingHealthMonitor() {
|
|
2027
|
+
const agentId = registerSpawn({
|
|
2028
|
+
type: AGENT_TYPES.STAGING_HEALTH_MONITOR,
|
|
2029
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2030
|
+
description: 'Staging health monitor check',
|
|
2031
|
+
prompt: '',
|
|
2032
|
+
metadata: {},
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
const prompt = `[Task][staging-health-monitor][AGENT:${agentId}] You are the STAGING Health Monitor.
|
|
2036
|
+
|
|
2037
|
+
## Mission
|
|
2038
|
+
|
|
2039
|
+
Check all deployment infrastructure for staging environment health. Query services, check for errors, and report any issues found.
|
|
2040
|
+
|
|
2041
|
+
## Process
|
|
2042
|
+
|
|
2043
|
+
### Step 1: Read Service Configuration
|
|
2044
|
+
|
|
2045
|
+
Read \`.claude/config/services.json\` to get Render staging service ID and Vercel project ID.
|
|
2046
|
+
If the file doesn't exist, report this as an issue and exit.
|
|
2047
|
+
|
|
2048
|
+
### Step 2: Check Render Staging
|
|
2049
|
+
|
|
2050
|
+
- Use \`mcp__render__render_get_service\` with the staging service ID for service status
|
|
2051
|
+
- Use \`mcp__render__render_list_deploys\` to check for recent deploy failures
|
|
2052
|
+
- Flag: service down, deploy failures, stuck deploys
|
|
2053
|
+
|
|
2054
|
+
### Step 3: Check Vercel Staging
|
|
2055
|
+
|
|
2056
|
+
- Use \`mcp__vercel__vercel_list_deployments\` for recent staging deployments
|
|
2057
|
+
- Flag: build failures, deployment errors
|
|
2058
|
+
|
|
2059
|
+
### Step 4: Query Elasticsearch for Errors
|
|
2060
|
+
|
|
2061
|
+
- Use \`mcp__elastic-logs__query_logs\` with query: \`level:error\`, from: \`now-3h\`, to: \`now\`
|
|
2062
|
+
- Use \`mcp__elastic-logs__get_log_stats\` grouped by service for error counts
|
|
2063
|
+
- Flag: error spikes, new error types, critical errors
|
|
2064
|
+
|
|
2065
|
+
### Step 5: Compile Health Report
|
|
2066
|
+
|
|
2067
|
+
**If issues found:**
|
|
2068
|
+
1. Call \`mcp__cto-reports__report_to_cto\` with:
|
|
2069
|
+
- reporting_agent: "staging-health-monitor"
|
|
2070
|
+
- title: "Staging Health Issue: [summary]"
|
|
2071
|
+
- summary: Full findings
|
|
2072
|
+
- category: "performance" or "blocker" based on severity
|
|
2073
|
+
- priority: "normal" or "high" based on severity
|
|
2074
|
+
|
|
2075
|
+
2. For actionable issues, create an urgent fix task:
|
|
2076
|
+
\`\`\`
|
|
2077
|
+
mcp__todo-db__create_task({
|
|
2078
|
+
section: "CODE-REVIEWER",
|
|
2079
|
+
title: "Fix staging health issue: [summary]",
|
|
2080
|
+
description: "[Detailed description of the issue and how to fix it. Include all relevant context: error messages, service IDs, etc.]",
|
|
2081
|
+
assigned_by: "staging-health-monitor",
|
|
2082
|
+
priority: "urgent"
|
|
2083
|
+
})
|
|
2084
|
+
\`\`\`
|
|
2085
|
+
|
|
2086
|
+
**If all clear:**
|
|
2087
|
+
- Log "Staging environment healthy" and exit
|
|
2088
|
+
|
|
2089
|
+
### Step 6: Update Persistent Alerts
|
|
2090
|
+
|
|
2091
|
+
Read \`.claude/state/persistent_alerts.json\` (create if missing with \`{"version":1,"alerts":{}}\`).
|
|
2092
|
+
|
|
2093
|
+
**If issues found:** Update or create alert with key \`staging_error\`:
|
|
2094
|
+
- Set \`last_detected_at\` to current ISO timestamp
|
|
2095
|
+
- Increment \`detection_count\`
|
|
2096
|
+
- Set \`severity\` to "high"
|
|
2097
|
+
- Set \`resolved\` to false, \`source\` to "staging-health-monitor"
|
|
2098
|
+
- If new alert, set \`first_detected_at\`, \`escalation_count\`: 0
|
|
2099
|
+
|
|
2100
|
+
**If all clear:** If \`staging_error\` alert exists and is unresolved, set \`resolved: true\`, \`resolved_at\` to current ISO timestamp.
|
|
2101
|
+
|
|
2102
|
+
## Timeout
|
|
2103
|
+
|
|
2104
|
+
Complete within 10 minutes. This is a read-only monitoring check.`;
|
|
2105
|
+
|
|
2106
|
+
// Store prompt now that it's built
|
|
2107
|
+
updateAgent(agentId, { prompt });
|
|
2108
|
+
|
|
2109
|
+
try {
|
|
2110
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
2111
|
+
const claude = spawn('claude', [
|
|
2112
|
+
'--dangerously-skip-permissions',
|
|
2113
|
+
'--mcp-config', mcpConfig,
|
|
2114
|
+
'--output-format', 'json',
|
|
2115
|
+
'-p',
|
|
2116
|
+
prompt,
|
|
2117
|
+
], {
|
|
2118
|
+
detached: true,
|
|
2119
|
+
stdio: 'ignore',
|
|
2120
|
+
cwd: PROJECT_DIR,
|
|
2121
|
+
env: buildSpawnEnv(agentId),
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
claude.unref();
|
|
2125
|
+
updateAgent(agentId, { pid: claude.pid, status: 'running' });
|
|
2126
|
+
return { success: true, pid: claude.pid };
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
log(`Staging health monitor spawn error: ${err.message}`);
|
|
2129
|
+
return { success: false, pid: null };
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Spawn Production Health Monitor (fire-and-forget)
|
|
2135
|
+
* GAP 4: Returns { success, pid } instead of boolean for deferred cooldown stamps.
|
|
2136
|
+
*/
|
|
2137
|
+
function spawnProductionHealthMonitor() {
|
|
2138
|
+
const agentId = registerSpawn({
|
|
2139
|
+
type: AGENT_TYPES.PRODUCTION_HEALTH_MONITOR,
|
|
2140
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2141
|
+
description: 'Production health monitor check',
|
|
2142
|
+
prompt: '',
|
|
2143
|
+
metadata: {},
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
const prompt = `[Task][production-health-monitor][AGENT:${agentId}] You are the PRODUCTION Health Monitor.
|
|
2147
|
+
|
|
2148
|
+
## Mission
|
|
2149
|
+
|
|
2150
|
+
Check all deployment infrastructure for production environment health. This is CRITICAL -- production issues must be escalated to both deputy-CTO and CTO.
|
|
2151
|
+
|
|
2152
|
+
## Process
|
|
2153
|
+
|
|
2154
|
+
### Step 1: Read Service Configuration
|
|
2155
|
+
|
|
2156
|
+
Read \`.claude/config/services.json\` to get Render production service ID and Vercel project ID.
|
|
2157
|
+
If the file doesn't exist, report this as an issue and exit.
|
|
2158
|
+
|
|
2159
|
+
### Step 2: Check Render Production
|
|
2160
|
+
|
|
2161
|
+
- Use \`mcp__render__render_get_service\` with the production service ID for service status
|
|
2162
|
+
- Use \`mcp__render__render_list_deploys\` to check for recent deploy failures
|
|
2163
|
+
- Flag: service down, deploy failures, stuck deploys
|
|
2164
|
+
|
|
2165
|
+
### Step 3: Check Vercel Production
|
|
2166
|
+
|
|
2167
|
+
- Use \`mcp__vercel__vercel_list_deployments\` for recent production deployments
|
|
2168
|
+
- Flag: build failures, deployment errors
|
|
2169
|
+
|
|
2170
|
+
### Step 4: Query Elasticsearch for Errors
|
|
2171
|
+
|
|
2172
|
+
- Use \`mcp__elastic-logs__query_logs\` with query: \`level:error\`, from: \`now-1h\`, to: \`now\`
|
|
2173
|
+
- Use \`mcp__elastic-logs__get_log_stats\` grouped by service for error counts
|
|
2174
|
+
- Flag: error spikes, new error types, critical errors
|
|
2175
|
+
|
|
2176
|
+
### Step 5: Compile Health Report
|
|
2177
|
+
|
|
2178
|
+
**If issues found:**
|
|
2179
|
+
1. Call \`mcp__cto-reports__report_to_cto\` with:
|
|
2180
|
+
- reporting_agent: "production-health-monitor"
|
|
2181
|
+
- title: "PRODUCTION Health Issue: [summary]"
|
|
2182
|
+
- summary: Full findings
|
|
2183
|
+
- category: "performance" or "blocker" based on severity
|
|
2184
|
+
- priority: "high" or "critical" based on severity
|
|
2185
|
+
|
|
2186
|
+
2. Call \`mcp__deputy-cto__add_question\` with:
|
|
2187
|
+
- type: "escalation"
|
|
2188
|
+
- title: "Production Health Issue: [summary]"
|
|
2189
|
+
- description: Full health report findings
|
|
2190
|
+
- recommendation: Your recommended fix or action based on the health findings
|
|
2191
|
+
- This creates a CTO decision task visible in /deputy-cto
|
|
2192
|
+
|
|
2193
|
+
3. For actionable issues, create an urgent fix task:
|
|
2194
|
+
\`\`\`
|
|
2195
|
+
mcp__todo-db__create_task({
|
|
2196
|
+
section: "CODE-REVIEWER",
|
|
2197
|
+
title: "Fix production health issue: [summary]",
|
|
2198
|
+
description: "[Detailed description of the issue and how to fix it. Include all relevant context: error messages, service IDs, etc.]",
|
|
2199
|
+
assigned_by: "production-health-monitor",
|
|
2200
|
+
priority: "urgent"
|
|
2201
|
+
})
|
|
2202
|
+
\`\`\`
|
|
2203
|
+
|
|
2204
|
+
**If all clear:**
|
|
2205
|
+
- Log "Production environment healthy" and exit
|
|
2206
|
+
|
|
2207
|
+
### Step 6: Update Persistent Alerts
|
|
2208
|
+
|
|
2209
|
+
Read \`.claude/state/persistent_alerts.json\` (create if missing with \`{"version":1,"alerts":{}}\`).
|
|
2210
|
+
|
|
2211
|
+
**If issues found:** Update or create alert with key \`production_error\`:
|
|
2212
|
+
- Set \`last_detected_at\` to current ISO timestamp
|
|
2213
|
+
- Increment \`detection_count\`
|
|
2214
|
+
- Set \`severity\` to "critical"
|
|
2215
|
+
- Set \`resolved\` to false, \`source\` to "production-health-monitor"
|
|
2216
|
+
- If new alert, set \`first_detected_at\`, \`escalation_count\`: 0
|
|
2217
|
+
|
|
2218
|
+
**If all clear:** If \`production_error\` alert exists and is unresolved, set \`resolved: true\`, \`resolved_at\` to current ISO timestamp.
|
|
2219
|
+
|
|
2220
|
+
## Timeout
|
|
2221
|
+
|
|
2222
|
+
Complete within 10 minutes. This is a read-only monitoring check.`;
|
|
2223
|
+
|
|
2224
|
+
try {
|
|
2225
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
2226
|
+
const claude = spawn('claude', [
|
|
2227
|
+
'--dangerously-skip-permissions',
|
|
2228
|
+
'--mcp-config', mcpConfig,
|
|
2229
|
+
'--output-format', 'json',
|
|
2230
|
+
'-p',
|
|
2231
|
+
prompt,
|
|
2232
|
+
], {
|
|
2233
|
+
detached: true,
|
|
2234
|
+
stdio: 'ignore',
|
|
2235
|
+
cwd: PROJECT_DIR,
|
|
2236
|
+
env: buildSpawnEnv(agentId),
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
claude.unref();
|
|
2240
|
+
updateAgent(agentId, { pid: claude.pid, status: 'running', prompt });
|
|
2241
|
+
return { success: true, pid: claude.pid };
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
log(`Production health monitor spawn error: ${err.message}`);
|
|
2244
|
+
return { success: false, pid: null };
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* Get random spec file for standalone compliance checker
|
|
2250
|
+
* Reads specs/global/*.md and specs/local/*.md, returns a random one
|
|
2251
|
+
*/
|
|
2252
|
+
function getRandomSpec() {
|
|
2253
|
+
const specsDir = path.join(PROJECT_DIR, 'specs');
|
|
2254
|
+
const specs = [];
|
|
2255
|
+
|
|
2256
|
+
for (const subdir of ['global', 'local']) {
|
|
2257
|
+
const dir = path.join(specsDir, subdir);
|
|
2258
|
+
if (fs.existsSync(dir)) {
|
|
2259
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
2260
|
+
for (const f of files) {
|
|
2261
|
+
specs.push({ path: `specs/${subdir}/${f}`, id: f.replace('.md', '') });
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (specs.length === 0) return null;
|
|
2267
|
+
return specs[Math.floor(Math.random() * specs.length)];
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
/**
|
|
2271
|
+
* Spawn Standalone Antipattern Hunter (fire-and-forget)
|
|
2272
|
+
* Scans entire codebase for spec violations, independent of git hooks
|
|
2273
|
+
*/
|
|
2274
|
+
function spawnStandaloneAntipatternHunter() {
|
|
2275
|
+
const agentId = registerSpawn({
|
|
2276
|
+
type: AGENT_TYPES.STANDALONE_ANTIPATTERN_HUNTER,
|
|
2277
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2278
|
+
description: 'Standalone antipattern hunt (3h schedule)',
|
|
2279
|
+
prompt: '',
|
|
2280
|
+
metadata: {},
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
const prompt = `[Task][standalone-antipattern-hunter][AGENT:${agentId}] STANDALONE ANTIPATTERN HUNT - Periodic repo-wide scan for spec violations.
|
|
2284
|
+
|
|
2285
|
+
You are a STANDALONE antipattern hunter running on a 3-hour schedule. Your job is to systematically scan
|
|
2286
|
+
the ENTIRE codebase looking for spec violations and technical debt.
|
|
2287
|
+
|
|
2288
|
+
## Your Focus Areas
|
|
2289
|
+
- Hunt across ALL directories: src/, packages/, products/, integrations/
|
|
2290
|
+
- Look for systemic patterns of violations
|
|
2291
|
+
- Prioritize high-severity specs (G001, G004, G009, G010, G016)
|
|
2292
|
+
|
|
2293
|
+
## Workflow
|
|
2294
|
+
|
|
2295
|
+
### Step 1: Load Specifications
|
|
2296
|
+
\`\`\`javascript
|
|
2297
|
+
mcp__specs-browser__list_specs({})
|
|
2298
|
+
mcp__specs-browser__get_spec({ spec_id: "G001" }) // No graceful fallbacks
|
|
2299
|
+
mcp__specs-browser__get_spec({ spec_id: "G004" }) // No hardcoded credentials
|
|
2300
|
+
mcp__specs-browser__get_spec({ spec_id: "G009" }) // RLS policies required
|
|
2301
|
+
mcp__specs-browser__get_spec({ spec_id: "G010" }) // Session auth validation
|
|
2302
|
+
mcp__specs-browser__get_spec({ spec_id: "G016" }) // Integration boundary
|
|
2303
|
+
\`\`\`
|
|
2304
|
+
|
|
2305
|
+
### Step 2: Hunt for Violations
|
|
2306
|
+
Use Grep to systematically scan for violation patterns:
|
|
2307
|
+
- G001: \`|| null\`, \`|| undefined\`, \`?? 0\`, \`|| []\`, \`|| {}\`
|
|
2308
|
+
- G002: \`TODO\`, \`FIXME\`, \`throw new Error('Not implemented')\`
|
|
2309
|
+
- G004: Hardcoded API keys, credentials, secrets
|
|
2310
|
+
- G011: \`MOCK_MODE\`, \`isSimulation\`, \`isMockMode\`
|
|
2311
|
+
|
|
2312
|
+
### Step 3: For Each Violation
|
|
2313
|
+
a. Create TODO item:
|
|
2314
|
+
\`\`\`javascript
|
|
2315
|
+
mcp__todo-db__create_task({
|
|
2316
|
+
section: "CODE-REVIEWER",
|
|
2317
|
+
title: "Fix [SPEC-ID] violation in [file]",
|
|
2318
|
+
description: "[Details and location]",
|
|
2319
|
+
assigned_by: "STANDALONE-ANTIPATTERN-HUNTER"
|
|
2320
|
+
})
|
|
2321
|
+
\`\`\`
|
|
2322
|
+
|
|
2323
|
+
### Step 4: Report Critical Issues to CTO
|
|
2324
|
+
Report when you find:
|
|
2325
|
+
- Security violations (G004 hardcoded credentials, G009 missing RLS, G010 missing auth)
|
|
2326
|
+
- Architecture boundary violations (cross-product separation)
|
|
2327
|
+
- Critical spec violations requiring immediate attention
|
|
2328
|
+
- Patterns of repeated violations (3+ similar issues)
|
|
2329
|
+
|
|
2330
|
+
\`\`\`javascript
|
|
2331
|
+
mcp__cto-reports__report_to_cto({
|
|
2332
|
+
reporting_agent: "standalone-antipattern-hunter",
|
|
2333
|
+
title: "Brief title (max 200 chars)",
|
|
2334
|
+
summary: "Detailed summary with file paths, line numbers, and severity (max 2000 chars)",
|
|
2335
|
+
category: "security" | "architecture" | "performance" | "other",
|
|
2336
|
+
priority: "low" | "normal" | "high" | "critical"
|
|
2337
|
+
})
|
|
2338
|
+
\`\`\`
|
|
2339
|
+
|
|
2340
|
+
### Step 5: END SESSION
|
|
2341
|
+
After creating TODO items and CTO reports, provide a summary and END YOUR SESSION.
|
|
2342
|
+
Do NOT implement fixes yourself.
|
|
2343
|
+
|
|
2344
|
+
Focus on finding SYSTEMIC issues across the codebase, not just isolated violations.`;
|
|
2345
|
+
|
|
2346
|
+
// Store prompt now that it's built
|
|
2347
|
+
updateAgent(agentId, { prompt });
|
|
2348
|
+
|
|
2349
|
+
try {
|
|
2350
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
2351
|
+
const claude = spawn('claude', [
|
|
2352
|
+
'--dangerously-skip-permissions',
|
|
2353
|
+
'--mcp-config', mcpConfig,
|
|
2354
|
+
'--output-format', 'json',
|
|
2355
|
+
'-p',
|
|
2356
|
+
prompt,
|
|
2357
|
+
], {
|
|
2358
|
+
detached: true,
|
|
2359
|
+
stdio: 'ignore',
|
|
2360
|
+
cwd: PROJECT_DIR,
|
|
2361
|
+
env: buildSpawnEnv(agentId),
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
claude.unref();
|
|
2365
|
+
updateAgent(agentId, { pid: claude.pid, status: 'running' });
|
|
2366
|
+
return true;
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
log(`Standalone antipattern hunter spawn error: ${err.message}`);
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Spawn Standalone Compliance Checker (fire-and-forget)
|
|
2375
|
+
* Picks a random spec and scans the codebase for violations of that specific spec
|
|
2376
|
+
*/
|
|
2377
|
+
function spawnStandaloneComplianceChecker(spec) {
|
|
2378
|
+
const agentId = registerSpawn({
|
|
2379
|
+
type: AGENT_TYPES.STANDALONE_COMPLIANCE_CHECKER,
|
|
2380
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2381
|
+
description: `Standalone compliance check: ${spec.id}`,
|
|
2382
|
+
prompt: '',
|
|
2383
|
+
metadata: { specId: spec.id, specPath: spec.path },
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
const prompt = `[Task][standalone-compliance-checker][AGENT:${agentId}] STANDALONE COMPLIANCE CHECK - Audit codebase against spec: ${spec.id}
|
|
2387
|
+
|
|
2388
|
+
You are a STANDALONE compliance checker running on a 1-hour schedule. You have been assigned ONE specific spec to audit the codebase against.
|
|
2389
|
+
|
|
2390
|
+
## Your Assigned Spec
|
|
2391
|
+
|
|
2392
|
+
**Spec ID:** ${spec.id}
|
|
2393
|
+
**Spec Path:** ${spec.path}
|
|
2394
|
+
|
|
2395
|
+
## Workflow
|
|
2396
|
+
|
|
2397
|
+
### Step 1: Load Your Assigned Spec
|
|
2398
|
+
\`\`\`javascript
|
|
2399
|
+
mcp__specs-browser__get_spec({ spec_id: "${spec.id}" })
|
|
2400
|
+
\`\`\`
|
|
2401
|
+
|
|
2402
|
+
Read the spec thoroughly. Understand every requirement, constraint, and rule it defines.
|
|
2403
|
+
|
|
2404
|
+
### Step 2: Systematically Scan the Codebase
|
|
2405
|
+
Based on the spec requirements:
|
|
2406
|
+
1. Use Grep to search for patterns that violate the spec
|
|
2407
|
+
2. Use Glob to find files that should comply with the spec
|
|
2408
|
+
3. Read relevant files to check for compliance
|
|
2409
|
+
4. Focus on areas most likely to have violations
|
|
2410
|
+
|
|
2411
|
+
### Step 3: For Each Violation Found
|
|
2412
|
+
Create a TODO item:
|
|
2413
|
+
\`\`\`javascript
|
|
2414
|
+
mcp__todo-db__create_task({
|
|
2415
|
+
section: "CODE-REVIEWER",
|
|
2416
|
+
title: "Fix ${spec.id} violation in [file]:[line]",
|
|
2417
|
+
description: "[Violation details and what the spec requires]",
|
|
2418
|
+
assigned_by: "STANDALONE-COMPLIANCE-CHECKER"
|
|
2419
|
+
})
|
|
2420
|
+
\`\`\`
|
|
2421
|
+
|
|
2422
|
+
### Step 4: Report Critical Issues
|
|
2423
|
+
If you find critical violations (security, data exposure, architectural), report to CTO:
|
|
2424
|
+
\`\`\`javascript
|
|
2425
|
+
mcp__cto-reports__report_to_cto({
|
|
2426
|
+
reporting_agent: "standalone-compliance-checker",
|
|
2427
|
+
title: "${spec.id} compliance issue: [summary]",
|
|
2428
|
+
summary: "Detailed findings with file paths and line numbers",
|
|
2429
|
+
category: "security" | "architecture" | "other",
|
|
2430
|
+
priority: "normal" | "high" | "critical"
|
|
2431
|
+
})
|
|
2432
|
+
\`\`\`
|
|
2433
|
+
|
|
2434
|
+
### Step 5: END SESSION
|
|
2435
|
+
Provide a compliance summary:
|
|
2436
|
+
- Total files checked
|
|
2437
|
+
- Violations found (count and severity)
|
|
2438
|
+
- Overall compliance status for ${spec.id}
|
|
2439
|
+
|
|
2440
|
+
Do NOT implement fixes yourself. Only report and create TODOs.`;
|
|
2441
|
+
|
|
2442
|
+
// Store prompt now that it's built
|
|
2443
|
+
updateAgent(agentId, { prompt });
|
|
2444
|
+
|
|
2445
|
+
try {
|
|
2446
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
2447
|
+
const claude = spawn('claude', [
|
|
2448
|
+
'--dangerously-skip-permissions',
|
|
2449
|
+
'--mcp-config', mcpConfig,
|
|
2450
|
+
'--output-format', 'json',
|
|
2451
|
+
'-p',
|
|
2452
|
+
prompt,
|
|
2453
|
+
], {
|
|
2454
|
+
detached: true,
|
|
2455
|
+
stdio: 'ignore',
|
|
2456
|
+
cwd: PROJECT_DIR,
|
|
2457
|
+
env: buildSpawnEnv(agentId),
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
claude.unref();
|
|
2461
|
+
updateAgent(agentId, { pid: claude.pid, status: 'running' });
|
|
2462
|
+
return true;
|
|
2463
|
+
} catch (err) {
|
|
2464
|
+
log(`Standalone compliance checker spawn error: ${err.message}`);
|
|
2465
|
+
return false;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/**
|
|
2470
|
+
* Main entry point
|
|
2471
|
+
*/
|
|
2472
|
+
async function main() {
|
|
2473
|
+
const startTime = Date.now();
|
|
2474
|
+
log('=== Hourly Automation Starting ===');
|
|
2475
|
+
|
|
2476
|
+
// Check config
|
|
2477
|
+
const config = getConfig();
|
|
2478
|
+
|
|
2479
|
+
if (!config.enabled) {
|
|
2480
|
+
log('Autonomous Deputy CTO Mode is DISABLED. Exiting.');
|
|
2481
|
+
registerHookExecution({
|
|
2482
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2483
|
+
status: 'skipped',
|
|
2484
|
+
durationMs: Date.now() - startTime,
|
|
2485
|
+
metadata: { reason: 'disabled' }
|
|
2486
|
+
});
|
|
2487
|
+
process.exit(0);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// CTO Activity Gate: require /deputy-cto within last 24h
|
|
2491
|
+
// GAP 5: Gate is now a flag, not an early exit. Monitoring steps (health monitors,
|
|
2492
|
+
// triage, CI checks, persistent alerts) always run. Gate-required steps (lint,
|
|
2493
|
+
// task runner, promotions, etc.) are skipped when gate is closed.
|
|
2494
|
+
const ctoGate = checkCtoActivityGate(config);
|
|
2495
|
+
const ctoGateOpen = ctoGate.open;
|
|
2496
|
+
if (!ctoGateOpen) {
|
|
2497
|
+
log(`CTO Activity Gate CLOSED: ${ctoGate.reason}`);
|
|
2498
|
+
log('Monitoring-only mode: health monitors, triage, and CI checks will still run.');
|
|
2499
|
+
} else {
|
|
2500
|
+
log(`Autonomous Deputy CTO Mode is ENABLED. ${ctoGate.reason}`);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// Credentials are resolved lazily on first agent spawn via ensureCredentials().
|
|
2504
|
+
// This avoids unnecessary `op` CLI calls on cycles where all tasks hit cooldowns.
|
|
2505
|
+
|
|
2506
|
+
// Check rotation proxy health (non-blocking, informational only)
|
|
2507
|
+
const proxyHealth = await checkProxyHealth();
|
|
2508
|
+
if (proxyHealth.running) {
|
|
2509
|
+
log(`Rotation proxy: UP (activeKey=${proxyHealth.activeKeyId?.slice(0, 8) || 'unknown'})`);
|
|
2510
|
+
} else {
|
|
2511
|
+
log('Rotation proxy: DOWN — agents will run without proxy-based rotation.');
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// Check for overdrive concurrency override
|
|
2515
|
+
let effectiveMaxConcurrent = MAX_CONCURRENT_AGENTS;
|
|
2516
|
+
try {
|
|
2517
|
+
const autoConfigPath = path.join(PROJECT_DIR, '.claude', 'state', 'automation-config.json');
|
|
2518
|
+
if (fs.existsSync(autoConfigPath)) {
|
|
2519
|
+
const autoConfig = JSON.parse(fs.readFileSync(autoConfigPath, 'utf8'));
|
|
2520
|
+
if (autoConfig.overdrive?.active && new Date() < new Date(autoConfig.overdrive.expires_at)) {
|
|
2521
|
+
const override = autoConfig.overdrive.max_concurrent_override;
|
|
2522
|
+
effectiveMaxConcurrent = (typeof override === 'number' && override >= 1 && override <= 20)
|
|
2523
|
+
? override : MAX_CONCURRENT_AGENTS;
|
|
2524
|
+
log(`Overdrive active: concurrency limit raised to ${effectiveMaxConcurrent}`);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
} catch {
|
|
2528
|
+
// Fail safe - use default
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// Reap completed agents before counting to free concurrency slots
|
|
2532
|
+
try {
|
|
2533
|
+
const { reapCompletedAgents } = await import(path.resolve(__dirname, '..', '..', 'scripts', 'reap-completed-agents.js'));
|
|
2534
|
+
const reapResult = reapCompletedAgents(PROJECT_DIR);
|
|
2535
|
+
if (reapResult.reaped.length > 0) {
|
|
2536
|
+
log(`Reaper: cleaned up ${reapResult.reaped.length} completed agent(s).`);
|
|
2537
|
+
}
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
// Non-fatal — count will be conservative
|
|
2540
|
+
log(`Reaper: skipped (${err.message})`);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Concurrency guard: skip cycle if too many agents are already running
|
|
2544
|
+
const runningAgents = countRunningAgents();
|
|
2545
|
+
if (runningAgents >= effectiveMaxConcurrent) {
|
|
2546
|
+
log(`Concurrency limit reached (${runningAgents}/${effectiveMaxConcurrent} agents running). Skipping this cycle.`);
|
|
2547
|
+
registerHookExecution({
|
|
2548
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2549
|
+
status: 'skipped',
|
|
2550
|
+
durationMs: Date.now() - startTime,
|
|
2551
|
+
metadata: { reason: 'concurrency_limit', runningAgents }
|
|
2552
|
+
});
|
|
2553
|
+
process.exit(0);
|
|
2554
|
+
}
|
|
2555
|
+
log(`Running agents: ${runningAgents}/${effectiveMaxConcurrent}`);
|
|
2556
|
+
|
|
2557
|
+
const state = getState();
|
|
2558
|
+
const now = Date.now();
|
|
2559
|
+
|
|
2560
|
+
// =========================================================================
|
|
2561
|
+
// USAGE OPTIMIZER (runs first - cheap: API call + math)
|
|
2562
|
+
// =========================================================================
|
|
2563
|
+
try {
|
|
2564
|
+
const optimizerResult = await runUsageOptimizer(log);
|
|
2565
|
+
if (optimizerResult.snapshotTaken) {
|
|
2566
|
+
log(`Usage optimizer: snapshot taken. Adjustment: ${optimizerResult.adjustmentMade ? 'yes' : 'no'}.`);
|
|
2567
|
+
}
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
log(`Usage optimizer error (non-fatal): ${err.message}`);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// =========================================================================
|
|
2573
|
+
// KEY SYNC (runs after usage optimizer - discovers keys from all sources)
|
|
2574
|
+
// Triggered by both 10-min timer and WatchPaths file change events
|
|
2575
|
+
// =========================================================================
|
|
2576
|
+
try {
|
|
2577
|
+
const syncResult = await syncKeys(log);
|
|
2578
|
+
if (syncResult.keysAdded > 0) {
|
|
2579
|
+
log(`Key sync: ${syncResult.keysAdded} new key(s) discovered.`);
|
|
2580
|
+
}
|
|
2581
|
+
if (syncResult.tokensRefreshed > 0) {
|
|
2582
|
+
log(`Key sync: ${syncResult.tokensRefreshed} token(s) refreshed.`);
|
|
2583
|
+
}
|
|
2584
|
+
} catch (err) {
|
|
2585
|
+
log(`Key sync error (non-fatal): ${err.message}`);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// =========================================================================
|
|
2589
|
+
// BINARY PATCH VERSION WATCH (runs after key sync — detects Claude updates)
|
|
2590
|
+
// =========================================================================
|
|
2591
|
+
try {
|
|
2592
|
+
const { checkAndRepatch } = await import(
|
|
2593
|
+
path.join(PROJECT_DIR, 'scripts', 'watch-claude-version.js')
|
|
2594
|
+
);
|
|
2595
|
+
await checkAndRepatch(log);
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
// Non-fatal: version watch is optional
|
|
2598
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND') {
|
|
2599
|
+
log(`Version watch error (non-fatal): ${err.message}`);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Dynamic cooldowns from config
|
|
2604
|
+
const TRIAGE_CHECK_INTERVAL_MS = getCooldown('triage_check', 5) * 60 * 1000;
|
|
2605
|
+
const HOURLY_COOLDOWN_MS = getCooldown('hourly_tasks', 55) * 60 * 1000;
|
|
2606
|
+
const LINT_COOLDOWN_MS = getCooldown('lint_checker', 30) * 60 * 1000;
|
|
2607
|
+
const PREVIEW_PROMOTION_COOLDOWN_MS = getCooldown('preview_promotion', 360) * 60 * 1000;
|
|
2608
|
+
const STAGING_PROMOTION_COOLDOWN_MS = getCooldown('staging_promotion', 1200) * 60 * 1000;
|
|
2609
|
+
const STAGING_HEALTH_COOLDOWN_MS = getCooldown('staging_health_monitor', 180) * 60 * 1000;
|
|
2610
|
+
const PRODUCTION_HEALTH_COOLDOWN_MS = getCooldown('production_health_monitor', 60) * 60 * 1000;
|
|
2611
|
+
const STANDALONE_ANTIPATTERN_COOLDOWN_MS = getCooldown('standalone_antipattern_hunter', 180) * 60 * 1000;
|
|
2612
|
+
const STANDALONE_COMPLIANCE_COOLDOWN_MS = getCooldown('standalone_compliance_checker', 60) * 60 * 1000;
|
|
2613
|
+
const USER_FEEDBACK_COOLDOWN_MS = getCooldown('user_feedback', 120) * 60 * 1000;
|
|
2614
|
+
|
|
2615
|
+
// =========================================================================
|
|
2616
|
+
// TRIAGE CHECK (dynamic interval, default 5 min)
|
|
2617
|
+
// Per-item cooldown is handled by the MCP server's get_reports_for_triage
|
|
2618
|
+
// =========================================================================
|
|
2619
|
+
const timeSinceLastTriageCheck = now - state.lastTriageCheck;
|
|
2620
|
+
|
|
2621
|
+
if (timeSinceLastTriageCheck >= TRIAGE_CHECK_INTERVAL_MS) {
|
|
2622
|
+
// Quick check if there are any pending reports
|
|
2623
|
+
if (hasReportsReadyForTriage()) {
|
|
2624
|
+
log('Pending reports found, spawning triage agent...');
|
|
2625
|
+
state.lastTriageCheck = now;
|
|
2626
|
+
saveState(state);
|
|
2627
|
+
|
|
2628
|
+
try {
|
|
2629
|
+
// The agent will call get_reports_for_triage which handles cooldown filtering
|
|
2630
|
+
const result = await spawnReportTriage();
|
|
2631
|
+
if (result.code === 0) {
|
|
2632
|
+
log('Report triage completed successfully.');
|
|
2633
|
+
} else {
|
|
2634
|
+
log(`Report triage exited with code ${result.code}`);
|
|
2635
|
+
}
|
|
2636
|
+
} catch (err) {
|
|
2637
|
+
log(`Report triage error: ${err.message}`);
|
|
2638
|
+
}
|
|
2639
|
+
} else {
|
|
2640
|
+
log('No pending reports found.');
|
|
2641
|
+
state.lastTriageCheck = now;
|
|
2642
|
+
saveState(state);
|
|
2643
|
+
}
|
|
2644
|
+
} else {
|
|
2645
|
+
const minutesLeft = Math.ceil((TRIAGE_CHECK_INTERVAL_MS - timeSinceLastTriageCheck) / 60000);
|
|
2646
|
+
log(`Triage check cooldown active. ${minutesLeft} minutes until next check.`);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// =========================================================================
|
|
2650
|
+
// STAGING HEALTH MONITOR (3h cooldown, fire-and-forget) [GATE-EXEMPT]
|
|
2651
|
+
// Checks staging infrastructure health
|
|
2652
|
+
// =========================================================================
|
|
2653
|
+
const timeSinceLastStagingHealth = now - (state.lastStagingHealthCheck || 0);
|
|
2654
|
+
const stagingHealthEnabled = config.stagingHealthMonitorEnabled !== false;
|
|
2655
|
+
|
|
2656
|
+
if (timeSinceLastStagingHealth >= STAGING_HEALTH_COOLDOWN_MS && stagingHealthEnabled) {
|
|
2657
|
+
try {
|
|
2658
|
+
execSync('git fetch origin staging --quiet 2>/dev/null || true', {
|
|
2659
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 30000, stdio: 'pipe',
|
|
2660
|
+
});
|
|
2661
|
+
} catch {
|
|
2662
|
+
log('Staging health monitor: git fetch failed.');
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
if (remoteBranchExists('staging')) {
|
|
2666
|
+
log('Staging health monitor: spawning health check...');
|
|
2667
|
+
const result = spawnStagingHealthMonitor();
|
|
2668
|
+
if (result.success) {
|
|
2669
|
+
const alive = await verifySpawnAlive(result.pid, 'Staging health monitor');
|
|
2670
|
+
if (alive) {
|
|
2671
|
+
state.lastStagingHealthCheck = now;
|
|
2672
|
+
saveState(state);
|
|
2673
|
+
}
|
|
2674
|
+
log('Staging health monitor: spawned (fire-and-forget).');
|
|
2675
|
+
} else {
|
|
2676
|
+
log('Staging health monitor: spawn failed.');
|
|
2677
|
+
}
|
|
2678
|
+
} else {
|
|
2679
|
+
log('Staging health monitor: staging branch does not exist, skipping.');
|
|
2680
|
+
}
|
|
2681
|
+
} else if (!stagingHealthEnabled) {
|
|
2682
|
+
log('Staging Health Monitor is disabled in config.');
|
|
2683
|
+
} else {
|
|
2684
|
+
const minutesLeft = Math.ceil((STAGING_HEALTH_COOLDOWN_MS - timeSinceLastStagingHealth) / 60000);
|
|
2685
|
+
log(`Staging health monitor cooldown active. ${minutesLeft} minutes until next check.`);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// =========================================================================
|
|
2689
|
+
// PRODUCTION HEALTH MONITOR (1h cooldown, fire-and-forget) [GATE-EXEMPT]
|
|
2690
|
+
// Checks production infrastructure health, escalates to CTO
|
|
2691
|
+
// =========================================================================
|
|
2692
|
+
const timeSinceLastProdHealth = now - (state.lastProductionHealthCheck || 0);
|
|
2693
|
+
const prodHealthEnabled = config.productionHealthMonitorEnabled !== false;
|
|
2694
|
+
|
|
2695
|
+
if (timeSinceLastProdHealth >= PRODUCTION_HEALTH_COOLDOWN_MS && prodHealthEnabled) {
|
|
2696
|
+
log('Production health monitor: spawning health check...');
|
|
2697
|
+
const result = spawnProductionHealthMonitor();
|
|
2698
|
+
if (result.success) {
|
|
2699
|
+
const alive = await verifySpawnAlive(result.pid, 'Production health monitor');
|
|
2700
|
+
if (alive) {
|
|
2701
|
+
state.lastProductionHealthCheck = now;
|
|
2702
|
+
saveState(state);
|
|
2703
|
+
}
|
|
2704
|
+
log('Production health monitor: spawned (fire-and-forget).');
|
|
2705
|
+
} else {
|
|
2706
|
+
log('Production health monitor: spawn failed.');
|
|
2707
|
+
}
|
|
2708
|
+
} else if (!prodHealthEnabled) {
|
|
2709
|
+
log('Production Health Monitor is disabled in config.');
|
|
2710
|
+
} else {
|
|
2711
|
+
const minutesLeft = Math.ceil((PRODUCTION_HEALTH_COOLDOWN_MS - timeSinceLastProdHealth) / 60000);
|
|
2712
|
+
log(`Production health monitor cooldown active. ${minutesLeft} minutes until next check.`);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// =========================================================================
|
|
2716
|
+
// CI MONITORING (every cycle, gate-exempt)
|
|
2717
|
+
// GAP 3: Check GitHub Actions CI status for main and staging branches
|
|
2718
|
+
// =========================================================================
|
|
2719
|
+
try {
|
|
2720
|
+
checkCiStatus();
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
log(`CI monitoring error (non-fatal): ${err.message}`);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// =========================================================================
|
|
2726
|
+
// MERGE CHAIN GAP CHECK (every cycle, gate-exempt)
|
|
2727
|
+
// GAP 7: Alert when staging is too far ahead of main (>50 commits)
|
|
2728
|
+
// =========================================================================
|
|
2729
|
+
try {
|
|
2730
|
+
// Ensure we have fresh refs (staging health monitor may have fetched staging already)
|
|
2731
|
+
try {
|
|
2732
|
+
execSync('git fetch origin staging main --quiet 2>/dev/null || true', {
|
|
2733
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 30000, stdio: 'pipe',
|
|
2734
|
+
});
|
|
2735
|
+
} catch {
|
|
2736
|
+
// Non-fatal, may already have fresh refs
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
if (remoteBranchExists('staging') && remoteBranchExists('main')) {
|
|
2740
|
+
const gapCommits = getNewCommits('staging', 'main');
|
|
2741
|
+
if (gapCommits.length >= MERGE_CHAIN_GAP_THRESHOLD) {
|
|
2742
|
+
log(`Merge chain gap: ${gapCommits.length} commits on staging not in main (threshold: ${MERGE_CHAIN_GAP_THRESHOLD}).`);
|
|
2743
|
+
recordAlert('merge_chain_gap', {
|
|
2744
|
+
title: `Merge chain gap: ${gapCommits.length} commits on staging not merged to main`,
|
|
2745
|
+
severity: 'high',
|
|
2746
|
+
source: 'merge-chain-monitor',
|
|
2747
|
+
});
|
|
2748
|
+
} else {
|
|
2749
|
+
resolveAlert('merge_chain_gap');
|
|
2750
|
+
log(`Merge chain gap: ${gapCommits.length} commits (under threshold ${MERGE_CHAIN_GAP_THRESHOLD}).`);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
log(`Merge chain gap check error (non-fatal): ${err.message}`);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// =========================================================================
|
|
2758
|
+
// PERSISTENT ALERT CHECK (every cycle, gate-exempt)
|
|
2759
|
+
// GAP 2: Re-escalate unresolved alerts past their threshold and GC old ones
|
|
2760
|
+
// =========================================================================
|
|
2761
|
+
try {
|
|
2762
|
+
const alertResult = checkPersistentAlerts();
|
|
2763
|
+
if (alertResult.escalated > 0 || alertResult.gcCount > 0) {
|
|
2764
|
+
log(`Persistent alerts: processed (${alertResult.escalated} escalated, ${alertResult.gcCount} gc'd).`);
|
|
2765
|
+
}
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
log(`Persistent alerts error (non-fatal): ${err.message}`);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// =========================================================================
|
|
2771
|
+
// URGENT TASK DISPATCHER (no cooldown, gate-exempt)
|
|
2772
|
+
// Dispatches priority='urgent' tasks immediately without age filter.
|
|
2773
|
+
// These are typically created by deputy-cto during triage self-handling.
|
|
2774
|
+
// =========================================================================
|
|
2775
|
+
if (Database) {
|
|
2776
|
+
const urgentTasks = getUrgentPendingTasks();
|
|
2777
|
+
if (urgentTasks.length > 0) {
|
|
2778
|
+
log(`Urgent dispatcher: found ${urgentTasks.length} urgent task(s).`);
|
|
2779
|
+
const currentRunning = countRunningAgents();
|
|
2780
|
+
const availableSlots = Math.max(0, effectiveMaxConcurrent - currentRunning);
|
|
2781
|
+
if (availableSlots === 0) {
|
|
2782
|
+
log(`Urgent dispatcher: no available slots (${currentRunning}/${effectiveMaxConcurrent}). Deferring urgent tasks.`);
|
|
2783
|
+
} else {
|
|
2784
|
+
log(`Urgent dispatcher: ${availableSlots} slot(s) available (${currentRunning}/${effectiveMaxConcurrent}).`);
|
|
2785
|
+
let dispatched = 0;
|
|
2786
|
+
for (const task of urgentTasks) {
|
|
2787
|
+
if (dispatched >= availableSlots) {
|
|
2788
|
+
log(`Urgent dispatcher: concurrency limit reached, deferring remaining urgent tasks.`);
|
|
2789
|
+
break;
|
|
2790
|
+
}
|
|
2791
|
+
const mapping = SECTION_AGENT_MAP[task.section];
|
|
2792
|
+
if (!mapping) continue;
|
|
2793
|
+
if (!markTaskInProgress(task.id)) {
|
|
2794
|
+
log(`Urgent dispatcher: skipping task ${task.id} (failed to mark in_progress).`);
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
const success = spawnTaskAgent(task);
|
|
2798
|
+
if (success) {
|
|
2799
|
+
log(`Urgent dispatcher: spawned ${mapping.agent} for "${task.title}" (${task.id})`);
|
|
2800
|
+
dispatched++;
|
|
2801
|
+
} else {
|
|
2802
|
+
resetTaskToPending(task.id);
|
|
2803
|
+
log(`Urgent dispatcher: spawn failed for task ${task.id}, reset to pending.`);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
log(`Urgent dispatcher: dispatched ${dispatched} agent(s).`);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// =========================================================================
|
|
2812
|
+
// CTO GATE CHECK — exit if gate is closed after all monitoring-only steps
|
|
2813
|
+
// GAP 5: Everything above this point (Usage Optimizer, Key Sync, Session
|
|
2814
|
+
// Reviver, Triage, Health Monitors, CI Monitoring, Persistent Alerts,
|
|
2815
|
+
// Merge Chain Gap, Urgent Dispatcher) runs regardless of CTO gate status.
|
|
2816
|
+
// Everything below requires the gate to be open.
|
|
2817
|
+
// =========================================================================
|
|
2818
|
+
if (!ctoGateOpen) {
|
|
2819
|
+
log('CTO gate closed — monitoring-only steps complete. Skipping gate-required steps.');
|
|
2820
|
+
registerHookExecution({
|
|
2821
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
2822
|
+
status: 'partial',
|
|
2823
|
+
durationMs: Date.now() - startTime,
|
|
2824
|
+
metadata: { reason: 'cto_gate_monitoring_only', hoursSinceLastBriefing: ctoGate.hoursSinceLastBriefing }
|
|
2825
|
+
});
|
|
2826
|
+
process.exit(0);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// =========================================================================
|
|
2830
|
+
// LINT CHECK (own cooldown, default 30 min)
|
|
2831
|
+
// =========================================================================
|
|
2832
|
+
const timeSinceLastLint = now - (state.lastLintCheck || 0);
|
|
2833
|
+
|
|
2834
|
+
if (timeSinceLastLint >= LINT_COOLDOWN_MS && config.lintCheckerEnabled) {
|
|
2835
|
+
log('Running lint check...');
|
|
2836
|
+
const lintResult = runLintCheck();
|
|
2837
|
+
|
|
2838
|
+
if (lintResult.hasErrors) {
|
|
2839
|
+
const errorCount = (lintResult.output.match(/\berror\b/gi) || []).length;
|
|
2840
|
+
log(`Lint check found ${errorCount} error(s), spawning fixer...`);
|
|
2841
|
+
|
|
2842
|
+
try {
|
|
2843
|
+
const result = await spawnLintFixer(lintResult.output);
|
|
2844
|
+
if (result.code === 0) {
|
|
2845
|
+
log('Lint fixer completed successfully.');
|
|
2846
|
+
} else {
|
|
2847
|
+
log(`Lint fixer exited with code ${result.code}`);
|
|
2848
|
+
}
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
log(`Lint fixer error: ${err.message}`);
|
|
2851
|
+
}
|
|
2852
|
+
} else {
|
|
2853
|
+
log('Lint check passed - no errors found.');
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
state.lastLintCheck = now;
|
|
2857
|
+
saveState(state);
|
|
2858
|
+
} else if (!config.lintCheckerEnabled) {
|
|
2859
|
+
log('Lint Checker is disabled in config.');
|
|
2860
|
+
} else {
|
|
2861
|
+
const minutesLeft = Math.ceil((LINT_COOLDOWN_MS - timeSinceLastLint) / 60000);
|
|
2862
|
+
log(`Lint check cooldown active. ${minutesLeft} minutes until next check.`);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// =========================================================================
|
|
2866
|
+
// TASK RUNNER CHECK (1h cooldown)
|
|
2867
|
+
// Spawns a separate Claude session for every pending TODO item >1h old
|
|
2868
|
+
// =========================================================================
|
|
2869
|
+
const TASK_RUNNER_COOLDOWN_MS = getCooldown('task_runner', 60) * 60 * 1000;
|
|
2870
|
+
const timeSinceLastTaskRunner = now - (state.lastTaskRunnerCheck || 0);
|
|
2871
|
+
|
|
2872
|
+
if (timeSinceLastTaskRunner >= TASK_RUNNER_COOLDOWN_MS && config.taskRunnerEnabled) {
|
|
2873
|
+
if (!Database) {
|
|
2874
|
+
log('Task runner: better-sqlite3 not available, skipping.');
|
|
2875
|
+
} else {
|
|
2876
|
+
log('Task runner: checking for pending tasks...');
|
|
2877
|
+
let candidates = getPendingTasksForRunner();
|
|
2878
|
+
|
|
2879
|
+
// Gate PRODUCT-MANAGER tasks on feature toggle
|
|
2880
|
+
if (!config.productManagerEnabled) {
|
|
2881
|
+
const before = candidates.length;
|
|
2882
|
+
candidates = candidates.filter(t => t.section !== 'PRODUCT-MANAGER');
|
|
2883
|
+
const filtered = before - candidates.length;
|
|
2884
|
+
if (filtered > 0) {
|
|
2885
|
+
log(`Task runner: filtered ${filtered} PRODUCT-MANAGER task(s) (feature disabled).`);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
if (candidates.length === 0) {
|
|
2890
|
+
log('Task runner: no eligible pending tasks found.');
|
|
2891
|
+
} else {
|
|
2892
|
+
log(`Task runner: found ${candidates.length} candidate task(s).`);
|
|
2893
|
+
let spawned = 0;
|
|
2894
|
+
|
|
2895
|
+
for (const task of candidates) {
|
|
2896
|
+
if (spawned >= MAX_TASKS_PER_CYCLE) {
|
|
2897
|
+
log(`Task runner: reached batch limit (${MAX_TASKS_PER_CYCLE}), deferring ${candidates.length - spawned} remaining tasks.`);
|
|
2898
|
+
break;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
const mapping = SECTION_AGENT_MAP[task.section];
|
|
2902
|
+
if (!mapping) continue;
|
|
2903
|
+
|
|
2904
|
+
if (!markTaskInProgress(task.id)) {
|
|
2905
|
+
log(`Task runner: skipping task ${task.id} (failed to mark in_progress).`);
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
const success = spawnTaskAgent(task);
|
|
2910
|
+
if (success) {
|
|
2911
|
+
log(`Task runner: spawning ${mapping.agent} for task "${task.title}" (${task.id})`);
|
|
2912
|
+
spawned++;
|
|
2913
|
+
} else {
|
|
2914
|
+
resetTaskToPending(task.id);
|
|
2915
|
+
log(`Task runner: spawn failed for task ${task.id}, reset to pending.`);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
log(`Task runner: spawned ${spawned} agent(s) this cycle.`);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
state.lastTaskRunnerCheck = now;
|
|
2924
|
+
saveState(state);
|
|
2925
|
+
} else if (!config.taskRunnerEnabled) {
|
|
2926
|
+
log('Task Runner is disabled in config.');
|
|
2927
|
+
} else {
|
|
2928
|
+
const minutesLeft = Math.ceil((TASK_RUNNER_COOLDOWN_MS - timeSinceLastTaskRunner) / 60000);
|
|
2929
|
+
log(`Task runner cooldown active. ${minutesLeft} minutes until next check.`);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// =========================================================================
|
|
2933
|
+
// STAGING -> PRODUCTION PROMOTION (midnight window, 20h cooldown)
|
|
2934
|
+
// Checks nightly for stable staging to promote to production
|
|
2935
|
+
// NOTE: Runs BEFORE preview→staging to prevent clock-reset starvation
|
|
2936
|
+
// =========================================================================
|
|
2937
|
+
const timeSinceLastStagingPromotion = now - (state.lastStagingPromotionCheck || 0);
|
|
2938
|
+
const stagingPromotionEnabled = config.stagingPromotionEnabled !== false;
|
|
2939
|
+
const currentHour = new Date().getHours();
|
|
2940
|
+
const currentMinute = new Date().getMinutes();
|
|
2941
|
+
const isMidnightWindow = currentHour === 0 && currentMinute <= 30;
|
|
2942
|
+
|
|
2943
|
+
if (isMidnightWindow && timeSinceLastStagingPromotion >= STAGING_PROMOTION_COOLDOWN_MS && stagingPromotionEnabled) {
|
|
2944
|
+
log('Staging promotion: midnight window - checking for promotable commits...');
|
|
2945
|
+
|
|
2946
|
+
try {
|
|
2947
|
+
execSync('git fetch origin staging main --quiet 2>/dev/null || true', {
|
|
2948
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 30000, stdio: 'pipe',
|
|
2949
|
+
});
|
|
2950
|
+
} catch {
|
|
2951
|
+
log('Staging promotion: git fetch failed, skipping.');
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (remoteBranchExists('staging') && remoteBranchExists('main')) {
|
|
2955
|
+
const newCommits = getNewCommits('staging', 'main');
|
|
2956
|
+
|
|
2957
|
+
if (newCommits.length === 0) {
|
|
2958
|
+
log('Staging promotion: no new commits on staging.');
|
|
2959
|
+
} else {
|
|
2960
|
+
const lastStagingTimestamp = getLastCommitTimestamp('staging');
|
|
2961
|
+
const hoursSinceLastStagingCommit = lastStagingTimestamp > 0
|
|
2962
|
+
? Math.floor((Date.now() / 1000 - lastStagingTimestamp) / 3600) : 0;
|
|
2963
|
+
|
|
2964
|
+
if (hoursSinceLastStagingCommit >= 24) {
|
|
2965
|
+
// GAP 6: Block promotion if production is in error state
|
|
2966
|
+
const alertData = readPersistentAlerts();
|
|
2967
|
+
const prodAlert = alertData.alerts['production_error'];
|
|
2968
|
+
if (prodAlert && !prodAlert.resolved) {
|
|
2969
|
+
const ageHours = Math.round((Date.now() - new Date(prodAlert.first_detected_at).getTime()) / 3600000);
|
|
2970
|
+
log(`Staging promotion: BLOCKED — production in error state for ${ageHours}h. Fix production before promoting.`);
|
|
2971
|
+
} else {
|
|
2972
|
+
log(`Staging promotion: ${newCommits.length} commits ready. Staging stable for ${hoursSinceLastStagingCommit}h.`);
|
|
2973
|
+
|
|
2974
|
+
try {
|
|
2975
|
+
const result = await spawnStagingPromotion(newCommits, hoursSinceLastStagingCommit);
|
|
2976
|
+
if (result.code === 0) {
|
|
2977
|
+
log('Staging promotion pipeline completed successfully.');
|
|
2978
|
+
} else {
|
|
2979
|
+
log(`Staging promotion pipeline exited with code ${result.code}`);
|
|
2980
|
+
}
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
log(`Staging promotion error: ${err.message}`);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
} else {
|
|
2986
|
+
log(`Staging promotion: staging only ${hoursSinceLastStagingCommit}h old (need 24h stability).`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
} else {
|
|
2990
|
+
log('Staging promotion: staging or main branch does not exist on remote.');
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
state.lastStagingPromotionCheck = now;
|
|
2994
|
+
saveState(state);
|
|
2995
|
+
} else if (!stagingPromotionEnabled) {
|
|
2996
|
+
log('Staging Promotion is disabled in config.');
|
|
2997
|
+
} else if (!isMidnightWindow) {
|
|
2998
|
+
// Only log this at debug level since it runs every 10 minutes
|
|
2999
|
+
} else {
|
|
3000
|
+
const minutesLeft = Math.ceil((STAGING_PROMOTION_COOLDOWN_MS - timeSinceLastStagingPromotion) / 60000);
|
|
3001
|
+
log(`Staging promotion cooldown active. ${minutesLeft} minutes until next check.`);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// =========================================================================
|
|
3005
|
+
// STAGING FREEZE: Pause preview→staging when staging approaches 24h stability
|
|
3006
|
+
// Prevents preview→staging from resetting the staging clock and starving
|
|
3007
|
+
// the staging→main midnight promotion window.
|
|
3008
|
+
// Fetch staging ref so freeze decisions use fresh data even outside midnight.
|
|
3009
|
+
// =========================================================================
|
|
3010
|
+
try {
|
|
3011
|
+
execSync('git fetch origin staging --quiet 2>/dev/null || true', {
|
|
3012
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 30000, stdio: 'pipe',
|
|
3013
|
+
});
|
|
3014
|
+
} catch { /* non-fatal */ }
|
|
3015
|
+
|
|
3016
|
+
const lastStagingTs = getLastCommitTimestamp('staging');
|
|
3017
|
+
const stagingAgeHours = lastStagingTs > 0 ? (Date.now() / 1000 - lastStagingTs) / 3600 : 0;
|
|
3018
|
+
|
|
3019
|
+
if (stagingAgeHours >= 18 && !state.stagingFreezeActive) {
|
|
3020
|
+
state.stagingFreezeActive = true;
|
|
3021
|
+
state.stagingFreezeActivatedAt = now;
|
|
3022
|
+
saveState(state);
|
|
3023
|
+
log(`Staging freeze ACTIVATED: staging is ${Math.floor(stagingAgeHours)}h old, pausing preview→staging until staging→main resolves.`);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// Clear freeze conditions:
|
|
3027
|
+
// 1. Staging age dropped below 18h (staging→main promoted, new merge is fresh)
|
|
3028
|
+
// 2. 48h safety valve (prevents permanent lockout)
|
|
3029
|
+
if (state.stagingFreezeActive) {
|
|
3030
|
+
const freezeAge = (now - state.stagingFreezeActivatedAt) / (1000 * 3600);
|
|
3031
|
+
if (stagingAgeHours < 18) {
|
|
3032
|
+
state.stagingFreezeActive = false;
|
|
3033
|
+
saveState(state);
|
|
3034
|
+
log('Staging freeze CLEARED: staging age dropped below 18h (promotion completed).');
|
|
3035
|
+
} else if (freezeAge >= 48) {
|
|
3036
|
+
state.stagingFreezeActive = false;
|
|
3037
|
+
saveState(state);
|
|
3038
|
+
log('Staging freeze CLEARED: 48h safety valve triggered.');
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// =========================================================================
|
|
3043
|
+
// PREVIEW -> STAGING PROMOTION (6h cooldown)
|
|
3044
|
+
// Checks for new commits on preview, spawns review + promotion pipeline
|
|
3045
|
+
// NOTE: Gated by staging freeze to prevent staging→main starvation
|
|
3046
|
+
// =========================================================================
|
|
3047
|
+
const timeSinceLastPreviewPromotion = now - (state.lastPreviewPromotionCheck || 0);
|
|
3048
|
+
const previewPromotionEnabled = config.previewPromotionEnabled !== false;
|
|
3049
|
+
|
|
3050
|
+
if (state.stagingFreezeActive) {
|
|
3051
|
+
log(`Preview promotion: PAUSED by staging freeze (staging ${Math.floor(stagingAgeHours)}h old, waiting for staging→main).`);
|
|
3052
|
+
// Do NOT update lastPreviewPromotionCheck — so it fires immediately when freeze lifts
|
|
3053
|
+
} else if (timeSinceLastPreviewPromotion >= PREVIEW_PROMOTION_COOLDOWN_MS && previewPromotionEnabled) {
|
|
3054
|
+
log('Preview promotion: checking for promotable commits...');
|
|
3055
|
+
|
|
3056
|
+
try {
|
|
3057
|
+
// Fetch latest remote state
|
|
3058
|
+
execSync('git fetch origin preview staging --quiet 2>/dev/null || true', {
|
|
3059
|
+
cwd: PROJECT_DIR, encoding: 'utf8', timeout: 30000, stdio: 'pipe',
|
|
3060
|
+
});
|
|
3061
|
+
} catch {
|
|
3062
|
+
log('Preview promotion: git fetch failed, skipping.');
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
if (remoteBranchExists('preview') && remoteBranchExists('staging')) {
|
|
3066
|
+
const newCommits = getNewCommits('preview', 'staging');
|
|
3067
|
+
|
|
3068
|
+
if (newCommits.length === 0) {
|
|
3069
|
+
log('Preview promotion: no new commits on preview.');
|
|
3070
|
+
} else {
|
|
3071
|
+
const lastStagingTimestamp = getLastCommitTimestamp('staging');
|
|
3072
|
+
const hoursSinceLastStagingMerge = lastStagingTimestamp > 0
|
|
3073
|
+
? Math.floor((Date.now() / 1000 - lastStagingTimestamp) / 3600) : 999;
|
|
3074
|
+
const hasBugFix = hasBugFixCommits(newCommits);
|
|
3075
|
+
|
|
3076
|
+
if (hoursSinceLastStagingMerge >= 24 || hasBugFix) {
|
|
3077
|
+
log(`Preview promotion: ${newCommits.length} commits ready. Staging age: ${hoursSinceLastStagingMerge}h. Bug fix: ${hasBugFix}.`);
|
|
3078
|
+
|
|
3079
|
+
try {
|
|
3080
|
+
const result = await spawnPreviewPromotion(newCommits, hoursSinceLastStagingMerge, hasBugFix);
|
|
3081
|
+
if (result.code === 0) {
|
|
3082
|
+
log('Preview promotion pipeline completed successfully.');
|
|
3083
|
+
state.lastPreviewToStagingMergeAt = now;
|
|
3084
|
+
saveState(state);
|
|
3085
|
+
} else {
|
|
3086
|
+
log(`Preview promotion pipeline exited with code ${result.code}`);
|
|
3087
|
+
}
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
log(`Preview promotion error: ${err.message}`);
|
|
3090
|
+
}
|
|
3091
|
+
} else {
|
|
3092
|
+
log(`Preview promotion: ${newCommits.length} commits pending but staging only ${hoursSinceLastStagingMerge}h old (need 24h or bug fix).`);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
} else {
|
|
3096
|
+
log('Preview promotion: preview or staging branch does not exist on remote.');
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
state.lastPreviewPromotionCheck = now;
|
|
3100
|
+
saveState(state);
|
|
3101
|
+
} else if (!previewPromotionEnabled) {
|
|
3102
|
+
log('Preview Promotion is disabled in config.');
|
|
3103
|
+
} else {
|
|
3104
|
+
const minutesLeft = Math.ceil((PREVIEW_PROMOTION_COOLDOWN_MS - timeSinceLastPreviewPromotion) / 60000);
|
|
3105
|
+
log(`Preview promotion cooldown active. ${minutesLeft} minutes until next check.`);
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// =========================================================================
|
|
3109
|
+
// WORKTREE CLEANUP (6h cooldown)
|
|
3110
|
+
// Removes worktrees whose feature branches have been merged to preview
|
|
3111
|
+
// =========================================================================
|
|
3112
|
+
const WORKTREE_CLEANUP_COOLDOWN_MS = getCooldown('worktree_cleanup', 360) * 60 * 1000;
|
|
3113
|
+
const timeSinceLastWorktreeCleanup = now - (state.lastWorktreeCleanup || 0);
|
|
3114
|
+
const worktreeCleanupEnabled = config.worktreeCleanupEnabled !== false;
|
|
3115
|
+
|
|
3116
|
+
if (timeSinceLastWorktreeCleanup >= WORKTREE_CLEANUP_COOLDOWN_MS && worktreeCleanupEnabled) {
|
|
3117
|
+
log('Worktree cleanup: checking for merged worktrees...');
|
|
3118
|
+
try {
|
|
3119
|
+
const cleaned = cleanupMergedWorktrees();
|
|
3120
|
+
if (cleaned > 0) {
|
|
3121
|
+
log(`Worktree cleanup: removed ${cleaned} merged worktree(s).`);
|
|
3122
|
+
} else {
|
|
3123
|
+
log('Worktree cleanup: no merged worktrees to remove.');
|
|
3124
|
+
}
|
|
3125
|
+
} catch (err) {
|
|
3126
|
+
log(`Worktree cleanup error (non-fatal): ${err.message}`);
|
|
3127
|
+
}
|
|
3128
|
+
state.lastWorktreeCleanup = now;
|
|
3129
|
+
saveState(state);
|
|
3130
|
+
} else if (!worktreeCleanupEnabled) {
|
|
3131
|
+
log('Worktree Cleanup is disabled in config.');
|
|
3132
|
+
} else {
|
|
3133
|
+
const minutesLeft = Math.ceil((WORKTREE_CLEANUP_COOLDOWN_MS - timeSinceLastWorktreeCleanup) / 60000);
|
|
3134
|
+
log(`Worktree cleanup cooldown active. ${minutesLeft} minutes until next check.`);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// =========================================================================
|
|
3138
|
+
// STALE WORK DETECTOR (24h cooldown)
|
|
3139
|
+
// Reports uncommitted changes, unpushed branches, and stale feature branches
|
|
3140
|
+
// =========================================================================
|
|
3141
|
+
const STALE_WORK_COOLDOWN_MS = getCooldown('stale_work_detector', 1440) * 60 * 1000;
|
|
3142
|
+
const timeSinceLastStaleCheck = now - (state.lastStaleWorkCheck || 0);
|
|
3143
|
+
const staleWorkEnabled = config.staleWorkDetectorEnabled !== false;
|
|
3144
|
+
|
|
3145
|
+
if (timeSinceLastStaleCheck >= STALE_WORK_COOLDOWN_MS && staleWorkEnabled) {
|
|
3146
|
+
log('Stale work detector: scanning for stale work...');
|
|
3147
|
+
try {
|
|
3148
|
+
const report = detectStaleWork();
|
|
3149
|
+
if (report.hasIssues) {
|
|
3150
|
+
const reportText = formatReport(report);
|
|
3151
|
+
log(`Stale work detector: issues found - ${report.uncommittedFiles.length} uncommitted, ${report.unpushedBranches.length} unpushed, ${report.staleBranches.length} stale branches.`);
|
|
3152
|
+
|
|
3153
|
+
// Report to deputy-CTO via agent-reports (if MCP available)
|
|
3154
|
+
try {
|
|
3155
|
+
const mcpConfig = path.join(PROJECT_DIR, '.mcp.json');
|
|
3156
|
+
if (fs.existsSync(mcpConfig)) {
|
|
3157
|
+
const reportPrompt = `[Task][stale-work-report] Report this stale work finding to the deputy-CTO.
|
|
3158
|
+
|
|
3159
|
+
Use mcp__agent-reports__report_to_deputy_cto with:
|
|
3160
|
+
- reporting_agent: "stale-work-detector"
|
|
3161
|
+
- title: "Stale Work Detected: ${report.uncommittedFiles.length} uncommitted, ${report.unpushedBranches.length} unpushed, ${report.staleBranches.length} stale branches"
|
|
3162
|
+
- summary: ${JSON.stringify(reportText).slice(0, 500)}
|
|
3163
|
+
- category: "git-hygiene"
|
|
3164
|
+
- priority: "${report.staleBranches.length > 0 ? 'medium' : 'low'}"
|
|
3165
|
+
|
|
3166
|
+
Then exit.`;
|
|
3167
|
+
|
|
3168
|
+
const reportAgent = spawn('claude', [
|
|
3169
|
+
'--dangerously-skip-permissions',
|
|
3170
|
+
'--mcp-config', mcpConfig,
|
|
3171
|
+
'--output-format', 'json',
|
|
3172
|
+
'-p', reportPrompt,
|
|
3173
|
+
], {
|
|
3174
|
+
detached: true,
|
|
3175
|
+
stdio: 'ignore',
|
|
3176
|
+
cwd: PROJECT_DIR,
|
|
3177
|
+
env: buildSpawnEnv(`stale-report-${Date.now()}`),
|
|
3178
|
+
});
|
|
3179
|
+
reportAgent.unref();
|
|
3180
|
+
}
|
|
3181
|
+
} catch (reportErr) {
|
|
3182
|
+
log(`Stale work detector: failed to spawn reporter: ${reportErr.message}`);
|
|
3183
|
+
}
|
|
3184
|
+
} else {
|
|
3185
|
+
log('Stale work detector: no issues found.');
|
|
3186
|
+
}
|
|
3187
|
+
} catch (err) {
|
|
3188
|
+
log(`Stale work detector error (non-fatal): ${err.message}`);
|
|
3189
|
+
}
|
|
3190
|
+
state.lastStaleWorkCheck = now;
|
|
3191
|
+
saveState(state);
|
|
3192
|
+
} else if (!staleWorkEnabled) {
|
|
3193
|
+
log('Stale Work Detector is disabled in config.');
|
|
3194
|
+
} else {
|
|
3195
|
+
const minutesLeft = Math.ceil((STALE_WORK_COOLDOWN_MS - timeSinceLastStaleCheck) / 60000);
|
|
3196
|
+
log(`Stale work detector cooldown active. ${minutesLeft} minutes until next check.`);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// =========================================================================
|
|
3200
|
+
// STANDALONE ANTIPATTERN HUNTER (3h cooldown, fire-and-forget)
|
|
3201
|
+
// Repo-wide spec violation scan, independent of git hooks
|
|
3202
|
+
// =========================================================================
|
|
3203
|
+
const timeSinceLastAntipatternHunt = now - (state.lastStandaloneAntipatternHunt || 0);
|
|
3204
|
+
const antipatternHuntEnabled = config.standaloneAntipatternHunterEnabled !== false;
|
|
3205
|
+
|
|
3206
|
+
if (timeSinceLastAntipatternHunt >= STANDALONE_ANTIPATTERN_COOLDOWN_MS && antipatternHuntEnabled) {
|
|
3207
|
+
log('Standalone antipattern hunter: spawning repo-wide scan...');
|
|
3208
|
+
const success = spawnStandaloneAntipatternHunter();
|
|
3209
|
+
if (success) {
|
|
3210
|
+
log('Standalone antipattern hunter: spawned (fire-and-forget).');
|
|
3211
|
+
} else {
|
|
3212
|
+
log('Standalone antipattern hunter: spawn failed.');
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
state.lastStandaloneAntipatternHunt = now;
|
|
3216
|
+
saveState(state);
|
|
3217
|
+
} else if (!antipatternHuntEnabled) {
|
|
3218
|
+
log('Standalone Antipattern Hunter is disabled in config.');
|
|
3219
|
+
} else {
|
|
3220
|
+
const minutesLeft = Math.ceil((STANDALONE_ANTIPATTERN_COOLDOWN_MS - timeSinceLastAntipatternHunt) / 60000);
|
|
3221
|
+
log(`Standalone antipattern hunter cooldown active. ${minutesLeft} minutes until next hunt.`);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// =========================================================================
|
|
3225
|
+
// STANDALONE COMPLIANCE CHECKER (1h cooldown, fire-and-forget)
|
|
3226
|
+
// Picks a random spec and audits the codebase against it
|
|
3227
|
+
// =========================================================================
|
|
3228
|
+
const timeSinceLastComplianceCheck = now - (state.lastStandaloneComplianceCheck || 0);
|
|
3229
|
+
const complianceCheckEnabled = config.standaloneComplianceCheckerEnabled !== false;
|
|
3230
|
+
|
|
3231
|
+
if (timeSinceLastComplianceCheck >= STANDALONE_COMPLIANCE_COOLDOWN_MS && complianceCheckEnabled) {
|
|
3232
|
+
const randomSpec = getRandomSpec();
|
|
3233
|
+
if (randomSpec) {
|
|
3234
|
+
log(`Standalone compliance checker: spawning audit for spec ${randomSpec.id}...`);
|
|
3235
|
+
const success = spawnStandaloneComplianceChecker(randomSpec);
|
|
3236
|
+
if (success) {
|
|
3237
|
+
log(`Standalone compliance checker: spawned for ${randomSpec.id} (fire-and-forget).`);
|
|
3238
|
+
} else {
|
|
3239
|
+
log('Standalone compliance checker: spawn failed.');
|
|
3240
|
+
}
|
|
3241
|
+
} else {
|
|
3242
|
+
log('Standalone compliance checker: no specs found in specs/global/ or specs/local/.');
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
state.lastStandaloneComplianceCheck = now;
|
|
3246
|
+
saveState(state);
|
|
3247
|
+
} else if (!complianceCheckEnabled) {
|
|
3248
|
+
log('Standalone Compliance Checker is disabled in config.');
|
|
3249
|
+
} else {
|
|
3250
|
+
const minutesLeft = Math.ceil((STANDALONE_COMPLIANCE_COOLDOWN_MS - timeSinceLastComplianceCheck) / 60000);
|
|
3251
|
+
log(`Standalone compliance checker cooldown active. ${minutesLeft} minutes until next check.`);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// =========================================================================
|
|
3255
|
+
// USER FEEDBACK PIPELINE (2h cooldown, fire-and-forget agents)
|
|
3256
|
+
// Detects staging changes, matches personas, spawns feedback agents
|
|
3257
|
+
// =========================================================================
|
|
3258
|
+
const userFeedbackEnabled = config.userFeedbackEnabled !== false;
|
|
3259
|
+
|
|
3260
|
+
if (userFeedbackEnabled) {
|
|
3261
|
+
try {
|
|
3262
|
+
const feedbackResult = await runFeedbackPipeline(log, state, saveState, USER_FEEDBACK_COOLDOWN_MS);
|
|
3263
|
+
if (feedbackResult.ran) {
|
|
3264
|
+
log(`User feedback: ${feedbackResult.reason}`);
|
|
3265
|
+
registerSpawn({
|
|
3266
|
+
type: AGENT_TYPES.FEEDBACK_ORCHESTRATOR,
|
|
3267
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
3268
|
+
description: feedbackResult.reason,
|
|
3269
|
+
prompt: '',
|
|
3270
|
+
metadata: { personasTriggered: feedbackResult.personasTriggered },
|
|
3271
|
+
});
|
|
3272
|
+
} else {
|
|
3273
|
+
log(`User feedback: skipped - ${feedbackResult.reason}`);
|
|
3274
|
+
}
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
log(`User feedback pipeline error (non-fatal): ${err.message}`);
|
|
3277
|
+
}
|
|
3278
|
+
} else {
|
|
3279
|
+
log('User Feedback Pipeline is disabled in config.');
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
// =========================================================================
|
|
3283
|
+
// HOURLY TASKS (dynamic cooldown, default 55 min)
|
|
3284
|
+
// =========================================================================
|
|
3285
|
+
const timeSinceLastRun = now - state.lastRun;
|
|
3286
|
+
|
|
3287
|
+
if (timeSinceLastRun < HOURLY_COOLDOWN_MS) {
|
|
3288
|
+
const minutesLeft = Math.ceil((HOURLY_COOLDOWN_MS - timeSinceLastRun) / 60000);
|
|
3289
|
+
log(`Hourly tasks cooldown active. ${minutesLeft} minutes until next run.`);
|
|
3290
|
+
log('=== Hourly Automation Complete ===');
|
|
3291
|
+
registerHookExecution({
|
|
3292
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
3293
|
+
status: 'success',
|
|
3294
|
+
durationMs: Date.now() - startTime,
|
|
3295
|
+
metadata: { fullRun: false, minutesUntilNext: minutesLeft }
|
|
3296
|
+
});
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// Update state for hourly tasks
|
|
3301
|
+
state.lastRun = now;
|
|
3302
|
+
saveState(state);
|
|
3303
|
+
|
|
3304
|
+
// Check CLAUDE.md size and run refactor if needed
|
|
3305
|
+
if (config.claudeMdRefactorEnabled) {
|
|
3306
|
+
const claudeMdSize = getClaudeMdSize();
|
|
3307
|
+
log(`CLAUDE.md size: ${claudeMdSize} characters (threshold: ${CLAUDE_MD_SIZE_THRESHOLD})`);
|
|
3308
|
+
|
|
3309
|
+
if (claudeMdSize > CLAUDE_MD_SIZE_THRESHOLD) {
|
|
3310
|
+
log('CLAUDE.md exceeds threshold, spawning refactor...');
|
|
3311
|
+
try {
|
|
3312
|
+
const result = await spawnClaudeMdRefactor();
|
|
3313
|
+
if (result.code === 0) {
|
|
3314
|
+
log('CLAUDE.md refactor completed.');
|
|
3315
|
+
state.lastClaudeMdRefactor = now;
|
|
3316
|
+
saveState(state);
|
|
3317
|
+
} else {
|
|
3318
|
+
log(`CLAUDE.md refactor exited with code ${result.code}`);
|
|
3319
|
+
}
|
|
3320
|
+
} catch (err) {
|
|
3321
|
+
log(`CLAUDE.md refactor error: ${err.message}`);
|
|
3322
|
+
}
|
|
3323
|
+
} else {
|
|
3324
|
+
log('CLAUDE.md size is within threshold.');
|
|
3325
|
+
}
|
|
3326
|
+
} else {
|
|
3327
|
+
log('CLAUDE.md Refactor is disabled in config.');
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
log('=== Hourly Automation Complete ===');
|
|
3331
|
+
|
|
3332
|
+
registerHookExecution({
|
|
3333
|
+
hookType: HOOK_TYPES.HOURLY_AUTOMATION,
|
|
3334
|
+
status: 'success',
|
|
3335
|
+
durationMs: Date.now() - startTime,
|
|
3336
|
+
metadata: { fullRun: true }
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
main();
|