@jokerized/getresearchdone 0.4.1

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 (711) hide show
  1. package/.claude-plugin/plugin.json +103 -0
  2. package/README.md +211 -0
  3. package/agents/grd-baseline-assessor.md +684 -0
  4. package/agents/grd-code-reviewer.md +300 -0
  5. package/agents/grd-codebase-mapper.md +355 -0
  6. package/agents/grd-critique-agent.md +119 -0
  7. package/agents/grd-debugger.md +519 -0
  8. package/agents/grd-deep-diver.md +737 -0
  9. package/agents/grd-eval-planner.md +913 -0
  10. package/agents/grd-eval-reporter.md +717 -0
  11. package/agents/grd-executor.md +683 -0
  12. package/agents/grd-feasibility-analyst.md +624 -0
  13. package/agents/grd-integration-checker.md +367 -0
  14. package/agents/grd-knowledge-miner.md +81 -0
  15. package/agents/grd-migrator.md +88 -0
  16. package/agents/grd-phase-researcher.md +697 -0
  17. package/agents/grd-plan-checker.md +443 -0
  18. package/agents/grd-planner.md +1532 -0
  19. package/agents/grd-product-owner.md +562 -0
  20. package/agents/grd-project-researcher.md +513 -0
  21. package/agents/grd-research-synthesizer.md +273 -0
  22. package/agents/grd-roadmapper.md +798 -0
  23. package/agents/grd-surveyor.md +566 -0
  24. package/agents/grd-verifier.md +893 -0
  25. package/bin/gd.js +4 -0
  26. package/bin/gd.ts +227 -0
  27. package/bin/grd-manifest.js +4 -0
  28. package/bin/grd-manifest.ts +286 -0
  29. package/bin/grd-mcp-server.js +4 -0
  30. package/bin/grd-mcp-server.ts +124 -0
  31. package/bin/grd-tools.js +4 -0
  32. package/bin/grd-tools.ts +2471 -0
  33. package/bin/postinstall.js +4 -0
  34. package/bin/postinstall.ts +80 -0
  35. package/commands/add-phase.md +123 -0
  36. package/commands/add-todo.md +87 -0
  37. package/commands/assess-baseline.md +289 -0
  38. package/commands/autopilot.md +100 -0
  39. package/commands/autoplan.md +55 -0
  40. package/commands/check-todos.md +87 -0
  41. package/commands/compare-methods.md +262 -0
  42. package/commands/complete-milestone.md +225 -0
  43. package/commands/debug.md +372 -0
  44. package/commands/deep-dive.md +288 -0
  45. package/commands/discover.md +281 -0
  46. package/commands/discuss-phase.md +188 -0
  47. package/commands/discuss.md +55 -0
  48. package/commands/eval-report.md +310 -0
  49. package/commands/evolve.md +79 -0
  50. package/commands/execute-phase.md +1017 -0
  51. package/commands/feasibility.md +292 -0
  52. package/commands/help.md +407 -0
  53. package/commands/init.md +1508 -0
  54. package/commands/insert-phase.md +113 -0
  55. package/commands/iterate.md +327 -0
  56. package/commands/list-phase-assumptions.md +217 -0
  57. package/commands/long-term-roadmap.md +202 -0
  58. package/commands/map-codebase.md +111 -0
  59. package/commands/migrate.md +159 -0
  60. package/commands/new-milestone.md +169 -0
  61. package/commands/pause-work.md +83 -0
  62. package/commands/plan-milestone-gaps.md +373 -0
  63. package/commands/plan-phase.md +655 -0
  64. package/commands/principles.md +328 -0
  65. package/commands/product-plan.md +319 -0
  66. package/commands/progress.md +481 -0
  67. package/commands/quick.md +167 -0
  68. package/commands/reapply-patches.md +154 -0
  69. package/commands/remove-phase.md +97 -0
  70. package/commands/requirement.md +96 -0
  71. package/commands/resume-project.md +113 -0
  72. package/commands/settings.md +1144 -0
  73. package/commands/survey.md +242 -0
  74. package/commands/sync.md +246 -0
  75. package/commands/tracker-setup.md +322 -0
  76. package/commands/update.md +202 -0
  77. package/commands/verify-phase.md +335 -0
  78. package/commands/verify-work.md +701 -0
  79. package/commands/wireup.md +29 -0
  80. package/dist/bin/gd.d.ts +3 -0
  81. package/dist/bin/gd.d.ts.map +1 -0
  82. package/dist/bin/gd.js +178 -0
  83. package/dist/bin/gd.js.map +1 -0
  84. package/dist/bin/grd-manifest.d.ts +3 -0
  85. package/dist/bin/grd-manifest.d.ts.map +1 -0
  86. package/dist/bin/grd-manifest.js +202 -0
  87. package/dist/bin/grd-manifest.js.map +1 -0
  88. package/dist/bin/grd-mcp-server.d.ts +3 -0
  89. package/dist/bin/grd-mcp-server.d.ts.map +1 -0
  90. package/dist/bin/grd-mcp-server.js +71 -0
  91. package/dist/bin/grd-mcp-server.js.map +1 -0
  92. package/dist/bin/grd-tools.d.ts +3 -0
  93. package/dist/bin/grd-tools.d.ts.map +1 -0
  94. package/dist/bin/grd-tools.js +1680 -0
  95. package/dist/bin/grd-tools.js.map +1 -0
  96. package/dist/bin/postinstall.d.ts +3 -0
  97. package/dist/bin/postinstall.d.ts.map +1 -0
  98. package/dist/bin/postinstall.js +61 -0
  99. package/dist/bin/postinstall.js.map +1 -0
  100. package/dist/lib/autopilot-milestone.d.ts +2 -0
  101. package/dist/lib/autopilot-milestone.d.ts.map +1 -0
  102. package/dist/lib/autopilot-milestone.js +94 -0
  103. package/dist/lib/autopilot-milestone.js.map +1 -0
  104. package/dist/lib/autopilot-pipeline.d.ts +2 -0
  105. package/dist/lib/autopilot-pipeline.d.ts.map +1 -0
  106. package/dist/lib/autopilot-pipeline.js +830 -0
  107. package/dist/lib/autopilot-pipeline.js.map +1 -0
  108. package/dist/lib/autopilot-waves.d.ts +2 -0
  109. package/dist/lib/autopilot-waves.d.ts.map +1 -0
  110. package/dist/lib/autopilot-waves.js +266 -0
  111. package/dist/lib/autopilot-waves.js.map +1 -0
  112. package/dist/lib/autopilot.d.ts +2 -0
  113. package/dist/lib/autopilot.d.ts.map +1 -0
  114. package/dist/lib/autopilot.js +1314 -0
  115. package/dist/lib/autopilot.js.map +1 -0
  116. package/dist/lib/autoplan.d.ts +2 -0
  117. package/dist/lib/autoplan.d.ts.map +1 -0
  118. package/dist/lib/autoplan.js +198 -0
  119. package/dist/lib/autoplan.js.map +1 -0
  120. package/dist/lib/autoresearch.d.ts +2 -0
  121. package/dist/lib/autoresearch.d.ts.map +1 -0
  122. package/dist/lib/autoresearch.js +626 -0
  123. package/dist/lib/autoresearch.js.map +1 -0
  124. package/dist/lib/backend.d.ts +2 -0
  125. package/dist/lib/backend.d.ts.map +1 -0
  126. package/dist/lib/backend.js +1036 -0
  127. package/dist/lib/backend.js.map +1 -0
  128. package/dist/lib/benchmark.d.ts +99 -0
  129. package/dist/lib/benchmark.d.ts.map +1 -0
  130. package/dist/lib/benchmark.js +278 -0
  131. package/dist/lib/benchmark.js.map +1 -0
  132. package/dist/lib/citations.d.ts +2 -0
  133. package/dist/lib/citations.d.ts.map +1 -0
  134. package/dist/lib/citations.js +642 -0
  135. package/dist/lib/citations.js.map +1 -0
  136. package/dist/lib/cleanup.d.ts +2 -0
  137. package/dist/lib/cleanup.d.ts.map +1 -0
  138. package/dist/lib/cleanup.js +1222 -0
  139. package/dist/lib/cleanup.js.map +1 -0
  140. package/dist/lib/cli/adapters.d.ts +10 -0
  141. package/dist/lib/cli/adapters.d.ts.map +1 -0
  142. package/dist/lib/cli/adapters.js +27 -0
  143. package/dist/lib/cli/adapters.js.map +1 -0
  144. package/dist/lib/cli/agent.d.ts +17 -0
  145. package/dist/lib/cli/agent.d.ts.map +1 -0
  146. package/dist/lib/cli/agent.js +53 -0
  147. package/dist/lib/cli/agent.js.map +1 -0
  148. package/dist/lib/cli/index.d.ts +21 -0
  149. package/dist/lib/cli/index.d.ts.map +1 -0
  150. package/dist/lib/cli/index.js +264 -0
  151. package/dist/lib/cli/index.js.map +1 -0
  152. package/dist/lib/cli/output.d.ts +20 -0
  153. package/dist/lib/cli/output.d.ts.map +1 -0
  154. package/dist/lib/cli/output.js +22 -0
  155. package/dist/lib/cli/output.js.map +1 -0
  156. package/dist/lib/cli/scan-dispatch.d.ts +9 -0
  157. package/dist/lib/cli/scan-dispatch.d.ts.map +1 -0
  158. package/dist/lib/cli/scan-dispatch.js +107 -0
  159. package/dist/lib/cli/scan-dispatch.js.map +1 -0
  160. package/dist/lib/cli/tools.d.ts +16 -0
  161. package/dist/lib/cli/tools.d.ts.map +1 -0
  162. package/dist/lib/cli/tools.js +168 -0
  163. package/dist/lib/cli/tools.js.map +1 -0
  164. package/dist/lib/commands/_dashboard-parsers.d.ts +2 -0
  165. package/dist/lib/commands/_dashboard-parsers.d.ts.map +1 -0
  166. package/dist/lib/commands/_dashboard-parsers.js +192 -0
  167. package/dist/lib/commands/_dashboard-parsers.js.map +1 -0
  168. package/dist/lib/commands/analysis.d.ts +2 -0
  169. package/dist/lib/commands/analysis.d.ts.map +1 -0
  170. package/dist/lib/commands/analysis.js +1418 -0
  171. package/dist/lib/commands/analysis.js.map +1 -0
  172. package/dist/lib/commands/assumptions.d.ts +2 -0
  173. package/dist/lib/commands/assumptions.d.ts.map +1 -0
  174. package/dist/lib/commands/assumptions.js +166 -0
  175. package/dist/lib/commands/assumptions.js.map +1 -0
  176. package/dist/lib/commands/blame.d.ts +2 -0
  177. package/dist/lib/commands/blame.d.ts.map +1 -0
  178. package/dist/lib/commands/blame.js +133 -0
  179. package/dist/lib/commands/blame.js.map +1 -0
  180. package/dist/lib/commands/budget.d.ts +2 -0
  181. package/dist/lib/commands/budget.d.ts.map +1 -0
  182. package/dist/lib/commands/budget.js +100 -0
  183. package/dist/lib/commands/budget.js.map +1 -0
  184. package/dist/lib/commands/check-plans.d.ts +2 -0
  185. package/dist/lib/commands/check-plans.d.ts.map +1 -0
  186. package/dist/lib/commands/check-plans.js +190 -0
  187. package/dist/lib/commands/check-plans.js.map +1 -0
  188. package/dist/lib/commands/config.d.ts +2 -0
  189. package/dist/lib/commands/config.d.ts.map +1 -0
  190. package/dist/lib/commands/config.js +188 -0
  191. package/dist/lib/commands/config.js.map +1 -0
  192. package/dist/lib/commands/dashboard.d.ts +2 -0
  193. package/dist/lib/commands/dashboard.d.ts.map +1 -0
  194. package/dist/lib/commands/dashboard.js +466 -0
  195. package/dist/lib/commands/dashboard.js.map +1 -0
  196. package/dist/lib/commands/estimate.d.ts +2 -0
  197. package/dist/lib/commands/estimate.d.ts.map +1 -0
  198. package/dist/lib/commands/estimate.js +148 -0
  199. package/dist/lib/commands/estimate.js.map +1 -0
  200. package/dist/lib/commands/eval-diff.d.ts +2 -0
  201. package/dist/lib/commands/eval-diff.d.ts.map +1 -0
  202. package/dist/lib/commands/eval-diff.js +213 -0
  203. package/dist/lib/commands/eval-diff.js.map +1 -0
  204. package/dist/lib/commands/freshness.d.ts +2 -0
  205. package/dist/lib/commands/freshness.d.ts.map +1 -0
  206. package/dist/lib/commands/freshness.js +163 -0
  207. package/dist/lib/commands/freshness.js.map +1 -0
  208. package/dist/lib/commands/health.d.ts +2 -0
  209. package/dist/lib/commands/health.d.ts.map +1 -0
  210. package/dist/lib/commands/health.js +435 -0
  211. package/dist/lib/commands/health.js.map +1 -0
  212. package/dist/lib/commands/index.d.ts +2 -0
  213. package/dist/lib/commands/index.d.ts.map +1 -0
  214. package/dist/lib/commands/index.js +128 -0
  215. package/dist/lib/commands/index.js.map +1 -0
  216. package/dist/lib/commands/install.d.ts +56 -0
  217. package/dist/lib/commands/install.d.ts.map +1 -0
  218. package/dist/lib/commands/install.js +214 -0
  219. package/dist/lib/commands/install.js.map +1 -0
  220. package/dist/lib/commands/knowhow-aggregator.d.ts +2 -0
  221. package/dist/lib/commands/knowhow-aggregator.d.ts.map +1 -0
  222. package/dist/lib/commands/knowhow-aggregator.js +279 -0
  223. package/dist/lib/commands/knowhow-aggregator.js.map +1 -0
  224. package/dist/lib/commands/knowledge-search.d.ts +2 -0
  225. package/dist/lib/commands/knowledge-search.d.ts.map +1 -0
  226. package/dist/lib/commands/knowledge-search.js +113 -0
  227. package/dist/lib/commands/knowledge-search.js.map +1 -0
  228. package/dist/lib/commands/long-term-roadmap.d.ts +2 -0
  229. package/dist/lib/commands/long-term-roadmap.d.ts.map +1 -0
  230. package/dist/lib/commands/long-term-roadmap.js +272 -0
  231. package/dist/lib/commands/long-term-roadmap.js.map +1 -0
  232. package/dist/lib/commands/patterns.d.ts +91 -0
  233. package/dist/lib/commands/patterns.d.ts.map +1 -0
  234. package/dist/lib/commands/patterns.js +391 -0
  235. package/dist/lib/commands/patterns.js.map +1 -0
  236. package/dist/lib/commands/phase-info.d.ts +2 -0
  237. package/dist/lib/commands/phase-info.d.ts.map +1 -0
  238. package/dist/lib/commands/phase-info.js +509 -0
  239. package/dist/lib/commands/phase-info.js.map +1 -0
  240. package/dist/lib/commands/plan-lint.d.ts +56 -0
  241. package/dist/lib/commands/plan-lint.d.ts.map +1 -0
  242. package/dist/lib/commands/plan-lint.js +481 -0
  243. package/dist/lib/commands/plan-lint.js.map +1 -0
  244. package/dist/lib/commands/plan-phase.d.ts +53 -0
  245. package/dist/lib/commands/plan-phase.d.ts.map +1 -0
  246. package/dist/lib/commands/plan-phase.js +288 -0
  247. package/dist/lib/commands/plan-phase.js.map +1 -0
  248. package/dist/lib/commands/progress.d.ts +2 -0
  249. package/dist/lib/commands/progress.d.ts.map +1 -0
  250. package/dist/lib/commands/progress.js +266 -0
  251. package/dist/lib/commands/progress.js.map +1 -0
  252. package/dist/lib/commands/quality.d.ts +2 -0
  253. package/dist/lib/commands/quality.d.ts.map +1 -0
  254. package/dist/lib/commands/quality.js +80 -0
  255. package/dist/lib/commands/quality.js.map +1 -0
  256. package/dist/lib/commands/rollback.d.ts +2 -0
  257. package/dist/lib/commands/rollback.d.ts.map +1 -0
  258. package/dist/lib/commands/rollback.js +145 -0
  259. package/dist/lib/commands/rollback.js.map +1 -0
  260. package/dist/lib/commands/scan.d.ts +25 -0
  261. package/dist/lib/commands/scan.d.ts.map +1 -0
  262. package/dist/lib/commands/scan.js +28 -0
  263. package/dist/lib/commands/scan.js.map +1 -0
  264. package/dist/lib/commands/search.d.ts +2 -0
  265. package/dist/lib/commands/search.d.ts.map +1 -0
  266. package/dist/lib/commands/search.js +212 -0
  267. package/dist/lib/commands/search.js.map +1 -0
  268. package/dist/lib/commands/select-candidate.d.ts +128 -0
  269. package/dist/lib/commands/select-candidate.d.ts.map +1 -0
  270. package/dist/lib/commands/select-candidate.js +518 -0
  271. package/dist/lib/commands/select-candidate.js.map +1 -0
  272. package/dist/lib/commands/singularity.d.ts +2 -0
  273. package/dist/lib/commands/singularity.d.ts.map +1 -0
  274. package/dist/lib/commands/singularity.js +185 -0
  275. package/dist/lib/commands/singularity.js.map +1 -0
  276. package/dist/lib/commands/slug-timestamp.d.ts +2 -0
  277. package/dist/lib/commands/slug-timestamp.d.ts.map +1 -0
  278. package/dist/lib/commands/slug-timestamp.js +54 -0
  279. package/dist/lib/commands/slug-timestamp.js.map +1 -0
  280. package/dist/lib/commands/tail.d.ts +2 -0
  281. package/dist/lib/commands/tail.d.ts.map +1 -0
  282. package/dist/lib/commands/tail.js +100 -0
  283. package/dist/lib/commands/tail.js.map +1 -0
  284. package/dist/lib/commands/todo.d.ts +2 -0
  285. package/dist/lib/commands/todo.d.ts.map +1 -0
  286. package/dist/lib/commands/todo.js +200 -0
  287. package/dist/lib/commands/todo.js.map +1 -0
  288. package/dist/lib/commands/watch.d.ts +2 -0
  289. package/dist/lib/commands/watch.d.ts.map +1 -0
  290. package/dist/lib/commands/watch.js +72 -0
  291. package/dist/lib/commands/watch.js.map +1 -0
  292. package/dist/lib/complexity.d.ts +55 -0
  293. package/dist/lib/complexity.d.ts.map +1 -0
  294. package/dist/lib/complexity.js +80 -0
  295. package/dist/lib/complexity.js.map +1 -0
  296. package/dist/lib/context/agents.d.ts +2 -0
  297. package/dist/lib/context/agents.d.ts.map +1 -0
  298. package/dist/lib/context/agents.js +344 -0
  299. package/dist/lib/context/agents.js.map +1 -0
  300. package/dist/lib/context/base.d.ts +2 -0
  301. package/dist/lib/context/base.d.ts.map +1 -0
  302. package/dist/lib/context/base.js +81 -0
  303. package/dist/lib/context/base.js.map +1 -0
  304. package/dist/lib/context/execute.d.ts +2 -0
  305. package/dist/lib/context/execute.d.ts.map +1 -0
  306. package/dist/lib/context/execute.js +753 -0
  307. package/dist/lib/context/execute.js.map +1 -0
  308. package/dist/lib/context/index.d.ts +2 -0
  309. package/dist/lib/context/index.d.ts.map +1 -0
  310. package/dist/lib/context/index.js +88 -0
  311. package/dist/lib/context/index.js.map +1 -0
  312. package/dist/lib/context/progress.d.ts +2 -0
  313. package/dist/lib/context/progress.d.ts.map +1 -0
  314. package/dist/lib/context/progress.js +178 -0
  315. package/dist/lib/context/progress.js.map +1 -0
  316. package/dist/lib/context/project.d.ts +2 -0
  317. package/dist/lib/context/project.d.ts.map +1 -0
  318. package/dist/lib/context/project.js +413 -0
  319. package/dist/lib/context/project.js.map +1 -0
  320. package/dist/lib/context/research.d.ts +2 -0
  321. package/dist/lib/context/research.d.ts.map +1 -0
  322. package/dist/lib/context/research.js +466 -0
  323. package/dist/lib/context/research.js.map +1 -0
  324. package/dist/lib/dead-ends.d.ts +28 -0
  325. package/dist/lib/dead-ends.d.ts.map +1 -0
  326. package/dist/lib/dead-ends.js +451 -0
  327. package/dist/lib/dead-ends.js.map +1 -0
  328. package/dist/lib/deps.d.ts +2 -0
  329. package/dist/lib/deps.d.ts.map +1 -0
  330. package/dist/lib/deps.js +630 -0
  331. package/dist/lib/deps.js.map +1 -0
  332. package/dist/lib/discussion.d.ts +2 -0
  333. package/dist/lib/discussion.d.ts.map +1 -0
  334. package/dist/lib/discussion.js +1041 -0
  335. package/dist/lib/discussion.js.map +1 -0
  336. package/dist/lib/drift.d.ts +36 -0
  337. package/dist/lib/drift.d.ts.map +1 -0
  338. package/dist/lib/drift.js +481 -0
  339. package/dist/lib/drift.js.map +1 -0
  340. package/dist/lib/evolve/_dimensions-features.d.ts +2 -0
  341. package/dist/lib/evolve/_dimensions-features.d.ts.map +1 -0
  342. package/dist/lib/evolve/_dimensions-features.js +369 -0
  343. package/dist/lib/evolve/_dimensions-features.js.map +1 -0
  344. package/dist/lib/evolve/_dimensions.d.ts +2 -0
  345. package/dist/lib/evolve/_dimensions.d.ts.map +1 -0
  346. package/dist/lib/evolve/_dimensions.js +358 -0
  347. package/dist/lib/evolve/_dimensions.js.map +1 -0
  348. package/dist/lib/evolve/_product-ideation.d.ts +2 -0
  349. package/dist/lib/evolve/_product-ideation.d.ts.map +1 -0
  350. package/dist/lib/evolve/_product-ideation.js +281 -0
  351. package/dist/lib/evolve/_product-ideation.js.map +1 -0
  352. package/dist/lib/evolve/_prompts.d.ts +2 -0
  353. package/dist/lib/evolve/_prompts.d.ts.map +1 -0
  354. package/dist/lib/evolve/_prompts.js +153 -0
  355. package/dist/lib/evolve/_prompts.js.map +1 -0
  356. package/dist/lib/evolve/cli.d.ts +2 -0
  357. package/dist/lib/evolve/cli.d.ts.map +1 -0
  358. package/dist/lib/evolve/cli.js +224 -0
  359. package/dist/lib/evolve/cli.js.map +1 -0
  360. package/dist/lib/evolve/discovery.d.ts +2 -0
  361. package/dist/lib/evolve/discovery.d.ts.map +1 -0
  362. package/dist/lib/evolve/discovery.js +391 -0
  363. package/dist/lib/evolve/discovery.js.map +1 -0
  364. package/dist/lib/evolve/index.d.ts +2 -0
  365. package/dist/lib/evolve/index.d.ts.map +1 -0
  366. package/dist/lib/evolve/index.js +88 -0
  367. package/dist/lib/evolve/index.js.map +1 -0
  368. package/dist/lib/evolve/orchestrator.d.ts +2 -0
  369. package/dist/lib/evolve/orchestrator.d.ts.map +1 -0
  370. package/dist/lib/evolve/orchestrator.js +851 -0
  371. package/dist/lib/evolve/orchestrator.js.map +1 -0
  372. package/dist/lib/evolve/scoring.d.ts +2 -0
  373. package/dist/lib/evolve/scoring.d.ts.map +1 -0
  374. package/dist/lib/evolve/scoring.js +118 -0
  375. package/dist/lib/evolve/scoring.js.map +1 -0
  376. package/dist/lib/evolve/state.d.ts +2 -0
  377. package/dist/lib/evolve/state.d.ts.map +1 -0
  378. package/dist/lib/evolve/state.js +264 -0
  379. package/dist/lib/evolve/state.js.map +1 -0
  380. package/dist/lib/evolve/types.d.ts +249 -0
  381. package/dist/lib/evolve/types.d.ts.map +1 -0
  382. package/dist/lib/evolve/types.js +3 -0
  383. package/dist/lib/evolve/types.js.map +1 -0
  384. package/dist/lib/frontmatter.d.ts +2 -0
  385. package/dist/lib/frontmatter.d.ts.map +1 -0
  386. package/dist/lib/frontmatter.js +513 -0
  387. package/dist/lib/frontmatter.js.map +1 -0
  388. package/dist/lib/gates.d.ts +2 -0
  389. package/dist/lib/gates.d.ts.map +1 -0
  390. package/dist/lib/gates.js +578 -0
  391. package/dist/lib/gates.js.map +1 -0
  392. package/dist/lib/genome.d.ts +10 -0
  393. package/dist/lib/genome.d.ts.map +1 -0
  394. package/dist/lib/genome.js +368 -0
  395. package/dist/lib/genome.js.map +1 -0
  396. package/dist/lib/got.d.ts +2 -0
  397. package/dist/lib/got.d.ts.map +1 -0
  398. package/dist/lib/got.js +280 -0
  399. package/dist/lib/got.js.map +1 -0
  400. package/dist/lib/invariants.d.ts +2 -0
  401. package/dist/lib/invariants.d.ts.map +1 -0
  402. package/dist/lib/invariants.js +298 -0
  403. package/dist/lib/invariants.js.map +1 -0
  404. package/dist/lib/knowledge.d.ts +2 -0
  405. package/dist/lib/knowledge.d.ts.map +1 -0
  406. package/dist/lib/knowledge.js +658 -0
  407. package/dist/lib/knowledge.js.map +1 -0
  408. package/dist/lib/long-term-roadmap.d.ts +2 -0
  409. package/dist/lib/long-term-roadmap.d.ts.map +1 -0
  410. package/dist/lib/long-term-roadmap.js +602 -0
  411. package/dist/lib/long-term-roadmap.js.map +1 -0
  412. package/dist/lib/markdown-split.d.ts +2 -0
  413. package/dist/lib/markdown-split.d.ts.map +1 -0
  414. package/dist/lib/markdown-split.js +199 -0
  415. package/dist/lib/markdown-split.js.map +1 -0
  416. package/dist/lib/mcp-server.d.ts +2 -0
  417. package/dist/lib/mcp-server.d.ts.map +1 -0
  418. package/dist/lib/mcp-server.js +2424 -0
  419. package/dist/lib/mcp-server.js.map +1 -0
  420. package/dist/lib/metrics.d.ts +16 -0
  421. package/dist/lib/metrics.d.ts.map +1 -0
  422. package/dist/lib/metrics.js +48 -0
  423. package/dist/lib/metrics.js.map +1 -0
  424. package/dist/lib/overstory.d.ts +2 -0
  425. package/dist/lib/overstory.d.ts.map +1 -0
  426. package/dist/lib/overstory.js +211 -0
  427. package/dist/lib/overstory.js.map +1 -0
  428. package/dist/lib/parallel.d.ts +2 -0
  429. package/dist/lib/parallel.d.ts.map +1 -0
  430. package/dist/lib/parallel.js +349 -0
  431. package/dist/lib/parallel.js.map +1 -0
  432. package/dist/lib/paths.d.ts +2 -0
  433. package/dist/lib/paths.d.ts.map +1 -0
  434. package/dist/lib/paths.js +254 -0
  435. package/dist/lib/paths.js.map +1 -0
  436. package/dist/lib/phase-complete-llm.d.ts +22 -0
  437. package/dist/lib/phase-complete-llm.d.ts.map +1 -0
  438. package/dist/lib/phase-complete-llm.js +331 -0
  439. package/dist/lib/phase-complete-llm.js.map +1 -0
  440. package/dist/lib/phase-complete.d.ts +46 -0
  441. package/dist/lib/phase-complete.d.ts.map +1 -0
  442. package/dist/lib/phase-complete.js +278 -0
  443. package/dist/lib/phase-complete.js.map +1 -0
  444. package/dist/lib/phase-io.d.ts +2 -0
  445. package/dist/lib/phase-io.d.ts.map +1 -0
  446. package/dist/lib/phase-io.js +126 -0
  447. package/dist/lib/phase-io.js.map +1 -0
  448. package/dist/lib/phase.d.ts +2 -0
  449. package/dist/lib/phase.d.ts.map +1 -0
  450. package/dist/lib/phase.js +1344 -0
  451. package/dist/lib/phase.js.map +1 -0
  452. package/dist/lib/plan-tournament.d.ts +63 -0
  453. package/dist/lib/plan-tournament.d.ts.map +1 -0
  454. package/dist/lib/plan-tournament.js +353 -0
  455. package/dist/lib/plan-tournament.js.map +1 -0
  456. package/dist/lib/refinement.d.ts +74 -0
  457. package/dist/lib/refinement.d.ts.map +1 -0
  458. package/dist/lib/refinement.js +283 -0
  459. package/dist/lib/refinement.js.map +1 -0
  460. package/dist/lib/requirements.d.ts +2 -0
  461. package/dist/lib/requirements.d.ts.map +1 -0
  462. package/dist/lib/requirements.js +355 -0
  463. package/dist/lib/requirements.js.map +1 -0
  464. package/dist/lib/research-bundle.d.ts +2 -0
  465. package/dist/lib/research-bundle.d.ts.map +1 -0
  466. package/dist/lib/research-bundle.js +246 -0
  467. package/dist/lib/research-bundle.js.map +1 -0
  468. package/dist/lib/roadmap.d.ts +2 -0
  469. package/dist/lib/roadmap.d.ts.map +1 -0
  470. package/dist/lib/roadmap.js +541 -0
  471. package/dist/lib/roadmap.js.map +1 -0
  472. package/dist/lib/sample.d.ts +16 -0
  473. package/dist/lib/sample.d.ts.map +1 -0
  474. package/dist/lib/sample.js +20 -0
  475. package/dist/lib/sample.js.map +1 -0
  476. package/dist/lib/scaffold.d.ts +2 -0
  477. package/dist/lib/scaffold.d.ts.map +1 -0
  478. package/dist/lib/scaffold.js +355 -0
  479. package/dist/lib/scaffold.js.map +1 -0
  480. package/dist/lib/scan/_utils.d.ts +11 -0
  481. package/dist/lib/scan/_utils.d.ts.map +1 -0
  482. package/dist/lib/scan/_utils.js +36 -0
  483. package/dist/lib/scan/_utils.js.map +1 -0
  484. package/dist/lib/scan/base64.d.ts +15 -0
  485. package/dist/lib/scan/base64.d.ts.map +1 -0
  486. package/dist/lib/scan/base64.js +66 -0
  487. package/dist/lib/scan/base64.js.map +1 -0
  488. package/dist/lib/scan/ignorefile.d.ts +30 -0
  489. package/dist/lib/scan/ignorefile.d.ts.map +1 -0
  490. package/dist/lib/scan/ignorefile.js +101 -0
  491. package/dist/lib/scan/ignorefile.js.map +1 -0
  492. package/dist/lib/scan/injection.d.ts +14 -0
  493. package/dist/lib/scan/injection.d.ts.map +1 -0
  494. package/dist/lib/scan/injection.js +39 -0
  495. package/dist/lib/scan/injection.js.map +1 -0
  496. package/dist/lib/scan/patterns.d.ts +17 -0
  497. package/dist/lib/scan/patterns.d.ts.map +1 -0
  498. package/dist/lib/scan/patterns.js +123 -0
  499. package/dist/lib/scan/patterns.js.map +1 -0
  500. package/dist/lib/scan/strip-markdown.d.ts +7 -0
  501. package/dist/lib/scan/strip-markdown.d.ts.map +1 -0
  502. package/dist/lib/scan/strip-markdown.js +38 -0
  503. package/dist/lib/scan/strip-markdown.js.map +1 -0
  504. package/dist/lib/scan/types.d.ts +23 -0
  505. package/dist/lib/scan/types.d.ts.map +1 -0
  506. package/dist/lib/scan/types.js +3 -0
  507. package/dist/lib/scan/types.js.map +1 -0
  508. package/dist/lib/scheduler-wait.d.ts +2 -0
  509. package/dist/lib/scheduler-wait.d.ts.map +1 -0
  510. package/dist/lib/scheduler-wait.js +59 -0
  511. package/dist/lib/scheduler-wait.js.map +1 -0
  512. package/dist/lib/scheduler.d.ts +254 -0
  513. package/dist/lib/scheduler.d.ts.map +1 -0
  514. package/dist/lib/scheduler.js +1147 -0
  515. package/dist/lib/scheduler.js.map +1 -0
  516. package/dist/lib/state.d.ts +2 -0
  517. package/dist/lib/state.d.ts.map +1 -0
  518. package/dist/lib/state.js +744 -0
  519. package/dist/lib/state.js.map +1 -0
  520. package/dist/lib/think.d.ts +18 -0
  521. package/dist/lib/think.d.ts.map +1 -0
  522. package/dist/lib/think.js +317 -0
  523. package/dist/lib/think.js.map +1 -0
  524. package/dist/lib/tracker.d.ts +2 -0
  525. package/dist/lib/tracker.d.ts.map +1 -0
  526. package/dist/lib/tracker.js +1121 -0
  527. package/dist/lib/tracker.js.map +1 -0
  528. package/dist/lib/types.d.ts +1514 -0
  529. package/dist/lib/types.d.ts.map +1 -0
  530. package/dist/lib/types.js +4 -0
  531. package/dist/lib/types.js.map +1 -0
  532. package/dist/lib/utils.d.ts +2 -0
  533. package/dist/lib/utils.d.ts.map +1 -0
  534. package/dist/lib/utils.js +1363 -0
  535. package/dist/lib/utils.js.map +1 -0
  536. package/dist/lib/verify.d.ts +2 -0
  537. package/dist/lib/verify.d.ts.map +1 -0
  538. package/dist/lib/verify.js +1153 -0
  539. package/dist/lib/verify.js.map +1 -0
  540. package/dist/lib/wireup/autofix.d.ts +2 -0
  541. package/dist/lib/wireup/autofix.d.ts.map +1 -0
  542. package/dist/lib/wireup/autofix.js +188 -0
  543. package/dist/lib/wireup/autofix.js.map +1 -0
  544. package/dist/lib/wireup/cli.d.ts +2 -0
  545. package/dist/lib/wireup/cli.d.ts.map +1 -0
  546. package/dist/lib/wireup/cli.js +194 -0
  547. package/dist/lib/wireup/cli.js.map +1 -0
  548. package/dist/lib/wireup/detection.d.ts +47 -0
  549. package/dist/lib/wireup/detection.d.ts.map +1 -0
  550. package/dist/lib/wireup/detection.js +410 -0
  551. package/dist/lib/wireup/detection.js.map +1 -0
  552. package/dist/lib/wireup/discovery.d.ts +2 -0
  553. package/dist/lib/wireup/discovery.d.ts.map +1 -0
  554. package/dist/lib/wireup/discovery.js +934 -0
  555. package/dist/lib/wireup/discovery.js.map +1 -0
  556. package/dist/lib/wireup/execution.d.ts +2 -0
  557. package/dist/lib/wireup/execution.d.ts.map +1 -0
  558. package/dist/lib/wireup/execution.js +573 -0
  559. package/dist/lib/wireup/execution.js.map +1 -0
  560. package/dist/lib/wireup/index.d.ts +2 -0
  561. package/dist/lib/wireup/index.d.ts.map +1 -0
  562. package/dist/lib/wireup/index.js +85 -0
  563. package/dist/lib/wireup/index.js.map +1 -0
  564. package/dist/lib/wireup/orchestrator.d.ts +2 -0
  565. package/dist/lib/wireup/orchestrator.d.ts.map +1 -0
  566. package/dist/lib/wireup/orchestrator.js +366 -0
  567. package/dist/lib/wireup/orchestrator.js.map +1 -0
  568. package/dist/lib/wireup/report.d.ts +47 -0
  569. package/dist/lib/wireup/report.d.ts.map +1 -0
  570. package/dist/lib/wireup/report.js +201 -0
  571. package/dist/lib/wireup/report.js.map +1 -0
  572. package/dist/lib/wireup/scenarios.d.ts +2 -0
  573. package/dist/lib/wireup/scenarios.d.ts.map +1 -0
  574. package/dist/lib/wireup/scenarios.js +516 -0
  575. package/dist/lib/wireup/scenarios.js.map +1 -0
  576. package/dist/lib/wireup/state.d.ts +2 -0
  577. package/dist/lib/wireup/state.d.ts.map +1 -0
  578. package/dist/lib/wireup/state.js +102 -0
  579. package/dist/lib/wireup/state.js.map +1 -0
  580. package/dist/lib/wireup/types.d.ts +376 -0
  581. package/dist/lib/wireup/types.d.ts.map +1 -0
  582. package/dist/lib/wireup/types.js +3 -0
  583. package/dist/lib/wireup/types.js.map +1 -0
  584. package/dist/lib/worktree.d.ts +2 -0
  585. package/dist/lib/worktree.d.ts.map +1 -0
  586. package/dist/lib/worktree.js +999 -0
  587. package/dist/lib/worktree.js.map +1 -0
  588. package/lib/autopilot-milestone.ts +136 -0
  589. package/lib/autopilot-pipeline.ts +1179 -0
  590. package/lib/autopilot-waves.ts +361 -0
  591. package/lib/autopilot.ts +1874 -0
  592. package/lib/autoplan.ts +280 -0
  593. package/lib/autoresearch.js +4 -0
  594. package/lib/autoresearch.ts +886 -0
  595. package/lib/backend.ts +1252 -0
  596. package/lib/benchmark.ts +341 -0
  597. package/lib/citations.ts +760 -0
  598. package/lib/cleanup.ts +1588 -0
  599. package/lib/cli/adapters.ts +41 -0
  600. package/lib/cli/agent.ts +83 -0
  601. package/lib/cli/index.ts +273 -0
  602. package/lib/cli/output.ts +33 -0
  603. package/lib/cli/scan-dispatch.ts +130 -0
  604. package/lib/cli/tools.ts +198 -0
  605. package/lib/commands/_dashboard-parsers.ts +275 -0
  606. package/lib/commands/analysis.ts +1851 -0
  607. package/lib/commands/assumptions.ts +232 -0
  608. package/lib/commands/blame.ts +174 -0
  609. package/lib/commands/budget.ts +148 -0
  610. package/lib/commands/check-plans.ts +233 -0
  611. package/lib/commands/config.ts +287 -0
  612. package/lib/commands/dashboard.ts +680 -0
  613. package/lib/commands/estimate.ts +204 -0
  614. package/lib/commands/eval-diff.ts +252 -0
  615. package/lib/commands/freshness.ts +213 -0
  616. package/lib/commands/health.ts +607 -0
  617. package/lib/commands/index.ts +266 -0
  618. package/lib/commands/install.ts +307 -0
  619. package/lib/commands/knowhow-aggregator.ts +345 -0
  620. package/lib/commands/knowledge-search.ts +153 -0
  621. package/lib/commands/long-term-roadmap.ts +390 -0
  622. package/lib/commands/patterns.ts +465 -0
  623. package/lib/commands/phase-info.ts +698 -0
  624. package/lib/commands/plan-lint.ts +546 -0
  625. package/lib/commands/plan-phase.ts +375 -0
  626. package/lib/commands/progress.ts +319 -0
  627. package/lib/commands/quality.ts +138 -0
  628. package/lib/commands/rollback.ts +195 -0
  629. package/lib/commands/scan.ts +72 -0
  630. package/lib/commands/search.ts +300 -0
  631. package/lib/commands/select-candidate.ts +687 -0
  632. package/lib/commands/singularity.ts +222 -0
  633. package/lib/commands/slug-timestamp.ts +74 -0
  634. package/lib/commands/tail.ts +129 -0
  635. package/lib/commands/todo.ts +273 -0
  636. package/lib/commands/watch.ts +80 -0
  637. package/lib/complexity.ts +117 -0
  638. package/lib/context/agents.ts +505 -0
  639. package/lib/context/base.ts +123 -0
  640. package/lib/context/execute.ts +977 -0
  641. package/lib/context/index.ts +110 -0
  642. package/lib/context/progress.ts +278 -0
  643. package/lib/context/project.ts +531 -0
  644. package/lib/context/research.ts +646 -0
  645. package/lib/dead-ends.ts +506 -0
  646. package/lib/deps.ts +773 -0
  647. package/lib/discussion.ts +1275 -0
  648. package/lib/drift.ts +519 -0
  649. package/lib/evolve/_dimensions-features.ts +525 -0
  650. package/lib/evolve/_dimensions.ts +511 -0
  651. package/lib/evolve/_product-ideation.ts +405 -0
  652. package/lib/evolve/_prompts.ts +178 -0
  653. package/lib/evolve/cli.ts +330 -0
  654. package/lib/evolve/discovery.ts +571 -0
  655. package/lib/evolve/index.ts +105 -0
  656. package/lib/evolve/orchestrator.ts +1139 -0
  657. package/lib/evolve/scoring.ts +167 -0
  658. package/lib/evolve/state.ts +330 -0
  659. package/lib/evolve/types.ts +290 -0
  660. package/lib/frontmatter.ts +615 -0
  661. package/lib/gates.ts +695 -0
  662. package/lib/genome.ts +402 -0
  663. package/lib/got.js +4 -0
  664. package/lib/got.ts +361 -0
  665. package/lib/invariants.ts +378 -0
  666. package/lib/knowledge.ts +768 -0
  667. package/lib/long-term-roadmap.ts +806 -0
  668. package/lib/markdown-split.ts +273 -0
  669. package/lib/mcp-server.ts +3292 -0
  670. package/lib/metrics.ts +49 -0
  671. package/lib/overstory.ts +270 -0
  672. package/lib/parallel.ts +570 -0
  673. package/lib/paths.ts +293 -0
  674. package/lib/phase-complete-llm.ts +376 -0
  675. package/lib/phase-complete.ts +366 -0
  676. package/lib/phase-io.ts +101 -0
  677. package/lib/phase.ts +1981 -0
  678. package/lib/plan-tournament.ts +426 -0
  679. package/lib/refinement.ts +349 -0
  680. package/lib/requirements.ts +469 -0
  681. package/lib/research-bundle.ts +300 -0
  682. package/lib/roadmap.ts +775 -0
  683. package/lib/scaffold.ts +480 -0
  684. package/lib/scan/_utils.ts +37 -0
  685. package/lib/scan/base64.ts +90 -0
  686. package/lib/scan/ignorefile.ts +109 -0
  687. package/lib/scan/injection.ts +67 -0
  688. package/lib/scan/patterns.ts +139 -0
  689. package/lib/scan/strip-markdown.ts +39 -0
  690. package/lib/scan/types.ts +28 -0
  691. package/lib/scheduler-wait.ts +58 -0
  692. package/lib/scheduler.ts +1370 -0
  693. package/lib/state.ts +1000 -0
  694. package/lib/think.ts +365 -0
  695. package/lib/tracker.ts +1591 -0
  696. package/lib/types.ts +1663 -0
  697. package/lib/utils.ts +1479 -0
  698. package/lib/verify.ts +1434 -0
  699. package/lib/wireup/autofix.ts +241 -0
  700. package/lib/wireup/cli.ts +278 -0
  701. package/lib/wireup/detection.ts +542 -0
  702. package/lib/wireup/discovery.ts +1063 -0
  703. package/lib/wireup/execution.ts +686 -0
  704. package/lib/wireup/index.ts +117 -0
  705. package/lib/wireup/orchestrator.ts +519 -0
  706. package/lib/wireup/report.ts +286 -0
  707. package/lib/wireup/scenarios.ts +616 -0
  708. package/lib/wireup/state.ts +139 -0
  709. package/lib/wireup/types.ts +436 -0
  710. package/lib/worktree.ts +1309 -0
  711. package/package.json +67 -0
@@ -0,0 +1,1370 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GRD Scheduler -- Backend subprocess spawning, account rotation, budget pressure tracking,
5
+ * and idle watchdog. Dispatches agents to claude/codex/gemini/opencode CLI backends with
6
+ * per-account token budget monitoring and adaptive model-tier downgrade under pressure.
7
+ *
8
+ * @module scheduler
9
+ */
10
+
11
+ import type {
12
+ BackendId,
13
+ AdapterBackendId,
14
+ BackendAdapter,
15
+ BackendUsageState,
16
+ UsageSample,
17
+ SpawnOpts,
18
+ AccountResolution,
19
+ SuperpowersConfig,
20
+ SchedulerConfig,
21
+ SchedulerSpawnResult,
22
+ BudgetPressureLevel,
23
+ BudgetPressureThresholds,
24
+ } from './types';
25
+ import type * as childProcess from 'child_process';
26
+
27
+ const { waitUntilOrAbort } = require('./scheduler-wait') as {
28
+ waitUntilOrAbort: (targetMs: number) => Promise<'waited' | 'aborted'>;
29
+ };
30
+
31
+ const { incrementCounter } = require('./metrics') as {
32
+ incrementCounter: (name: string, delta?: number) => void;
33
+ };
34
+
35
+ // ─── Per-backend CLI Adapters ─────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Map of backend adapters for all supported CLI backends.
39
+ * Each adapter encapsulates binary name, argument building, token parsing,
40
+ * and rate-limit detection for a specific backend CLI.
41
+ *
42
+ * Meta-backends (superpowers, grd) are not included — they are scheduling
43
+ * strategies that resolve to one of these real adapters at spawn time.
44
+ */
45
+ const _claudeAdapter: BackendAdapter = {
46
+ binary: 'claude',
47
+ buildArgs(prompt: string, opts: SpawnOpts): string[] {
48
+ const args = ['-p', prompt, '--verbose', '--dangerously-skip-permissions'];
49
+ if (opts.maxTurns) {
50
+ args.push('--max-turns', String(opts.maxTurns));
51
+ }
52
+ if (opts.model) {
53
+ args.push('--model', opts.model);
54
+ }
55
+ args.push('--output-format', 'json');
56
+ return args;
57
+ },
58
+ parseTokenUsage(stderr: string): number | null {
59
+ const totalMatch = stderr.match(/[Tt]otal.tokens:\s*(\d+)/);
60
+ if (totalMatch) return parseInt(totalMatch[1], 10);
61
+ const inputMatch = stderr.match(/input_tokens:\s*(\d+)/);
62
+ const outputMatch = stderr.match(/output_tokens:\s*(\d+)/);
63
+ if (inputMatch && outputMatch) {
64
+ return parseInt(inputMatch[1], 10) + parseInt(outputMatch[1], 10);
65
+ }
66
+ return null;
67
+ },
68
+ isRateLimited(exitCode: number, stderr: string): boolean {
69
+ if (exitCode === 0) return false;
70
+ return /rate.limit|429|overloaded_error|too many requests/i.test(stderr);
71
+ },
72
+ };
73
+
74
+ export const ADAPTERS: Record<AdapterBackendId, BackendAdapter> = {
75
+ claude: _claudeAdapter,
76
+
77
+ codex: {
78
+ binary: 'codex',
79
+ buildArgs(prompt: string, opts: SpawnOpts): string[] {
80
+ const args = ['--prompt', prompt, '--approval-mode', 'full-auto'];
81
+ if (opts.model) {
82
+ args.push('--model', opts.model);
83
+ }
84
+ return args;
85
+ },
86
+ parseTokenUsage(stderr: string): number | null {
87
+ const match = stderr.match(/"total_tokens":\s*(\d+)/);
88
+ return match ? parseInt(match[1], 10) : null;
89
+ },
90
+ isRateLimited(_exitCode: number, stderr: string): boolean {
91
+ return /rate.limit|429|rate_limit_exceeded/i.test(stderr);
92
+ },
93
+ },
94
+
95
+ gemini: {
96
+ binary: 'gemini',
97
+ buildArgs(prompt: string, opts: SpawnOpts): string[] {
98
+ const args = ['-p', prompt, '--sandbox', 'off'];
99
+ if (opts.model) {
100
+ args.push('--model', opts.model);
101
+ }
102
+ return args;
103
+ },
104
+ parseTokenUsage(stderr: string): number | null {
105
+ const match = stderr.match(/tokenCount["\s:]*(\d+)/);
106
+ return match ? parseInt(match[1], 10) : null;
107
+ },
108
+ isRateLimited(_exitCode: number, stderr: string): boolean {
109
+ return /rate.limit|429|RESOURCE_EXHAUSTED|quota/i.test(stderr);
110
+ },
111
+ },
112
+
113
+ opencode: {
114
+ binary: 'opencode',
115
+ buildArgs(prompt: string, opts: SpawnOpts): string[] {
116
+ const args = ['--non-interactive', '--prompt', prompt];
117
+ if (opts.model) {
118
+ args.push('--model', opts.model);
119
+ }
120
+ return args;
121
+ },
122
+ parseTokenUsage(stderr: string): number | null {
123
+ const match = stderr.match(/(?:total_tokens|tokens?.used)[\s:"]*(\d+)/i);
124
+ return match ? parseInt(match[1], 10) : null;
125
+ },
126
+ isRateLimited(_exitCode: number, stderr: string): boolean {
127
+ return /rate.limit|429|too many requests|quota/i.test(stderr);
128
+ },
129
+ },
130
+
131
+ overstory: {
132
+ binary: 'ov',
133
+ buildArgs(prompt: string, opts: SpawnOpts): string[] {
134
+ const args = ['run', '--prompt', prompt];
135
+ if (opts.model) {
136
+ args.push('--model', opts.model);
137
+ }
138
+ return args;
139
+ },
140
+ parseTokenUsage(stderr: string): number | null {
141
+ const match = stderr.match(/tokens?:\s*(\d+)/i);
142
+ return match ? parseInt(match[1], 10) : null;
143
+ },
144
+ isRateLimited(_exitCode: number, stderr: string): boolean {
145
+ return /rate.limit|429|quota/i.test(stderr);
146
+ },
147
+ },
148
+ };
149
+
150
+ /**
151
+ * Maps each adapter backend to its config-directory environment variable.
152
+ * Used by account rotation to override which account a CLI binary uses.
153
+ */
154
+ export const ENV_VAR_MAP: Record<AdapterBackendId, string> = {
155
+ claude: 'CLAUDE_CONFIG_DIR',
156
+ codex: 'CODEX_HOME',
157
+ gemini: 'GEMINI_CLI_HOME',
158
+ opencode: 'OPENCODE_CONFIG_DIR',
159
+ overstory: 'OVERSTORY_HOME',
160
+ };
161
+
162
+ // ─── EWMA and Rolling Window ──────────────────────────────────────────────────
163
+
164
+ /** Default token-per-minute budget for backends with no explicit limit configured. */
165
+ const DEFAULT_BUDGET_TPM = 40000;
166
+
167
+ /** Token-per-minute budget for the free-fallback backend (effectively unlimited). */
168
+ export const FREE_FALLBACK_BUDGET = 1000000;
169
+
170
+ /**
171
+ * Creates a fresh BackendUsageState with the given token budget.
172
+ *
173
+ * @param tokenBudget - tokens-per-minute budget for this backend
174
+ * @returns initialized state with zeroed counters
175
+ */
176
+ export function createBackendState(tokenBudget: number): BackendUsageState {
177
+ return {
178
+ samples: [],
179
+ ewma_tokens_per_task: 0,
180
+ tokens_consumed_in_window: 0,
181
+ tokens_reserved: 0,
182
+ in_flight_count: 0,
183
+ token_budget: tokenBudget,
184
+ budget_learned: false,
185
+ budget_confidence: 0,
186
+ cooldown_until: undefined,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Updates the EWMA estimate in-place with a new token observation.
192
+ * On first observation (ewma === 0), sets directly to the observed value.
193
+ *
194
+ * @param state - backend usage state to update
195
+ * @param tokens - observed token count for the latest task
196
+ * @param alpha - EWMA smoothing factor (0 < alpha < 1)
197
+ */
198
+ export function updateEWMA(state: BackendUsageState, tokens: number, alpha: number): void {
199
+ if (state.ewma_tokens_per_task === 0) {
200
+ state.ewma_tokens_per_task = tokens;
201
+ } else {
202
+ state.ewma_tokens_per_task = alpha * tokens + (1 - alpha) * state.ewma_tokens_per_task;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Removes samples older than windowMinutes from state and recalculates
208
+ * tokens_consumed_in_window from the remaining samples.
209
+ *
210
+ * @param state - backend usage state to mutate
211
+ * @param windowMinutes - rolling window duration in minutes
212
+ */
213
+ export function evictExpiredSamples(state: BackendUsageState, windowMinutes: number): void {
214
+ if (windowMinutes <= 0) return;
215
+ const cutoff = Date.now() - windowMinutes * 60 * 1000;
216
+ state.samples = state.samples.filter((s) => s.timestamp >= cutoff);
217
+ state.tokens_consumed_in_window = state.samples.reduce((sum, s) => sum + s.tokenEstimate, 0);
218
+ }
219
+
220
+ /**
221
+ * Records a completed usage sample, evicts stale samples from the window,
222
+ * updates EWMA, and recalculates budget_confidence.
223
+ *
224
+ * @param state - backend usage state to update
225
+ * @param sample - new usage sample to record
226
+ * @param windowMinutes - rolling window duration in minutes
227
+ * @param alpha - EWMA smoothing factor
228
+ */
229
+ export function recordSample(
230
+ state: BackendUsageState,
231
+ sample: UsageSample,
232
+ windowMinutes: number,
233
+ alpha: number
234
+ ): void {
235
+ state.samples.push(sample);
236
+ evictExpiredSamples(state, windowMinutes);
237
+ updateEWMA(state, sample.tokenEstimate, alpha);
238
+ state.budget_confidence = 1 - 1 / (1 + state.samples.length * 0.2);
239
+ }
240
+
241
+ // ─── Backend Picker with Concurrency Accounting ───────────────────────────────
242
+
243
+ /**
244
+ * Selects the highest-priority backend that has sufficient token headroom.
245
+ * Skips backends in cooldown or without enough remaining capacity (accounting
246
+ * for in-flight reservations). Falls back to freeFallback if none qualify.
247
+ *
248
+ * @param priority - ordered list of backend IDs to try
249
+ * @param states - map of backend ID to usage state
250
+ * @param safetyMargin - minimum remaining tasks before a backend is considered full
251
+ * @param freeFallback - fallback backend used when all priority backends are exhausted
252
+ * @returns selected BackendId
253
+ */
254
+ export function pickBackend(
255
+ priority: BackendId[],
256
+ states: Map<string, BackendUsageState>,
257
+ safetyMargin: number,
258
+ freeFallback: { backend: BackendId }
259
+ ): BackendId {
260
+ const now = Date.now();
261
+ for (const backend of priority) {
262
+ const state = states.get(backend);
263
+ if (!state) continue;
264
+ if (state.cooldown_until && state.cooldown_until > now) continue;
265
+ if (state.ewma_tokens_per_task === 0) return backend;
266
+ const effective = state.tokens_consumed_in_window + state.tokens_reserved;
267
+ const remaining = state.token_budget - effective;
268
+ const tasksRemaining = remaining / state.ewma_tokens_per_task;
269
+ if (tasksRemaining >= safetyMargin) return backend;
270
+ }
271
+ return freeFallback.backend;
272
+ }
273
+
274
+ // ─── Account Resolution Waterfall ─────────────────────────────────────────────
275
+
276
+ /**
277
+ * Checks whether a single account state key has sufficient headroom for
278
+ * scheduling, considering EWMA prediction, in-flight reservations, and cooldown.
279
+ *
280
+ * @param state - the account's usage state
281
+ * @param safetyMargin - minimum remaining tasks before considered full
282
+ * @returns true if the account has capacity or no EWMA data yet
283
+ */
284
+ function _hasHeadroom(state: BackendUsageState, safetyMargin: number): boolean {
285
+ const now = Date.now();
286
+ if (state.cooldown_until && state.cooldown_until > now) return false;
287
+ if (state.ewma_tokens_per_task === 0) return true;
288
+ const effective = state.tokens_consumed_in_window + state.tokens_reserved;
289
+ const remaining = state.token_budget - effective;
290
+ const tasksRemaining = remaining / state.ewma_tokens_per_task;
291
+ return tasksRemaining >= safetyMargin;
292
+ }
293
+
294
+ /**
295
+ * Sends `signal` to the process group of `child` on POSIX platforms, or to
296
+ * the direct child on Windows. Using a negative PID with process.kill ensures
297
+ * grandchildren (e.g., tool-invocation forks spawned by the backend CLI) are
298
+ * also terminated.
299
+ *
300
+ * Requires the child to have been spawned with `detached: true` so that it
301
+ * gets its own process group (pgid === pid).
302
+ *
303
+ * @param child - the spawned ChildProcess whose process group to signal
304
+ * @param signal - signal to send (e.g. 'SIGTERM', 'SIGKILL')
305
+ */
306
+ export function _killProcessTree(
307
+ child: Pick<childProcess.ChildProcess, 'pid' | 'kill'>,
308
+ signal: NodeJS.Signals
309
+ ): void {
310
+ if (child.pid === undefined) return;
311
+ if (process.platform === 'win32') {
312
+ // Windows: just kill the direct child (no POSIX process groups)
313
+ try {
314
+ child.kill(signal);
315
+ } catch {
316
+ /* already dead */
317
+ }
318
+ return;
319
+ }
320
+ // POSIX: signal the whole process group via negative pid
321
+ try {
322
+ process.kill(-child.pid, signal);
323
+ } catch (e) {
324
+ // ESRCH (no such process) is benign — process already exited.
325
+ // Fall back to direct kill in case the group wasn't created (e.g., race).
326
+ if ((e as NodeJS.ErrnoException).code !== 'ESRCH') {
327
+ try {
328
+ child.kill(signal);
329
+ } catch {
330
+ /* already dead */
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Resolves the idle timeout in seconds for the given backend, applying the
338
+ * lookup order: per-backend override → global idle_timeout_seconds → default 900.
339
+ *
340
+ * @param backend - backend ID (e.g. 'claude', 'gemini')
341
+ * @param config - subset of SchedulerConfig with timeout fields
342
+ * @returns resolved idle timeout in seconds
343
+ */
344
+ export function _resolveIdleTimeoutSeconds(
345
+ backend: string,
346
+ config: {
347
+ idle_timeout_seconds_by_backend?: Record<string, number>;
348
+ idle_timeout_seconds?: number;
349
+ }
350
+ ): number {
351
+ return config.idle_timeout_seconds_by_backend?.[backend] ?? config.idle_timeout_seconds ?? 900;
352
+ }
353
+
354
+ /**
355
+ * Starts an idle watchdog that invokes `onIdle` when no markActivity
356
+ * has been called for longer than `idleTimeoutMs`. Returns markActivity
357
+ * and stop functions.
358
+ *
359
+ * Polls every 1000ms. Fires at most once — subsequent ticks are no-ops.
360
+ */
361
+ export function _startIdleWatchdog(
362
+ idleTimeoutMs: number,
363
+ onIdle: () => void
364
+ ): { markActivity: () => void; stop: () => void } {
365
+ const POLL_INTERVAL_MS = 1000;
366
+ let lastActivityAt = Date.now();
367
+ let stopped = false;
368
+
369
+ const timer = setInterval(() => {
370
+ if (stopped) return;
371
+ if (Date.now() - lastActivityAt >= idleTimeoutMs) {
372
+ stopped = true;
373
+ clearInterval(timer);
374
+ onIdle();
375
+ }
376
+ }, POLL_INTERVAL_MS);
377
+
378
+ return {
379
+ markActivity: () => {
380
+ lastActivityAt = Date.now();
381
+ },
382
+ stop: () => {
383
+ if (stopped) return;
384
+ stopped = true;
385
+ clearInterval(timer);
386
+ },
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Returns true iff at least one account in the priority list has headroom.
392
+ * Small helper used by the _spawnWithRetry wait-branch decision.
393
+ */
394
+ export function _anyPriorityHasHeadroom(
395
+ priority: BackendId[],
396
+ accounts: SuperpowersConfig['accounts'],
397
+ states: Map<string, BackendUsageState>,
398
+ safetyMargin: number
399
+ ): boolean {
400
+ for (const backend of priority) {
401
+ const backendAccounts = accounts[backend as AdapterBackendId] || [];
402
+ for (const account of backendAccounts) {
403
+ const stateKey = `${backend}/${account.config_dir}`;
404
+ const state = states.get(stateKey);
405
+ if (!state) continue;
406
+ if (_hasHeadroom(state, safetyMargin)) return true;
407
+ }
408
+ }
409
+ return false;
410
+ }
411
+
412
+ /**
413
+ * Computes the earliest timestamp (ms since epoch) at which ANY priority
414
+ * account will regain headroom based on sample aging out of the rolling
415
+ * window. Used by the wait-loop in _spawnWithRetry when all priority
416
+ * accounts are currently exhausted.
417
+ *
418
+ * For each priority account, walks its samples oldest-first, hypothetically
419
+ * dropping each one and recomputing projected headroom. The latest-dropped
420
+ * sample's timestamp + windowMinutes is the moment that account will have
421
+ * enough headroom for one more EWMA-sized task.
422
+ *
423
+ * Returns null if:
424
+ * - No priority account has samples (nothing to wait for)
425
+ * - Soonest recovery across all accounts is beyond Date.now() + maxWaitMs
426
+ * - All considered accounts have zero ewma_tokens_per_task (no prediction data)
427
+ *
428
+ * Pattern adopted from gsd-2 v2.67 auto-timeout-recovery.ts — but
429
+ * sample-based rather than attempt-based.
430
+ *
431
+ * Note: tokens_reserved (in-flight EWMA cost) is held constant during the
432
+ * simulation because in-flight tasks are expected to complete independently
433
+ * of sample aging. This makes the estimate slightly pessimistic — actual
434
+ * headroom may return sooner.
435
+ */
436
+ export function computeSoonestRecovery(
437
+ states: Map<string, BackendUsageState>,
438
+ priority: BackendId[],
439
+ accounts: SuperpowersConfig['accounts'],
440
+ windowMinutes: number,
441
+ maxWaitMs: number
442
+ ): number | null {
443
+ const now = Date.now();
444
+ let soonest = Infinity;
445
+
446
+ for (const backend of priority) {
447
+ const backendAccounts = accounts[backend as AdapterBackendId] || [];
448
+ for (const account of backendAccounts) {
449
+ const stateKey = `${backend}/${account.config_dir}`;
450
+ const state = states.get(stateKey);
451
+ if (!state || state.samples.length === 0) continue;
452
+ if (state.ewma_tokens_per_task === 0) continue;
453
+
454
+ const sortedSamples = [...state.samples].sort((a, b) => a.timestamp - b.timestamp);
455
+
456
+ const ewmaCost = state.ewma_tokens_per_task;
457
+ let consumed = state.tokens_consumed_in_window;
458
+ const reserved = state.tokens_reserved;
459
+ let latestDroppedTs: number | null = null;
460
+
461
+ for (const sample of sortedSamples) {
462
+ const projectedRemaining = state.token_budget - consumed - reserved;
463
+ if (projectedRemaining >= ewmaCost) break;
464
+ consumed -= sample.tokenEstimate;
465
+ latestDroppedTs = sample.timestamp;
466
+ }
467
+
468
+ if (latestDroppedTs === null) continue;
469
+ const recoveryTime = latestDroppedTs + windowMinutes * 60 * 1000;
470
+ if (recoveryTime < soonest) soonest = recoveryTime;
471
+ }
472
+ }
473
+
474
+ if (soonest === Infinity) return null;
475
+ if (soonest > now + maxWaitMs) return null;
476
+ return soonest;
477
+ }
478
+
479
+ // ─── Spec 4: budget pressure detection ────────────────────────────────────
480
+
481
+ /**
482
+ * Default thresholds for budget pressure classification. Overridable
483
+ * via SchedulerConfig.budget_pressure_thresholds.
484
+ */
485
+ const DEFAULT_PRESSURE_THRESHOLDS: BudgetPressureThresholds = {
486
+ warning: 0.6,
487
+ high: 0.8,
488
+ critical: 0.95,
489
+ };
490
+
491
+ /**
492
+ * Returns true if any priority account has consumed more than the warning
493
+ * threshold (default 60%) of its rolling-window budget. Pure function.
494
+ */
495
+ export function isBudgetPressured(
496
+ states: Map<string, BackendUsageState>,
497
+ priority: BackendId[],
498
+ accounts: SuperpowersConfig['accounts'],
499
+ thresholds?: BudgetPressureThresholds
500
+ ): boolean {
501
+ return computeBudgetPressureLevel(states, priority, accounts, thresholds) !== 'none';
502
+ }
503
+
504
+ // Module-level state for transition-based logging
505
+ const _lastLoggedPressure: Map<string, BudgetPressureLevel> = new Map();
506
+
507
+ // Monotonic counter for unique per-scheduler session keys. Each
508
+ // createScheduler call gets its own ID so _lastLoggedPressure
509
+ // transitions are tracked independently (O3).
510
+ let _nextSchedulerSessionId = 0;
511
+
512
+ /**
513
+ * Logs a single stderr line when the pressure level has changed since
514
+ * the last call with the same sessionKey. Safe to call per spawn —
515
+ * only emits on transitions. Noop when current == previous.
516
+ *
517
+ * The sessionKey lets multiple sessions in the same process have
518
+ * independent transition state. Autopilot/evolve/autoresearch
519
+ * typically pass process.pid.toString().
520
+ */
521
+ export function logPressureTransition(
522
+ sessionKey: string,
523
+ current: BudgetPressureLevel,
524
+ agentType: string,
525
+ baseTier: string,
526
+ effectiveTier: string
527
+ ): void {
528
+ const previous = _lastLoggedPressure.get(sessionKey) || 'none';
529
+ if (previous === current) return;
530
+ _lastLoggedPressure.set(sessionKey, current);
531
+
532
+ incrementCounter(`scheduler.pressure_transitions.${current}`);
533
+
534
+ if (current === 'none') return;
535
+ const tierNote =
536
+ baseTier === effectiveTier
537
+ ? ''
538
+ : ` — downgrading ${agentType} from ${baseTier} to ${effectiveTier}`;
539
+ process.stderr.write(`[scheduler] budget pressure detected — level=${current}${tierNote}\n`);
540
+ }
541
+
542
+ /**
543
+ * Classifies the worst pressure level across all priority accounts.
544
+ * Returns 'none' | 'warning' | 'high' | 'critical'. Pure function.
545
+ *
546
+ * For each priority account, computes (consumed + reserved) / budget
547
+ * and picks the worst ratio across all accounts (i.e., the one closest
548
+ * to exhaustion determines the level for the whole session).
549
+ */
550
+ export function computeBudgetPressureLevel(
551
+ states: Map<string, BackendUsageState>,
552
+ priority: BackendId[],
553
+ accounts: SuperpowersConfig['accounts'],
554
+ thresholds?: BudgetPressureThresholds
555
+ ): BudgetPressureLevel {
556
+ const t = thresholds || DEFAULT_PRESSURE_THRESHOLDS;
557
+ let worstRatio = 0;
558
+
559
+ for (const backend of priority) {
560
+ const backendAccounts = accounts[backend as AdapterBackendId] || [];
561
+ for (const account of backendAccounts) {
562
+ const stateKey = `${backend}/${account.config_dir}`;
563
+ const state = states.get(stateKey);
564
+ if (!state) continue;
565
+ if (state.token_budget <= 0) continue;
566
+ const ratio = (state.tokens_consumed_in_window + state.tokens_reserved) / state.token_budget;
567
+ if (ratio > worstRatio) worstRatio = ratio;
568
+ }
569
+ }
570
+
571
+ if (worstRatio >= t.critical) return 'critical';
572
+ if (worstRatio >= t.high) return 'high';
573
+ if (worstRatio >= t.warning) return 'warning';
574
+ return 'none';
575
+ }
576
+
577
+ /**
578
+ * Resolves which backend and account to use for the next scheduled task.
579
+ * Walks the backend_priority list, and within each backend tries every
580
+ * configured account in order. Falls back to the free_fallback backend
581
+ * when all priority accounts are exhausted.
582
+ *
583
+ * Edge cases:
584
+ * - Empty accounts ({}) — returns default_backend with no config dir override
585
+ * - Empty account array ([]) — skips that backend
586
+ * - Backend in priority but missing from accounts — skipped
587
+ *
588
+ * @param superpowersConfig - superpowers configuration with accounts
589
+ * @param schedulerConfig - scheduler configuration with priority and fallback
590
+ * @param states - map of compound state keys to usage state
591
+ * @param safetyMargin - minimum remaining tasks before an account is considered full
592
+ * @returns resolved backend, account, and state key
593
+ */
594
+ export function resolveAccount(
595
+ superpowersConfig: SuperpowersConfig,
596
+ schedulerConfig: SchedulerConfig,
597
+ states: Map<string, BackendUsageState>,
598
+ safetyMargin: number
599
+ ): AccountResolution {
600
+ const accounts = superpowersConfig.accounts;
601
+
602
+ // Edge case: accounts is empty — use default_backend with no config dir
603
+ const hasAnyAccounts = Object.keys(accounts).some(
604
+ (k) => (accounts[k as AdapterBackendId] || []).length > 0
605
+ );
606
+ if (!hasAnyAccounts) {
607
+ return {
608
+ backend: superpowersConfig.default_backend as AdapterBackendId,
609
+ account: { config_dir: '' },
610
+ stateKey: superpowersConfig.default_backend,
611
+ };
612
+ }
613
+
614
+ // Walk priority list, try each account within each backend
615
+ for (const backend of schedulerConfig.backend_priority) {
616
+ const backendAccounts = accounts[backend];
617
+ if (!backendAccounts || backendAccounts.length === 0) continue;
618
+
619
+ for (const account of backendAccounts) {
620
+ const stateKey = `${backend}/${account.config_dir}`;
621
+ const state = states.get(stateKey);
622
+ if (!state) continue;
623
+ if (_hasHeadroom(state, safetyMargin)) {
624
+ return { backend, account, stateKey };
625
+ }
626
+ }
627
+ }
628
+
629
+ // Exhaustion fallback: use free_fallback backend
630
+ const fallbackBackend = schedulerConfig.free_fallback.backend;
631
+ const fallbackAccounts = accounts[fallbackBackend];
632
+ if (fallbackAccounts && fallbackAccounts.length > 0) {
633
+ return {
634
+ backend: fallbackBackend,
635
+ account: fallbackAccounts[0],
636
+ stateKey: `${fallbackBackend}/${fallbackAccounts[0].config_dir}`,
637
+ };
638
+ }
639
+
640
+ // No accounts configured for fallback — use default account (empty config_dir)
641
+ return {
642
+ backend: fallbackBackend,
643
+ account: { config_dir: '' },
644
+ stateKey: fallbackBackend,
645
+ };
646
+ }
647
+
648
+ /**
649
+ * Marks one task as in-flight, incrementing the in-flight counter and
650
+ * reserving the EWMA-predicted token cost.
651
+ *
652
+ * @param state - backend usage state to mutate
653
+ */
654
+ export function markInFlight(state: BackendUsageState): void {
655
+ state.in_flight_count += 1;
656
+ state.tokens_reserved += state.ewma_tokens_per_task;
657
+ }
658
+
659
+ /**
660
+ * Marks one in-flight task as complete, decrementing the counter and
661
+ * recalculating tokens_reserved from the updated in-flight count.
662
+ *
663
+ * @param state - backend usage state to mutate
664
+ */
665
+ export function markComplete(state: BackendUsageState): void {
666
+ state.in_flight_count = Math.max(0, state.in_flight_count - 1);
667
+ state.tokens_reserved = state.ewma_tokens_per_task * state.in_flight_count;
668
+ }
669
+
670
+ // ─── Shared Helpers ──────────────────────────────────────────────────────────
671
+
672
+ /**
673
+ * Checks whether a CLI binary is available on the system PATH.
674
+ * Uses 'where' on Windows and 'which' on POSIX (I8 fix).
675
+ */
676
+ export function checkBinary(binary: string): boolean {
677
+ try {
678
+ const { execFileSync } = require('child_process') as typeof import('child_process');
679
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
680
+ execFileSync(cmd, [binary], { stdio: 'ignore' });
681
+ return true;
682
+ } catch {
683
+ return false;
684
+ }
685
+ }
686
+
687
+ // ─── Scheduler Interface and Factory ─────────────────────────────────────────
688
+
689
+ /**
690
+ * High-level scheduler that selects backends, spawns CLI processes,
691
+ * records usage samples, and persists learned state across sessions.
692
+ */
693
+ export interface Scheduler {
694
+ /**
695
+ * Unique per-createScheduler session key used to namespace pressure
696
+ * transition logging. Format: 'pid-<pid>-session-<counter>'. Read-only.
697
+ * (O3 fix — multiple createScheduler calls in the same process no
698
+ * longer share _lastLoggedPressure state.)
699
+ */
700
+ readonly sessionKey: string;
701
+ spawn(prompt: string, opts: SpawnOpts): Promise<SchedulerSpawnResult>;
702
+ getState(stateKey: string): BackendUsageState | undefined;
703
+ /**
704
+ * Returns a snapshot of the current per-account states map. Used by
705
+ * the Spec 4 budget pressure detection and complexity estimation
706
+ * wire-ups. Do NOT mutate the returned map — it is shared with the
707
+ * scheduler's internal state.
708
+ */
709
+ getStates(): Map<string, BackendUsageState>;
710
+ recordExternalSample(stateKey: string, sample: UsageSample): void;
711
+ persistState(planningDir: string): void;
712
+ loadPersistedState(planningDir: string): void;
713
+ }
714
+
715
+ /**
716
+ * Initializes per-account states when account rotation is enabled.
717
+ * Creates a BackendUsageState for each account across all priority backends
718
+ * and the fallback backend, using compound keys like "claude/~/.claude-personal".
719
+ *
720
+ * @param states - state map to populate
721
+ * @param schedulerConfig - scheduler configuration with priority and fallback
722
+ * @param superpowersConfig - superpowers configuration with accounts
723
+ */
724
+ function _initAccountStates(
725
+ states: Map<string, BackendUsageState>,
726
+ schedulerConfig: SchedulerConfig,
727
+ superpowersConfig: SuperpowersConfig
728
+ ): void {
729
+ const accounts = superpowersConfig.accounts;
730
+ const allBackends = new Set([
731
+ ...schedulerConfig.backend_priority,
732
+ schedulerConfig.free_fallback.backend,
733
+ ]);
734
+
735
+ for (const backend of allBackends) {
736
+ const backendAccounts = accounts[backend];
737
+ if (!backendAccounts || backendAccounts.length === 0) continue;
738
+
739
+ const limit = schedulerConfig.backend_limits?.[backend]?.tpm;
740
+ const isFallback = backend === schedulerConfig.free_fallback.backend;
741
+ const budget = limit ?? (isFallback ? FREE_FALLBACK_BUDGET : DEFAULT_BUDGET_TPM);
742
+
743
+ for (const account of backendAccounts) {
744
+ const stateKey = `${backend}/${account.config_dir}`;
745
+ states.set(stateKey, createBackendState(budget));
746
+ }
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Computes the maximum number of 429 retries allowed for account rotation.
752
+ * Equals total number of accounts across all priority backends.
753
+ *
754
+ * @param schedulerConfig - scheduler configuration with priority
755
+ * @param superpowersConfig - superpowers configuration with accounts
756
+ * @returns maximum retry count
757
+ */
758
+ function _computeMaxRetries(
759
+ schedulerConfig: SchedulerConfig,
760
+ superpowersConfig: SuperpowersConfig
761
+ ): number {
762
+ let maxAccountsPerBackend = 0;
763
+ for (const backend of schedulerConfig.backend_priority) {
764
+ const backendAccounts = superpowersConfig.accounts[backend];
765
+ if (backendAccounts) {
766
+ maxAccountsPerBackend = Math.max(maxAccountsPerBackend, backendAccounts.length);
767
+ }
768
+ }
769
+ return schedulerConfig.backend_priority.length * Math.max(maxAccountsPerBackend, 1);
770
+ }
771
+
772
+ /**
773
+ * Creates a Scheduler instance from the given config, or returns null
774
+ * when no config is provided (pass-through / disabled mode).
775
+ *
776
+ * When superpowersConfig is provided with account_rotation enabled, the
777
+ * scheduler tracks per-account state and uses resolveAccount() for backend
778
+ * selection. Otherwise, it uses the simple pickBackend() flow.
779
+ *
780
+ * @param config - scheduler configuration, or undefined to disable
781
+ * @param superpowersConfig - optional superpowers configuration for account rotation
782
+ * @returns Scheduler instance, or null if config is absent
783
+ */
784
+ export function createScheduler(
785
+ config: SchedulerConfig | undefined,
786
+ superpowersConfig?: SuperpowersConfig
787
+ ): Scheduler | null {
788
+ if (!config) return null;
789
+
790
+ // Unique key for this scheduler instance, used to namespace
791
+ // _lastLoggedPressure so multiple schedulers in the same process do not
792
+ // share transition state (O3).
793
+ const sessionKey = `pid-${process.pid}-session-${_nextSchedulerSessionId++}`;
794
+
795
+ // Apply Spec 2A defaults here so the rest of the scheduler can rely on
796
+ // a fully-populated config. Spread-merge avoids mutating caller input.
797
+ const schedulerConfig: SchedulerConfig = {
798
+ ...config,
799
+ max_wait_minutes: config.max_wait_minutes ?? 90,
800
+ };
801
+ const states = new Map<string, BackendUsageState>();
802
+ const prediction = schedulerConfig.prediction;
803
+ const accountRotation = !!superpowersConfig?.account_rotation;
804
+
805
+ if (accountRotation && superpowersConfig) {
806
+ // Per-account state initialization
807
+ _initAccountStates(states, schedulerConfig, superpowersConfig);
808
+
809
+ // Also initialize fallback backend with no config_dir for the exhaustion case
810
+ const fallbackBackend = schedulerConfig.free_fallback.backend;
811
+ if (!states.has(fallbackBackend)) {
812
+ const limit = schedulerConfig.backend_limits?.[fallbackBackend]?.tpm;
813
+ const budget = limit ?? FREE_FALLBACK_BUDGET;
814
+ states.set(fallbackBackend, createBackendState(budget));
815
+ }
816
+
817
+ // If no accounts at all, initialize default_backend with no config_dir
818
+ const hasAnyAccounts = Object.keys(superpowersConfig.accounts).some(
819
+ (k) => (superpowersConfig.accounts[k as AdapterBackendId] || []).length > 0
820
+ );
821
+ if (!hasAnyAccounts) {
822
+ const defaultBackend = superpowersConfig.default_backend as AdapterBackendId;
823
+ if (!states.has(defaultBackend)) {
824
+ const limit = schedulerConfig.backend_limits?.[defaultBackend]?.tpm;
825
+ const budget = limit ?? DEFAULT_BUDGET_TPM;
826
+ states.set(defaultBackend, createBackendState(budget));
827
+ }
828
+ }
829
+ } else {
830
+ // Simple per-backend state initialization (existing behavior)
831
+ const allBackends = [
832
+ ...schedulerConfig.backend_priority,
833
+ schedulerConfig.free_fallback.backend,
834
+ ];
835
+ for (const backend of new Set(allBackends)) {
836
+ const limit = schedulerConfig.backend_limits?.[backend]?.tpm;
837
+ const isFallback = backend === schedulerConfig.free_fallback.backend;
838
+ const budget = limit ?? (isFallback ? FREE_FALLBACK_BUDGET : DEFAULT_BUDGET_TPM);
839
+ states.set(backend, createBackendState(budget));
840
+ }
841
+ }
842
+
843
+ // Check which backend binaries are available
844
+ const availableBackends = new Set<string>();
845
+ const allBackendIds = new Set([
846
+ ...schedulerConfig.backend_priority,
847
+ schedulerConfig.free_fallback.backend,
848
+ ]);
849
+ for (const backend of allBackendIds) {
850
+ const adapter = ADAPTERS[backend];
851
+ if (adapter && checkBinary(adapter.binary)) availableBackends.add(backend);
852
+ }
853
+
854
+ const maxRetries =
855
+ accountRotation && superpowersConfig
856
+ ? _computeMaxRetries(schedulerConfig, superpowersConfig)
857
+ : schedulerConfig.backend_priority.length;
858
+
859
+ /**
860
+ * Internal spawn implementation with retry counter for 429 rate-limit retries.
861
+ * Capped at maxRetries to prevent infinite loops when all accounts are exhausted.
862
+ */
863
+ async function _spawnWithRetry(
864
+ prompt: string,
865
+ opts: SpawnOpts,
866
+ retryCount: number,
867
+ lastRecoveryTime: number | null = null
868
+ ): Promise<SchedulerSpawnResult> {
869
+ let backend: AdapterBackendId;
870
+ let stateKey: string;
871
+ const envOverrides: Record<string, string> = {};
872
+
873
+ if (accountRotation && superpowersConfig) {
874
+ // Account-rotation path: resolve backend + account
875
+ const resolution = resolveAccount(
876
+ superpowersConfig,
877
+ schedulerConfig,
878
+ states,
879
+ prediction.safety_margin_tasks
880
+ );
881
+ backend = resolution.backend;
882
+ stateKey = resolution.stateKey;
883
+
884
+ // Set env var for the account's config directory
885
+ if (resolution.account.config_dir) {
886
+ envOverrides[ENV_VAR_MAP[backend]] = resolution.account.config_dir;
887
+ }
888
+ } else {
889
+ // Simple backend picker path (existing behavior)
890
+ const filteredPriority = schedulerConfig.backend_priority.filter((b) =>
891
+ availableBackends.has(b)
892
+ );
893
+ backend = pickBackend(
894
+ filteredPriority,
895
+ states,
896
+ prediction.safety_margin_tasks,
897
+ schedulerConfig.free_fallback
898
+ ) as AdapterBackendId;
899
+ stateKey = backend;
900
+ }
901
+
902
+ // Spec 2A: bounded wait for soonest recovery when all priority accounts
903
+ // are exhausted and resolveAccount fell through to free_fallback.
904
+ if (
905
+ accountRotation &&
906
+ superpowersConfig &&
907
+ backend === schedulerConfig.free_fallback.backend &&
908
+ schedulerConfig.backend_priority.length > 0 &&
909
+ !_anyPriorityHasHeadroom(
910
+ schedulerConfig.backend_priority,
911
+ superpowersConfig.accounts,
912
+ states,
913
+ prediction.safety_margin_tasks
914
+ )
915
+ ) {
916
+ // Defensive: createScheduler applies 90 default, but TS can't narrow through
917
+ // the spread-merge. The ?? 90 keeps TypeScript happy and guards against
918
+ // direct construction of the SchedulerConfig bypassing createScheduler.
919
+ const maxWaitMinutes = schedulerConfig.max_wait_minutes ?? 90;
920
+ if (maxWaitMinutes > 0) {
921
+ const maxWaitMs = maxWaitMinutes * 60 * 1000;
922
+ const recoveryTime = computeSoonestRecovery(
923
+ states,
924
+ schedulerConfig.backend_priority,
925
+ superpowersConfig.accounts,
926
+ prediction.window_minutes,
927
+ maxWaitMs
928
+ );
929
+ if (recoveryTime !== null && recoveryTime === lastRecoveryTime) {
930
+ // Infinite-loop guard: if this is the same timestamp we already
931
+ // waited for, sample state didn't change. Fall through to
932
+ // free_fallback instead of waiting again (pre-Spec 2A behavior).
933
+ } else if (recoveryTime !== null) {
934
+ const waitMs = recoveryTime - Date.now();
935
+ if (waitMs <= 0) {
936
+ // Recovery target already elapsed — waiting would be a no-op and
937
+ // recursing may not progress. Fall through to free_fallback (I9).
938
+ process.stderr.write(
939
+ `[scheduler] recovery target already elapsed, falling through to free_fallback\n`
940
+ );
941
+ // Fall through to normal spawn with the fallback backend
942
+ } else {
943
+ const displayMinutes = Math.max(0, Math.ceil(waitMs / 60_000));
944
+ process.stderr.write(
945
+ `[scheduler] all priority accounts exhausted, waiting ${displayMinutes}m for soonest recovery (target=${new Date(recoveryTime).toISOString()})\n`
946
+ );
947
+ const waitResult = await waitUntilOrAbort(recoveryTime);
948
+ if (waitResult === 'aborted') {
949
+ throw new Error('scheduler: wait for account recovery interrupted by SIGINT');
950
+ }
951
+ return _spawnWithRetry(prompt, opts, retryCount, recoveryTime);
952
+ }
953
+ }
954
+ }
955
+ }
956
+
957
+ const adapter = ADAPTERS[backend] || ADAPTERS.claude;
958
+ let state = states.get(stateKey);
959
+ if (!state) {
960
+ // Register the new state in the shared map so markInFlight/markComplete
961
+ // mutations are visible to subsequent dispatches (previously a throw-away
962
+ // orphan object silently lost budget accounting — I1).
963
+ state = createBackendState(DEFAULT_BUDGET_TPM);
964
+ states.set(stateKey, state);
965
+ }
966
+ const args = adapter.buildArgs(prompt, opts);
967
+ const workItemId = opts.workItemId || `task-${Date.now()}`;
968
+
969
+ markInFlight(state);
970
+ const startTime = Date.now();
971
+
972
+ try {
973
+ const { spawn } = require('child_process') as typeof import('child_process');
974
+ // Codex r7 P2: callers can request "unlimited" total timeout by
975
+ // passing 0 explicitly (autoresearch --time-budget 0 path).
976
+ // Distinguish missing (undefined → 2hr default) from explicit 0
977
+ // (no total timeout — only idle watchdog applies).
978
+ const totalTimeoutMs =
979
+ opts.timeout === 0
980
+ ? null
981
+ : (typeof opts.timeout === 'number' ? opts.timeout : 120 * 60 * 1000);
982
+ const idleTimeoutMs = _resolveIdleTimeoutSeconds(backend, schedulerConfig) * 1000;
983
+ const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
984
+
985
+ const result = await new Promise<SchedulerSpawnResult>((resolve) => {
986
+ const isWindows = process.platform === 'win32';
987
+ const child = spawn(adapter.binary, args, {
988
+ cwd: opts.cwd || process.cwd(),
989
+ env: { ...process.env, ...envOverrides },
990
+ stdio: ['ignore', 'pipe', 'pipe'],
991
+ // Create a new process group on POSIX so we can signal children + grandchildren.
992
+ // Windows doesn't support process groups — fall back to default.
993
+ detached: !isWindows,
994
+ });
995
+
996
+ let stdoutBuf = '';
997
+ let stderrBuf = '';
998
+ let stdoutOverflowed = false;
999
+ let idleTimedOut = false;
1000
+ let totalTimedOut = false;
1001
+ let resolved = false;
1002
+ // Track SIGKILL escalation timers so they can be cleared when the
1003
+ // child exits, preventing stale kill signals to recycled PIDs (I2).
1004
+ let idleKillTimer: ReturnType<typeof setTimeout> | undefined;
1005
+ let totalKillTimer: ReturnType<typeof setTimeout> | undefined;
1006
+
1007
+ const safeResolve = (r: SchedulerSpawnResult): void => {
1008
+ if (resolved) return;
1009
+ resolved = true;
1010
+ resolve(r);
1011
+ };
1012
+
1013
+ const watchdog = _startIdleWatchdog(idleTimeoutMs, () => {
1014
+ idleTimedOut = true;
1015
+ incrementCounter('scheduler.idle_kills_total');
1016
+ process.stderr.write(
1017
+ `[scheduler] spawn idle ${Math.round(idleTimeoutMs / 1000)}s, killing ${adapter.binary} (stateKey=${stateKey}, workItemId=${workItemId})\n`
1018
+ );
1019
+ _killProcessTree(child, 'SIGTERM');
1020
+ idleKillTimer = setTimeout(() => {
1021
+ if (child.exitCode === null && child.signalCode === null) {
1022
+ _killProcessTree(child, 'SIGKILL');
1023
+ }
1024
+ }, 5000);
1025
+ });
1026
+
1027
+ const totalTimer =
1028
+ totalTimeoutMs === null
1029
+ ? null
1030
+ : setTimeout(() => {
1031
+ totalTimedOut = true;
1032
+ _killProcessTree(child, 'SIGTERM');
1033
+ totalKillTimer = setTimeout(() => {
1034
+ if (child.exitCode === null && child.signalCode === null) {
1035
+ _killProcessTree(child, 'SIGKILL');
1036
+ }
1037
+ }, 5000);
1038
+ }, totalTimeoutMs);
1039
+
1040
+ // Codex r44 P1 #6: detect live spinning subprocesses. The
1041
+ // prior implementation only ran detectSpin on the captured
1042
+ // stdout buffer in the `close` handler, so an actively-
1043
+ // looping subprocess that keeps printing would never trigger
1044
+ // until total timeout. Keep a rolling window of recent stdout
1045
+ // chunks (each chunk = ~one streamed Claude output token block),
1046
+ // and run detectSpin every SPIN_CHECK_EVERY chunks. If detected
1047
+ // and not yet acted on, kill the subprocess so the parent sees
1048
+ // the spinEvent and can react.
1049
+ const SPIN_CHECK_WINDOW = 5;
1050
+ const SPIN_CHECK_EVERY = 5;
1051
+ const recentChunks: string[] = [];
1052
+ let liveSpinEvent: import('./scheduler').SpinDetectedEvent | null = null;
1053
+ let chunksSinceCheck = 0;
1054
+
1055
+ child.stdout?.on('data', (chunk: Buffer) => {
1056
+ watchdog.markActivity();
1057
+ if (stdoutBuf.length + chunk.length > MAX_BUFFER_BYTES) {
1058
+ stdoutOverflowed = true;
1059
+ return;
1060
+ }
1061
+ const text = chunk.toString('utf-8');
1062
+ stdoutBuf += text;
1063
+ recentChunks.push(text);
1064
+ if (recentChunks.length > SPIN_CHECK_WINDOW) recentChunks.shift();
1065
+ chunksSinceCheck++;
1066
+ if (
1067
+ !liveSpinEvent &&
1068
+ recentChunks.length >= SPIN_CHECK_WINDOW &&
1069
+ chunksSinceCheck >= SPIN_CHECK_EVERY
1070
+ ) {
1071
+ chunksSinceCheck = 0;
1072
+ const evt = detectSpin(recentChunks);
1073
+ if (evt.detected) {
1074
+ liveSpinEvent = evt;
1075
+ _killProcessTree(child, 'SIGTERM');
1076
+ }
1077
+ }
1078
+ });
1079
+
1080
+ child.stderr?.on('data', (chunk: Buffer) => {
1081
+ watchdog.markActivity();
1082
+ if (stderrBuf.length + chunk.length > MAX_BUFFER_BYTES) return;
1083
+ stderrBuf += chunk.toString('utf-8');
1084
+ });
1085
+
1086
+ child.on('error', (err) => {
1087
+ watchdog.stop();
1088
+ if (totalTimer) clearTimeout(totalTimer);
1089
+ if (idleKillTimer) clearTimeout(idleKillTimer);
1090
+ if (totalKillTimer) clearTimeout(totalKillTimer);
1091
+ markComplete(state);
1092
+ safeResolve({
1093
+ exitCode: 1,
1094
+ stdout: undefined,
1095
+ stderr: err.message,
1096
+ timedOut: false,
1097
+ idleTimedOut: false,
1098
+ backend: backend as BackendId,
1099
+ tokensUsed: 0,
1100
+ workItemId,
1101
+ });
1102
+ });
1103
+
1104
+ child.on('close', (code) => {
1105
+ watchdog.stop();
1106
+ if (totalTimer) clearTimeout(totalTimer);
1107
+ if (idleKillTimer) clearTimeout(idleKillTimer);
1108
+ if (totalKillTimer) clearTimeout(totalKillTimer);
1109
+ const duration = Date.now() - startTime;
1110
+ // Codex r45 P1 #6 followup: when we killed the subprocess
1111
+ // because of a live spin detection, the process exits with
1112
+ // code=null (SIGTERM). The prior `code ?? 0` fallback then
1113
+ // reported success, so callers wrote SPIN-REPORT.md AND
1114
+ // marked the phase completed. Treat a spin-kill as a
1115
+ // failure exit so autopilot does not advance on a killed
1116
+ // step.
1117
+ const exitCode =
1118
+ code ?? (idleTimedOut || totalTimedOut || liveSpinEvent ? 1 : 0);
1119
+ const tokens = adapter.parseTokenUsage(stderrBuf) ?? Math.round(duration * 10);
1120
+
1121
+ const sample: UsageSample = {
1122
+ backend: backend as BackendId,
1123
+ stateKey,
1124
+ agentType: opts.agentType, // M2: record per-agent type for complexity routing
1125
+ timestamp: Date.now(),
1126
+ duration,
1127
+ tokenEstimate: tokens,
1128
+ exitCode,
1129
+ workItemId,
1130
+ };
1131
+
1132
+ markComplete(state);
1133
+ recordSample(state, sample, prediction.window_minutes, prediction.ewma_alpha);
1134
+
1135
+ // Periodic persistence: every 10 samples across all backends
1136
+ const totalSamples = Array.from(states.values()).reduce(
1137
+ (sum, s) => sum + s.samples.length,
1138
+ 0
1139
+ );
1140
+ if (totalSamples % 10 === 0 && opts.cwd) {
1141
+ const { join } = require('path') as typeof import('path');
1142
+ scheduler.persistState(join(opts.cwd, '.planning'));
1143
+ }
1144
+
1145
+ // Codex r15 P2 / r44 P1 #6: prefer the live spin event if
1146
+ // detected during streaming (the process was killed for it);
1147
+ // otherwise scan the final buffer for post-hoc detection.
1148
+ let spinEvent: import('./scheduler').SpinDetectedEvent | undefined =
1149
+ liveSpinEvent ?? undefined;
1150
+ if (!spinEvent && stdoutBuf.length > 500) {
1151
+ const chunkSize = Math.max(200, Math.floor(stdoutBuf.length / 12));
1152
+ const chunks: string[] = [];
1153
+ for (let i = 0; i < stdoutBuf.length; i += chunkSize) {
1154
+ chunks.push(stdoutBuf.slice(i, i + chunkSize));
1155
+ }
1156
+ const evt = detectSpin(chunks);
1157
+ if (evt.detected) spinEvent = evt;
1158
+ }
1159
+ safeResolve({
1160
+ exitCode,
1161
+ stdout: opts.captureOutput && !stdoutOverflowed ? stdoutBuf : undefined,
1162
+ stderr: stderrBuf || undefined,
1163
+ timedOut: totalTimedOut,
1164
+ idleTimedOut,
1165
+ backend: backend as BackendId,
1166
+ tokensUsed: tokens,
1167
+ workItemId,
1168
+ spinEvent,
1169
+ });
1170
+ });
1171
+ });
1172
+
1173
+ // Rate limit retry: if rate-limited despite prediction, cooldown and retry
1174
+ if (adapter.isRateLimited(result.exitCode, result.stderr || '')) {
1175
+ // Enforce a minimum 5-minute cooldown so a zero/missing window_minutes never skips the guard
1176
+ state.cooldown_until = Date.now() + Math.max(prediction.window_minutes || 60, 5) * 60 * 1000;
1177
+
1178
+ // Max retry guard: cap recursive retries
1179
+ if (retryCount >= maxRetries) {
1180
+ return result; // Exhausted all retries, return last result
1181
+ }
1182
+
1183
+ return _spawnWithRetry(prompt, opts, retryCount + 1);
1184
+ }
1185
+
1186
+ return result;
1187
+ } catch (_err) {
1188
+ markComplete(state);
1189
+ return {
1190
+ exitCode: 1,
1191
+ timedOut: false,
1192
+ backend: backend as BackendId,
1193
+ tokensUsed: 0,
1194
+ workItemId,
1195
+ };
1196
+ }
1197
+ }
1198
+
1199
+ const scheduler: Scheduler = {
1200
+ sessionKey,
1201
+
1202
+ getState(stateKey: string): BackendUsageState | undefined {
1203
+ return states.get(stateKey);
1204
+ },
1205
+
1206
+ getStates(): Map<string, BackendUsageState> {
1207
+ return states;
1208
+ },
1209
+
1210
+ recordExternalSample(stateKey: string, sample: UsageSample): void {
1211
+ let state = states.get(stateKey);
1212
+ if (!state) {
1213
+ state = createBackendState(DEFAULT_BUDGET_TPM);
1214
+ states.set(stateKey, state);
1215
+ }
1216
+ recordSample(state, sample, prediction.window_minutes, prediction.ewma_alpha);
1217
+ },
1218
+
1219
+ async spawn(prompt: string, opts: SpawnOpts): Promise<SchedulerSpawnResult> {
1220
+ return _spawnWithRetry(prompt, opts, 0);
1221
+ },
1222
+
1223
+ persistState(planningDir: string): void {
1224
+ const { writeFileSync } = require('fs') as typeof import('fs');
1225
+ const { join } = require('path') as typeof import('path');
1226
+ const data: Record<string, unknown> = { version: 1, backends: {} };
1227
+ const backends = data.backends as Record<string, unknown>;
1228
+ for (const [key, state] of states) {
1229
+ backends[key] = {
1230
+ token_budget: state.token_budget,
1231
+ ewma_tokens_per_task: state.ewma_tokens_per_task,
1232
+ budget_learned: state.budget_learned,
1233
+ budget_confidence: state.budget_confidence,
1234
+ last_updated: Date.now(),
1235
+ };
1236
+ }
1237
+ writeFileSync(
1238
+ join(planningDir, 'scheduler-state.json'),
1239
+ JSON.stringify(data, null, 2) + '\n'
1240
+ );
1241
+ },
1242
+
1243
+ loadPersistedState(planningDir: string): void {
1244
+ const {
1245
+ safeReadJSON,
1246
+ }: { safeReadJSON: (p: string, d?: unknown) => unknown } = require('./utils');
1247
+ const { join } = require('path') as typeof import('path');
1248
+ const raw = safeReadJSON(join(planningDir, 'scheduler-state.json')) as {
1249
+ version?: number;
1250
+ backends?: Record<
1251
+ string,
1252
+ {
1253
+ token_budget: number;
1254
+ ewma_tokens_per_task: number;
1255
+ budget_learned: boolean;
1256
+ budget_confidence: number;
1257
+ }
1258
+ >;
1259
+ } | null;
1260
+ if (!raw || raw.version !== 1 || !raw.backends) return;
1261
+ for (const [key, saved] of Object.entries(raw.backends)) {
1262
+ const state = states.get(key);
1263
+ if (!state) continue;
1264
+ if (saved.budget_learned) state.token_budget = saved.token_budget;
1265
+ state.ewma_tokens_per_task = saved.ewma_tokens_per_task;
1266
+ state.budget_learned = saved.budget_learned;
1267
+ state.budget_confidence = saved.budget_confidence;
1268
+ }
1269
+ },
1270
+ };
1271
+
1272
+ return scheduler;
1273
+ }
1274
+
1275
+ // ─── Spin Detection ──────────────────────────────────────────────────────────
1276
+
1277
+ /** Result from detectSpin analysis. */
1278
+ export interface SpinDetectedEvent {
1279
+ detected: boolean;
1280
+ /** The repeated pattern excerpt (first 200 chars of the repeated chunk). */
1281
+ repeated_pattern: string;
1282
+ /** Number of consecutive similar chunks detected. */
1283
+ consecutive_count: number;
1284
+ /** Similarity score of the most repeated pair (0-1). */
1285
+ max_similarity: number;
1286
+ }
1287
+
1288
+ /**
1289
+ * Analyze the last N stdout chunks from an agent run to detect spin (looping on
1290
+ * the same error without progress). Uses bigram overlap (Jaccard similarity) to
1291
+ * compare consecutive chunks.
1292
+ *
1293
+ * Returns SpinDetectedEvent with detected=true if 3+ consecutive pairs have
1294
+ * similarity >= threshold (default 0.80).
1295
+ *
1296
+ * @param chunks - Array of stdout text chunks (ordered chronologically)
1297
+ * @param threshold - Similarity threshold for spin detection (default 0.80)
1298
+ * @param windowSize - Number of recent chunks to analyze (default 5)
1299
+ */
1300
+ export function detectSpin(chunks: string[], threshold = 0.80, windowSize = 5): SpinDetectedEvent {
1301
+ const NO_SPIN: SpinDetectedEvent = { detected: false, repeated_pattern: '', consecutive_count: 0, max_similarity: 0 };
1302
+ const recent = chunks.slice(-windowSize);
1303
+ if (recent.length < 3) return NO_SPIN;
1304
+
1305
+ /** Compute bigram set from a normalized text chunk. */
1306
+ function bigrams(text: string): Set<string> {
1307
+ const normalized = text.toLowerCase().replace(/[^a-z0-9\n]/g, ' ').replace(/\s+/g, ' ').trim();
1308
+ const set = new Set<string>();
1309
+ for (let i = 0; i + 1 < normalized.length; i++) {
1310
+ set.add(normalized.slice(i, i + 2));
1311
+ }
1312
+ return set;
1313
+ }
1314
+
1315
+ function jaccard(a: Set<string>, b: Set<string>): number {
1316
+ if (a.size === 0 && b.size === 0) return 1;
1317
+ let intersection = 0;
1318
+ for (const t of a) { if (b.has(t)) intersection++; }
1319
+ const union = a.size + b.size - intersection;
1320
+ return union === 0 ? 0 : intersection / union;
1321
+ }
1322
+
1323
+ let consecutiveCount = 0;
1324
+ let maxSimilarity = 0;
1325
+ let repeatedPattern = '';
1326
+
1327
+ for (let i = 1; i < recent.length; i++) {
1328
+ const sim = jaccard(bigrams(recent[i - 1]), bigrams(recent[i]));
1329
+ if (sim >= threshold) {
1330
+ consecutiveCount++;
1331
+ if (sim > maxSimilarity) {
1332
+ maxSimilarity = sim;
1333
+ repeatedPattern = recent[i].slice(0, 200);
1334
+ }
1335
+ } else {
1336
+ consecutiveCount = 0;
1337
+ }
1338
+ }
1339
+
1340
+ if (consecutiveCount >= 2) {
1341
+ return { detected: true, repeated_pattern: repeatedPattern, consecutive_count: consecutiveCount + 1, max_similarity: Math.round(maxSimilarity * 100) / 100 };
1342
+ }
1343
+ return NO_SPIN;
1344
+ }
1345
+
1346
+ module.exports = {
1347
+ ADAPTERS,
1348
+ ENV_VAR_MAP,
1349
+ FREE_FALLBACK_BUDGET,
1350
+ checkBinary,
1351
+ _checkBinary: checkBinary,
1352
+ createBackendState,
1353
+ updateEWMA,
1354
+ evictExpiredSamples,
1355
+ recordSample,
1356
+ pickBackend,
1357
+ resolveAccount,
1358
+ markInFlight,
1359
+ markComplete,
1360
+ createScheduler,
1361
+ computeSoonestRecovery,
1362
+ _anyPriorityHasHeadroom,
1363
+ _startIdleWatchdog,
1364
+ _resolveIdleTimeoutSeconds,
1365
+ _killProcessTree,
1366
+ isBudgetPressured,
1367
+ computeBudgetPressureLevel,
1368
+ logPressureTransition,
1369
+ detectSpin,
1370
+ };