@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
package/lib/utils.ts ADDED
@@ -0,0 +1,1479 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GRD Shared Utilities -- Constants, helpers, and validation functions
5
+ *
6
+ * Extracted from bin/grd-tools.js during Phase 03 modularization.
7
+ * These are zero-dependency foundations used by all other modules.
8
+ */
9
+
10
+ import type {
11
+ GrdConfig,
12
+ GrdTimeouts,
13
+ ExecGitResult,
14
+ PhaseInfo,
15
+ MilestoneInfo,
16
+ ModelTier,
17
+ ModelProfileName,
18
+ RunCache,
19
+ AgentModelProfiles,
20
+ BackendRolesConfig,
21
+ DiscussionConfig,
22
+ EffortAxisLevel,
23
+ EffortKnobName,
24
+ } from './types';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const os = require('os');
29
+ const { execFileSync } = require('child_process');
30
+ const {
31
+ detectBackend,
32
+ resolveBackendModel,
33
+ resolveEffortLevel,
34
+ getBackendCapabilities,
35
+ VALID_BACKENDS,
36
+ } = require('./backend');
37
+ const { phasesDir: getPhasesDirPath } = require('./paths');
38
+
39
+ // ─── Git Operation Whitelist ────────────────────────────────────────────────
40
+
41
+ const GIT_ALLOWED_COMMANDS: Set<string> = new Set([
42
+ 'add',
43
+ 'commit',
44
+ 'log',
45
+ 'status',
46
+ 'diff',
47
+ 'show',
48
+ 'rev-parse',
49
+ 'cat-file',
50
+ 'check-ignore',
51
+ 'ls-files',
52
+ 'branch',
53
+ 'checkout',
54
+ 'merge',
55
+ 'rebase',
56
+ 'cherry-pick',
57
+ 'tag',
58
+ 'stash',
59
+ 'remote',
60
+ 'fetch',
61
+ 'pull',
62
+ ]);
63
+ const GIT_BLOCKED_COMMANDS: Set<string> = new Set(['config', 'push', 'clean']);
64
+ const GIT_BLOCKED_FLAGS: Set<string> = new Set(['--force', '-f', '--hard', '--delete', '-D']);
65
+
66
+ // ─── Model Profile Table ─────────────────────────────────────────────────────
67
+
68
+ const MODEL_PROFILES: AgentModelProfiles = {
69
+ 'grd-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
70
+ 'grd-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
71
+ 'grd-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
72
+ 'grd-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
73
+ 'grd-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
74
+ 'grd-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
75
+ 'grd-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
76
+ 'grd-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
77
+ 'grd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
78
+ // NERFIFY refinement-loop critic (codex r43 P2: was routed through
79
+ // grd-verifier, which never loaded the agent definition).
80
+ 'grd-critique-agent': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
81
+ 'grd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
82
+ 'grd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
83
+ // R&D-specific agents
84
+ 'grd-surveyor': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
85
+ 'grd-deep-diver': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
86
+ 'grd-feasibility-analyst': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
87
+ 'grd-eval-planner': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
88
+ 'grd-eval-reporter': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
89
+ 'grd-product-owner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
90
+ 'grd-baseline-assessor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
91
+ // Development practice agents
92
+ 'grd-code-reviewer': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
93
+ };
94
+
95
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
96
+
97
+ /**
98
+ * Parse --include flag from CLI args into a Set of included items.
99
+ * @param args - CLI argument array
100
+ * @returns Set of comma-separated include values, or empty Set if not present
101
+ */
102
+ function parseIncludeFlag(args: string[]): Set<string> {
103
+ const includeIndex: number = args.indexOf('--include');
104
+ if (includeIndex === -1) return new Set();
105
+ const includeValue: string | undefined = args[includeIndex + 1];
106
+ if (!includeValue) return new Set();
107
+ return new Set(includeValue.split(',').map((s: string) => s.trim()));
108
+ }
109
+
110
+ /**
111
+ * Read file returning content or null on error.
112
+ * @param filePath - Absolute path to the file
113
+ * @returns File content as UTF-8 string, or null if read fails
114
+ */
115
+ function safeReadFile(filePath: string): string | null {
116
+ try {
117
+ return fs.readFileSync(filePath, 'utf-8');
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Read a markdown file, transparently handling GRD split-format index files.
125
+ * If the file is a GRD index (contains <!-- GRD-INDEX --> marker), partials are
126
+ * automatically reassembled. Otherwise, returns the file content as-is.
127
+ * @param filePath - Absolute path to the markdown file
128
+ * @returns File content (reassembled if split), or null on error
129
+ */
130
+ function safeReadMarkdown(filePath: string): string | null {
131
+ try {
132
+ // Lazy require to avoid circular dependency (markdown-split.js imports safeReadFile from utils.js)
133
+ const { readMarkdownWithPartials } = require('./markdown-split');
134
+ return readMarkdownWithPartials(filePath) as string;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Read and parse a JSON file, returning the default value on any error.
142
+ * @param filePath - Absolute path to the JSON file
143
+ * @param defaultValue - Value to return if read or parse fails
144
+ * @returns Parsed JSON object, or defaultValue on error
145
+ */
146
+ function safeReadJSON(filePath: string, defaultValue: unknown = null): unknown {
147
+ try {
148
+ const raw: string = fs.readFileSync(filePath, 'utf-8');
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ return defaultValue;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Extract content under a markdown heading (## or ### level).
157
+ * @param content - Full markdown content
158
+ * @param heading - Heading text to find (case-insensitive)
159
+ * @param level - Heading level (2 for ##, 3 for ###)
160
+ * @returns Section content (without the heading line), or null if not found
161
+ */
162
+ function extractMarkdownSection(
163
+ content: string,
164
+ heading: string,
165
+ level: number = 2
166
+ ): string | null {
167
+ const prefix: string = '#'.repeat(level);
168
+ const regex: RegExp = new RegExp(
169
+ `${prefix}\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\n([\\s\\S]*?)(?=\\n${prefix}\\s|$)`,
170
+ 'i'
171
+ );
172
+ const match: RegExpMatchArray | null = content.match(regex);
173
+ return match ? match[1] : null;
174
+ }
175
+
176
+ // ─── Levenshtein Distance ────────────────────────────────────────────────────
177
+
178
+ /**
179
+ * Compute the Levenshtein edit distance between two strings.
180
+ * @param s1 - First string
181
+ * @param s2 - Second string
182
+ * @returns Edit distance
183
+ */
184
+ function levenshteinDistance(s1: string, s2: string): number {
185
+ const m: number = s1.length;
186
+ const n: number = s2.length;
187
+ const dp: number[][] = [];
188
+ for (let i = 0; i <= m; i++) {
189
+ dp[i] = [i];
190
+ for (let j = 1; j <= n; j++) {
191
+ if (i === 0) {
192
+ dp[i][j] = j;
193
+ } else {
194
+ dp[i][j] = 0;
195
+ }
196
+ }
197
+ }
198
+ for (let i = 1; i <= m; i++) {
199
+ for (let j = 1; j <= n; j++) {
200
+ if (s1[i - 1] === s2[j - 1]) {
201
+ dp[i][j] = dp[i - 1][j - 1];
202
+ } else {
203
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
204
+ }
205
+ }
206
+ }
207
+ return dp[m][n];
208
+ }
209
+
210
+ /**
211
+ * Find the closest command name from a list of commands using Levenshtein distance.
212
+ * Returns null if no command is close enough (distance > threshold) or if input is invalid.
213
+ * @param input - User input to match
214
+ * @param commands - List of valid command names
215
+ * @returns The closest command name, or null if too different
216
+ */
217
+ function findClosestCommand(input: string | null, commands: string[]): string | null {
218
+ if (!input || !commands || commands.length === 0) return null;
219
+ const lower: string = input.toLowerCase();
220
+ let best: string | null = null;
221
+ let bestDist: number = Infinity;
222
+ for (const cmd of commands) {
223
+ const dist: number = levenshteinDistance(lower, cmd.toLowerCase());
224
+ if (dist < bestDist) {
225
+ bestDist = dist;
226
+ best = cmd;
227
+ }
228
+ }
229
+ // Threshold: only suggest if distance is at most 3 (reasonable typo range)
230
+ const threshold: number = Math.max(3, Math.floor(best ? best.length / 3 : 3));
231
+ if (bestDist > threshold) return null;
232
+ return best;
233
+ }
234
+
235
+ // ─── Phase Cache ─────────────────────────────────────────────────────────────
236
+
237
+ const _phaseCache: Map<string, string> = new Map();
238
+
239
+ /**
240
+ * Clear the internal phase directory cache.
241
+ */
242
+ function clearPhaseCache(): void {
243
+ _phaseCache.clear();
244
+ }
245
+
246
+ /**
247
+ * Known top-level config keys (used for unrecognized key warnings).
248
+ */
249
+ const KNOWN_CONFIG_KEYS: Set<string> = new Set([
250
+ 'model_profile',
251
+ 'commit_docs',
252
+ 'search_gitignored',
253
+ 'branching_strategy',
254
+ 'phase_branch_template',
255
+ 'milestone_branch_template',
256
+ 'base_branch',
257
+ 'research',
258
+ 'plan_checker',
259
+ 'verifier',
260
+ 'parallelization',
261
+ 'code_review_enabled',
262
+ 'code_review_timing',
263
+ 'code_review_severity_gate',
264
+ 'code_review_auto_fix_warnings',
265
+ 'use_teams',
266
+ 'team_timeout_minutes',
267
+ 'max_concurrent_teammates',
268
+ 'backend',
269
+ 'backend_models',
270
+ 'autonomous_mode',
271
+ // Nested section keys (objects)
272
+ 'code_review',
273
+ 'execution',
274
+ 'git',
275
+ 'planning',
276
+ 'workflow',
277
+ 'tracker',
278
+ 'eval_config',
279
+ 'ceremony',
280
+ 'phase_cleanup',
281
+ 'research_gates',
282
+ 'confirmation_gates',
283
+ 'timeouts',
284
+ 'evolve',
285
+ // New-project command keys
286
+ 'mode',
287
+ 'depth',
288
+ // YOLO saved state keys
289
+ '_saved_research_gates',
290
+ '_saved_confirmation_gates',
291
+ 'yolo_decision_log',
292
+ // Backend-specific keys
293
+ 'overstory',
294
+ // Scheduler config
295
+ 'scheduler',
296
+ // Superpowers config
297
+ 'superpowers',
298
+ // Discussion config
299
+ 'backend_roles',
300
+ 'discussion',
301
+ // Citation gate
302
+ 'citation_gate',
303
+ // Transitive citation gate
304
+ 'transitive_citation_gate',
305
+ // Refinement loop
306
+ 'refinement_loop',
307
+ // LLM fallback for phase completion (Spec 3B)
308
+ 'phase_complete_llm_fallback',
309
+ // LLM fallback retry count with exponential backoff
310
+ 'phase_complete_llm_fallback_retries',
311
+ // Drift score (Tier-2 #7 of Ouroboros integration)
312
+ 'drift',
313
+ // Autopilot termination knobs (Tier-3 #10 of Ouroboros integration)
314
+ 'autopilot',
315
+ // Evolve r9 + r14: surface-level keys consumed by Ouroboros r9 CLIs.
316
+ // research_staleness_days drives `gd health`'s STALE_RESEARCH blocker;
317
+ // `survey` carries staleness_days for `gd progress`'s freshness warn.
318
+ 'research_staleness_days',
319
+ 'survey',
320
+ // Plug-in / context-mode knowledge stats path
321
+ 'token_profile',
322
+ // v0.4 Phase 1: orthogonal effort axis
323
+ 'effort',
324
+ ]);
325
+
326
+ // ─── Effort Axis (v0.4 Phase 1) ─────────────────────────────────────────────
327
+
328
+ /**
329
+ * v0.4 effort-scaled knobs. Single-knob scope by design (codex r6) — only
330
+ * `candidates_per_plan_phase` is wired in v0.4. The table is structured
331
+ * (object keyed by knob name) so v0.5+ can add knobs without changing the
332
+ * `resolveEffortKnob` signature.
333
+ */
334
+ const EFFORT_PROFILES: Record<EffortAxisLevel, Record<EffortKnobName, number>> = {
335
+ thrifty: { candidates_per_plan_phase: 1 },
336
+ balanced: { candidates_per_plan_phase: 3 },
337
+ deep: { candidates_per_plan_phase: 7 },
338
+ };
339
+
340
+ /**
341
+ * Return the integer value of an effort-scaled knob for the current
342
+ * `effort` setting in config. Defaults to 'balanced' when unset.
343
+ *
344
+ * @param config - GrdConfig (effort field optional, defaults to 'balanced')
345
+ * @param knob - Name of the effort-scaled knob to resolve
346
+ * @returns Integer value for the knob under the active effort level
347
+ */
348
+ function resolveEffortKnob(config: GrdConfig, knob: EffortKnobName): number {
349
+ // Codex review P2: loadConfig preserves invalid `effort` values (warns but
350
+ // keeps them), so guard against EFFORT_PROFILES[level] being undefined.
351
+ // An unrecognized level falls back to 'balanced' rather than crashing.
352
+ const raw = config.effort;
353
+ const level: EffortAxisLevel =
354
+ raw !== undefined && Object.prototype.hasOwnProperty.call(EFFORT_PROFILES, raw)
355
+ ? raw
356
+ : 'balanced';
357
+ return EFFORT_PROFILES[level][knob];
358
+ }
359
+
360
+ /**
361
+ * Load and merge .planning/config.json with default configuration values.
362
+ * @param cwd - Project working directory
363
+ * @returns Merged configuration object with all fields populated
364
+ */
365
+ function loadConfig(cwd: string): GrdConfig {
366
+ const configPath: string = path.join(cwd, '.planning', 'config.json');
367
+ const defaultTimeouts: GrdTimeouts = {
368
+ jest_ms: 120000,
369
+ lint_ms: 60000,
370
+ format_ms: 60000,
371
+ consistency_ms: 30000,
372
+ tracker_gh_ms: 30000,
373
+ tracker_auth_ms: 10000,
374
+ backend_detect_ms: 10000,
375
+ autopilot_check_ms: 5000,
376
+ autoresearch_test_ms: 120000,
377
+ autoresearch_coverage_ms: 180000,
378
+ autoresearch_lint_ms: 60000,
379
+ backend_probe_ms: 5000,
380
+ discussion_git_ms: 10000,
381
+ overstory_probe_ms: 5000,
382
+ overstory_install_ms: 120000,
383
+ };
384
+ const defaults: GrdConfig = {
385
+ model_profile: 'balanced',
386
+ commit_docs: true,
387
+ search_gitignored: false,
388
+ branching_strategy: 'none',
389
+ phase_branch_template: 'grd/{milestone}/{phase}-{slug}',
390
+ milestone_branch_template: 'grd/{milestone}-{slug}',
391
+ base_branch: 'main',
392
+ research: true,
393
+ plan_checker: true,
394
+ verifier: true,
395
+ parallelization: true,
396
+ // Code review defaults
397
+ code_review_enabled: true,
398
+ code_review_timing: 'per_wave',
399
+ code_review_severity_gate: 'blocker',
400
+ code_review_auto_fix_warnings: false,
401
+ // Execution defaults
402
+ use_teams: false,
403
+ team_timeout_minutes: 30,
404
+ max_concurrent_teammates: 4,
405
+ // Backend config (pass-through, no defaults)
406
+ backend: undefined,
407
+ backend_models: undefined,
408
+ // Autonomous mode
409
+ autonomous_mode: false,
410
+ // Ceremony config
411
+ ceremony: undefined,
412
+ // Timeout defaults (ms)
413
+ timeouts: defaultTimeouts,
414
+ // Evolve config
415
+ evolve: undefined,
416
+ };
417
+
418
+ try {
419
+ const raw: string = fs.readFileSync(configPath, 'utf-8');
420
+ const parsed: Record<string, unknown> = JSON.parse(raw);
421
+
422
+ // Warn about unrecognized top-level config keys
423
+ for (const key of Object.keys(parsed)) {
424
+ if (!KNOWN_CONFIG_KEYS.has(key)) {
425
+ process.stderr.write(
426
+ `Warning: Unrecognized config key "${key}" in .planning/config.json\n`
427
+ );
428
+ }
429
+ }
430
+
431
+ // Warn about invalid model_profile values
432
+ const validProfiles: string[] = ['quality', 'balanced', 'budget'];
433
+ if (
434
+ parsed.model_profile !== undefined &&
435
+ !validProfiles.includes(parsed.model_profile as string)
436
+ ) {
437
+ process.stderr.write(
438
+ `Warning: Invalid model_profile value "${parsed.model_profile}" in .planning/config.json. Valid values: ${validProfiles.join(', ')}\n`
439
+ );
440
+ }
441
+
442
+ // Warn about invalid effort values (v0.4 Phase 1)
443
+ const validEffortLevels: string[] = ['thrifty', 'balanced', 'deep'];
444
+ if (
445
+ parsed.effort !== undefined &&
446
+ !validEffortLevels.includes(parsed.effort as string)
447
+ ) {
448
+ process.stderr.write(
449
+ `Warning: Invalid effort value "${parsed.effort}" in .planning/config.json. Valid values: ${validEffortLevels.join(', ')}\n`
450
+ );
451
+ }
452
+
453
+ const get = (key: string, nested?: { section: string; field: string }): unknown => {
454
+ if (parsed[key] !== undefined) return parsed[key];
455
+ if (nested) {
456
+ const section = parsed[nested.section] as Record<string, unknown> | undefined;
457
+ if (section && section[nested.field] !== undefined) {
458
+ return section[nested.field];
459
+ }
460
+ }
461
+ return undefined;
462
+ };
463
+
464
+ const parallelization: boolean = ((): boolean => {
465
+ const val: unknown = get('parallelization');
466
+ if (typeof val === 'boolean') return val;
467
+ if (typeof val === 'object' && val !== null && 'enabled' in (val as Record<string, unknown>))
468
+ return (val as Record<string, unknown>).enabled as boolean;
469
+ return defaults.parallelization;
470
+ })();
471
+
472
+ return {
473
+ model_profile: (get('model_profile') ?? defaults.model_profile) as ModelProfileName,
474
+ // v0.4 Phase 1: effort axis. Invalid values pass through as warnings
475
+ // above; we keep raw here so resolveEffortKnob's default ('balanced')
476
+ // takes effect when the field is absent.
477
+ ...(parsed.effort !== undefined ? { effort: parsed.effort as EffortAxisLevel } : {}),
478
+ commit_docs: (get('commit_docs', { section: 'planning', field: 'commit_docs' }) ??
479
+ defaults.commit_docs) as boolean,
480
+ search_gitignored: (get('search_gitignored', {
481
+ section: 'planning',
482
+ field: 'search_gitignored',
483
+ }) ?? defaults.search_gitignored) as boolean,
484
+ branching_strategy: (get('branching_strategy', {
485
+ section: 'git',
486
+ field: 'branching_strategy',
487
+ }) ?? defaults.branching_strategy) as string,
488
+ phase_branch_template: (get('phase_branch_template', {
489
+ section: 'git',
490
+ field: 'phase_branch_template',
491
+ }) ?? defaults.phase_branch_template) as string,
492
+ milestone_branch_template: (get('milestone_branch_template', {
493
+ section: 'git',
494
+ field: 'milestone_branch_template',
495
+ }) ?? defaults.milestone_branch_template) as string,
496
+ base_branch: (get('base_branch', { section: 'git', field: 'base_branch' }) ??
497
+ defaults.base_branch) as string,
498
+ research: (get('research', { section: 'workflow', field: 'research' }) ??
499
+ defaults.research) as boolean,
500
+ plan_checker: (get('plan_checker', { section: 'workflow', field: 'plan_check' }) ??
501
+ defaults.plan_checker) as boolean,
502
+ verifier: (get('verifier', { section: 'workflow', field: 'verifier' }) ??
503
+ defaults.verifier) as boolean,
504
+ parallelization,
505
+ // Code review config
506
+ code_review_enabled: (get('code_review_enabled', {
507
+ section: 'code_review',
508
+ field: 'enabled',
509
+ }) ?? defaults.code_review_enabled) as boolean,
510
+ code_review_timing: (get('code_review_timing', { section: 'code_review', field: 'timing' }) ??
511
+ defaults.code_review_timing) as string,
512
+ code_review_severity_gate: (get('code_review_severity_gate', {
513
+ section: 'code_review',
514
+ field: 'severity_gate',
515
+ }) ?? defaults.code_review_severity_gate) as string,
516
+ code_review_auto_fix_warnings: (get('code_review_auto_fix_warnings', {
517
+ section: 'code_review',
518
+ field: 'auto_fix_warnings',
519
+ }) ?? defaults.code_review_auto_fix_warnings) as boolean,
520
+ // Execution config
521
+ use_teams: (get('use_teams', { section: 'execution', field: 'use_teams' }) ??
522
+ defaults.use_teams) as boolean,
523
+ team_timeout_minutes: (get('team_timeout_minutes', {
524
+ section: 'execution',
525
+ field: 'team_timeout_minutes',
526
+ }) ?? defaults.team_timeout_minutes) as number,
527
+ max_concurrent_teammates: (get('max_concurrent_teammates', {
528
+ section: 'execution',
529
+ field: 'max_concurrent_teammates',
530
+ }) ?? defaults.max_concurrent_teammates) as number,
531
+ // Backend config (pass-through, no defaults)
532
+ backend: (parsed.backend || undefined) as string | undefined,
533
+ backend_models: (parsed.backend_models || undefined) as GrdConfig['backend_models'],
534
+ // Autonomous mode
535
+ autonomous_mode: (get('autonomous_mode') ?? false) as boolean,
536
+ // Ceremony config (pass-through)
537
+ ceremony: (parsed.ceremony || undefined) as GrdConfig['ceremony'],
538
+ // Evolve config
539
+ evolve: ((): GrdConfig['evolve'] => {
540
+ const e =
541
+ parsed.evolve && typeof parsed.evolve === 'object'
542
+ ? (parsed.evolve as Record<string, unknown>)
543
+ : null;
544
+ if (!e) return undefined;
545
+ return {
546
+ auto_commit: (e.auto_commit ?? true) as boolean,
547
+ create_pr: (e.create_pr ?? true) as boolean,
548
+ // Tier-2 #8 auto-genome follow-up. Default off — opt-in.
549
+ auto_genome_snapshot: (e.auto_genome_snapshot ?? false) as boolean,
550
+ };
551
+ })(),
552
+ // Scheduler config (pass-through)
553
+ scheduler: (parsed.scheduler || undefined) as GrdConfig['scheduler'],
554
+ // Superpowers config (pass-through)
555
+ superpowers: (parsed.superpowers || undefined) as GrdConfig['superpowers'],
556
+ // Drift score config (pass-through; defaults applied in lib/drift.ts)
557
+ drift: (parsed.drift || undefined) as GrdConfig['drift'],
558
+ // Autopilot termination knobs (pass-through; Tier-3 #10)
559
+ autopilot: (parsed.autopilot || undefined) as GrdConfig['autopilot'],
560
+ // Codex r14 P2/P3: Ouroboros r9 staleness knobs need to survive
561
+ // loadConfig so cmdHealth + cmdProgress can read them. Cast the
562
+ // whole return at the bottom to widen the type.
563
+ ...(parsed.research_staleness_days !== undefined
564
+ ? { research_staleness_days: parsed.research_staleness_days as number }
565
+ : {}),
566
+ ...(parsed.survey !== undefined ? { survey: parsed.survey } : {}),
567
+ // Backend roles config: validate each value against VALID_BACKENDS
568
+ backend_roles: ((): BackendRolesConfig | undefined => {
569
+ const raw = parsed.backend_roles;
570
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
571
+ const rawRoles = raw as Record<string, unknown>;
572
+ const validRoles: string[] = ['reviewer', 'brainstormer', 'verifier', 'executor'];
573
+ const result: BackendRolesConfig = {};
574
+ for (const [role, backendVal] of Object.entries(rawRoles)) {
575
+ if (!validRoles.includes(role)) {
576
+ process.stderr.write(
577
+ `Warning: Unrecognized backend_roles role "${role}" in .planning/config.json\n`
578
+ );
579
+ continue;
580
+ }
581
+ if (
582
+ typeof backendVal !== 'string' ||
583
+ !(VALID_BACKENDS as readonly string[]).includes(backendVal)
584
+ ) {
585
+ process.stderr.write(
586
+ `Warning: Invalid backend "${String(backendVal)}" for role "${role}" in backend_roles — must be a valid BackendId\n`
587
+ );
588
+ continue;
589
+ }
590
+ (result as Record<string, string>)[role] = backendVal;
591
+ }
592
+ return Object.keys(result).length > 0 ? result : undefined;
593
+ })(),
594
+ // Discussion config: validate fields, apply defaults
595
+ discussion: ((): DiscussionConfig | undefined => {
596
+ const raw = parsed.discussion;
597
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
598
+ const d = raw as Record<string, unknown>;
599
+ const enabled = typeof d.enabled === 'boolean' ? d.enabled : true;
600
+ if (!enabled) {
601
+ return {
602
+ enabled: false,
603
+ before_planning: typeof d.before_planning === 'boolean' ? d.before_planning : true,
604
+ before_execution: typeof d.before_execution === 'boolean' ? d.before_execution : false,
605
+ max_rounds: 2,
606
+ timeout_per_round_seconds: 180,
607
+ synthesizer: 'claude',
608
+ };
609
+ }
610
+ let max_rounds = typeof d.max_rounds === 'number' ? Math.round(d.max_rounds) : 2;
611
+ if (max_rounds < 1) max_rounds = 1;
612
+ if (max_rounds > 3) max_rounds = 3;
613
+ const timeout_per_round_seconds =
614
+ typeof d.timeout_per_round_seconds === 'number' && d.timeout_per_round_seconds > 0
615
+ ? d.timeout_per_round_seconds
616
+ : 180;
617
+ const synthRaw = d.synthesizer;
618
+ const synthesizer: string =
619
+ typeof synthRaw === 'string' && (VALID_BACKENDS as readonly string[]).includes(synthRaw)
620
+ ? synthRaw
621
+ : 'claude';
622
+ return {
623
+ enabled,
624
+ before_planning: typeof d.before_planning === 'boolean' ? d.before_planning : true,
625
+ before_execution: typeof d.before_execution === 'boolean' ? d.before_execution : false,
626
+ max_rounds,
627
+ timeout_per_round_seconds,
628
+ synthesizer: synthesizer as DiscussionConfig['synthesizer'],
629
+ };
630
+ })(),
631
+ // Citation gate (optional boolean, default: false)
632
+ citation_gate: typeof parsed.citation_gate === 'boolean' ? parsed.citation_gate : false,
633
+ // Transitive citation gate (optional boolean, default: false)
634
+ transitive_citation_gate:
635
+ typeof parsed.transitive_citation_gate === 'boolean'
636
+ ? parsed.transitive_citation_gate
637
+ : false,
638
+ // Refinement loop (optional boolean, default: false)
639
+ refinement_loop: typeof parsed.refinement_loop === 'boolean' ? parsed.refinement_loop : false,
640
+ // LLM fallback for phase completion (Spec 3B, optional boolean, default: false)
641
+ phase_complete_llm_fallback:
642
+ typeof parsed.phase_complete_llm_fallback === 'boolean'
643
+ ? parsed.phase_complete_llm_fallback
644
+ : undefined,
645
+ // LLM fallback retry count (optional number, default: 0)
646
+ phase_complete_llm_fallback_retries:
647
+ typeof parsed.phase_complete_llm_fallback_retries === 'number'
648
+ ? Math.max(0, parsed.phase_complete_llm_fallback_retries)
649
+ : undefined,
650
+ // Timeouts config
651
+ timeouts: ((): GrdTimeouts => {
652
+ const t: Record<string, unknown> =
653
+ parsed.timeouts && typeof parsed.timeouts === 'object'
654
+ ? (parsed.timeouts as Record<string, unknown>)
655
+ : {};
656
+ const d: GrdTimeouts = defaultTimeouts;
657
+ return {
658
+ jest_ms: (t.jest_ms ?? d.jest_ms) as number,
659
+ lint_ms: (t.lint_ms ?? d.lint_ms) as number,
660
+ format_ms: (t.format_ms ?? d.format_ms) as number,
661
+ consistency_ms: (t.consistency_ms ?? d.consistency_ms) as number,
662
+ tracker_gh_ms: (t.tracker_gh_ms ?? d.tracker_gh_ms) as number,
663
+ tracker_auth_ms: (t.tracker_auth_ms ?? d.tracker_auth_ms) as number,
664
+ backend_detect_ms: (t.backend_detect_ms ?? d.backend_detect_ms) as number,
665
+ autopilot_check_ms: (t.autopilot_check_ms ?? d.autopilot_check_ms) as number,
666
+ autoresearch_test_ms: (t.autoresearch_test_ms ?? d.autoresearch_test_ms) as number,
667
+ autoresearch_coverage_ms: (t.autoresearch_coverage_ms ?? d.autoresearch_coverage_ms) as number,
668
+ autoresearch_lint_ms: (t.autoresearch_lint_ms ?? d.autoresearch_lint_ms) as number,
669
+ backend_probe_ms: (t.backend_probe_ms ?? d.backend_probe_ms) as number,
670
+ discussion_git_ms: (t.discussion_git_ms ?? d.discussion_git_ms) as number,
671
+ overstory_probe_ms: (t.overstory_probe_ms ?? d.overstory_probe_ms) as number,
672
+ overstory_install_ms: (t.overstory_install_ms ?? d.overstory_install_ms) as number,
673
+ };
674
+ })(),
675
+ };
676
+ } catch {
677
+ return defaults;
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Check if a file path is git-ignored via git check-ignore.
683
+ * @param cwd - Project working directory
684
+ * @param targetPath - Path to check against .gitignore rules
685
+ * @returns True if the path is git-ignored, false otherwise
686
+ */
687
+ function isGitIgnored(cwd: string, targetPath: string): boolean {
688
+ if (targetPath.includes('\0')) return false;
689
+ try {
690
+ execFileSync('git', ['check-ignore', '-q', '--', targetPath], {
691
+ cwd,
692
+ stdio: 'pipe',
693
+ });
694
+ return true;
695
+ } catch {
696
+ return false;
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Execute a git command with whitelist enforcement for security.
702
+ * @param cwd - Project working directory
703
+ * @param args - Git command arguments (first element is the subcommand)
704
+ * @param opts - Options object
705
+ * @returns Command result with exit code and output
706
+ */
707
+ function execGit(
708
+ cwd: string,
709
+ args: string[],
710
+ opts: { allowBlocked?: boolean } = {}
711
+ ): ExecGitResult {
712
+ // Git operation whitelist enforcement
713
+ const subcommand: string | undefined = args[0];
714
+ if (subcommand && GIT_BLOCKED_COMMANDS.has(subcommand) && !opts.allowBlocked) {
715
+ return {
716
+ exitCode: 1,
717
+ stdout: '',
718
+ stderr: `Blocked: "git ${subcommand}" is not allowed by the GRD security policy. Pass { allowBlocked: true } to override.`,
719
+ };
720
+ }
721
+ if (!opts.allowBlocked) {
722
+ for (const arg of args) {
723
+ if (GIT_BLOCKED_FLAGS.has(arg)) {
724
+ return {
725
+ exitCode: 1,
726
+ stdout: '',
727
+ stderr: `Blocked: flag "${arg}" is not allowed by the GRD security policy. Pass { allowBlocked: true } to override.`,
728
+ };
729
+ }
730
+ }
731
+ }
732
+ try {
733
+ const stdout: string = execFileSync('git', args, {
734
+ cwd,
735
+ stdio: 'pipe',
736
+ encoding: 'utf-8',
737
+ });
738
+ return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
739
+ } catch (err: unknown) {
740
+ const gitErr = err as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string };
741
+ return {
742
+ exitCode: gitErr.status ?? 1,
743
+ stdout: (gitErr.stdout ?? '').toString().trim(),
744
+ stderr: (gitErr.stderr ?? '').toString().trim(),
745
+ };
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Normalize a phase identifier by zero-padding and validating format.
751
+ * @param phase - Phase identifier (e.g., "7", "07", "07.1")
752
+ * @returns Normalized phase name with zero-padded number (e.g., "07", "07.1")
753
+ * @throws If phase is not a string or contains path traversal/directory separators
754
+ */
755
+ function normalizePhaseName(phase: string): string {
756
+ if (typeof phase !== 'string') throw new Error('Phase must be a string');
757
+ if (phase.includes('..')) throw new Error('Phase name must not contain path traversal (..)');
758
+ if (phase.includes('/') || phase.includes('\\'))
759
+ throw new Error('Phase name must not contain directory separators');
760
+ const match: RegExpMatchArray | null = phase.match(/^(\d+(?:\.\d+)?)/);
761
+ if (!match) return phase;
762
+ const num: string = match[1];
763
+ const parts: string[] = num.split('.');
764
+ const padded: string = parts[0].padStart(2, '0');
765
+ return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
766
+ }
767
+
768
+ const CODE_EXTENSIONS: Set<string> = new Set([
769
+ '.ts',
770
+ '.js',
771
+ '.py',
772
+ '.go',
773
+ '.rs',
774
+ '.swift',
775
+ '.java',
776
+ ]);
777
+
778
+ /**
779
+ * Recursively find code files up to a maximum depth, capped at 5 results.
780
+ * @param dir - Directory to search in
781
+ * @param maxDepth - Maximum directory depth to recurse into
782
+ * @param found - Accumulator array of found file paths
783
+ * @param depth - Current recursion depth
784
+ * @returns Array of absolute paths to code files found
785
+ */
786
+ function findCodeFiles(dir: string, maxDepth: number, found: string[], depth: number): string[] {
787
+ if (depth > maxDepth || found.length >= 5) return found;
788
+ let entries: { name: string; isDirectory: () => boolean; isFile: () => boolean }[];
789
+ try {
790
+ entries = fs.readdirSync(dir, { withFileTypes: true });
791
+ } catch {
792
+ return found;
793
+ }
794
+ for (const entry of entries) {
795
+ if (found.length >= 5) break;
796
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
797
+ const fullPath: string = path.join(dir, entry.name);
798
+ if (entry.isDirectory()) {
799
+ findCodeFiles(fullPath, maxDepth, found, depth + 1);
800
+ } else if (entry.isFile()) {
801
+ const ext: string = path.extname(entry.name).toLowerCase();
802
+ if (CODE_EXTENSIONS.has(ext)) {
803
+ found.push(fullPath);
804
+ }
805
+ }
806
+ }
807
+ return found;
808
+ }
809
+
810
+ // ─── Input Validation ────────────────────────────────────────────────────────
811
+
812
+ /**
813
+ * Validate phase name format strictly, rejecting path traversal and invalid characters.
814
+ * @param phase - Phase name to validate
815
+ * @returns The validated phase name if valid
816
+ * @throws If phase format is invalid, contains traversal, null bytes, or separators
817
+ */
818
+ function validatePhaseName(phase: string): string {
819
+ if (typeof phase !== 'string') throw new Error('Phase must be a string');
820
+ if (phase.includes('..')) throw new Error('Phase name must not contain path traversal (..)');
821
+ if (phase.includes('/') || phase.includes('\\'))
822
+ throw new Error('Phase name must not contain directory separators');
823
+ if (phase.includes('\0')) throw new Error('Phase name must not contain null bytes');
824
+ if (!/^\d+(?:\.\d+)?(?:-[a-zA-Z0-9-]+)?$/.test(phase)) {
825
+ throw new Error(
826
+ 'Invalid phase name format: must be digits with optional decimal and kebab-case suffix'
827
+ );
828
+ }
829
+ return phase;
830
+ }
831
+
832
+ /**
833
+ * Validate that a file path does not escape the project directory.
834
+ * @param filePath - File path to validate (relative or absolute)
835
+ * @param cwd - Project working directory used as security boundary
836
+ * @returns The validated file path if safe
837
+ * @throws If path is not a string, contains null bytes, or escapes project directory
838
+ */
839
+ function validateFilePath(filePath: string, cwd: string): string {
840
+ if (typeof filePath !== 'string') throw new Error('File path must be a string');
841
+ if (filePath.includes('\0')) throw new Error('File path must not contain null bytes');
842
+ const resolved: string = path.resolve(cwd, filePath);
843
+ if (!resolved.startsWith(cwd)) throw new Error('File path must not escape project directory');
844
+ return filePath;
845
+ }
846
+
847
+ /**
848
+ * Validate git ref format, preventing flag injection and path traversal.
849
+ * @param ref - Git reference to validate (commit hash, branch name, tag)
850
+ * @returns The validated git ref if valid
851
+ * @throws If ref is invalid, starts with dash, contains traversal, or has invalid characters
852
+ */
853
+ function validateGitRef(ref: string): string {
854
+ if (typeof ref !== 'string') throw new Error('Git ref must be a string');
855
+ if (ref.startsWith('-')) throw new Error('Git ref must not start with a dash (flag injection)');
856
+ if (ref.includes('..')) throw new Error('Git ref must not contain path traversal (..)');
857
+ if (!/^[a-zA-Z0-9._\-/~^]+$/.test(ref)) throw new Error('Git ref contains invalid characters');
858
+ if (ref.length > 256) throw new Error('Git ref too long');
859
+ return ref;
860
+ }
861
+
862
+ // ─── CLI Argument Validation ──────────────────────────────────────────────────
863
+
864
+ /**
865
+ * Validate CLI phase number argument, ensuring it is present and well-formed.
866
+ * @param phase - Phase number from CLI arguments
867
+ * @returns The validated phase number
868
+ * @throws If phase is missing or not in valid format (digits with optional decimal)
869
+ */
870
+ function validatePhaseArg(phase: string): string {
871
+ if (phase == null || phase === '') throw new Error('Phase number is required');
872
+ if (typeof phase !== 'string') throw new Error('Phase number is required');
873
+ if (!/^\d+(?:\.\d+)?(?:-[a-zA-Z0-9-]+)?$/.test(phase)) {
874
+ throw new Error('Invalid phase number: must be digits with optional decimal (e.g., 01, 02.1)');
875
+ }
876
+ return phase;
877
+ }
878
+
879
+ /**
880
+ * Validate CLI file path argument, ensuring it is present and does not escape project.
881
+ * @param filePath - File path from CLI arguments
882
+ * @param cwd - Project working directory used as security boundary
883
+ * @returns The validated file path
884
+ * @throws If file path is missing or escapes project directory
885
+ */
886
+ function validateFileArg(filePath: string, cwd: string): string {
887
+ if (filePath == null || filePath === '') throw new Error('File path is required');
888
+ return validateFilePath(filePath, cwd);
889
+ }
890
+
891
+ /**
892
+ * Validate CLI subcommand against a list of valid subcommands.
893
+ * @param sub - Subcommand string from CLI arguments
894
+ * @param validSubs - Array of valid subcommand names
895
+ * @param parentCmd - Parent command name for error messages
896
+ * @returns The validated subcommand
897
+ * @throws If subcommand is missing or not in the valid list
898
+ */
899
+ function validateSubcommand(sub: string, validSubs: string[], parentCmd: string): string {
900
+ if (sub == null || sub === '') {
901
+ throw new Error(`Subcommand required for '${parentCmd}'. Available: ${validSubs.join(', ')}`);
902
+ }
903
+ if (!validSubs.includes(sub)) {
904
+ throw new Error(
905
+ `Unknown ${parentCmd} subcommand: '${sub}'. Available: ${validSubs.join(', ')}`
906
+ );
907
+ }
908
+ return sub;
909
+ }
910
+
911
+ /**
912
+ * Validate that a required CLI argument is present and non-empty.
913
+ * @param value - Argument value to check
914
+ * @param argName - Name of the argument for error messages
915
+ * @returns The validated value
916
+ * @throws If value is null, undefined, or empty string
917
+ */
918
+ function validateRequiredArg(value: unknown, argName: string): unknown {
919
+ if (value == null || value === '') throw new Error(`${argName} is required`);
920
+ return value;
921
+ }
922
+
923
+ // ─── Output ──────────────────────────────────────────────────────────────────
924
+
925
+ /**
926
+ * Write JSON or raw output to stdout and exit with code 0.
927
+ * @param result - Result object to serialize as JSON
928
+ * @param raw - If true, output rawValue as plain text instead of JSON
929
+ * @param rawValue - Plain text value to output when raw is true
930
+ */
931
+ function output(result: unknown, raw: boolean, rawValue?: unknown): never {
932
+ if (raw && rawValue !== undefined) {
933
+ process.stdout.write(String(rawValue));
934
+ } else {
935
+ process.stdout.write(JSON.stringify(result, null, 2));
936
+ }
937
+ process.exit(0);
938
+ }
939
+
940
+ /**
941
+ * Write error message to stderr and exit with code 1.
942
+ * @param message - Error message to display
943
+ */
944
+ function error(message: string): never {
945
+ process.stderr.write('Error: ' + message + '\n');
946
+ process.exit(1);
947
+ }
948
+
949
+ /**
950
+ * Write debug message to stderr when GRD_DEBUG environment variable is set.
951
+ * No-op in normal operation; enabled by setting GRD_DEBUG=1 (or any truthy value).
952
+ * @param message - Debug message to display
953
+ * @param data - Optional data to JSON-stringify alongside the message
954
+ */
955
+ function debugLog(message: string, data?: unknown): void {
956
+ if (!process.env.GRD_DEBUG) return;
957
+ const prefix: string = '[grd:debug] ';
958
+ if (data !== undefined) {
959
+ process.stderr.write(prefix + message + ' ' + JSON.stringify(data) + '\n');
960
+ } else {
961
+ process.stderr.write(prefix + message + '\n');
962
+ }
963
+ }
964
+
965
+ // ─── Shared Cache Factory ─────────────────────────────────────────────────────
966
+
967
+ /**
968
+ * Create a run-scoped file content cache.
969
+ * Returns a cache object with `get`, `init`, and `reset` methods.
970
+ * Modules can use this to avoid redundant disk reads within a single CLI invocation.
971
+ *
972
+ * Usage pattern:
973
+ * const cache = createRunCache();
974
+ * cache.init(); // activate caching for this run
975
+ * cache.get(p, reader) // returns cached value or calls reader(p) and stores result
976
+ * cache.reset(); // deactivate and clear
977
+ *
978
+ * @returns Cache object with init, reset, and get methods
979
+ */
980
+ function createRunCache(): RunCache {
981
+ let _map: Map<string, unknown> | null = null;
982
+ return {
983
+ /** Activate the cache (creates a new Map). */
984
+ init(): void {
985
+ _map = new Map();
986
+ },
987
+ /** Deactivate and clear the cache. */
988
+ reset(): void {
989
+ _map = null;
990
+ },
991
+ /**
992
+ * Get a cached value, or compute it with `reader(key)` and store the result.
993
+ * Falls back to calling `reader(key)` directly if the cache is not active.
994
+ * @param key - Cache key (typically a file path)
995
+ * @param reader - Function to produce the value if not cached
996
+ * @returns The cached or freshly computed value
997
+ */
998
+ get(key: string, reader: (key: string) => unknown): unknown {
999
+ if (!_map) return reader(key);
1000
+ if (!_map.has(key)) _map.set(key, reader(key));
1001
+ return _map.get(key);
1002
+ },
1003
+ };
1004
+ }
1005
+
1006
+ // ─── Phase Directory Utilities ────────────────────────────────────────────────
1007
+
1008
+ /**
1009
+ * Find a phase directory name inside `phasesDir` that matches `phaseArg`.
1010
+ * Matches by exact normalized name or by prefix `<normalized>-`.
1011
+ * @param phasesDir - Absolute path to the phases directory
1012
+ * @param phaseArg - Phase identifier (e.g., '1', '01', '1.1')
1013
+ * @returns The matching directory name, or null if not found
1014
+ */
1015
+ function findPhaseDir(phasesDir: string, phaseArg: string): string | null {
1016
+ const normalized: string = normalizePhaseName(phaseArg);
1017
+ try {
1018
+ const entries: { name: string; isDirectory: () => boolean }[] = fs.readdirSync(phasesDir, {
1019
+ withFileTypes: true,
1020
+ });
1021
+ const dirs: string[] = entries
1022
+ .filter((e: { isDirectory: () => boolean }) => e.isDirectory())
1023
+ .map((e: { name: string }) => e.name)
1024
+ .sort();
1025
+ return dirs.find((d: string) => d.startsWith(normalized + '-') || d === normalized) || null;
1026
+ } catch {
1027
+ return null;
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * Parse a phase number from a directory name or plain string.
1033
+ * Handles formats like '01-feature-name', '1.2-sub', '1', etc.
1034
+ * @param str - Directory name or phase string to parse
1035
+ * @returns The numeric phase number as a string (e.g. '1', '1.2'), or null
1036
+ */
1037
+ function parsePhaseNumber(str: string): string | null {
1038
+ if (!str) return null;
1039
+ const match: RegExpMatchArray | null = str.match(/^(\d+(?:\.\d+)?)/);
1040
+ return match ? match[1] : null;
1041
+ }
1042
+
1043
+ // ─── Directory Walking ────────────────────────────────────────────────────────
1044
+
1045
+ /**
1046
+ * Recursively collect JavaScript file paths under `rootDir`.
1047
+ * Skips `node_modules`, `.git`, `.planning`, and any paths matching `excludePatterns`.
1048
+ * @param rootDir - Root directory to walk (returned paths are relative to this)
1049
+ * @param excludePatterns - Substrings; any relative path containing one is skipped
1050
+ * @returns Relative paths of all .js files found
1051
+ */
1052
+ function walkJsFiles(rootDir: string, excludePatterns: string[] = []): string[] {
1053
+ const results: string[] = [];
1054
+ _walkJsDir(rootDir, rootDir, results, excludePatterns);
1055
+ return results;
1056
+ }
1057
+
1058
+ function _walkJsDir(
1059
+ rootDir: string,
1060
+ currentDir: string,
1061
+ results: string[],
1062
+ excludePatterns: string[]
1063
+ ): void {
1064
+ let entries: { name: string; isDirectory: () => boolean; isFile: () => boolean }[];
1065
+ try {
1066
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
1067
+ } catch {
1068
+ return;
1069
+ }
1070
+ for (const entry of entries) {
1071
+ const fullPath: string = path.join(currentDir, entry.name);
1072
+ const relPath: string = path.relative(rootDir, fullPath);
1073
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.planning') {
1074
+ continue;
1075
+ }
1076
+ if (excludePatterns.some((p: string) => relPath.includes(p))) continue;
1077
+ if (entry.isDirectory()) {
1078
+ _walkJsDir(rootDir, fullPath, results, excludePatterns);
1079
+ } else if (entry.isFile() && entry.name.endsWith('.js')) {
1080
+ results.push(relPath);
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ // ─── Compound Helpers ────────────────────────────────────────────────────────
1086
+
1087
+ /**
1088
+ * Resolve the model name for a given agent type from project configuration.
1089
+ * @param cwd - Project working directory
1090
+ * @param agentType - Agent type key (e.g., 'grd-executor', 'grd-planner')
1091
+ * @returns Model name (e.g., 'opus', 'sonnet', 'haiku')
1092
+ */
1093
+ function resolveModelInternal(cwd: string, agentType: string): string {
1094
+ const config: GrdConfig = loadConfig(cwd);
1095
+ const profile: string = config.model_profile || 'balanced';
1096
+ const agentModels: Record<ModelProfileName, ModelTier> | undefined = MODEL_PROFILES[agentType];
1097
+ if (!agentModels) {
1098
+ // Unknown agent type: resolve 'sonnet' tier through backend
1099
+ const backend: unknown = detectBackend(cwd);
1100
+ return resolveBackendModel(backend, 'sonnet', config, cwd) as string;
1101
+ }
1102
+ const tier: ModelTier =
1103
+ agentModels[profile as ModelProfileName] || agentModels['balanced'] || 'sonnet';
1104
+ const backend: unknown = detectBackend(cwd);
1105
+ return resolveBackendModel(backend, tier, config, cwd) as string;
1106
+ }
1107
+
1108
+ /**
1109
+ * Find a phase directory and enumerate its plans, summaries, and metadata.
1110
+ * @param cwd - Project working directory
1111
+ * @param phase - Phase identifier to search for
1112
+ * @returns Phase info object with directory, plans, summaries, and flags, or null if not found
1113
+ */
1114
+ function findPhaseInternal(cwd: string, phase: string): PhaseInfo | null {
1115
+ if (!phase) return null;
1116
+
1117
+ const phasesDir: string = getPhasesDirPath(cwd) as string;
1118
+ const normalized: string = normalizePhaseName(phase);
1119
+
1120
+ try {
1121
+ const entries: { name: string; isDirectory: () => boolean }[] = fs.readdirSync(phasesDir, {
1122
+ withFileTypes: true,
1123
+ });
1124
+ const dirs: string[] = entries
1125
+ .filter((e: { isDirectory: () => boolean }) => e.isDirectory())
1126
+ .map((e: { name: string }) => e.name)
1127
+ .sort();
1128
+ const match: string | undefined = dirs.find(
1129
+ (d: string) => d.startsWith(normalized + '-') || d === normalized
1130
+ );
1131
+ if (!match) return null;
1132
+
1133
+ const dirMatch: RegExpMatchArray | null = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
1134
+ const phaseNumber: string = dirMatch ? dirMatch[1] : normalized;
1135
+ const phaseName: string | null = dirMatch && dirMatch[2] ? dirMatch[2] : null;
1136
+ const phaseDir: string = path.join(phasesDir, match);
1137
+ const phaseFiles: string[] = fs.readdirSync(phaseDir);
1138
+
1139
+ const plans: string[] = phaseFiles
1140
+ .filter((f: string) => f.endsWith('-PLAN.md') || f === 'PLAN.md')
1141
+ .sort();
1142
+ const summaries: string[] = phaseFiles
1143
+ .filter((f: string) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md')
1144
+ .sort();
1145
+ const hasResearch: boolean = phaseFiles.some(
1146
+ (f: string) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'
1147
+ );
1148
+ const hasContext: boolean = phaseFiles.some(
1149
+ (f: string) => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'
1150
+ );
1151
+ const hasVerification: boolean = phaseFiles.some(
1152
+ (f: string) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'
1153
+ );
1154
+
1155
+ // Determine incomplete plans (plans without matching summaries)
1156
+ const completedPlanIds: Set<string> = new Set(
1157
+ summaries.map((s: string) => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
1158
+ );
1159
+ const incompletePlans: string[] = plans.filter((p: string) => {
1160
+ const planId: string = p.replace('-PLAN.md', '').replace('PLAN.md', '');
1161
+ return !completedPlanIds.has(planId);
1162
+ });
1163
+
1164
+ // Check if phase exists in ROADMAP.md (non-blocking consistency check)
1165
+ let consistencyWarning: string | null = null;
1166
+ try {
1167
+ const roadmapPath: string = path.join(cwd, '.planning', 'ROADMAP.md');
1168
+ const roadmapContent: string = fs.readFileSync(roadmapPath, 'utf-8');
1169
+ const unpadded: string = String(parseInt(phaseNumber, 10));
1170
+ const phasePattern: RegExp = new RegExp(
1171
+ `#{2,}\\s*Phase\\s+(?:${phaseNumber}|${unpadded})\\s*:`,
1172
+ 'i'
1173
+ );
1174
+ if (!phasePattern.test(roadmapContent)) {
1175
+ consistencyWarning = `Phase ${phaseNumber} found on disk but not in ROADMAP.md — may be from a previous milestone`;
1176
+ }
1177
+ } catch {
1178
+ // ROADMAP.md may not exist yet; skip check
1179
+ }
1180
+
1181
+ return {
1182
+ found: true,
1183
+ directory: path.relative(cwd, path.join(phasesDir, match)),
1184
+ phase_number: phaseNumber,
1185
+ phase_name: phaseName,
1186
+ phase_slug: phaseName
1187
+ ? phaseName
1188
+ .toLowerCase()
1189
+ .replace(/[^a-z0-9]+/g, '-')
1190
+ .replace(/^-+|-+$/g, '')
1191
+ : null,
1192
+ plans,
1193
+ summaries,
1194
+ incomplete_plans: incompletePlans,
1195
+ has_research: hasResearch,
1196
+ has_context: hasContext,
1197
+ has_verification: hasVerification,
1198
+ consistency_warning: consistencyWarning,
1199
+ };
1200
+ } catch {
1201
+ return null;
1202
+ }
1203
+ }
1204
+
1205
+ /**
1206
+ * Check if a path exists on the filesystem.
1207
+ * @param cwd - Project working directory (used for resolving relative paths)
1208
+ * @param targetPath - Path to check, relative or absolute
1209
+ * @returns True if the path exists, false otherwise
1210
+ */
1211
+ function pathExistsInternal(cwd: string, targetPath: string): boolean {
1212
+ const fullPath: string = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
1213
+ try {
1214
+ fs.statSync(fullPath);
1215
+ return true;
1216
+ } catch {
1217
+ return false;
1218
+ }
1219
+ }
1220
+
1221
+ /**
1222
+ * Generate a kebab-case slug from text, stripping non-alphanumeric characters.
1223
+ * @param text - Input text to slugify
1224
+ * @returns Kebab-case slug, or null if text is falsy
1225
+ */
1226
+ function generateSlugInternal(text: string): string | null {
1227
+ if (!text) return null;
1228
+ return text
1229
+ .toLowerCase()
1230
+ .replace(/[^a-z0-9]+/g, '-')
1231
+ .replace(/^-+|-+$/g, '');
1232
+ }
1233
+
1234
+ /**
1235
+ * Strip shipped milestone sections wrapped in <details> blocks.
1236
+ * These contain archived milestone content that should not contaminate
1237
+ * active milestone parsing (phase numbers, version detection, etc.).
1238
+ * @param content - Raw ROADMAP.md content
1239
+ * @returns Content with <details>...</details> blocks removed
1240
+ */
1241
+ function stripShippedSections(content: string): string {
1242
+ if (!content) return content;
1243
+ return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
1244
+ }
1245
+
1246
+ /**
1247
+ * Extract milestone version and name from ROADMAP.md.
1248
+ * @param cwd - Project working directory
1249
+ * @returns Milestone info with version (e.g., 'v1.0') and name
1250
+ */
1251
+ function getMilestoneInfo(cwd: string): MilestoneInfo {
1252
+ try {
1253
+ const roadmap: string = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
1254
+ const active: string = stripShippedSections(roadmap);
1255
+
1256
+ // Strategy 1: Find "(in progress)" milestone from bullet list
1257
+ const inProgressMatch: RegExpMatchArray | null = active.match(
1258
+ /-\s+(v[\d.]+)\s+([^\n(]+?)\s*\(in progress\)/im
1259
+ );
1260
+ if (inProgressMatch) {
1261
+ return { version: inProgressMatch[1], name: inProgressMatch[2].trim() };
1262
+ }
1263
+
1264
+ // Strategy 2: Last non-shipped milestone bullet
1265
+ const bulletRegex: RegExp = /-\s+(v[\d.]+)\s+([^\n(]+?)(?:\s*\(([^)]*)\))?\s*$/gim;
1266
+ let lastNonShipped: MilestoneInfo | null = null;
1267
+ let bm: RegExpExecArray | null;
1268
+ while ((bm = bulletRegex.exec(active)) !== null) {
1269
+ const status: string = bm[3] || '';
1270
+ if (!/shipped/i.test(status)) {
1271
+ lastNonShipped = { version: bm[1], name: bm[2].trim() };
1272
+ }
1273
+ }
1274
+ if (lastNonShipped) return lastNonShipped;
1275
+
1276
+ // Strategy 3: Active heading "## ... vX.Y.Z ... (In Progress)"
1277
+ const headingMatch: RegExpMatchArray | null = active.match(
1278
+ /##\s*.*?(v\d+\.\d+(?:\.\d+)?)\s*[:\s]+([^\n(]+)/
1279
+ );
1280
+ if (headingMatch) {
1281
+ return { version: headingMatch[1], name: headingMatch[2].trim() };
1282
+ }
1283
+
1284
+ // Strategy 4: Fallback -- first version found (with 3-part support)
1285
+ const versionMatch: RegExpMatchArray | null = active.match(/v(\d+\.\d+(?:\.\d+)?)/);
1286
+ return {
1287
+ version: versionMatch ? versionMatch[0] : 'v1.0',
1288
+ name: 'milestone',
1289
+ };
1290
+ } catch {
1291
+ return { version: 'v1.0', name: 'milestone' };
1292
+ }
1293
+ }
1294
+
1295
+ /**
1296
+ * Resolve model name from a config object without disk I/O.
1297
+ * When cwd is provided, resolves to backend-specific model name.
1298
+ * When cwd is omitted, returns abstract tier name (backward compatible).
1299
+ * @param config - Configuration object with model_profile field
1300
+ * @param agentType - Agent type key to look up in MODEL_PROFILES
1301
+ * @param cwd - Optional project working directory for backend-specific resolution
1302
+ * @param options - Optional overrides. When options.effectiveTierOverride is set,
1303
+ * it replaces the MODEL_PROFILES lookup entirely. Callers that omit this parameter
1304
+ * get identical behavior to before (backward compatible).
1305
+ * @returns Model name (e.g., 'opus', 'sonnet', 'haiku', or backend-specific name)
1306
+ */
1307
+ function resolveModelForAgent(
1308
+ config: GrdConfig,
1309
+ agentType: string,
1310
+ cwd?: string,
1311
+ options?: { effectiveTierOverride?: ModelTier }
1312
+ ): string {
1313
+ const profile: string = (config.model_profile || 'balanced').toLowerCase();
1314
+ const agentModels: Record<ModelProfileName, ModelTier> | undefined = MODEL_PROFILES[agentType];
1315
+ const baseTier: ModelTier = agentModels
1316
+ ? agentModels[profile as ModelProfileName] || agentModels['balanced'] || 'sonnet'
1317
+ : 'sonnet';
1318
+ // Use override when provided; otherwise fall back to MODEL_PROFILES lookup
1319
+ const tier: ModelTier = options?.effectiveTierOverride || baseTier;
1320
+ // If cwd provided, resolve to backend-specific model name
1321
+ if (cwd) {
1322
+ const backend: unknown = detectBackend(cwd);
1323
+ return resolveBackendModel(backend, tier, config, cwd) as string;
1324
+ }
1325
+ // Backward compatible: no cwd means return tier name (existing behavior)
1326
+ return tier;
1327
+ }
1328
+
1329
+ /**
1330
+ * Resolve effort level for a given agent type from project configuration.
1331
+ * Returns null if the backend does not support effort levels.
1332
+ * @param config - Project configuration
1333
+ * @param agentType - Agent type key (e.g., 'grd-executor', 'grd-planner')
1334
+ * @param cwd - Optional project working directory for backend detection
1335
+ * @returns Effort level string ('low', 'medium', 'high') or null if unsupported
1336
+ */
1337
+ function resolveEffortForAgent(config: GrdConfig, agentType: string, cwd?: string): string | null {
1338
+ const backend = cwd ? detectBackend(cwd) : 'claude';
1339
+ const caps = getBackendCapabilities(backend);
1340
+ if (!caps.effort) return null;
1341
+ const profile: ModelProfileName = (config.model_profile || 'balanced') as ModelProfileName;
1342
+ return resolveEffortLevel(agentType, profile);
1343
+ }
1344
+
1345
+ // ─── Config Drift Validator ───────────────────────────────────────────────────
1346
+
1347
+ /**
1348
+ * Known config keys with their default values, organized for drift detection.
1349
+ * Each entry has: key (dot-path for nested keys), default value, and the gd settings
1350
+ * command to fix it.
1351
+ */
1352
+ // Codex r2 P2: `gd settings` tool-mode only accepts token_profile and
1353
+ // phase_complete_llm_fallback. All other keys must use `gd config-set
1354
+ // <dot.path> <value>` (canonical route at lib/cli/index.ts:42), so emit
1355
+ // runnable commands for those instead of broken `gd settings ...` hints.
1356
+ const CONFIG_DRIFT_KEYS: Array<{ key: string; default: unknown; fix: string }> = [
1357
+ { key: 'token_profile', default: 'balanced', fix: 'gd settings token_profile balanced' },
1358
+ // v0.4 Phase 1: effort axis (tool-mode settings key, alongside the other two).
1359
+ { key: 'effort', default: 'balanced', fix: 'gd settings effort balanced' },
1360
+ { key: 'phase_complete_llm_fallback', default: false, fix: 'gd settings phase_complete_llm_fallback false' },
1361
+ { key: 'autonomous_mode', default: false, fix: 'gd config-set autonomous_mode false' },
1362
+ { key: 'branching_strategy', default: 'none', fix: 'gd config-set branching_strategy none' },
1363
+ { key: 'scheduler.idle_timeout_seconds', default: 900, fix: 'gd config-set scheduler.idle_timeout_seconds 900' },
1364
+ { key: 'scheduler.budget_pressure_thresholds', default: { warning: 0.6, high: 0.8, critical: 0.95 }, fix: 'gd config-set scheduler.budget_pressure_thresholds \'{"warning":0.6,"high":0.8,"critical":0.95}\'' },
1365
+ // Codex r27 P2: cmdHealth uses DEFAULT_WEIGHTS from lib/drift.ts
1366
+ // (0.5/0.3/0.2) when `drift` is missing. The fix command must
1367
+ // materialize the same runtime default so users don't accidentally
1368
+ // change drift-scoring semantics by applying it.
1369
+ { key: 'drift', default: { weights: { goal: 0.5, constraint: 0.3, ontology: 0.2 }, threshold: 0.3 }, fix: 'gd config-set drift \'{"weights":{"goal":0.5,"constraint":0.3,"ontology":0.2},"threshold":0.3}\'' },
1370
+ { key: 'autopilot', default: {}, fix: 'gd config-set autopilot \'{}\''},
1371
+ ];
1372
+
1373
+ interface DriftReport {
1374
+ missing_keys: Array<{ key: string; default: unknown; fix_command: string }>;
1375
+ deprecated_keys: string[];
1376
+ total_checks: number;
1377
+ }
1378
+
1379
+ /**
1380
+ * Validate config.json against the current schema defaults, identifying keys that
1381
+ * were added after initial `gd init` and are missing from the user's config.
1382
+ *
1383
+ * @param cwd - Project working directory
1384
+ * @returns DriftReport with missing keys, fix commands, and deprecated keys
1385
+ */
1386
+ function validateConfigDrift(cwd: string): DriftReport {
1387
+ const configPath = path.join(cwd, '.planning', 'config.json');
1388
+ let parsed: Record<string, unknown> = {};
1389
+ try {
1390
+ const raw = fs.readFileSync(configPath, 'utf-8');
1391
+ parsed = JSON.parse(raw);
1392
+ } catch {
1393
+ // Config doesn't exist or is malformed — all keys are "missing"
1394
+ }
1395
+
1396
+ const missing: DriftReport['missing_keys'] = [];
1397
+
1398
+ for (const entry of CONFIG_DRIFT_KEYS) {
1399
+ // Support dot-path for nested keys (e.g. "scheduler.idle_timeout_seconds")
1400
+ const parts = entry.key.split('.');
1401
+ let cur: unknown = parsed;
1402
+ for (const part of parts) {
1403
+ if (cur === null || typeof cur !== 'object') { cur = undefined; break; }
1404
+ cur = (cur as Record<string, unknown>)[part];
1405
+ }
1406
+ if (cur === undefined) {
1407
+ missing.push({ key: entry.key, default: entry.default, fix_command: entry.fix });
1408
+ }
1409
+ }
1410
+
1411
+ return {
1412
+ missing_keys: missing,
1413
+ deprecated_keys: [],
1414
+ total_checks: CONFIG_DRIFT_KEYS.length,
1415
+ };
1416
+ }
1417
+
1418
+ // ─── Exports ─────────────────────────────────────────────────────────────────
1419
+
1420
+ module.exports = {
1421
+ // Node built-ins (re-exported for convenience)
1422
+ fs,
1423
+ path,
1424
+ os,
1425
+ execFileSync,
1426
+ // Constants
1427
+ GIT_ALLOWED_COMMANDS,
1428
+ GIT_BLOCKED_COMMANDS,
1429
+ GIT_BLOCKED_FLAGS,
1430
+ MODEL_PROFILES,
1431
+ CODE_EXTENSIONS,
1432
+ // Helpers
1433
+ parseIncludeFlag,
1434
+ safeReadFile,
1435
+ safeReadMarkdown,
1436
+ safeReadJSON,
1437
+ extractMarkdownSection,
1438
+ loadConfig,
1439
+ isGitIgnored,
1440
+ execGit,
1441
+ normalizePhaseName,
1442
+ findCodeFiles,
1443
+ // Input validation
1444
+ validatePhaseName,
1445
+ validateFilePath,
1446
+ validateGitRef,
1447
+ // CLI argument validation
1448
+ validatePhaseArg,
1449
+ validateFileArg,
1450
+ validateSubcommand,
1451
+ validateRequiredArg,
1452
+ // Caching
1453
+ createRunCache,
1454
+ // Phase directory utilities
1455
+ findPhaseDir,
1456
+ parsePhaseNumber,
1457
+ // Directory walking
1458
+ walkJsFiles,
1459
+ // Output
1460
+ output,
1461
+ error,
1462
+ debugLog,
1463
+ // Compound helpers
1464
+ resolveModelInternal,
1465
+ findPhaseInternal,
1466
+ pathExistsInternal,
1467
+ generateSlugInternal,
1468
+ getMilestoneInfo,
1469
+ stripShippedSections,
1470
+ resolveModelForAgent,
1471
+ resolveEffortForAgent,
1472
+ levenshteinDistance,
1473
+ findClosestCommand,
1474
+ clearPhaseCache,
1475
+ validateConfigDrift,
1476
+ // v0.4 Phase 1: effort axis
1477
+ EFFORT_PROFILES,
1478
+ resolveEffortKnob,
1479
+ };