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.
Files changed (599) hide show
  1. package/.claude/agents/antipattern-hunter.md +176 -0
  2. package/.claude/agents/code-reviewer.md +205 -0
  3. package/.claude/agents/code-writer.md +154 -0
  4. package/.claude/agents/deputy-cto.md +309 -0
  5. package/.claude/agents/feedback-agent.md +101 -0
  6. package/.claude/agents/investigator.md +136 -0
  7. package/.claude/agents/product-manager.md +97 -0
  8. package/.claude/agents/project-manager.md +116 -0
  9. package/.claude/agents/repo-hygiene-expert.md +626 -0
  10. package/.claude/agents/secret-manager.md +324 -0
  11. package/.claude/agents/test-writer.md +354 -0
  12. package/.claude/commands/configure-personas.md +144 -0
  13. package/.claude/commands/cto-report.md +36 -0
  14. package/.claude/commands/demo.md +89 -0
  15. package/.claude/commands/deputy-cto.md +345 -0
  16. package/.claude/commands/hotfix.md +31 -0
  17. package/.claude/commands/overdrive-gentyr.md +167 -0
  18. package/.claude/commands/product-manager.md +32 -0
  19. package/.claude/commands/push-migrations.md +86 -0
  20. package/.claude/commands/push-secrets.md +97 -0
  21. package/.claude/commands/services.json.example +30 -0
  22. package/.claude/commands/setup-gentyr.md +396 -0
  23. package/.claude/commands/show.md +42 -0
  24. package/.claude/commands/spawn-tasks.md +79 -0
  25. package/.claude/commands/toggle-automation-gentyr.md +75 -0
  26. package/.claude/commands/toggle-product-manager.md +19 -0
  27. package/.claude/commands/triage.md +69 -0
  28. package/.claude/hooks/README.md +686 -0
  29. package/.claude/hooks/__tests__/README.md +129 -0
  30. package/.claude/hooks/agent-tracker.js +434 -0
  31. package/.claude/hooks/antipattern-hunter-hook.js +401 -0
  32. package/.claude/hooks/api-key-watcher.js +289 -0
  33. package/.claude/hooks/block-no-verify.js +301 -0
  34. package/.claude/hooks/bypass-approval-hook.js +313 -0
  35. package/.claude/hooks/compliance-checker.js +1309 -0
  36. package/.claude/hooks/config-reader.js +143 -0
  37. package/.claude/hooks/credential-file-guard.js +1139 -0
  38. package/.claude/hooks/credential-health-check.js +168 -0
  39. package/.claude/hooks/credential-sync-hook.js +79 -0
  40. package/.claude/hooks/cto-notification-hook.js +656 -0
  41. package/.claude/hooks/feedback-launcher.js +424 -0
  42. package/.claude/hooks/feedback-orchestrator.js +367 -0
  43. package/.claude/hooks/gentyr-splash.js +47 -0
  44. package/.claude/hooks/gentyr-sync.js +389 -0
  45. package/.claude/hooks/hourly-automation.js +3340 -0
  46. package/.claude/hooks/key-sync.js +899 -0
  47. package/.claude/hooks/lib/approval-utils.js +731 -0
  48. package/.claude/hooks/lib/feature-branch-helper.js +102 -0
  49. package/.claude/hooks/lib/worktree-manager.js +330 -0
  50. package/.claude/hooks/mapping-validator.js +285 -0
  51. package/.claude/hooks/plan-executor.js +398 -0
  52. package/.claude/hooks/playwright-cli-guard.js +104 -0
  53. package/.claude/hooks/playwright-health-check.js +71 -0
  54. package/.claude/hooks/pre-commit-review.js +725 -0
  55. package/.claude/hooks/prompts/local-spec-enforcement.md +310 -0
  56. package/.claude/hooks/prompts/mapping-fix.md +92 -0
  57. package/.claude/hooks/prompts/mapping-review.md +140 -0
  58. package/.claude/hooks/prompts/schema-mapper.md +185 -0
  59. package/.claude/hooks/prompts/spec-enforcement.md +233 -0
  60. package/.claude/hooks/protected-action-approval-hook.js +336 -0
  61. package/.claude/hooks/protected-action-gate.js +562 -0
  62. package/.claude/hooks/protected-actions.json +208 -0
  63. package/.claude/hooks/protected-actions.json.template +122 -0
  64. package/.claude/hooks/quota-monitor.js +490 -0
  65. package/.claude/hooks/reporters/jest-failure-reporter.js +401 -0
  66. package/.claude/hooks/reporters/playwright-failure-reporter.js +446 -0
  67. package/.claude/hooks/reporters/vitest-failure-reporter.js +443 -0
  68. package/.claude/hooks/schema-mapper-hook.js +544 -0
  69. package/.claude/hooks/secret-leak-detector.js +216 -0
  70. package/.claude/hooks/session-reviver.js +514 -0
  71. package/.claude/hooks/slash-command-prefetch.js +1145 -0
  72. package/.claude/hooks/stale-work-detector.js +205 -0
  73. package/.claude/hooks/stop-continue-hook.js +414 -0
  74. package/.claude/hooks/todo-maintenance.js +522 -0
  75. package/.claude/hooks/todo-processing-prompt.md +75 -0
  76. package/.claude/hooks/usage-optimizer.js +791 -0
  77. package/.claude/mcp/README.md +246 -0
  78. package/.claude/settings.json.template +168 -0
  79. package/.mcp.json.template +207 -0
  80. package/CLAUDE.md +340 -0
  81. package/CLAUDE.md.gentyr-section +89 -0
  82. package/LICENSE +21 -0
  83. package/README.md +297 -0
  84. package/cli/commands/init.js +471 -0
  85. package/cli/commands/migrate.js +132 -0
  86. package/cli/commands/protect.js +271 -0
  87. package/cli/commands/scaffold.js +48 -0
  88. package/cli/commands/status.js +133 -0
  89. package/cli/commands/sync.js +101 -0
  90. package/cli/commands/uninstall.js +207 -0
  91. package/cli/index.js +111 -0
  92. package/cli/lib/config-gen.js +214 -0
  93. package/cli/lib/resolve-framework.js +97 -0
  94. package/cli/lib/state.js +140 -0
  95. package/cli/lib/symlinks.js +260 -0
  96. package/docs/AUTOMATION-SYSTEMS.md +484 -0
  97. package/docs/BINARY-PATCHING.md +212 -0
  98. package/docs/CHANGELOG.md +2830 -0
  99. package/docs/CREDENTIAL-DETECTION.md +151 -0
  100. package/docs/CTO-DASHBOARD.md +476 -0
  101. package/docs/DEPLOYMENT-FLOW.md +477 -0
  102. package/docs/DEVELOPER.md +116 -0
  103. package/docs/Executive.md +372 -0
  104. package/docs/SECRET-PATHS.md +77 -0
  105. package/docs/SETUP-GUIDE.md +419 -0
  106. package/docs/STACK.md +109 -0
  107. package/docs/TESTING.md +440 -0
  108. package/docs/assets/claude-logo.svg +3 -0
  109. package/docs/sessions/2026-01-24-spec-suite-implementation.md +190 -0
  110. package/docs/sessions/2026-02-15-feedback-e2e-audit.md +484 -0
  111. package/docs/sessions/2026-02-20-credential-rotation-experiments.md +340 -0
  112. package/docs/sessions/TEST-COVERAGE-REPORT-2026-02-20.md +168 -0
  113. package/docs/shared/EPHEMERAL-STATE-FILES.md +115 -0
  114. package/docs/shared/PROTECTION-SYSTEM.md +341 -0
  115. package/husky/post-commit +10 -0
  116. package/husky/pre-commit +40 -0
  117. package/husky/pre-push +94 -0
  118. package/package.json +43 -0
  119. package/packages/cto-dashboard/package-lock.json +3510 -0
  120. package/packages/cto-dashboard/package.json +41 -0
  121. package/packages/cto-dashboard/pnpm-lock.yaml +2168 -0
  122. package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts +220 -0
  123. package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts.map +1 -0
  124. package/packages/mcp-servers/dist/__testUtils__/fixtures.js +376 -0
  125. package/packages/mcp-servers/dist/__testUtils__/fixtures.js.map +1 -0
  126. package/packages/mcp-servers/dist/__testUtils__/index.d.ts +121 -0
  127. package/packages/mcp-servers/dist/__testUtils__/index.d.ts.map +1 -0
  128. package/packages/mcp-servers/dist/__testUtils__/index.js +180 -0
  129. package/packages/mcp-servers/dist/__testUtils__/index.js.map +1 -0
  130. package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts +84 -0
  131. package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts.map +1 -0
  132. package/packages/mcp-servers/dist/__testUtils__/schemas.js +309 -0
  133. package/packages/mcp-servers/dist/__testUtils__/schemas.js.map +1 -0
  134. package/packages/mcp-servers/dist/agent-reports/index.d.ts +7 -0
  135. package/packages/mcp-servers/dist/agent-reports/index.d.ts.map +1 -0
  136. package/packages/mcp-servers/dist/agent-reports/index.js +8 -0
  137. package/packages/mcp-servers/dist/agent-reports/index.js.map +1 -0
  138. package/packages/mcp-servers/dist/agent-reports/server.d.ts +22 -0
  139. package/packages/mcp-servers/dist/agent-reports/server.d.ts.map +1 -0
  140. package/packages/mcp-servers/dist/agent-reports/server.js +535 -0
  141. package/packages/mcp-servers/dist/agent-reports/server.js.map +1 -0
  142. package/packages/mcp-servers/dist/agent-reports/types.d.ts +258 -0
  143. package/packages/mcp-servers/dist/agent-reports/types.d.ts.map +1 -0
  144. package/packages/mcp-servers/dist/agent-reports/types.js +81 -0
  145. package/packages/mcp-servers/dist/agent-reports/types.js.map +1 -0
  146. package/packages/mcp-servers/dist/agent-tracker/index.d.ts +5 -0
  147. package/packages/mcp-servers/dist/agent-tracker/index.d.ts.map +1 -0
  148. package/packages/mcp-servers/dist/agent-tracker/index.js +5 -0
  149. package/packages/mcp-servers/dist/agent-tracker/index.js.map +1 -0
  150. package/packages/mcp-servers/dist/agent-tracker/server.d.ts +12 -0
  151. package/packages/mcp-servers/dist/agent-tracker/server.d.ts.map +1 -0
  152. package/packages/mcp-servers/dist/agent-tracker/server.js +919 -0
  153. package/packages/mcp-servers/dist/agent-tracker/server.js.map +1 -0
  154. package/packages/mcp-servers/dist/agent-tracker/types.d.ts +328 -0
  155. package/packages/mcp-servers/dist/agent-tracker/types.d.ts.map +1 -0
  156. package/packages/mcp-servers/dist/agent-tracker/types.js +128 -0
  157. package/packages/mcp-servers/dist/agent-tracker/types.js.map +1 -0
  158. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts +27 -0
  159. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts.map +1 -0
  160. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js +167 -0
  161. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js.map +1 -0
  162. package/packages/mcp-servers/dist/chrome-bridge/index.d.ts +6 -0
  163. package/packages/mcp-servers/dist/chrome-bridge/index.d.ts.map +1 -0
  164. package/packages/mcp-servers/dist/chrome-bridge/index.js +6 -0
  165. package/packages/mcp-servers/dist/chrome-bridge/index.js.map +1 -0
  166. package/packages/mcp-servers/dist/chrome-bridge/server.d.ts +13 -0
  167. package/packages/mcp-servers/dist/chrome-bridge/server.d.ts.map +1 -0
  168. package/packages/mcp-servers/dist/chrome-bridge/server.js +959 -0
  169. package/packages/mcp-servers/dist/chrome-bridge/server.js.map +1 -0
  170. package/packages/mcp-servers/dist/chrome-bridge/types.d.ts +41 -0
  171. package/packages/mcp-servers/dist/chrome-bridge/types.d.ts.map +1 -0
  172. package/packages/mcp-servers/dist/chrome-bridge/types.js +8 -0
  173. package/packages/mcp-servers/dist/chrome-bridge/types.js.map +1 -0
  174. package/packages/mcp-servers/dist/cloudflare/index.d.ts +8 -0
  175. package/packages/mcp-servers/dist/cloudflare/index.d.ts.map +1 -0
  176. package/packages/mcp-servers/dist/cloudflare/index.js +8 -0
  177. package/packages/mcp-servers/dist/cloudflare/index.js.map +1 -0
  178. package/packages/mcp-servers/dist/cloudflare/server.d.ts +16 -0
  179. package/packages/mcp-servers/dist/cloudflare/server.d.ts.map +1 -0
  180. package/packages/mcp-servers/dist/cloudflare/server.js +253 -0
  181. package/packages/mcp-servers/dist/cloudflare/server.js.map +1 -0
  182. package/packages/mcp-servers/dist/cloudflare/types.d.ts +141 -0
  183. package/packages/mcp-servers/dist/cloudflare/types.d.ts.map +1 -0
  184. package/packages/mcp-servers/dist/cloudflare/types.js +53 -0
  185. package/packages/mcp-servers/dist/cloudflare/types.js.map +1 -0
  186. package/packages/mcp-servers/dist/codecov/index.d.ts +7 -0
  187. package/packages/mcp-servers/dist/codecov/index.d.ts.map +1 -0
  188. package/packages/mcp-servers/dist/codecov/index.js +7 -0
  189. package/packages/mcp-servers/dist/codecov/index.js.map +1 -0
  190. package/packages/mcp-servers/dist/codecov/server.d.ts +21 -0
  191. package/packages/mcp-servers/dist/codecov/server.d.ts.map +1 -0
  192. package/packages/mcp-servers/dist/codecov/server.js +376 -0
  193. package/packages/mcp-servers/dist/codecov/server.js.map +1 -0
  194. package/packages/mcp-servers/dist/codecov/types.d.ts +269 -0
  195. package/packages/mcp-servers/dist/codecov/types.d.ts.map +1 -0
  196. package/packages/mcp-servers/dist/codecov/types.js +128 -0
  197. package/packages/mcp-servers/dist/codecov/types.js.map +1 -0
  198. package/packages/mcp-servers/dist/cto-report/index.d.ts +9 -0
  199. package/packages/mcp-servers/dist/cto-report/index.d.ts.map +1 -0
  200. package/packages/mcp-servers/dist/cto-report/index.js +9 -0
  201. package/packages/mcp-servers/dist/cto-report/index.js.map +1 -0
  202. package/packages/mcp-servers/dist/cto-report/server.d.ts +14 -0
  203. package/packages/mcp-servers/dist/cto-report/server.d.ts.map +1 -0
  204. package/packages/mcp-servers/dist/cto-report/server.js +859 -0
  205. package/packages/mcp-servers/dist/cto-report/server.js.map +1 -0
  206. package/packages/mcp-servers/dist/cto-report/types.d.ts +213 -0
  207. package/packages/mcp-servers/dist/cto-report/types.d.ts.map +1 -0
  208. package/packages/mcp-servers/dist/cto-report/types.js +29 -0
  209. package/packages/mcp-servers/dist/cto-report/types.js.map +1 -0
  210. package/packages/mcp-servers/dist/cto-reports/index.d.ts +7 -0
  211. package/packages/mcp-servers/dist/cto-reports/index.d.ts.map +1 -0
  212. package/packages/mcp-servers/dist/cto-reports/index.js +8 -0
  213. package/packages/mcp-servers/dist/cto-reports/index.js.map +1 -0
  214. package/packages/mcp-servers/dist/cto-reports/server.d.ts +20 -0
  215. package/packages/mcp-servers/dist/cto-reports/server.d.ts.map +1 -0
  216. package/packages/mcp-servers/dist/cto-reports/server.js +538 -0
  217. package/packages/mcp-servers/dist/cto-reports/server.js.map +1 -0
  218. package/packages/mcp-servers/dist/cto-reports/types.d.ts +236 -0
  219. package/packages/mcp-servers/dist/cto-reports/types.d.ts.map +1 -0
  220. package/packages/mcp-servers/dist/cto-reports/types.js +77 -0
  221. package/packages/mcp-servers/dist/cto-reports/types.js.map +1 -0
  222. package/packages/mcp-servers/dist/deputy-cto/index.d.ts +7 -0
  223. package/packages/mcp-servers/dist/deputy-cto/index.d.ts.map +1 -0
  224. package/packages/mcp-servers/dist/deputy-cto/index.js +8 -0
  225. package/packages/mcp-servers/dist/deputy-cto/index.js.map +1 -0
  226. package/packages/mcp-servers/dist/deputy-cto/server.d.ts +23 -0
  227. package/packages/mcp-servers/dist/deputy-cto/server.d.ts.map +1 -0
  228. package/packages/mcp-servers/dist/deputy-cto/server.js +1700 -0
  229. package/packages/mcp-servers/dist/deputy-cto/server.js.map +1 -0
  230. package/packages/mcp-servers/dist/deputy-cto/types.d.ts +439 -0
  231. package/packages/mcp-servers/dist/deputy-cto/types.d.ts.map +1 -0
  232. package/packages/mcp-servers/dist/deputy-cto/types.js +102 -0
  233. package/packages/mcp-servers/dist/deputy-cto/types.js.map +1 -0
  234. package/packages/mcp-servers/dist/elastic-logs/index.d.ts +5 -0
  235. package/packages/mcp-servers/dist/elastic-logs/index.d.ts.map +1 -0
  236. package/packages/mcp-servers/dist/elastic-logs/index.js +5 -0
  237. package/packages/mcp-servers/dist/elastic-logs/index.js.map +1 -0
  238. package/packages/mcp-servers/dist/elastic-logs/server.d.ts +18 -0
  239. package/packages/mcp-servers/dist/elastic-logs/server.d.ts.map +1 -0
  240. package/packages/mcp-servers/dist/elastic-logs/server.js +259 -0
  241. package/packages/mcp-servers/dist/elastic-logs/server.js.map +1 -0
  242. package/packages/mcp-servers/dist/elastic-logs/types.d.ts +107 -0
  243. package/packages/mcp-servers/dist/elastic-logs/types.d.ts.map +1 -0
  244. package/packages/mcp-servers/dist/elastic-logs/types.js +31 -0
  245. package/packages/mcp-servers/dist/elastic-logs/types.js.map +1 -0
  246. package/packages/mcp-servers/dist/feedback-explorer/index.d.ts +2 -0
  247. package/packages/mcp-servers/dist/feedback-explorer/index.d.ts.map +1 -0
  248. package/packages/mcp-servers/dist/feedback-explorer/index.js +2 -0
  249. package/packages/mcp-servers/dist/feedback-explorer/index.js.map +1 -0
  250. package/packages/mcp-servers/dist/feedback-explorer/server.d.ts +21 -0
  251. package/packages/mcp-servers/dist/feedback-explorer/server.d.ts.map +1 -0
  252. package/packages/mcp-servers/dist/feedback-explorer/server.js +580 -0
  253. package/packages/mcp-servers/dist/feedback-explorer/server.js.map +1 -0
  254. package/packages/mcp-servers/dist/feedback-explorer/types.d.ts +331 -0
  255. package/packages/mcp-servers/dist/feedback-explorer/types.d.ts.map +1 -0
  256. package/packages/mcp-servers/dist/feedback-explorer/types.js +40 -0
  257. package/packages/mcp-servers/dist/feedback-explorer/types.js.map +1 -0
  258. package/packages/mcp-servers/dist/feedback-reporter/index.d.ts +9 -0
  259. package/packages/mcp-servers/dist/feedback-reporter/index.d.ts.map +1 -0
  260. package/packages/mcp-servers/dist/feedback-reporter/index.js +9 -0
  261. package/packages/mcp-servers/dist/feedback-reporter/index.js.map +1 -0
  262. package/packages/mcp-servers/dist/feedback-reporter/server.d.ts +36 -0
  263. package/packages/mcp-servers/dist/feedback-reporter/server.d.ts.map +1 -0
  264. package/packages/mcp-servers/dist/feedback-reporter/server.js +392 -0
  265. package/packages/mcp-servers/dist/feedback-reporter/server.js.map +1 -0
  266. package/packages/mcp-servers/dist/feedback-reporter/types.d.ts +152 -0
  267. package/packages/mcp-servers/dist/feedback-reporter/types.d.ts.map +1 -0
  268. package/packages/mcp-servers/dist/feedback-reporter/types.js +67 -0
  269. package/packages/mcp-servers/dist/feedback-reporter/types.js.map +1 -0
  270. package/packages/mcp-servers/dist/github/index.d.ts +7 -0
  271. package/packages/mcp-servers/dist/github/index.d.ts.map +1 -0
  272. package/packages/mcp-servers/dist/github/index.js +7 -0
  273. package/packages/mcp-servers/dist/github/index.js.map +1 -0
  274. package/packages/mcp-servers/dist/github/server.d.ts +15 -0
  275. package/packages/mcp-servers/dist/github/server.d.ts.map +1 -0
  276. package/packages/mcp-servers/dist/github/server.js +686 -0
  277. package/packages/mcp-servers/dist/github/server.js.map +1 -0
  278. package/packages/mcp-servers/dist/github/types.d.ts +660 -0
  279. package/packages/mcp-servers/dist/github/types.d.ts.map +1 -0
  280. package/packages/mcp-servers/dist/github/types.js +209 -0
  281. package/packages/mcp-servers/dist/github/types.js.map +1 -0
  282. package/packages/mcp-servers/dist/index.d.ts +30 -0
  283. package/packages/mcp-servers/dist/index.d.ts.map +1 -0
  284. package/packages/mcp-servers/dist/index.js +32 -0
  285. package/packages/mcp-servers/dist/index.js.map +1 -0
  286. package/packages/mcp-servers/dist/makerkit-docs/index.d.ts +5 -0
  287. package/packages/mcp-servers/dist/makerkit-docs/index.d.ts.map +1 -0
  288. package/packages/mcp-servers/dist/makerkit-docs/index.js +5 -0
  289. package/packages/mcp-servers/dist/makerkit-docs/index.js.map +1 -0
  290. package/packages/mcp-servers/dist/makerkit-docs/server.d.ts +15 -0
  291. package/packages/mcp-servers/dist/makerkit-docs/server.d.ts.map +1 -0
  292. package/packages/mcp-servers/dist/makerkit-docs/server.js +252 -0
  293. package/packages/mcp-servers/dist/makerkit-docs/server.js.map +1 -0
  294. package/packages/mcp-servers/dist/makerkit-docs/types.d.ts +74 -0
  295. package/packages/mcp-servers/dist/makerkit-docs/types.d.ts.map +1 -0
  296. package/packages/mcp-servers/dist/makerkit-docs/types.js +20 -0
  297. package/packages/mcp-servers/dist/makerkit-docs/types.js.map +1 -0
  298. package/packages/mcp-servers/dist/onepassword/index.d.ts +2 -0
  299. package/packages/mcp-servers/dist/onepassword/index.d.ts.map +1 -0
  300. package/packages/mcp-servers/dist/onepassword/index.js +2 -0
  301. package/packages/mcp-servers/dist/onepassword/index.js.map +1 -0
  302. package/packages/mcp-servers/dist/onepassword/server.d.ts +2 -0
  303. package/packages/mcp-servers/dist/onepassword/server.d.ts.map +1 -0
  304. package/packages/mcp-servers/dist/onepassword/server.js +159 -0
  305. package/packages/mcp-servers/dist/onepassword/server.js.map +1 -0
  306. package/packages/mcp-servers/dist/onepassword/types.d.ts +55 -0
  307. package/packages/mcp-servers/dist/onepassword/types.d.ts.map +1 -0
  308. package/packages/mcp-servers/dist/onepassword/types.js +22 -0
  309. package/packages/mcp-servers/dist/onepassword/types.js.map +1 -0
  310. package/packages/mcp-servers/dist/playwright/helpers.d.ts +20 -0
  311. package/packages/mcp-servers/dist/playwright/helpers.d.ts.map +1 -0
  312. package/packages/mcp-servers/dist/playwright/helpers.js +31 -0
  313. package/packages/mcp-servers/dist/playwright/helpers.js.map +1 -0
  314. package/packages/mcp-servers/dist/playwright/index.d.ts +5 -0
  315. package/packages/mcp-servers/dist/playwright/index.d.ts.map +1 -0
  316. package/packages/mcp-servers/dist/playwright/index.js +5 -0
  317. package/packages/mcp-servers/dist/playwright/index.js.map +1 -0
  318. package/packages/mcp-servers/dist/playwright/server.d.ts +13 -0
  319. package/packages/mcp-servers/dist/playwright/server.d.ts.map +1 -0
  320. package/packages/mcp-servers/dist/playwright/server.js +1201 -0
  321. package/packages/mcp-servers/dist/playwright/server.js.map +1 -0
  322. package/packages/mcp-servers/dist/playwright/types.d.ts +216 -0
  323. package/packages/mcp-servers/dist/playwright/types.d.ts.map +1 -0
  324. package/packages/mcp-servers/dist/playwright/types.js +172 -0
  325. package/packages/mcp-servers/dist/playwright/types.js.map +1 -0
  326. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts +39 -0
  327. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts.map +1 -0
  328. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js +71 -0
  329. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js.map +1 -0
  330. package/packages/mcp-servers/dist/playwright-feedback/index.d.ts +5 -0
  331. package/packages/mcp-servers/dist/playwright-feedback/index.d.ts.map +1 -0
  332. package/packages/mcp-servers/dist/playwright-feedback/index.js +5 -0
  333. package/packages/mcp-servers/dist/playwright-feedback/index.js.map +1 -0
  334. package/packages/mcp-servers/dist/playwright-feedback/server.d.ts +34 -0
  335. package/packages/mcp-servers/dist/playwright-feedback/server.d.ts.map +1 -0
  336. package/packages/mcp-servers/dist/playwright-feedback/server.js +538 -0
  337. package/packages/mcp-servers/dist/playwright-feedback/server.js.map +1 -0
  338. package/packages/mcp-servers/dist/playwright-feedback/types.d.ts +305 -0
  339. package/packages/mcp-servers/dist/playwright-feedback/types.d.ts.map +1 -0
  340. package/packages/mcp-servers/dist/playwright-feedback/types.js +123 -0
  341. package/packages/mcp-servers/dist/playwright-feedback/types.js.map +1 -0
  342. package/packages/mcp-servers/dist/product-manager/server.d.ts +17 -0
  343. package/packages/mcp-servers/dist/product-manager/server.d.ts.map +1 -0
  344. package/packages/mcp-servers/dist/product-manager/server.js +690 -0
  345. package/packages/mcp-servers/dist/product-manager/server.js.map +1 -0
  346. package/packages/mcp-servers/dist/product-manager/types.d.ts +286 -0
  347. package/packages/mcp-servers/dist/product-manager/types.d.ts.map +1 -0
  348. package/packages/mcp-servers/dist/product-manager/types.js +99 -0
  349. package/packages/mcp-servers/dist/product-manager/types.js.map +1 -0
  350. package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts +7 -0
  351. package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts.map +1 -0
  352. package/packages/mcp-servers/dist/programmatic-feedback/index.js +7 -0
  353. package/packages/mcp-servers/dist/programmatic-feedback/index.js.map +1 -0
  354. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts +19 -0
  355. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts.map +1 -0
  356. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js +174 -0
  357. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js.map +1 -0
  358. package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts +35 -0
  359. package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts.map +1 -0
  360. package/packages/mcp-servers/dist/programmatic-feedback/server.js +465 -0
  361. package/packages/mcp-servers/dist/programmatic-feedback/server.js.map +1 -0
  362. package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts +127 -0
  363. package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts.map +1 -0
  364. package/packages/mcp-servers/dist/programmatic-feedback/types.js +80 -0
  365. package/packages/mcp-servers/dist/programmatic-feedback/types.js.map +1 -0
  366. package/packages/mcp-servers/dist/render/index.d.ts +8 -0
  367. package/packages/mcp-servers/dist/render/index.d.ts.map +1 -0
  368. package/packages/mcp-servers/dist/render/index.js +8 -0
  369. package/packages/mcp-servers/dist/render/index.js.map +1 -0
  370. package/packages/mcp-servers/dist/render/server.d.ts +15 -0
  371. package/packages/mcp-servers/dist/render/server.d.ts.map +1 -0
  372. package/packages/mcp-servers/dist/render/server.js +428 -0
  373. package/packages/mcp-servers/dist/render/server.js.map +1 -0
  374. package/packages/mcp-servers/dist/render/types.d.ts +273 -0
  375. package/packages/mcp-servers/dist/render/types.d.ts.map +1 -0
  376. package/packages/mcp-servers/dist/render/types.js +102 -0
  377. package/packages/mcp-servers/dist/render/types.js.map +1 -0
  378. package/packages/mcp-servers/dist/resend/index.d.ts +7 -0
  379. package/packages/mcp-servers/dist/resend/index.d.ts.map +1 -0
  380. package/packages/mcp-servers/dist/resend/index.js +7 -0
  381. package/packages/mcp-servers/dist/resend/index.js.map +1 -0
  382. package/packages/mcp-servers/dist/resend/server.d.ts +15 -0
  383. package/packages/mcp-servers/dist/resend/server.d.ts.map +1 -0
  384. package/packages/mcp-servers/dist/resend/server.js +298 -0
  385. package/packages/mcp-servers/dist/resend/server.js.map +1 -0
  386. package/packages/mcp-servers/dist/resend/types.d.ts +222 -0
  387. package/packages/mcp-servers/dist/resend/types.d.ts.map +1 -0
  388. package/packages/mcp-servers/dist/resend/types.js +58 -0
  389. package/packages/mcp-servers/dist/resend/types.js.map +1 -0
  390. package/packages/mcp-servers/dist/review-queue/index.d.ts +6 -0
  391. package/packages/mcp-servers/dist/review-queue/index.d.ts.map +1 -0
  392. package/packages/mcp-servers/dist/review-queue/index.js +6 -0
  393. package/packages/mcp-servers/dist/review-queue/index.js.map +1 -0
  394. package/packages/mcp-servers/dist/review-queue/server.d.ts +17 -0
  395. package/packages/mcp-servers/dist/review-queue/server.d.ts.map +1 -0
  396. package/packages/mcp-servers/dist/review-queue/server.js +348 -0
  397. package/packages/mcp-servers/dist/review-queue/server.js.map +1 -0
  398. package/packages/mcp-servers/dist/review-queue/types.d.ts +162 -0
  399. package/packages/mcp-servers/dist/review-queue/types.d.ts.map +1 -0
  400. package/packages/mcp-servers/dist/review-queue/types.js +56 -0
  401. package/packages/mcp-servers/dist/review-queue/types.js.map +1 -0
  402. package/packages/mcp-servers/dist/secret-sync/server.d.ts +19 -0
  403. package/packages/mcp-servers/dist/secret-sync/server.d.ts.map +1 -0
  404. package/packages/mcp-servers/dist/secret-sync/server.js +1139 -0
  405. package/packages/mcp-servers/dist/secret-sync/server.js.map +1 -0
  406. package/packages/mcp-servers/dist/secret-sync/types.d.ts +442 -0
  407. package/packages/mcp-servers/dist/secret-sync/types.d.ts.map +1 -0
  408. package/packages/mcp-servers/dist/secret-sync/types.js +113 -0
  409. package/packages/mcp-servers/dist/secret-sync/types.js.map +1 -0
  410. package/packages/mcp-servers/dist/session-events/index.d.ts +5 -0
  411. package/packages/mcp-servers/dist/session-events/index.d.ts.map +1 -0
  412. package/packages/mcp-servers/dist/session-events/index.js +5 -0
  413. package/packages/mcp-servers/dist/session-events/index.js.map +1 -0
  414. package/packages/mcp-servers/dist/session-events/server.d.ts +11 -0
  415. package/packages/mcp-servers/dist/session-events/server.d.ts.map +1 -0
  416. package/packages/mcp-servers/dist/session-events/server.js +290 -0
  417. package/packages/mcp-servers/dist/session-events/server.js.map +1 -0
  418. package/packages/mcp-servers/dist/session-events/types.d.ts +213 -0
  419. package/packages/mcp-servers/dist/session-events/types.d.ts.map +1 -0
  420. package/packages/mcp-servers/dist/session-events/types.js +69 -0
  421. package/packages/mcp-servers/dist/session-events/types.js.map +1 -0
  422. package/packages/mcp-servers/dist/session-restart/index.d.ts +9 -0
  423. package/packages/mcp-servers/dist/session-restart/index.d.ts.map +1 -0
  424. package/packages/mcp-servers/dist/session-restart/index.js +9 -0
  425. package/packages/mcp-servers/dist/session-restart/index.js.map +1 -0
  426. package/packages/mcp-servers/dist/session-restart/server.d.ts +20 -0
  427. package/packages/mcp-servers/dist/session-restart/server.d.ts.map +1 -0
  428. package/packages/mcp-servers/dist/session-restart/server.js +411 -0
  429. package/packages/mcp-servers/dist/session-restart/server.js.map +1 -0
  430. package/packages/mcp-servers/dist/session-restart/types.d.ts +26 -0
  431. package/packages/mcp-servers/dist/session-restart/types.d.ts.map +1 -0
  432. package/packages/mcp-servers/dist/session-restart/types.js +16 -0
  433. package/packages/mcp-servers/dist/session-restart/types.js.map +1 -0
  434. package/packages/mcp-servers/dist/setup-helper/index.d.ts +5 -0
  435. package/packages/mcp-servers/dist/setup-helper/index.d.ts.map +1 -0
  436. package/packages/mcp-servers/dist/setup-helper/index.js +5 -0
  437. package/packages/mcp-servers/dist/setup-helper/index.js.map +1 -0
  438. package/packages/mcp-servers/dist/setup-helper/server.d.ts +14 -0
  439. package/packages/mcp-servers/dist/setup-helper/server.d.ts.map +1 -0
  440. package/packages/mcp-servers/dist/setup-helper/server.js +454 -0
  441. package/packages/mcp-servers/dist/setup-helper/server.js.map +1 -0
  442. package/packages/mcp-servers/dist/setup-helper/types.d.ts +81 -0
  443. package/packages/mcp-servers/dist/setup-helper/types.d.ts.map +1 -0
  444. package/packages/mcp-servers/dist/setup-helper/types.js +41 -0
  445. package/packages/mcp-servers/dist/setup-helper/types.js.map +1 -0
  446. package/packages/mcp-servers/dist/shared/audited-server.d.ts +31 -0
  447. package/packages/mcp-servers/dist/shared/audited-server.d.ts.map +1 -0
  448. package/packages/mcp-servers/dist/shared/audited-server.js +126 -0
  449. package/packages/mcp-servers/dist/shared/audited-server.js.map +1 -0
  450. package/packages/mcp-servers/dist/shared/constants.d.ts +26 -0
  451. package/packages/mcp-servers/dist/shared/constants.d.ts.map +1 -0
  452. package/packages/mcp-servers/dist/shared/constants.js +41 -0
  453. package/packages/mcp-servers/dist/shared/constants.js.map +1 -0
  454. package/packages/mcp-servers/dist/shared/index.d.ts +6 -0
  455. package/packages/mcp-servers/dist/shared/index.d.ts.map +1 -0
  456. package/packages/mcp-servers/dist/shared/index.js +6 -0
  457. package/packages/mcp-servers/dist/shared/index.js.map +1 -0
  458. package/packages/mcp-servers/dist/shared/readonly-db.d.ts +11 -0
  459. package/packages/mcp-servers/dist/shared/readonly-db.d.ts.map +1 -0
  460. package/packages/mcp-servers/dist/shared/readonly-db.js +47 -0
  461. package/packages/mcp-servers/dist/shared/readonly-db.js.map +1 -0
  462. package/packages/mcp-servers/dist/shared/resolve-framework.d.ts +20 -0
  463. package/packages/mcp-servers/dist/shared/resolve-framework.d.ts.map +1 -0
  464. package/packages/mcp-servers/dist/shared/resolve-framework.js +65 -0
  465. package/packages/mcp-servers/dist/shared/resolve-framework.js.map +1 -0
  466. package/packages/mcp-servers/dist/shared/server.d.ts +86 -0
  467. package/packages/mcp-servers/dist/shared/server.d.ts.map +1 -0
  468. package/packages/mcp-servers/dist/shared/server.js +291 -0
  469. package/packages/mcp-servers/dist/shared/server.js.map +1 -0
  470. package/packages/mcp-servers/dist/shared/types.d.ts +113 -0
  471. package/packages/mcp-servers/dist/shared/types.d.ts.map +1 -0
  472. package/packages/mcp-servers/dist/shared/types.js +36 -0
  473. package/packages/mcp-servers/dist/shared/types.js.map +1 -0
  474. package/packages/mcp-servers/dist/show/server.d.ts +12 -0
  475. package/packages/mcp-servers/dist/show/server.d.ts.map +1 -0
  476. package/packages/mcp-servers/dist/show/server.js +97 -0
  477. package/packages/mcp-servers/dist/show/server.js.map +1 -0
  478. package/packages/mcp-servers/dist/show/types.d.ts +19 -0
  479. package/packages/mcp-servers/dist/show/types.d.ts.map +1 -0
  480. package/packages/mcp-servers/dist/show/types.js +32 -0
  481. package/packages/mcp-servers/dist/show/types.js.map +1 -0
  482. package/packages/mcp-servers/dist/specs-browser/index.d.ts +5 -0
  483. package/packages/mcp-servers/dist/specs-browser/index.d.ts.map +1 -0
  484. package/packages/mcp-servers/dist/specs-browser/index.js +5 -0
  485. package/packages/mcp-servers/dist/specs-browser/index.js.map +1 -0
  486. package/packages/mcp-servers/dist/specs-browser/server.d.ts +13 -0
  487. package/packages/mcp-servers/dist/specs-browser/server.d.ts.map +1 -0
  488. package/packages/mcp-servers/dist/specs-browser/server.js +692 -0
  489. package/packages/mcp-servers/dist/specs-browser/server.js.map +1 -0
  490. package/packages/mcp-servers/dist/specs-browser/types.d.ts +337 -0
  491. package/packages/mcp-servers/dist/specs-browser/types.d.ts.map +1 -0
  492. package/packages/mcp-servers/dist/specs-browser/types.js +134 -0
  493. package/packages/mcp-servers/dist/specs-browser/types.js.map +1 -0
  494. package/packages/mcp-servers/dist/supabase/index.d.ts +10 -0
  495. package/packages/mcp-servers/dist/supabase/index.d.ts.map +1 -0
  496. package/packages/mcp-servers/dist/supabase/index.js +10 -0
  497. package/packages/mcp-servers/dist/supabase/index.js.map +1 -0
  498. package/packages/mcp-servers/dist/supabase/server.d.ts +20 -0
  499. package/packages/mcp-servers/dist/supabase/server.d.ts.map +1 -0
  500. package/packages/mcp-servers/dist/supabase/server.js +451 -0
  501. package/packages/mcp-servers/dist/supabase/server.js.map +1 -0
  502. package/packages/mcp-servers/dist/supabase/types.d.ts +196 -0
  503. package/packages/mcp-servers/dist/supabase/types.d.ts.map +1 -0
  504. package/packages/mcp-servers/dist/supabase/types.js +76 -0
  505. package/packages/mcp-servers/dist/supabase/types.js.map +1 -0
  506. package/packages/mcp-servers/dist/todo-db/index.d.ts +5 -0
  507. package/packages/mcp-servers/dist/todo-db/index.d.ts.map +1 -0
  508. package/packages/mcp-servers/dist/todo-db/index.js +5 -0
  509. package/packages/mcp-servers/dist/todo-db/index.js.map +1 -0
  510. package/packages/mcp-servers/dist/todo-db/server.d.ts +13 -0
  511. package/packages/mcp-servers/dist/todo-db/server.d.ts.map +1 -0
  512. package/packages/mcp-servers/dist/todo-db/server.js +649 -0
  513. package/packages/mcp-servers/dist/todo-db/server.js.map +1 -0
  514. package/packages/mcp-servers/dist/todo-db/types.d.ts +225 -0
  515. package/packages/mcp-servers/dist/todo-db/types.d.ts.map +1 -0
  516. package/packages/mcp-servers/dist/todo-db/types.js +69 -0
  517. package/packages/mcp-servers/dist/todo-db/types.js.map +1 -0
  518. package/packages/mcp-servers/dist/user-feedback/index.d.ts +7 -0
  519. package/packages/mcp-servers/dist/user-feedback/index.d.ts.map +1 -0
  520. package/packages/mcp-servers/dist/user-feedback/index.js +8 -0
  521. package/packages/mcp-servers/dist/user-feedback/index.js.map +1 -0
  522. package/packages/mcp-servers/dist/user-feedback/server.d.ts +25 -0
  523. package/packages/mcp-servers/dist/user-feedback/server.d.ts.map +1 -0
  524. package/packages/mcp-servers/dist/user-feedback/server.js +914 -0
  525. package/packages/mcp-servers/dist/user-feedback/server.js.map +1 -0
  526. package/packages/mcp-servers/dist/user-feedback/types.d.ts +415 -0
  527. package/packages/mcp-servers/dist/user-feedback/types.d.ts.map +1 -0
  528. package/packages/mcp-servers/dist/user-feedback/types.js +132 -0
  529. package/packages/mcp-servers/dist/user-feedback/types.js.map +1 -0
  530. package/packages/mcp-servers/dist/vercel/index.d.ts +9 -0
  531. package/packages/mcp-servers/dist/vercel/index.d.ts.map +1 -0
  532. package/packages/mcp-servers/dist/vercel/index.js +9 -0
  533. package/packages/mcp-servers/dist/vercel/index.js.map +1 -0
  534. package/packages/mcp-servers/dist/vercel/server.d.ts +17 -0
  535. package/packages/mcp-servers/dist/vercel/server.d.ts.map +1 -0
  536. package/packages/mcp-servers/dist/vercel/server.js +265 -0
  537. package/packages/mcp-servers/dist/vercel/server.js.map +1 -0
  538. package/packages/mcp-servers/dist/vercel/types.d.ts +189 -0
  539. package/packages/mcp-servers/dist/vercel/types.d.ts.map +1 -0
  540. package/packages/mcp-servers/dist/vercel/types.js +65 -0
  541. package/packages/mcp-servers/dist/vercel/types.js.map +1 -0
  542. package/packages/mcp-servers/package-lock.json +3765 -0
  543. package/packages/mcp-servers/package.json +64 -0
  544. package/packages/mcp-servers/test/reporters/test-failure-reporter.ts +372 -0
  545. package/packages/mcp-servers/vitest.config.ts +27 -0
  546. package/scripts/__tests__/README.md +163 -0
  547. package/scripts/apply-credential-hardening.sh +271 -0
  548. package/scripts/credential-providers/manual.js +56 -0
  549. package/scripts/credential-providers/onepassword.js +85 -0
  550. package/scripts/credential-providers/provider-interface.js +104 -0
  551. package/scripts/encrypt-credential.js +337 -0
  552. package/scripts/feedback-launcher.js +338 -0
  553. package/scripts/feedback-orchestrator.js +373 -0
  554. package/scripts/fix-mcp-launcher-issues.sh +97 -0
  555. package/scripts/force-spawn-tasks.js +651 -0
  556. package/scripts/force-triage-reports.js +560 -0
  557. package/scripts/generate-protected-actions-spec.js +142 -0
  558. package/scripts/generate-proxy-certs.sh +158 -0
  559. package/scripts/grant-chrome-ext-permissions.sh +242 -0
  560. package/scripts/mcp-launcher.js +125 -0
  561. package/scripts/merge-settings.cjs +167 -0
  562. package/scripts/patch-clawd.py +844 -0
  563. package/scripts/patch-credential-cache.py +313 -0
  564. package/scripts/patches/credential-file-guard-patched.mjs +573 -0
  565. package/scripts/patches/credential-file-guard.js.patched +573 -0
  566. package/scripts/patches/verify-tokenizer.mjs +132 -0
  567. package/scripts/protect-framework.sh +478 -0
  568. package/scripts/readme-chrome.template +12 -0
  569. package/scripts/reap-completed-agents.js +439 -0
  570. package/scripts/reinstall.sh +86 -0
  571. package/scripts/resign-node.sh +185 -0
  572. package/scripts/rotation-proxy.js +656 -0
  573. package/scripts/rotation-stress-monitor.mjs +862 -0
  574. package/scripts/setup-automation-service.sh +648 -0
  575. package/scripts/setup-check.js +251 -0
  576. package/scripts/watch-claude-version.js +142 -0
  577. package/specs/framework/CORE-INVARIANTS.md +161 -0
  578. package/specs/patterns/AGENT-PATTERNS.md +223 -0
  579. package/specs/patterns/HOOK-PATTERNS.md +242 -0
  580. package/specs/patterns/MCP-SERVER-PATTERNS.md +144 -0
  581. package/templates/config/gitignore.template +14 -0
  582. package/templates/config/merge-chain-check.yml.template +51 -0
  583. package/templates/config/package.json.template +18 -0
  584. package/templates/config/pnpm-workspace.yaml +5 -0
  585. package/templates/config/services.json.template +18 -0
  586. package/templates/config/tsconfig.base.json +17 -0
  587. package/templates/scaffold/integrations/_template/.gitkeep +0 -0
  588. package/templates/scaffold/packages/logger/package.json +17 -0
  589. package/templates/scaffold/packages/logger/src/logger.ts +44 -0
  590. package/templates/scaffold/packages/shared/package.json +17 -0
  591. package/templates/scaffold/packages/shared/src/errors.ts +43 -0
  592. package/templates/scaffold/products/_product/apps/backend/package.json +21 -0
  593. package/templates/scaffold/products/_product/apps/backend/src/index.ts +17 -0
  594. package/templates/scaffold/products/_product/apps/extension/.gitkeep +0 -0
  595. package/templates/scaffold/products/_product/apps/web/.gitkeep +0 -0
  596. package/templates/scaffold/specs/global/.gitkeep +0 -0
  597. package/templates/scaffold/specs/local/.gitkeep +0 -0
  598. package/templates/scaffold/specs/reference/.gitkeep +0 -0
  599. 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();