@jokerized/getresearchdone 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (711) hide show
  1. package/.claude-plugin/plugin.json +103 -0
  2. package/README.md +211 -0
  3. package/agents/grd-baseline-assessor.md +684 -0
  4. package/agents/grd-code-reviewer.md +300 -0
  5. package/agents/grd-codebase-mapper.md +355 -0
  6. package/agents/grd-critique-agent.md +119 -0
  7. package/agents/grd-debugger.md +519 -0
  8. package/agents/grd-deep-diver.md +737 -0
  9. package/agents/grd-eval-planner.md +913 -0
  10. package/agents/grd-eval-reporter.md +717 -0
  11. package/agents/grd-executor.md +683 -0
  12. package/agents/grd-feasibility-analyst.md +624 -0
  13. package/agents/grd-integration-checker.md +367 -0
  14. package/agents/grd-knowledge-miner.md +81 -0
  15. package/agents/grd-migrator.md +88 -0
  16. package/agents/grd-phase-researcher.md +697 -0
  17. package/agents/grd-plan-checker.md +443 -0
  18. package/agents/grd-planner.md +1532 -0
  19. package/agents/grd-product-owner.md +562 -0
  20. package/agents/grd-project-researcher.md +513 -0
  21. package/agents/grd-research-synthesizer.md +273 -0
  22. package/agents/grd-roadmapper.md +798 -0
  23. package/agents/grd-surveyor.md +566 -0
  24. package/agents/grd-verifier.md +893 -0
  25. package/bin/gd.js +4 -0
  26. package/bin/gd.ts +227 -0
  27. package/bin/grd-manifest.js +4 -0
  28. package/bin/grd-manifest.ts +286 -0
  29. package/bin/grd-mcp-server.js +4 -0
  30. package/bin/grd-mcp-server.ts +124 -0
  31. package/bin/grd-tools.js +4 -0
  32. package/bin/grd-tools.ts +2471 -0
  33. package/bin/postinstall.js +4 -0
  34. package/bin/postinstall.ts +80 -0
  35. package/commands/add-phase.md +123 -0
  36. package/commands/add-todo.md +87 -0
  37. package/commands/assess-baseline.md +289 -0
  38. package/commands/autopilot.md +100 -0
  39. package/commands/autoplan.md +55 -0
  40. package/commands/check-todos.md +87 -0
  41. package/commands/compare-methods.md +262 -0
  42. package/commands/complete-milestone.md +225 -0
  43. package/commands/debug.md +372 -0
  44. package/commands/deep-dive.md +288 -0
  45. package/commands/discover.md +281 -0
  46. package/commands/discuss-phase.md +188 -0
  47. package/commands/discuss.md +55 -0
  48. package/commands/eval-report.md +310 -0
  49. package/commands/evolve.md +79 -0
  50. package/commands/execute-phase.md +1017 -0
  51. package/commands/feasibility.md +292 -0
  52. package/commands/help.md +407 -0
  53. package/commands/init.md +1508 -0
  54. package/commands/insert-phase.md +113 -0
  55. package/commands/iterate.md +327 -0
  56. package/commands/list-phase-assumptions.md +217 -0
  57. package/commands/long-term-roadmap.md +202 -0
  58. package/commands/map-codebase.md +111 -0
  59. package/commands/migrate.md +159 -0
  60. package/commands/new-milestone.md +169 -0
  61. package/commands/pause-work.md +83 -0
  62. package/commands/plan-milestone-gaps.md +373 -0
  63. package/commands/plan-phase.md +655 -0
  64. package/commands/principles.md +328 -0
  65. package/commands/product-plan.md +319 -0
  66. package/commands/progress.md +481 -0
  67. package/commands/quick.md +167 -0
  68. package/commands/reapply-patches.md +154 -0
  69. package/commands/remove-phase.md +97 -0
  70. package/commands/requirement.md +96 -0
  71. package/commands/resume-project.md +113 -0
  72. package/commands/settings.md +1144 -0
  73. package/commands/survey.md +242 -0
  74. package/commands/sync.md +246 -0
  75. package/commands/tracker-setup.md +322 -0
  76. package/commands/update.md +202 -0
  77. package/commands/verify-phase.md +335 -0
  78. package/commands/verify-work.md +701 -0
  79. package/commands/wireup.md +29 -0
  80. package/dist/bin/gd.d.ts +3 -0
  81. package/dist/bin/gd.d.ts.map +1 -0
  82. package/dist/bin/gd.js +178 -0
  83. package/dist/bin/gd.js.map +1 -0
  84. package/dist/bin/grd-manifest.d.ts +3 -0
  85. package/dist/bin/grd-manifest.d.ts.map +1 -0
  86. package/dist/bin/grd-manifest.js +202 -0
  87. package/dist/bin/grd-manifest.js.map +1 -0
  88. package/dist/bin/grd-mcp-server.d.ts +3 -0
  89. package/dist/bin/grd-mcp-server.d.ts.map +1 -0
  90. package/dist/bin/grd-mcp-server.js +71 -0
  91. package/dist/bin/grd-mcp-server.js.map +1 -0
  92. package/dist/bin/grd-tools.d.ts +3 -0
  93. package/dist/bin/grd-tools.d.ts.map +1 -0
  94. package/dist/bin/grd-tools.js +1680 -0
  95. package/dist/bin/grd-tools.js.map +1 -0
  96. package/dist/bin/postinstall.d.ts +3 -0
  97. package/dist/bin/postinstall.d.ts.map +1 -0
  98. package/dist/bin/postinstall.js +61 -0
  99. package/dist/bin/postinstall.js.map +1 -0
  100. package/dist/lib/autopilot-milestone.d.ts +2 -0
  101. package/dist/lib/autopilot-milestone.d.ts.map +1 -0
  102. package/dist/lib/autopilot-milestone.js +94 -0
  103. package/dist/lib/autopilot-milestone.js.map +1 -0
  104. package/dist/lib/autopilot-pipeline.d.ts +2 -0
  105. package/dist/lib/autopilot-pipeline.d.ts.map +1 -0
  106. package/dist/lib/autopilot-pipeline.js +830 -0
  107. package/dist/lib/autopilot-pipeline.js.map +1 -0
  108. package/dist/lib/autopilot-waves.d.ts +2 -0
  109. package/dist/lib/autopilot-waves.d.ts.map +1 -0
  110. package/dist/lib/autopilot-waves.js +266 -0
  111. package/dist/lib/autopilot-waves.js.map +1 -0
  112. package/dist/lib/autopilot.d.ts +2 -0
  113. package/dist/lib/autopilot.d.ts.map +1 -0
  114. package/dist/lib/autopilot.js +1314 -0
  115. package/dist/lib/autopilot.js.map +1 -0
  116. package/dist/lib/autoplan.d.ts +2 -0
  117. package/dist/lib/autoplan.d.ts.map +1 -0
  118. package/dist/lib/autoplan.js +198 -0
  119. package/dist/lib/autoplan.js.map +1 -0
  120. package/dist/lib/autoresearch.d.ts +2 -0
  121. package/dist/lib/autoresearch.d.ts.map +1 -0
  122. package/dist/lib/autoresearch.js +626 -0
  123. package/dist/lib/autoresearch.js.map +1 -0
  124. package/dist/lib/backend.d.ts +2 -0
  125. package/dist/lib/backend.d.ts.map +1 -0
  126. package/dist/lib/backend.js +1036 -0
  127. package/dist/lib/backend.js.map +1 -0
  128. package/dist/lib/benchmark.d.ts +99 -0
  129. package/dist/lib/benchmark.d.ts.map +1 -0
  130. package/dist/lib/benchmark.js +278 -0
  131. package/dist/lib/benchmark.js.map +1 -0
  132. package/dist/lib/citations.d.ts +2 -0
  133. package/dist/lib/citations.d.ts.map +1 -0
  134. package/dist/lib/citations.js +642 -0
  135. package/dist/lib/citations.js.map +1 -0
  136. package/dist/lib/cleanup.d.ts +2 -0
  137. package/dist/lib/cleanup.d.ts.map +1 -0
  138. package/dist/lib/cleanup.js +1222 -0
  139. package/dist/lib/cleanup.js.map +1 -0
  140. package/dist/lib/cli/adapters.d.ts +10 -0
  141. package/dist/lib/cli/adapters.d.ts.map +1 -0
  142. package/dist/lib/cli/adapters.js +27 -0
  143. package/dist/lib/cli/adapters.js.map +1 -0
  144. package/dist/lib/cli/agent.d.ts +17 -0
  145. package/dist/lib/cli/agent.d.ts.map +1 -0
  146. package/dist/lib/cli/agent.js +53 -0
  147. package/dist/lib/cli/agent.js.map +1 -0
  148. package/dist/lib/cli/index.d.ts +21 -0
  149. package/dist/lib/cli/index.d.ts.map +1 -0
  150. package/dist/lib/cli/index.js +264 -0
  151. package/dist/lib/cli/index.js.map +1 -0
  152. package/dist/lib/cli/output.d.ts +20 -0
  153. package/dist/lib/cli/output.d.ts.map +1 -0
  154. package/dist/lib/cli/output.js +22 -0
  155. package/dist/lib/cli/output.js.map +1 -0
  156. package/dist/lib/cli/scan-dispatch.d.ts +9 -0
  157. package/dist/lib/cli/scan-dispatch.d.ts.map +1 -0
  158. package/dist/lib/cli/scan-dispatch.js +107 -0
  159. package/dist/lib/cli/scan-dispatch.js.map +1 -0
  160. package/dist/lib/cli/tools.d.ts +16 -0
  161. package/dist/lib/cli/tools.d.ts.map +1 -0
  162. package/dist/lib/cli/tools.js +168 -0
  163. package/dist/lib/cli/tools.js.map +1 -0
  164. package/dist/lib/commands/_dashboard-parsers.d.ts +2 -0
  165. package/dist/lib/commands/_dashboard-parsers.d.ts.map +1 -0
  166. package/dist/lib/commands/_dashboard-parsers.js +192 -0
  167. package/dist/lib/commands/_dashboard-parsers.js.map +1 -0
  168. package/dist/lib/commands/analysis.d.ts +2 -0
  169. package/dist/lib/commands/analysis.d.ts.map +1 -0
  170. package/dist/lib/commands/analysis.js +1418 -0
  171. package/dist/lib/commands/analysis.js.map +1 -0
  172. package/dist/lib/commands/assumptions.d.ts +2 -0
  173. package/dist/lib/commands/assumptions.d.ts.map +1 -0
  174. package/dist/lib/commands/assumptions.js +166 -0
  175. package/dist/lib/commands/assumptions.js.map +1 -0
  176. package/dist/lib/commands/blame.d.ts +2 -0
  177. package/dist/lib/commands/blame.d.ts.map +1 -0
  178. package/dist/lib/commands/blame.js +133 -0
  179. package/dist/lib/commands/blame.js.map +1 -0
  180. package/dist/lib/commands/budget.d.ts +2 -0
  181. package/dist/lib/commands/budget.d.ts.map +1 -0
  182. package/dist/lib/commands/budget.js +100 -0
  183. package/dist/lib/commands/budget.js.map +1 -0
  184. package/dist/lib/commands/check-plans.d.ts +2 -0
  185. package/dist/lib/commands/check-plans.d.ts.map +1 -0
  186. package/dist/lib/commands/check-plans.js +190 -0
  187. package/dist/lib/commands/check-plans.js.map +1 -0
  188. package/dist/lib/commands/config.d.ts +2 -0
  189. package/dist/lib/commands/config.d.ts.map +1 -0
  190. package/dist/lib/commands/config.js +188 -0
  191. package/dist/lib/commands/config.js.map +1 -0
  192. package/dist/lib/commands/dashboard.d.ts +2 -0
  193. package/dist/lib/commands/dashboard.d.ts.map +1 -0
  194. package/dist/lib/commands/dashboard.js +466 -0
  195. package/dist/lib/commands/dashboard.js.map +1 -0
  196. package/dist/lib/commands/estimate.d.ts +2 -0
  197. package/dist/lib/commands/estimate.d.ts.map +1 -0
  198. package/dist/lib/commands/estimate.js +148 -0
  199. package/dist/lib/commands/estimate.js.map +1 -0
  200. package/dist/lib/commands/eval-diff.d.ts +2 -0
  201. package/dist/lib/commands/eval-diff.d.ts.map +1 -0
  202. package/dist/lib/commands/eval-diff.js +213 -0
  203. package/dist/lib/commands/eval-diff.js.map +1 -0
  204. package/dist/lib/commands/freshness.d.ts +2 -0
  205. package/dist/lib/commands/freshness.d.ts.map +1 -0
  206. package/dist/lib/commands/freshness.js +163 -0
  207. package/dist/lib/commands/freshness.js.map +1 -0
  208. package/dist/lib/commands/health.d.ts +2 -0
  209. package/dist/lib/commands/health.d.ts.map +1 -0
  210. package/dist/lib/commands/health.js +435 -0
  211. package/dist/lib/commands/health.js.map +1 -0
  212. package/dist/lib/commands/index.d.ts +2 -0
  213. package/dist/lib/commands/index.d.ts.map +1 -0
  214. package/dist/lib/commands/index.js +128 -0
  215. package/dist/lib/commands/index.js.map +1 -0
  216. package/dist/lib/commands/install.d.ts +56 -0
  217. package/dist/lib/commands/install.d.ts.map +1 -0
  218. package/dist/lib/commands/install.js +214 -0
  219. package/dist/lib/commands/install.js.map +1 -0
  220. package/dist/lib/commands/knowhow-aggregator.d.ts +2 -0
  221. package/dist/lib/commands/knowhow-aggregator.d.ts.map +1 -0
  222. package/dist/lib/commands/knowhow-aggregator.js +279 -0
  223. package/dist/lib/commands/knowhow-aggregator.js.map +1 -0
  224. package/dist/lib/commands/knowledge-search.d.ts +2 -0
  225. package/dist/lib/commands/knowledge-search.d.ts.map +1 -0
  226. package/dist/lib/commands/knowledge-search.js +113 -0
  227. package/dist/lib/commands/knowledge-search.js.map +1 -0
  228. package/dist/lib/commands/long-term-roadmap.d.ts +2 -0
  229. package/dist/lib/commands/long-term-roadmap.d.ts.map +1 -0
  230. package/dist/lib/commands/long-term-roadmap.js +272 -0
  231. package/dist/lib/commands/long-term-roadmap.js.map +1 -0
  232. package/dist/lib/commands/patterns.d.ts +91 -0
  233. package/dist/lib/commands/patterns.d.ts.map +1 -0
  234. package/dist/lib/commands/patterns.js +391 -0
  235. package/dist/lib/commands/patterns.js.map +1 -0
  236. package/dist/lib/commands/phase-info.d.ts +2 -0
  237. package/dist/lib/commands/phase-info.d.ts.map +1 -0
  238. package/dist/lib/commands/phase-info.js +509 -0
  239. package/dist/lib/commands/phase-info.js.map +1 -0
  240. package/dist/lib/commands/plan-lint.d.ts +56 -0
  241. package/dist/lib/commands/plan-lint.d.ts.map +1 -0
  242. package/dist/lib/commands/plan-lint.js +481 -0
  243. package/dist/lib/commands/plan-lint.js.map +1 -0
  244. package/dist/lib/commands/plan-phase.d.ts +53 -0
  245. package/dist/lib/commands/plan-phase.d.ts.map +1 -0
  246. package/dist/lib/commands/plan-phase.js +288 -0
  247. package/dist/lib/commands/plan-phase.js.map +1 -0
  248. package/dist/lib/commands/progress.d.ts +2 -0
  249. package/dist/lib/commands/progress.d.ts.map +1 -0
  250. package/dist/lib/commands/progress.js +266 -0
  251. package/dist/lib/commands/progress.js.map +1 -0
  252. package/dist/lib/commands/quality.d.ts +2 -0
  253. package/dist/lib/commands/quality.d.ts.map +1 -0
  254. package/dist/lib/commands/quality.js +80 -0
  255. package/dist/lib/commands/quality.js.map +1 -0
  256. package/dist/lib/commands/rollback.d.ts +2 -0
  257. package/dist/lib/commands/rollback.d.ts.map +1 -0
  258. package/dist/lib/commands/rollback.js +145 -0
  259. package/dist/lib/commands/rollback.js.map +1 -0
  260. package/dist/lib/commands/scan.d.ts +25 -0
  261. package/dist/lib/commands/scan.d.ts.map +1 -0
  262. package/dist/lib/commands/scan.js +28 -0
  263. package/dist/lib/commands/scan.js.map +1 -0
  264. package/dist/lib/commands/search.d.ts +2 -0
  265. package/dist/lib/commands/search.d.ts.map +1 -0
  266. package/dist/lib/commands/search.js +212 -0
  267. package/dist/lib/commands/search.js.map +1 -0
  268. package/dist/lib/commands/select-candidate.d.ts +128 -0
  269. package/dist/lib/commands/select-candidate.d.ts.map +1 -0
  270. package/dist/lib/commands/select-candidate.js +518 -0
  271. package/dist/lib/commands/select-candidate.js.map +1 -0
  272. package/dist/lib/commands/singularity.d.ts +2 -0
  273. package/dist/lib/commands/singularity.d.ts.map +1 -0
  274. package/dist/lib/commands/singularity.js +185 -0
  275. package/dist/lib/commands/singularity.js.map +1 -0
  276. package/dist/lib/commands/slug-timestamp.d.ts +2 -0
  277. package/dist/lib/commands/slug-timestamp.d.ts.map +1 -0
  278. package/dist/lib/commands/slug-timestamp.js +54 -0
  279. package/dist/lib/commands/slug-timestamp.js.map +1 -0
  280. package/dist/lib/commands/tail.d.ts +2 -0
  281. package/dist/lib/commands/tail.d.ts.map +1 -0
  282. package/dist/lib/commands/tail.js +100 -0
  283. package/dist/lib/commands/tail.js.map +1 -0
  284. package/dist/lib/commands/todo.d.ts +2 -0
  285. package/dist/lib/commands/todo.d.ts.map +1 -0
  286. package/dist/lib/commands/todo.js +200 -0
  287. package/dist/lib/commands/todo.js.map +1 -0
  288. package/dist/lib/commands/watch.d.ts +2 -0
  289. package/dist/lib/commands/watch.d.ts.map +1 -0
  290. package/dist/lib/commands/watch.js +72 -0
  291. package/dist/lib/commands/watch.js.map +1 -0
  292. package/dist/lib/complexity.d.ts +55 -0
  293. package/dist/lib/complexity.d.ts.map +1 -0
  294. package/dist/lib/complexity.js +80 -0
  295. package/dist/lib/complexity.js.map +1 -0
  296. package/dist/lib/context/agents.d.ts +2 -0
  297. package/dist/lib/context/agents.d.ts.map +1 -0
  298. package/dist/lib/context/agents.js +344 -0
  299. package/dist/lib/context/agents.js.map +1 -0
  300. package/dist/lib/context/base.d.ts +2 -0
  301. package/dist/lib/context/base.d.ts.map +1 -0
  302. package/dist/lib/context/base.js +81 -0
  303. package/dist/lib/context/base.js.map +1 -0
  304. package/dist/lib/context/execute.d.ts +2 -0
  305. package/dist/lib/context/execute.d.ts.map +1 -0
  306. package/dist/lib/context/execute.js +753 -0
  307. package/dist/lib/context/execute.js.map +1 -0
  308. package/dist/lib/context/index.d.ts +2 -0
  309. package/dist/lib/context/index.d.ts.map +1 -0
  310. package/dist/lib/context/index.js +88 -0
  311. package/dist/lib/context/index.js.map +1 -0
  312. package/dist/lib/context/progress.d.ts +2 -0
  313. package/dist/lib/context/progress.d.ts.map +1 -0
  314. package/dist/lib/context/progress.js +178 -0
  315. package/dist/lib/context/progress.js.map +1 -0
  316. package/dist/lib/context/project.d.ts +2 -0
  317. package/dist/lib/context/project.d.ts.map +1 -0
  318. package/dist/lib/context/project.js +413 -0
  319. package/dist/lib/context/project.js.map +1 -0
  320. package/dist/lib/context/research.d.ts +2 -0
  321. package/dist/lib/context/research.d.ts.map +1 -0
  322. package/dist/lib/context/research.js +466 -0
  323. package/dist/lib/context/research.js.map +1 -0
  324. package/dist/lib/dead-ends.d.ts +28 -0
  325. package/dist/lib/dead-ends.d.ts.map +1 -0
  326. package/dist/lib/dead-ends.js +451 -0
  327. package/dist/lib/dead-ends.js.map +1 -0
  328. package/dist/lib/deps.d.ts +2 -0
  329. package/dist/lib/deps.d.ts.map +1 -0
  330. package/dist/lib/deps.js +630 -0
  331. package/dist/lib/deps.js.map +1 -0
  332. package/dist/lib/discussion.d.ts +2 -0
  333. package/dist/lib/discussion.d.ts.map +1 -0
  334. package/dist/lib/discussion.js +1041 -0
  335. package/dist/lib/discussion.js.map +1 -0
  336. package/dist/lib/drift.d.ts +36 -0
  337. package/dist/lib/drift.d.ts.map +1 -0
  338. package/dist/lib/drift.js +481 -0
  339. package/dist/lib/drift.js.map +1 -0
  340. package/dist/lib/evolve/_dimensions-features.d.ts +2 -0
  341. package/dist/lib/evolve/_dimensions-features.d.ts.map +1 -0
  342. package/dist/lib/evolve/_dimensions-features.js +369 -0
  343. package/dist/lib/evolve/_dimensions-features.js.map +1 -0
  344. package/dist/lib/evolve/_dimensions.d.ts +2 -0
  345. package/dist/lib/evolve/_dimensions.d.ts.map +1 -0
  346. package/dist/lib/evolve/_dimensions.js +358 -0
  347. package/dist/lib/evolve/_dimensions.js.map +1 -0
  348. package/dist/lib/evolve/_product-ideation.d.ts +2 -0
  349. package/dist/lib/evolve/_product-ideation.d.ts.map +1 -0
  350. package/dist/lib/evolve/_product-ideation.js +281 -0
  351. package/dist/lib/evolve/_product-ideation.js.map +1 -0
  352. package/dist/lib/evolve/_prompts.d.ts +2 -0
  353. package/dist/lib/evolve/_prompts.d.ts.map +1 -0
  354. package/dist/lib/evolve/_prompts.js +153 -0
  355. package/dist/lib/evolve/_prompts.js.map +1 -0
  356. package/dist/lib/evolve/cli.d.ts +2 -0
  357. package/dist/lib/evolve/cli.d.ts.map +1 -0
  358. package/dist/lib/evolve/cli.js +224 -0
  359. package/dist/lib/evolve/cli.js.map +1 -0
  360. package/dist/lib/evolve/discovery.d.ts +2 -0
  361. package/dist/lib/evolve/discovery.d.ts.map +1 -0
  362. package/dist/lib/evolve/discovery.js +391 -0
  363. package/dist/lib/evolve/discovery.js.map +1 -0
  364. package/dist/lib/evolve/index.d.ts +2 -0
  365. package/dist/lib/evolve/index.d.ts.map +1 -0
  366. package/dist/lib/evolve/index.js +88 -0
  367. package/dist/lib/evolve/index.js.map +1 -0
  368. package/dist/lib/evolve/orchestrator.d.ts +2 -0
  369. package/dist/lib/evolve/orchestrator.d.ts.map +1 -0
  370. package/dist/lib/evolve/orchestrator.js +851 -0
  371. package/dist/lib/evolve/orchestrator.js.map +1 -0
  372. package/dist/lib/evolve/scoring.d.ts +2 -0
  373. package/dist/lib/evolve/scoring.d.ts.map +1 -0
  374. package/dist/lib/evolve/scoring.js +118 -0
  375. package/dist/lib/evolve/scoring.js.map +1 -0
  376. package/dist/lib/evolve/state.d.ts +2 -0
  377. package/dist/lib/evolve/state.d.ts.map +1 -0
  378. package/dist/lib/evolve/state.js +264 -0
  379. package/dist/lib/evolve/state.js.map +1 -0
  380. package/dist/lib/evolve/types.d.ts +249 -0
  381. package/dist/lib/evolve/types.d.ts.map +1 -0
  382. package/dist/lib/evolve/types.js +3 -0
  383. package/dist/lib/evolve/types.js.map +1 -0
  384. package/dist/lib/frontmatter.d.ts +2 -0
  385. package/dist/lib/frontmatter.d.ts.map +1 -0
  386. package/dist/lib/frontmatter.js +513 -0
  387. package/dist/lib/frontmatter.js.map +1 -0
  388. package/dist/lib/gates.d.ts +2 -0
  389. package/dist/lib/gates.d.ts.map +1 -0
  390. package/dist/lib/gates.js +578 -0
  391. package/dist/lib/gates.js.map +1 -0
  392. package/dist/lib/genome.d.ts +10 -0
  393. package/dist/lib/genome.d.ts.map +1 -0
  394. package/dist/lib/genome.js +368 -0
  395. package/dist/lib/genome.js.map +1 -0
  396. package/dist/lib/got.d.ts +2 -0
  397. package/dist/lib/got.d.ts.map +1 -0
  398. package/dist/lib/got.js +280 -0
  399. package/dist/lib/got.js.map +1 -0
  400. package/dist/lib/invariants.d.ts +2 -0
  401. package/dist/lib/invariants.d.ts.map +1 -0
  402. package/dist/lib/invariants.js +298 -0
  403. package/dist/lib/invariants.js.map +1 -0
  404. package/dist/lib/knowledge.d.ts +2 -0
  405. package/dist/lib/knowledge.d.ts.map +1 -0
  406. package/dist/lib/knowledge.js +658 -0
  407. package/dist/lib/knowledge.js.map +1 -0
  408. package/dist/lib/long-term-roadmap.d.ts +2 -0
  409. package/dist/lib/long-term-roadmap.d.ts.map +1 -0
  410. package/dist/lib/long-term-roadmap.js +602 -0
  411. package/dist/lib/long-term-roadmap.js.map +1 -0
  412. package/dist/lib/markdown-split.d.ts +2 -0
  413. package/dist/lib/markdown-split.d.ts.map +1 -0
  414. package/dist/lib/markdown-split.js +199 -0
  415. package/dist/lib/markdown-split.js.map +1 -0
  416. package/dist/lib/mcp-server.d.ts +2 -0
  417. package/dist/lib/mcp-server.d.ts.map +1 -0
  418. package/dist/lib/mcp-server.js +2424 -0
  419. package/dist/lib/mcp-server.js.map +1 -0
  420. package/dist/lib/metrics.d.ts +16 -0
  421. package/dist/lib/metrics.d.ts.map +1 -0
  422. package/dist/lib/metrics.js +48 -0
  423. package/dist/lib/metrics.js.map +1 -0
  424. package/dist/lib/overstory.d.ts +2 -0
  425. package/dist/lib/overstory.d.ts.map +1 -0
  426. package/dist/lib/overstory.js +211 -0
  427. package/dist/lib/overstory.js.map +1 -0
  428. package/dist/lib/parallel.d.ts +2 -0
  429. package/dist/lib/parallel.d.ts.map +1 -0
  430. package/dist/lib/parallel.js +349 -0
  431. package/dist/lib/parallel.js.map +1 -0
  432. package/dist/lib/paths.d.ts +2 -0
  433. package/dist/lib/paths.d.ts.map +1 -0
  434. package/dist/lib/paths.js +254 -0
  435. package/dist/lib/paths.js.map +1 -0
  436. package/dist/lib/phase-complete-llm.d.ts +22 -0
  437. package/dist/lib/phase-complete-llm.d.ts.map +1 -0
  438. package/dist/lib/phase-complete-llm.js +331 -0
  439. package/dist/lib/phase-complete-llm.js.map +1 -0
  440. package/dist/lib/phase-complete.d.ts +46 -0
  441. package/dist/lib/phase-complete.d.ts.map +1 -0
  442. package/dist/lib/phase-complete.js +278 -0
  443. package/dist/lib/phase-complete.js.map +1 -0
  444. package/dist/lib/phase-io.d.ts +2 -0
  445. package/dist/lib/phase-io.d.ts.map +1 -0
  446. package/dist/lib/phase-io.js +126 -0
  447. package/dist/lib/phase-io.js.map +1 -0
  448. package/dist/lib/phase.d.ts +2 -0
  449. package/dist/lib/phase.d.ts.map +1 -0
  450. package/dist/lib/phase.js +1344 -0
  451. package/dist/lib/phase.js.map +1 -0
  452. package/dist/lib/plan-tournament.d.ts +63 -0
  453. package/dist/lib/plan-tournament.d.ts.map +1 -0
  454. package/dist/lib/plan-tournament.js +353 -0
  455. package/dist/lib/plan-tournament.js.map +1 -0
  456. package/dist/lib/refinement.d.ts +74 -0
  457. package/dist/lib/refinement.d.ts.map +1 -0
  458. package/dist/lib/refinement.js +283 -0
  459. package/dist/lib/refinement.js.map +1 -0
  460. package/dist/lib/requirements.d.ts +2 -0
  461. package/dist/lib/requirements.d.ts.map +1 -0
  462. package/dist/lib/requirements.js +355 -0
  463. package/dist/lib/requirements.js.map +1 -0
  464. package/dist/lib/research-bundle.d.ts +2 -0
  465. package/dist/lib/research-bundle.d.ts.map +1 -0
  466. package/dist/lib/research-bundle.js +246 -0
  467. package/dist/lib/research-bundle.js.map +1 -0
  468. package/dist/lib/roadmap.d.ts +2 -0
  469. package/dist/lib/roadmap.d.ts.map +1 -0
  470. package/dist/lib/roadmap.js +541 -0
  471. package/dist/lib/roadmap.js.map +1 -0
  472. package/dist/lib/sample.d.ts +16 -0
  473. package/dist/lib/sample.d.ts.map +1 -0
  474. package/dist/lib/sample.js +20 -0
  475. package/dist/lib/sample.js.map +1 -0
  476. package/dist/lib/scaffold.d.ts +2 -0
  477. package/dist/lib/scaffold.d.ts.map +1 -0
  478. package/dist/lib/scaffold.js +355 -0
  479. package/dist/lib/scaffold.js.map +1 -0
  480. package/dist/lib/scan/_utils.d.ts +11 -0
  481. package/dist/lib/scan/_utils.d.ts.map +1 -0
  482. package/dist/lib/scan/_utils.js +36 -0
  483. package/dist/lib/scan/_utils.js.map +1 -0
  484. package/dist/lib/scan/base64.d.ts +15 -0
  485. package/dist/lib/scan/base64.d.ts.map +1 -0
  486. package/dist/lib/scan/base64.js +66 -0
  487. package/dist/lib/scan/base64.js.map +1 -0
  488. package/dist/lib/scan/ignorefile.d.ts +30 -0
  489. package/dist/lib/scan/ignorefile.d.ts.map +1 -0
  490. package/dist/lib/scan/ignorefile.js +101 -0
  491. package/dist/lib/scan/ignorefile.js.map +1 -0
  492. package/dist/lib/scan/injection.d.ts +14 -0
  493. package/dist/lib/scan/injection.d.ts.map +1 -0
  494. package/dist/lib/scan/injection.js +39 -0
  495. package/dist/lib/scan/injection.js.map +1 -0
  496. package/dist/lib/scan/patterns.d.ts +17 -0
  497. package/dist/lib/scan/patterns.d.ts.map +1 -0
  498. package/dist/lib/scan/patterns.js +123 -0
  499. package/dist/lib/scan/patterns.js.map +1 -0
  500. package/dist/lib/scan/strip-markdown.d.ts +7 -0
  501. package/dist/lib/scan/strip-markdown.d.ts.map +1 -0
  502. package/dist/lib/scan/strip-markdown.js +38 -0
  503. package/dist/lib/scan/strip-markdown.js.map +1 -0
  504. package/dist/lib/scan/types.d.ts +23 -0
  505. package/dist/lib/scan/types.d.ts.map +1 -0
  506. package/dist/lib/scan/types.js +3 -0
  507. package/dist/lib/scan/types.js.map +1 -0
  508. package/dist/lib/scheduler-wait.d.ts +2 -0
  509. package/dist/lib/scheduler-wait.d.ts.map +1 -0
  510. package/dist/lib/scheduler-wait.js +59 -0
  511. package/dist/lib/scheduler-wait.js.map +1 -0
  512. package/dist/lib/scheduler.d.ts +254 -0
  513. package/dist/lib/scheduler.d.ts.map +1 -0
  514. package/dist/lib/scheduler.js +1147 -0
  515. package/dist/lib/scheduler.js.map +1 -0
  516. package/dist/lib/state.d.ts +2 -0
  517. package/dist/lib/state.d.ts.map +1 -0
  518. package/dist/lib/state.js +744 -0
  519. package/dist/lib/state.js.map +1 -0
  520. package/dist/lib/think.d.ts +18 -0
  521. package/dist/lib/think.d.ts.map +1 -0
  522. package/dist/lib/think.js +317 -0
  523. package/dist/lib/think.js.map +1 -0
  524. package/dist/lib/tracker.d.ts +2 -0
  525. package/dist/lib/tracker.d.ts.map +1 -0
  526. package/dist/lib/tracker.js +1121 -0
  527. package/dist/lib/tracker.js.map +1 -0
  528. package/dist/lib/types.d.ts +1514 -0
  529. package/dist/lib/types.d.ts.map +1 -0
  530. package/dist/lib/types.js +4 -0
  531. package/dist/lib/types.js.map +1 -0
  532. package/dist/lib/utils.d.ts +2 -0
  533. package/dist/lib/utils.d.ts.map +1 -0
  534. package/dist/lib/utils.js +1363 -0
  535. package/dist/lib/utils.js.map +1 -0
  536. package/dist/lib/verify.d.ts +2 -0
  537. package/dist/lib/verify.d.ts.map +1 -0
  538. package/dist/lib/verify.js +1153 -0
  539. package/dist/lib/verify.js.map +1 -0
  540. package/dist/lib/wireup/autofix.d.ts +2 -0
  541. package/dist/lib/wireup/autofix.d.ts.map +1 -0
  542. package/dist/lib/wireup/autofix.js +188 -0
  543. package/dist/lib/wireup/autofix.js.map +1 -0
  544. package/dist/lib/wireup/cli.d.ts +2 -0
  545. package/dist/lib/wireup/cli.d.ts.map +1 -0
  546. package/dist/lib/wireup/cli.js +194 -0
  547. package/dist/lib/wireup/cli.js.map +1 -0
  548. package/dist/lib/wireup/detection.d.ts +47 -0
  549. package/dist/lib/wireup/detection.d.ts.map +1 -0
  550. package/dist/lib/wireup/detection.js +410 -0
  551. package/dist/lib/wireup/detection.js.map +1 -0
  552. package/dist/lib/wireup/discovery.d.ts +2 -0
  553. package/dist/lib/wireup/discovery.d.ts.map +1 -0
  554. package/dist/lib/wireup/discovery.js +934 -0
  555. package/dist/lib/wireup/discovery.js.map +1 -0
  556. package/dist/lib/wireup/execution.d.ts +2 -0
  557. package/dist/lib/wireup/execution.d.ts.map +1 -0
  558. package/dist/lib/wireup/execution.js +573 -0
  559. package/dist/lib/wireup/execution.js.map +1 -0
  560. package/dist/lib/wireup/index.d.ts +2 -0
  561. package/dist/lib/wireup/index.d.ts.map +1 -0
  562. package/dist/lib/wireup/index.js +85 -0
  563. package/dist/lib/wireup/index.js.map +1 -0
  564. package/dist/lib/wireup/orchestrator.d.ts +2 -0
  565. package/dist/lib/wireup/orchestrator.d.ts.map +1 -0
  566. package/dist/lib/wireup/orchestrator.js +366 -0
  567. package/dist/lib/wireup/orchestrator.js.map +1 -0
  568. package/dist/lib/wireup/report.d.ts +47 -0
  569. package/dist/lib/wireup/report.d.ts.map +1 -0
  570. package/dist/lib/wireup/report.js +201 -0
  571. package/dist/lib/wireup/report.js.map +1 -0
  572. package/dist/lib/wireup/scenarios.d.ts +2 -0
  573. package/dist/lib/wireup/scenarios.d.ts.map +1 -0
  574. package/dist/lib/wireup/scenarios.js +516 -0
  575. package/dist/lib/wireup/scenarios.js.map +1 -0
  576. package/dist/lib/wireup/state.d.ts +2 -0
  577. package/dist/lib/wireup/state.d.ts.map +1 -0
  578. package/dist/lib/wireup/state.js +102 -0
  579. package/dist/lib/wireup/state.js.map +1 -0
  580. package/dist/lib/wireup/types.d.ts +376 -0
  581. package/dist/lib/wireup/types.d.ts.map +1 -0
  582. package/dist/lib/wireup/types.js +3 -0
  583. package/dist/lib/wireup/types.js.map +1 -0
  584. package/dist/lib/worktree.d.ts +2 -0
  585. package/dist/lib/worktree.d.ts.map +1 -0
  586. package/dist/lib/worktree.js +999 -0
  587. package/dist/lib/worktree.js.map +1 -0
  588. package/lib/autopilot-milestone.ts +136 -0
  589. package/lib/autopilot-pipeline.ts +1179 -0
  590. package/lib/autopilot-waves.ts +361 -0
  591. package/lib/autopilot.ts +1874 -0
  592. package/lib/autoplan.ts +280 -0
  593. package/lib/autoresearch.js +4 -0
  594. package/lib/autoresearch.ts +886 -0
  595. package/lib/backend.ts +1252 -0
  596. package/lib/benchmark.ts +341 -0
  597. package/lib/citations.ts +760 -0
  598. package/lib/cleanup.ts +1588 -0
  599. package/lib/cli/adapters.ts +41 -0
  600. package/lib/cli/agent.ts +83 -0
  601. package/lib/cli/index.ts +273 -0
  602. package/lib/cli/output.ts +33 -0
  603. package/lib/cli/scan-dispatch.ts +130 -0
  604. package/lib/cli/tools.ts +198 -0
  605. package/lib/commands/_dashboard-parsers.ts +275 -0
  606. package/lib/commands/analysis.ts +1851 -0
  607. package/lib/commands/assumptions.ts +232 -0
  608. package/lib/commands/blame.ts +174 -0
  609. package/lib/commands/budget.ts +148 -0
  610. package/lib/commands/check-plans.ts +233 -0
  611. package/lib/commands/config.ts +287 -0
  612. package/lib/commands/dashboard.ts +680 -0
  613. package/lib/commands/estimate.ts +204 -0
  614. package/lib/commands/eval-diff.ts +252 -0
  615. package/lib/commands/freshness.ts +213 -0
  616. package/lib/commands/health.ts +607 -0
  617. package/lib/commands/index.ts +266 -0
  618. package/lib/commands/install.ts +307 -0
  619. package/lib/commands/knowhow-aggregator.ts +345 -0
  620. package/lib/commands/knowledge-search.ts +153 -0
  621. package/lib/commands/long-term-roadmap.ts +390 -0
  622. package/lib/commands/patterns.ts +465 -0
  623. package/lib/commands/phase-info.ts +698 -0
  624. package/lib/commands/plan-lint.ts +546 -0
  625. package/lib/commands/plan-phase.ts +375 -0
  626. package/lib/commands/progress.ts +319 -0
  627. package/lib/commands/quality.ts +138 -0
  628. package/lib/commands/rollback.ts +195 -0
  629. package/lib/commands/scan.ts +72 -0
  630. package/lib/commands/search.ts +300 -0
  631. package/lib/commands/select-candidate.ts +687 -0
  632. package/lib/commands/singularity.ts +222 -0
  633. package/lib/commands/slug-timestamp.ts +74 -0
  634. package/lib/commands/tail.ts +129 -0
  635. package/lib/commands/todo.ts +273 -0
  636. package/lib/commands/watch.ts +80 -0
  637. package/lib/complexity.ts +117 -0
  638. package/lib/context/agents.ts +505 -0
  639. package/lib/context/base.ts +123 -0
  640. package/lib/context/execute.ts +977 -0
  641. package/lib/context/index.ts +110 -0
  642. package/lib/context/progress.ts +278 -0
  643. package/lib/context/project.ts +531 -0
  644. package/lib/context/research.ts +646 -0
  645. package/lib/dead-ends.ts +506 -0
  646. package/lib/deps.ts +773 -0
  647. package/lib/discussion.ts +1275 -0
  648. package/lib/drift.ts +519 -0
  649. package/lib/evolve/_dimensions-features.ts +525 -0
  650. package/lib/evolve/_dimensions.ts +511 -0
  651. package/lib/evolve/_product-ideation.ts +405 -0
  652. package/lib/evolve/_prompts.ts +178 -0
  653. package/lib/evolve/cli.ts +330 -0
  654. package/lib/evolve/discovery.ts +571 -0
  655. package/lib/evolve/index.ts +105 -0
  656. package/lib/evolve/orchestrator.ts +1139 -0
  657. package/lib/evolve/scoring.ts +167 -0
  658. package/lib/evolve/state.ts +330 -0
  659. package/lib/evolve/types.ts +290 -0
  660. package/lib/frontmatter.ts +615 -0
  661. package/lib/gates.ts +695 -0
  662. package/lib/genome.ts +402 -0
  663. package/lib/got.js +4 -0
  664. package/lib/got.ts +361 -0
  665. package/lib/invariants.ts +378 -0
  666. package/lib/knowledge.ts +768 -0
  667. package/lib/long-term-roadmap.ts +806 -0
  668. package/lib/markdown-split.ts +273 -0
  669. package/lib/mcp-server.ts +3292 -0
  670. package/lib/metrics.ts +49 -0
  671. package/lib/overstory.ts +270 -0
  672. package/lib/parallel.ts +570 -0
  673. package/lib/paths.ts +293 -0
  674. package/lib/phase-complete-llm.ts +376 -0
  675. package/lib/phase-complete.ts +366 -0
  676. package/lib/phase-io.ts +101 -0
  677. package/lib/phase.ts +1981 -0
  678. package/lib/plan-tournament.ts +426 -0
  679. package/lib/refinement.ts +349 -0
  680. package/lib/requirements.ts +469 -0
  681. package/lib/research-bundle.ts +300 -0
  682. package/lib/roadmap.ts +775 -0
  683. package/lib/scaffold.ts +480 -0
  684. package/lib/scan/_utils.ts +37 -0
  685. package/lib/scan/base64.ts +90 -0
  686. package/lib/scan/ignorefile.ts +109 -0
  687. package/lib/scan/injection.ts +67 -0
  688. package/lib/scan/patterns.ts +139 -0
  689. package/lib/scan/strip-markdown.ts +39 -0
  690. package/lib/scan/types.ts +28 -0
  691. package/lib/scheduler-wait.ts +58 -0
  692. package/lib/scheduler.ts +1370 -0
  693. package/lib/state.ts +1000 -0
  694. package/lib/think.ts +365 -0
  695. package/lib/tracker.ts +1591 -0
  696. package/lib/types.ts +1663 -0
  697. package/lib/utils.ts +1479 -0
  698. package/lib/verify.ts +1434 -0
  699. package/lib/wireup/autofix.ts +241 -0
  700. package/lib/wireup/cli.ts +278 -0
  701. package/lib/wireup/detection.ts +542 -0
  702. package/lib/wireup/discovery.ts +1063 -0
  703. package/lib/wireup/execution.ts +686 -0
  704. package/lib/wireup/index.ts +117 -0
  705. package/lib/wireup/orchestrator.ts +519 -0
  706. package/lib/wireup/report.ts +286 -0
  707. package/lib/wireup/scenarios.ts +616 -0
  708. package/lib/wireup/state.ts +139 -0
  709. package/lib/wireup/types.ts +436 -0
  710. package/lib/worktree.ts +1309 -0
  711. package/package.json +67 -0
@@ -0,0 +1,1851 @@
1
+ 'use strict';
2
+
3
+ /** GRD Commands/Analysis -- Project analysis: risk assessment, citation tracking,
4
+ * eval regression, time budget, config diff, readiness, health score,
5
+ * decision timeline, knowledge import, todo duplicates */
6
+
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const {
11
+ safeReadFile,
12
+ safeReadJSON,
13
+ output,
14
+ error,
15
+ }: {
16
+ safeReadFile: (p: string) => string | null;
17
+ safeReadJSON: (p: string, def: unknown) => unknown;
18
+ output: (result: unknown, raw: boolean, rawValue?: unknown) => never;
19
+ error: (message: string) => never;
20
+ } = require('../utils');
21
+ const {
22
+ planningDir: getPlanningDir,
23
+ phasesDir: getPhasesDirPath,
24
+ researchDir: getResearchDir,
25
+ todosDir: getTodosDir,
26
+ currentMilestone,
27
+ }: {
28
+ planningDir: (cwd: string) => string;
29
+ phasesDir: (cwd: string, milestone?: string | null) => string;
30
+ researchDir: (cwd: string, milestone?: string | null) => string;
31
+ todosDir: (cwd: string, milestone?: string | null) => string;
32
+ currentMilestone: (cwd: string) => string;
33
+ } = require('../paths');
34
+
35
+ // ─── Domain Types ─────────────────────────────────────────────────────────────
36
+
37
+ interface RiskSignal {
38
+ category: string;
39
+ signal: string;
40
+ severity: 'low' | 'medium' | 'high';
41
+ remediation: string;
42
+ }
43
+
44
+ interface RiskAssessmentResult {
45
+ phase: string;
46
+ risk_score: number;
47
+ risk_level: 'low' | 'medium' | 'high' | 'critical';
48
+ signals: RiskSignal[];
49
+ plan_count: number;
50
+ plans_analyzed: string[];
51
+ }
52
+
53
+ interface CitationBacklink {
54
+ paper_id: string;
55
+ paper_title: string;
56
+ references: Array<{ file: string; line: number; context: string }>;
57
+ reference_count: number;
58
+ }
59
+
60
+ interface EvalMetric {
61
+ name: string;
62
+ value: number;
63
+ baseline: number | null;
64
+ regression: boolean;
65
+ delta: number | null;
66
+ }
67
+
68
+ interface EvalRegressionResult {
69
+ phase: string;
70
+ threshold_pct: number;
71
+ regressions: EvalMetric[];
72
+ improvements: EvalMetric[];
73
+ stable: EvalMetric[];
74
+ has_regressions: boolean;
75
+ }
76
+
77
+ interface PhaseTimeBudgetEntry {
78
+ phase: string;
79
+ name: string;
80
+ estimated_days: number;
81
+ actual_min: number | null;
82
+ estimated_min: number;
83
+ variance_pct: number | null;
84
+ over_budget: boolean;
85
+ }
86
+
87
+ interface ConfigChangeEntry {
88
+ key: string;
89
+ old_value: unknown;
90
+ new_value: unknown;
91
+ explanation: string;
92
+ }
93
+
94
+ interface ReadinessCheck {
95
+ name: string;
96
+ passed: boolean;
97
+ detail: string;
98
+ }
99
+
100
+ interface PhaseReadinessResult {
101
+ phase: string;
102
+ ready: boolean;
103
+ score: number;
104
+ checks: ReadinessCheck[];
105
+ blockers: string[];
106
+ }
107
+
108
+ interface MilestoneHealthResult {
109
+ milestone: string;
110
+ score: number;
111
+ grade: 'A' | 'B' | 'C' | 'D' | 'F';
112
+ components: {
113
+ completion_rate: number;
114
+ blocker_penalty: number;
115
+ eval_hit_rate: number;
116
+ estimate_accuracy: number;
117
+ };
118
+ summary: string;
119
+ }
120
+
121
+ interface DecisionEntry {
122
+ phase: string | null;
123
+ date: string | null;
124
+ summary: string;
125
+ rationale: string;
126
+ source: string;
127
+ }
128
+
129
+ interface ImportResult {
130
+ source: string;
131
+ type: string;
132
+ destination: string;
133
+ imported: boolean;
134
+ conflict: boolean;
135
+ message: string;
136
+ }
137
+
138
+ interface TodoDuplicatePair {
139
+ a: string;
140
+ b: string;
141
+ similarity: number;
142
+ a_title: string;
143
+ b_title: string;
144
+ }
145
+
146
+ // ─── Internal Helpers ─────────────────────────────────────────────────────────
147
+
148
+ /** Recursively collect all .md files under a directory. */
149
+ function _collectMarkdownFiles(dir: string): string[] {
150
+ const results: string[] = [];
151
+ try {
152
+ const entries: { isDirectory: () => boolean; isFile: () => boolean; name: string }[] =
153
+ fs.readdirSync(dir, { withFileTypes: true });
154
+ for (const entry of entries) {
155
+ const fullPath = path.join(dir, entry.name);
156
+ if (entry.isDirectory()) {
157
+ results.push(..._collectMarkdownFiles(fullPath));
158
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
159
+ results.push(fullPath);
160
+ }
161
+ }
162
+ } catch {
163
+ /* Directory doesn't exist */
164
+ }
165
+ return results;
166
+ }
167
+
168
+ /** Find all phase directories for the current milestone. */
169
+ function _listPhaseDirs(cwd: string): string[] {
170
+ const phasesPath = getPhasesDirPath(cwd);
171
+ try {
172
+ return fs
173
+ .readdirSync(phasesPath, { withFileTypes: true })
174
+ .filter((e: { isDirectory: () => boolean }) => e.isDirectory())
175
+ .map((e: { name: string }) => path.join(phasesPath, e.name));
176
+ } catch {
177
+ return [];
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Collect all PLAN.md files in a specific phase directory. Codex r7 P2:
183
+ * include bare `PLAN.md` alongside the prefixed `*-PLAN.md` form so
184
+ * single-plan phases are not silently skipped by callers like
185
+ * `gd estimate-phase`.
186
+ */
187
+ function _collectPlanFiles(phasePath: string): string[] {
188
+ try {
189
+ return fs
190
+ .readdirSync(phasePath)
191
+ .filter((f: string) => f === 'PLAN.md' || f.endsWith('-PLAN.md'))
192
+ .map((f: string) => path.join(phasePath, f));
193
+ } catch {
194
+ return [];
195
+ }
196
+ }
197
+
198
+ /** Find a phase directory by phase number prefix (e.g., "01" or "1"). */
199
+ function _findPhaseDirByNumber(cwd: string, phaseNum: string): string | null {
200
+ const phasesPath = getPhasesDirPath(cwd);
201
+ const normalized = phaseNum.padStart(2, '0');
202
+ try {
203
+ const entries: { isDirectory: () => boolean; name: string }[] = fs.readdirSync(phasesPath, {
204
+ withFileTypes: true,
205
+ });
206
+ const match = entries.find(
207
+ (e) =>
208
+ e.isDirectory() &&
209
+ (e.name.startsWith(normalized + '-') || e.name.startsWith(phaseNum + '-'))
210
+ );
211
+ return match ? path.join(phasesPath, match.name) : null;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ /** Compute Jaccard similarity between two sets of words. */
218
+ function _jaccardSimilarity(a: string, b: string): number {
219
+ const wordsA = new Set(
220
+ a
221
+ .toLowerCase()
222
+ .split(/\W+/)
223
+ .filter((w) => w.length > 2)
224
+ );
225
+ const wordsB = new Set(
226
+ b
227
+ .toLowerCase()
228
+ .split(/\W+/)
229
+ .filter((w) => w.length > 2)
230
+ );
231
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
232
+ let intersection = 0;
233
+ for (const w of wordsA) {
234
+ if (wordsB.has(w)) intersection++;
235
+ }
236
+ const union = wordsA.size + wordsB.size - intersection;
237
+ return union === 0 ? 0 : intersection / union;
238
+ }
239
+
240
+ /** Extract the first non-blank line as a title from file content. */
241
+ function _extractTodoTitle(content: string): string {
242
+ const lines = content.split('\n');
243
+ for (const line of lines) {
244
+ const trimmed = line.replace(/^#+\s*/, '').trim();
245
+ if (trimmed && !trimmed.startsWith('---')) return trimmed.slice(0, 80);
246
+ }
247
+ return '(untitled)';
248
+ }
249
+
250
+ /** Parse numeric metrics from EVAL.md or SUMMARY.md content using matchAll. */
251
+ function _parseMetricsFromContent(content: string): Record<string, number> {
252
+ const metrics: Record<string, number> = {};
253
+ const metricPattern =
254
+ /\b([A-Za-z][A-Za-z0-9_\-/ ]{0,30}):\s*([\d]+(?:\.[\d]+)?)\s*(?:%|dB|ms|s|min)?\b/g;
255
+ for (const m of content.matchAll(metricPattern)) {
256
+ const key = m[1].trim().toLowerCase().replace(/\s+/g, '_');
257
+ const val = parseFloat(m[2]);
258
+ if (!isNaN(val) && key.length > 1) {
259
+ metrics[key] = val;
260
+ }
261
+ }
262
+ return metrics;
263
+ }
264
+
265
+ /** Explain what a config key change means for agent behavior. */
266
+ function _explainConfigChange(key: string, oldVal: unknown, newVal: unknown): string {
267
+ const explanations: Record<string, string> = {
268
+ autonomous_mode: 'Controls YOLO mode — disables all gates when enabled',
269
+ 'ceremony.default_level': 'Controls which agents run (light/standard/full)',
270
+ research_gates: 'Human review points for research decisions',
271
+ 'code_review.enabled': 'Toggles automatic code review after execution',
272
+ 'execution.agent_teams': 'Enables parallel agent team execution',
273
+ 'git.enabled': 'Enables isolated git worktree per phase',
274
+ 'tracker.provider': 'Sets issue tracker integration (github/mcp-atlassian/none)',
275
+ };
276
+ const keyLower = key.toLowerCase();
277
+ for (const [pattern, explanation] of Object.entries(explanations)) {
278
+ if (keyLower.includes(pattern.toLowerCase())) {
279
+ return `${explanation} (changed from ${JSON.stringify(oldVal)} to ${JSON.stringify(newVal)})`;
280
+ }
281
+ }
282
+ return `Changed from ${JSON.stringify(oldVal)} to ${JSON.stringify(newVal)}`;
283
+ }
284
+
285
+ /** Flatten a nested object into dot-separated keys. */
286
+ function _flattenObject(obj: Record<string, unknown>, prefix = ''): Record<string, unknown> {
287
+ const result: Record<string, unknown> = {};
288
+ for (const [k, v] of Object.entries(obj)) {
289
+ const key = prefix ? `${prefix}.${k}` : k;
290
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
291
+ Object.assign(result, _flattenObject(v as Record<string, unknown>, key));
292
+ } else {
293
+ result[key] = v;
294
+ }
295
+ }
296
+ return result;
297
+ }
298
+
299
+ // ─── CLI: Phase Risk Assessment ───────────────────────────────────────────────
300
+
301
+ /**
302
+ * CLI command: Analyze PLAN.md files in a phase for risk signals.
303
+ * Assigns a risk score (0-10) and returns actionable remediation suggestions.
304
+ * @param cwd - Project root
305
+ * @param phase - Phase number string (e.g. "1" or "01")
306
+ * @param raw - Raw output flag
307
+ */
308
+ function cmdPhaseRiskAssessment(cwd: string, phase: string, raw: boolean): void {
309
+ if (!phase) {
310
+ error('Phase number required');
311
+ return;
312
+ }
313
+
314
+ const phaseDir = _findPhaseDirByNumber(cwd, phase);
315
+ if (!phaseDir) {
316
+ output(
317
+ {
318
+ error: `Phase ${phase} directory not found`,
319
+ phase,
320
+ signals: [],
321
+ risk_score: 0,
322
+ risk_level: 'low',
323
+ plan_count: 0,
324
+ plans_analyzed: [],
325
+ },
326
+ raw
327
+ );
328
+ return;
329
+ }
330
+
331
+ const planFiles = _collectPlanFiles(phaseDir);
332
+ const signals: RiskSignal[] = [];
333
+ const plansAnalyzed: string[] = [];
334
+
335
+ for (const planFile of planFiles) {
336
+ const content = safeReadFile(planFile);
337
+ if (!content) continue;
338
+ const filename = path.basename(planFile);
339
+ plansAnalyzed.push(filename);
340
+ const lower = content.toLowerCase();
341
+
342
+ // Risk: vague success criteria (no numbers in success section)
343
+ const successSection = content.match(
344
+ /##\s+(?:success|acceptance|criteria|must[_\s]haves?)[^\n]*\n([\s\S]*?)(?=\n##|$)/i
345
+ );
346
+ if (successSection) {
347
+ const hasNumbers = /\d+(?:\.\d+)?/.test(successSection[1]);
348
+ const hasSpecificTerms = /must|shall|exactly|at least|no more than|< |> |>=|<=/i.test(
349
+ successSection[1]
350
+ );
351
+ if (!hasNumbers && !hasSpecificTerms) {
352
+ signals.push({
353
+ category: 'success_criteria',
354
+ signal: `${filename}: Success criteria lack quantitative targets`,
355
+ severity: 'high',
356
+ remediation:
357
+ 'Add specific numeric thresholds (e.g., "accuracy >= 85%", "latency < 200ms")',
358
+ });
359
+ }
360
+ } else {
361
+ signals.push({
362
+ category: 'success_criteria',
363
+ signal: `${filename}: No success criteria section found`,
364
+ severity: 'high',
365
+ remediation: 'Add a ## Success Criteria or ## Must Haves section with measurable outcomes',
366
+ });
367
+ }
368
+
369
+ // Risk: missing baseline reference
370
+ if (
371
+ !lower.includes('baseline') &&
372
+ !lower.includes('comparison') &&
373
+ !lower.includes('benchmark')
374
+ ) {
375
+ signals.push({
376
+ category: 'baseline',
377
+ signal: `${filename}: No baseline or comparison reference`,
378
+ severity: 'medium',
379
+ remediation:
380
+ 'Reference a baseline metric so evaluation can measure improvement (see BASELINE.md)',
381
+ });
382
+ }
383
+
384
+ // Risk: overly large scope
385
+ const taskCount = (content.match(/^[-*]\s+\[/gm) || []).length;
386
+ if (taskCount > 20) {
387
+ signals.push({
388
+ category: 'scope',
389
+ signal: `${filename}: ${taskCount} checklist items — may be too large for one plan`,
390
+ severity: 'medium',
391
+ remediation: 'Consider splitting into multiple plans or waves; aim for <15 tasks per plan',
392
+ });
393
+ }
394
+
395
+ // Risk: no fallback strategy
396
+ if (
397
+ !lower.includes('fallback') &&
398
+ !lower.includes('alternative') &&
399
+ !lower.includes('if this fails') &&
400
+ !lower.includes('rollback')
401
+ ) {
402
+ signals.push({
403
+ category: 'fallback',
404
+ signal: `${filename}: No fallback or rollback strategy mentioned`,
405
+ severity: 'low',
406
+ remediation:
407
+ 'Add a fallback plan for if the primary approach fails (e.g., simpler model, cached results)',
408
+ });
409
+ }
410
+
411
+ // Risk: unclear dependencies
412
+ if (
413
+ !lower.includes('depend') &&
414
+ !lower.includes('requires') &&
415
+ !lower.includes('prerequisite') &&
416
+ !lower.includes('phase')
417
+ ) {
418
+ signals.push({
419
+ category: 'dependencies',
420
+ signal: `${filename}: No explicit dependencies stated`,
421
+ severity: 'low',
422
+ remediation:
423
+ 'List phase/plan prerequisites explicitly so blockers are visible before execution starts',
424
+ });
425
+ }
426
+ }
427
+
428
+ if (planFiles.length === 0) {
429
+ signals.push({
430
+ category: 'plans',
431
+ signal: 'No PLAN.md files found in phase directory',
432
+ severity: 'high',
433
+ remediation: 'Run /grd:plan-phase to generate execution plans before risk assessment',
434
+ });
435
+ }
436
+
437
+ // Score: 0=none, 10=critical. High=3pts, medium=2pts, low=1pt
438
+ let rawScore = 0;
439
+ for (const s of signals) {
440
+ if (s.severity === 'high') rawScore += 3;
441
+ else if (s.severity === 'medium') rawScore += 2;
442
+ else rawScore += 1;
443
+ }
444
+ const risk_score = Math.min(10, rawScore);
445
+ const risk_level: RiskAssessmentResult['risk_level'] =
446
+ risk_score >= 8 ? 'critical' : risk_score >= 5 ? 'high' : risk_score >= 3 ? 'medium' : 'low';
447
+
448
+ const result: RiskAssessmentResult = {
449
+ phase,
450
+ risk_score,
451
+ risk_level,
452
+ signals,
453
+ plan_count: planFiles.length,
454
+ plans_analyzed: plansAnalyzed,
455
+ };
456
+
457
+ output(
458
+ result,
459
+ raw,
460
+ `Phase ${phase} risk: ${risk_level} (score=${risk_score}, signals=${signals.length})`
461
+ );
462
+ }
463
+
464
+ // ─── CLI: Citation Backlink Tracker ──────────────────────────────────────────
465
+
466
+ /**
467
+ * CLI command: Build a reverse index of which planning files reference each paper
468
+ * from PAPERS.md. Shows which papers drove real implementation decisions.
469
+ * @param cwd - Project root
470
+ * @param raw - Raw output flag
471
+ */
472
+ function cmdCitationBacklinks(cwd: string, raw: boolean): void {
473
+ const researchPath = getResearchDir(cwd);
474
+ const papersPath = path.join(researchPath, 'PAPERS.md');
475
+ const papersContent = safeReadFile(papersPath);
476
+ const milestone = currentMilestone(cwd);
477
+
478
+ if (!papersContent) {
479
+ output({ error: 'PAPERS.md not found', path: papersPath, backlinks: [] }, raw);
480
+ return;
481
+ }
482
+
483
+ // Extract paper headings from PAPERS.md
484
+ const papers: Array<{ id: string; title: string }> = [];
485
+ for (const m of papersContent.matchAll(/^##\s+(.+)$/gm)) {
486
+ const title = m[1].trim();
487
+ const id = title
488
+ .toLowerCase()
489
+ .replace(/[^a-z0-9]+/g, '-')
490
+ .replace(/^-|-$/g, '')
491
+ .slice(0, 50);
492
+ if (id) papers.push({ id, title });
493
+ }
494
+ // Also match slug-style entries: **[slug]**
495
+ for (const m of papersContent.matchAll(/\*\*\[([a-z0-9-]+)\]\*\*/g)) {
496
+ const id = m[1].trim();
497
+ if (!papers.find((p) => p.id === id)) papers.push({ id, title: id });
498
+ }
499
+
500
+ if (papers.length === 0) {
501
+ output(
502
+ { milestone, papers_found: 0, backlinks: [], note: 'No paper headings found in PAPERS.md' },
503
+ raw
504
+ );
505
+ return;
506
+ }
507
+
508
+ // Scan all .planning/ markdown files for references
509
+ const planningPath = getPlanningDir(cwd);
510
+ const allMdFiles = _collectMarkdownFiles(planningPath);
511
+ const backlinks: CitationBacklink[] = [];
512
+
513
+ for (const paper of papers) {
514
+ const refs: CitationBacklink['references'] = [];
515
+ const searchTerms = [paper.id, ...paper.title.split(/\s+/).filter((w) => w.length > 4)];
516
+
517
+ for (const mdFile of allMdFiles) {
518
+ if (mdFile === papersPath) continue;
519
+ const content = safeReadFile(mdFile);
520
+ if (!content) continue;
521
+ const lines = content.split('\n');
522
+
523
+ for (let i = 0; i < lines.length; i++) {
524
+ const lineLower = lines[i].toLowerCase();
525
+ const matched = searchTerms.some((term) => lineLower.includes(term.toLowerCase()));
526
+ if (matched) {
527
+ refs.push({
528
+ file: path.relative(planningPath, mdFile),
529
+ line: i + 1,
530
+ context: lines[i].trim().slice(0, 120),
531
+ });
532
+ break; // one reference per file
533
+ }
534
+ }
535
+ }
536
+
537
+ backlinks.push({
538
+ paper_id: paper.id,
539
+ paper_title: paper.title,
540
+ references: refs,
541
+ reference_count: refs.length,
542
+ });
543
+ }
544
+
545
+ backlinks.sort((a, b) => b.reference_count - a.reference_count);
546
+ const unreferenced = backlinks.filter((b) => b.reference_count === 0);
547
+
548
+ output(
549
+ {
550
+ milestone,
551
+ papers_indexed: papers.length,
552
+ total_references: backlinks.reduce((s, b) => s + b.reference_count, 0),
553
+ unreferenced_count: unreferenced.length,
554
+ backlinks,
555
+ },
556
+ raw
557
+ );
558
+ }
559
+
560
+ // ─── CLI: Eval Regression Check ───────────────────────────────────────────────
561
+
562
+ /**
563
+ * CLI command: Compare eval metrics from a phase's EVAL.md against BASELINE.md,
564
+ * emitting warnings if metrics regressed beyond the threshold.
565
+ * @param cwd - Project root
566
+ * @param phase - Phase number string
567
+ * @param raw - Raw output flag
568
+ * @param thresholdPct - Regression threshold percentage (default 5%)
569
+ */
570
+ function cmdEvalRegressionCheck(cwd: string, phase: string, raw: boolean, thresholdPct = 5): void {
571
+ if (!phase) {
572
+ error('Phase number required');
573
+ return;
574
+ }
575
+
576
+ const phaseDir = _findPhaseDirByNumber(cwd, phase);
577
+ if (!phaseDir) {
578
+ output(
579
+ { error: `Phase ${phase} not found`, phase, has_regressions: false, regressions: [] },
580
+ raw
581
+ );
582
+ return;
583
+ }
584
+
585
+ let evalFiles: string[] = [];
586
+ try {
587
+ evalFiles = fs.readdirSync(phaseDir).filter((f: string) => f.endsWith('-EVAL.md'));
588
+ } catch {
589
+ /* ignore */
590
+ }
591
+
592
+ if (evalFiles.length === 0) {
593
+ output(
594
+ {
595
+ phase,
596
+ note: 'No EVAL.md found in phase directory',
597
+ has_regressions: false,
598
+ regressions: [],
599
+ },
600
+ raw
601
+ );
602
+ return;
603
+ }
604
+
605
+ const evalContent = safeReadFile(path.join(phaseDir, evalFiles[0]));
606
+ if (!evalContent) {
607
+ output({ phase, note: 'Could not read EVAL.md', has_regressions: false, regressions: [] }, raw);
608
+ return;
609
+ }
610
+
611
+ const baselinePath = path.join(getPlanningDir(cwd), 'BASELINE.md');
612
+ const baselineContent = safeReadFile(baselinePath);
613
+
614
+ const evalMetrics = _parseMetricsFromContent(evalContent);
615
+ const baselineMetrics = baselineContent ? _parseMetricsFromContent(baselineContent) : {};
616
+
617
+ const regressions: EvalMetric[] = [];
618
+ const improvements: EvalMetric[] = [];
619
+ const stable: EvalMetric[] = [];
620
+
621
+ for (const [name, value] of Object.entries(evalMetrics)) {
622
+ const baseline = baselineMetrics[name] ?? null;
623
+ if (baseline === null) {
624
+ stable.push({ name, value, baseline: null, regression: false, delta: null });
625
+ continue;
626
+ }
627
+ const delta = value - baseline;
628
+ const deltaPct = baseline !== 0 ? Math.abs(delta / baseline) * 100 : 0;
629
+ const regressed = delta < 0 && deltaPct > thresholdPct;
630
+ const improved = delta > 0 && deltaPct > thresholdPct;
631
+
632
+ const entry: EvalMetric = {
633
+ name,
634
+ value,
635
+ baseline,
636
+ regression: regressed,
637
+ delta: parseFloat(delta.toFixed(4)),
638
+ };
639
+ if (regressed) regressions.push(entry);
640
+ else if (improved) improvements.push(entry);
641
+ else stable.push(entry);
642
+ }
643
+
644
+ const result: EvalRegressionResult = {
645
+ phase,
646
+ threshold_pct: thresholdPct,
647
+ regressions,
648
+ improvements,
649
+ stable,
650
+ has_regressions: regressions.length > 0,
651
+ };
652
+
653
+ output(
654
+ result,
655
+ raw,
656
+ regressions.length > 0
657
+ ? `WARNING: ${regressions.length} metric(s) regressed in phase ${phase}`
658
+ : `Phase ${phase}: No eval regressions detected`
659
+ );
660
+ }
661
+
662
+ // ─── CLI: Phase Time Budget Tracking ─────────────────────────────────────────
663
+
664
+ /**
665
+ * CLI command: Compare estimated durations (from ROADMAP.md Duration fields)
666
+ * against actual execution time (from STATE.md metric records) for all phases.
667
+ * @param cwd - Project root
668
+ * @param raw - Raw output flag
669
+ */
670
+ function cmdPhaseTimeBudget(cwd: string, raw: boolean): void {
671
+ const roadmapPath = path.join(getPlanningDir(cwd), 'ROADMAP.md');
672
+ const statePath = path.join(getPlanningDir(cwd), 'STATE.md');
673
+ const roadmapContent = safeReadFile(roadmapPath);
674
+ const stateContent = safeReadFile(statePath);
675
+
676
+ if (!roadmapContent) {
677
+ output({ error: 'ROADMAP.md not found', phases: [] }, raw);
678
+ return;
679
+ }
680
+
681
+ // Scan roadmap lines for phase + duration pairs
682
+ const phases: PhaseTimeBudgetEntry[] = [];
683
+ const roadmapLines = roadmapContent.split('\n');
684
+ let currentPhase: string | null = null;
685
+ let currentName: string | null = null;
686
+
687
+ for (const line of roadmapLines) {
688
+ const phaseMatch = line.match(/^[-*]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)[:\s]+(.+)/i);
689
+ if (phaseMatch) {
690
+ currentPhase = phaseMatch[1].trim();
691
+ currentName = phaseMatch[2].replace(/\s*\(.*\)$/, '').trim();
692
+ }
693
+
694
+ const durMatch = line.match(/\*\*Duration:\*\*\s*(\d+(?:\.\d+)?)\s*d(?:ay)?s?\b/i);
695
+ if (durMatch && currentPhase) {
696
+ const estimatedDays = parseFloat(durMatch[1]);
697
+ const estimatedMin = estimatedDays * 8 * 60; // 8h/day
698
+
699
+ phases.push({
700
+ phase: currentPhase,
701
+ name: currentName || currentPhase,
702
+ estimated_days: estimatedDays,
703
+ actual_min: null,
704
+ estimated_min: estimatedMin,
705
+ variance_pct: null,
706
+ over_budget: false,
707
+ });
708
+ currentPhase = null;
709
+ currentName = null;
710
+ }
711
+ }
712
+
713
+ // Parse actual durations from STATE.md
714
+ if (stateContent) {
715
+ const actualByPhase: Record<string, number> = {};
716
+ for (const m of stateContent.matchAll(/Phase\s+(\d+(?:\.\d+)?)[^:]*:\s*.*?(\d+)\s*min/gi)) {
717
+ const phaseNum = m[1];
718
+ const mins = parseInt(m[2], 10);
719
+ if (!isNaN(mins)) {
720
+ actualByPhase[phaseNum] = (actualByPhase[phaseNum] || 0) + mins;
721
+ }
722
+ }
723
+
724
+ for (const entry of phases) {
725
+ const actual = actualByPhase[entry.phase] ?? null;
726
+ if (actual !== null) {
727
+ entry.actual_min = actual;
728
+ entry.variance_pct =
729
+ entry.estimated_min > 0
730
+ ? parseFloat((((actual - entry.estimated_min) / entry.estimated_min) * 100).toFixed(1))
731
+ : null;
732
+ entry.over_budget = entry.variance_pct !== null && entry.variance_pct > 20;
733
+ }
734
+ }
735
+ }
736
+
737
+ const over = phases.filter((p) => p.over_budget);
738
+ const tracked = phases.filter((p) => p.actual_min !== null);
739
+
740
+ output(
741
+ {
742
+ total_phases: phases.length,
743
+ tracked_phases: tracked.length,
744
+ over_budget_count: over.length,
745
+ avg_variance_pct:
746
+ tracked.length > 0
747
+ ? parseFloat(
748
+ (tracked.reduce((s, p) => s + (p.variance_pct ?? 0), 0) / tracked.length).toFixed(1)
749
+ )
750
+ : null,
751
+ phases,
752
+ },
753
+ raw
754
+ );
755
+ }
756
+
757
+ // ─── CLI: Config Change Diff Viewer ──────────────────────────────────────────
758
+
759
+ /**
760
+ * CLI command: Diff current config.json against a stored snapshot.
761
+ * On first run (or with --reset), saves a snapshot. Subsequent runs show
762
+ * what changed and explain the impact on agent behavior.
763
+ * @param cwd - Project root
764
+ * @param raw - Raw output flag
765
+ * @param reset - If true, overwrite snapshot with current config
766
+ */
767
+ function cmdConfigDiff(cwd: string, raw: boolean, reset = false, dryRun = false): void {
768
+ const configPath = path.join(getPlanningDir(cwd), 'config.json');
769
+ const snapshotPath = path.join(getPlanningDir(cwd), '.config-snapshot.json');
770
+
771
+ const currentConfig = safeReadJSON(configPath, null) as Record<string, unknown> | null;
772
+ if (!currentConfig) {
773
+ output({ error: 'config.json not found', path: configPath }, raw);
774
+ return;
775
+ }
776
+
777
+ if (dryRun) {
778
+ const snapshot = fs.existsSync(snapshotPath)
779
+ ? (safeReadJSON(snapshotPath, null) as Record<string, unknown> | null)
780
+ : null;
781
+ if (!snapshot) {
782
+ output({ dry_run: true, action: 'would_create_snapshot', path: snapshotPath, note: 'No snapshot exists yet; --dry-run shows current config only' }, raw, 'DRY RUN: would save config snapshot');
783
+ return;
784
+ }
785
+ const flatCurrent = _flattenObject(currentConfig);
786
+ const flatSnapshot = _flattenObject(snapshot);
787
+ const changes: ConfigChangeEntry[] = [];
788
+ const allKeys = new Set([...Object.keys(flatCurrent), ...Object.keys(flatSnapshot)]);
789
+ for (const key of allKeys) {
790
+ const cur = flatCurrent[key];
791
+ const snap = flatSnapshot[key];
792
+ if (JSON.stringify(cur) !== JSON.stringify(snap)) {
793
+ changes.push({ key, old_value: snap ?? undefined, new_value: cur ?? undefined, explanation: _explainConfigChange(key, snap, cur) });
794
+ }
795
+ }
796
+ output({ dry_run: true, changes_count: changes.length, has_changes: changes.length > 0, changes, note: 'No files written (--dry-run)' }, raw, `DRY RUN: ${changes.length} change(s) detected — no snapshot updated`);
797
+ return;
798
+ }
799
+
800
+ if (reset || !fs.existsSync(snapshotPath)) {
801
+ fs.writeFileSync(snapshotPath, JSON.stringify(currentConfig, null, 2));
802
+ output(
803
+ {
804
+ action: 'snapshot_saved',
805
+ message: 'Config snapshot saved. Run again to see diffs.',
806
+ path: snapshotPath,
807
+ },
808
+ raw
809
+ );
810
+ return;
811
+ }
812
+
813
+ const snapshot = safeReadJSON(snapshotPath, null) as Record<string, unknown> | null;
814
+ if (!snapshot) {
815
+ fs.writeFileSync(snapshotPath, JSON.stringify(currentConfig, null, 2));
816
+ output(
817
+ {
818
+ action: 'snapshot_saved',
819
+ message: 'Config snapshot saved (previous was unreadable).',
820
+ path: snapshotPath,
821
+ },
822
+ raw
823
+ );
824
+ return;
825
+ }
826
+
827
+ const flatCurrent = _flattenObject(currentConfig);
828
+ const flatSnapshot = _flattenObject(snapshot);
829
+
830
+ const changes: ConfigChangeEntry[] = [];
831
+ const allKeys = new Set([...Object.keys(flatCurrent), ...Object.keys(flatSnapshot)]);
832
+
833
+ for (const key of allKeys) {
834
+ const cur = flatCurrent[key];
835
+ const snap = flatSnapshot[key];
836
+ if (JSON.stringify(cur) !== JSON.stringify(snap)) {
837
+ changes.push({
838
+ key,
839
+ old_value: snap ?? undefined,
840
+ new_value: cur ?? undefined,
841
+ explanation: _explainConfigChange(key, snap, cur),
842
+ });
843
+ }
844
+ }
845
+
846
+ output(
847
+ {
848
+ changes_count: changes.length,
849
+ has_changes: changes.length > 0,
850
+ changes,
851
+ snapshot_path: snapshotPath,
852
+ },
853
+ raw,
854
+ changes.length > 0
855
+ ? `${changes.length} config change(s) detected since last snapshot`
856
+ : 'No config changes since last snapshot'
857
+ );
858
+ }
859
+
860
+ // ─── CLI: Phase Kickoff Readiness Checklist ───────────────────────────────────
861
+
862
+ /**
863
+ * CLI command: Run a pre-flight readiness checklist before executing a phase.
864
+ * Verifies baseline, dependencies, eval plan, and research questions are addressed.
865
+ * @param cwd - Project root
866
+ * @param phase - Phase number string
867
+ * @param raw - Raw output flag
868
+ */
869
+ function cmdPhaseReadiness(cwd: string, phase: string, raw: boolean): void {
870
+ if (!phase) {
871
+ error('Phase number required');
872
+ return;
873
+ }
874
+
875
+ const phaseDir = _findPhaseDirByNumber(cwd, phase);
876
+ const checks: ReadinessCheck[] = [];
877
+ const blockers: string[] = [];
878
+
879
+ // Check 1: Phase directory exists
880
+ checks.push({
881
+ name: 'Phase directory exists',
882
+ passed: phaseDir !== null,
883
+ detail: phaseDir ?? `No directory found for phase ${phase} in ${getPhasesDirPath(cwd)}`,
884
+ });
885
+ if (!phaseDir) blockers.push('Phase directory not found — run /grd:plan-phase first');
886
+
887
+ // Check 2: PLAN.md files exist
888
+ const planFiles = phaseDir ? _collectPlanFiles(phaseDir) : [];
889
+ checks.push({
890
+ name: 'Execution plans exist',
891
+ passed: planFiles.length > 0,
892
+ detail: planFiles.length > 0 ? `${planFiles.length} plan file(s) found` : 'No PLAN.md files',
893
+ });
894
+ if (planFiles.length === 0) blockers.push('No PLAN.md files — run /grd:plan-phase');
895
+
896
+ // Check 3: BASELINE.md exists
897
+ const baselinePath = path.join(getPlanningDir(cwd), 'BASELINE.md');
898
+ const baselineExists = fs.existsSync(baselinePath);
899
+ checks.push({
900
+ name: 'Baseline metrics captured',
901
+ passed: baselineExists,
902
+ detail: baselineExists
903
+ ? 'BASELINE.md exists'
904
+ : 'BASELINE.md missing — run /grd:assess-baseline',
905
+ });
906
+ if (!baselineExists)
907
+ blockers.push('No baseline metrics — run /grd:assess-baseline before executing');
908
+
909
+ // Check 4: EVAL plan exists for this phase
910
+ let evalFile: string | undefined;
911
+ if (phaseDir) {
912
+ try {
913
+ evalFile = fs.readdirSync(phaseDir).find((f: string) => f.endsWith('-EVAL.md'));
914
+ } catch {
915
+ /* ignore */
916
+ }
917
+ }
918
+ checks.push({
919
+ name: 'Eval plan written',
920
+ passed: !!evalFile,
921
+ detail: evalFile ? `${evalFile} found` : 'No EVAL.md in phase directory',
922
+ });
923
+
924
+ // Check 5: No active blockers in STATE.md
925
+ const statePath = path.join(getPlanningDir(cwd), 'STATE.md');
926
+ const stateContent = safeReadFile(statePath) || '';
927
+ const blockerSection = stateContent.match(/##\s+Blockers?\s*\n([\s\S]*?)(?=\n##|$)/i);
928
+ const activeBlockers = blockerSection
929
+ ? blockerSection[1]
930
+ .split('\n')
931
+ .filter(
932
+ (l: string) => l.trim().startsWith('-') && !l.includes('[x]') && l.trim().length > 2
933
+ )
934
+ : [];
935
+ checks.push({
936
+ name: 'No active blockers in STATE.md',
937
+ passed: activeBlockers.length === 0,
938
+ detail:
939
+ activeBlockers.length > 0
940
+ ? `${activeBlockers.length} blocker(s): ${activeBlockers[0]?.trim().slice(0, 60)}`
941
+ : 'No active blockers',
942
+ });
943
+ if (activeBlockers.length > 0)
944
+ blockers.push(`${activeBlockers.length} active blocker(s) in STATE.md`);
945
+
946
+ // Check 6: Prior phase is complete (if phase > 1)
947
+ const phaseNum = parseFloat(phase);
948
+ if (phaseNum > 1) {
949
+ const priorPhaseNum = Math.floor(phaseNum - 1);
950
+ const priorDir = _findPhaseDirByNumber(cwd, String(priorPhaseNum));
951
+ if (priorDir) {
952
+ const priorPlans = _collectPlanFiles(priorDir);
953
+ const priorSummaries = priorPlans.filter((p) =>
954
+ fs.existsSync(p.replace('-PLAN.md', '-SUMMARY.md'))
955
+ );
956
+ const priorComplete = priorPlans.length === 0 || priorSummaries.length >= priorPlans.length;
957
+ checks.push({
958
+ name: `Prior phase (${priorPhaseNum}) complete`,
959
+ passed: priorComplete,
960
+ detail: priorComplete
961
+ ? `Phase ${priorPhaseNum} has ${priorSummaries.length}/${priorPlans.length} summaries`
962
+ : `Phase ${priorPhaseNum} missing ${priorPlans.length - priorSummaries.length} summary file(s)`,
963
+ });
964
+ if (!priorComplete) blockers.push(`Phase ${priorPhaseNum} not fully complete`);
965
+ }
966
+ }
967
+
968
+ const passed = checks.filter((c) => c.passed).length;
969
+ const total = checks.length;
970
+ const score = Math.round((passed / total) * 100);
971
+ const ready = blockers.length === 0;
972
+
973
+ const result: PhaseReadinessResult = { phase, ready, score, checks, blockers };
974
+ output(
975
+ result,
976
+ raw,
977
+ ready
978
+ ? `Phase ${phase} is READY to execute (${score}% checks passed)`
979
+ : `Phase ${phase} NOT ready: ${blockers.length} blocker(s) (${score}% passed)`
980
+ );
981
+ }
982
+
983
+ // ─── CLI: Milestone Health Score ─────────────────────────────────────────────
984
+
985
+ /**
986
+ * CLI command: Compute a composite health score (0-100) for the current milestone
987
+ * from phase completion rate, blockers, eval hit rate, and estimate accuracy.
988
+ * @param cwd - Project root
989
+ * @param raw - Raw output flag
990
+ */
991
+ function cmdMilestoneHealth(cwd: string, raw: boolean): void {
992
+ const milestone = currentMilestone(cwd);
993
+ const phasesPath = getPhasesDirPath(cwd);
994
+ const statePath = path.join(getPlanningDir(cwd), 'STATE.md');
995
+ const stateContent = safeReadFile(statePath) || '';
996
+
997
+ // Component 1: Phase completion rate (40 pts)
998
+ let totalPhases = 0;
999
+ let completedPhases = 0;
1000
+ try {
1001
+ const phaseDirs: string[] = fs
1002
+ .readdirSync(phasesPath, { withFileTypes: true })
1003
+ .filter((e: { isDirectory: () => boolean }) => e.isDirectory())
1004
+ .map((e: { name: string }) => e.name);
1005
+ totalPhases = phaseDirs.length;
1006
+ for (const dir of phaseDirs) {
1007
+ const plans = _collectPlanFiles(path.join(phasesPath, dir));
1008
+ if (plans.length === 0) continue;
1009
+ const summaries = plans.filter((p) => fs.existsSync(p.replace('-PLAN.md', '-SUMMARY.md')));
1010
+ if (summaries.length === plans.length) completedPhases++;
1011
+ }
1012
+ } catch {
1013
+ /* no phases dir */
1014
+ }
1015
+
1016
+ const completionRate = totalPhases > 0 ? completedPhases / totalPhases : 0;
1017
+ const completionScore = Math.round(completionRate * 40);
1018
+
1019
+ // Component 2: Blocker penalty (20 pts max, deduct per blocker)
1020
+ const blockerMatch = stateContent.match(/##\s+Blockers?\s*\n([\s\S]*?)(?=\n##|$)/i);
1021
+ const activeBlockerCount = blockerMatch
1022
+ ? blockerMatch[1]
1023
+ .split('\n')
1024
+ .filter(
1025
+ (l: string) => l.trim().startsWith('-') && !l.includes('[x]') && l.trim().length > 2
1026
+ ).length
1027
+ : 0;
1028
+ const blockerPenalty = Math.min(20, activeBlockerCount * 5);
1029
+ const blockerScore = 20 - blockerPenalty;
1030
+
1031
+ // Component 3: Eval hit rate (30 pts)
1032
+ let evalTargetsTotal = 0;
1033
+ let evalTargetsMet = 0;
1034
+ const evalFiles = _collectMarkdownFiles(phasesPath).filter((f) => f.endsWith('-EVAL.md'));
1035
+ for (const ef of evalFiles) {
1036
+ const content = safeReadFile(ef);
1037
+ if (!content) continue;
1038
+ const passes = (content.match(/\bPASS\b/gi) || []).length;
1039
+ const fails = (content.match(/\bFAIL\b/gi) || []).length;
1040
+ evalTargetsTotal += passes + fails;
1041
+ evalTargetsMet += passes;
1042
+ }
1043
+ const evalHitRate = evalTargetsTotal > 0 ? evalTargetsMet / evalTargetsTotal : 0.5;
1044
+ const evalScore = Math.round(evalHitRate * 30);
1045
+
1046
+ // Component 4: Estimate accuracy (10 pts)
1047
+ const velocityMatch = stateContent.match(/avg[_\s]duration[_\s]min[:\s]+(\d+)/i);
1048
+ const estimateScore = velocityMatch ? 8 : 5;
1049
+
1050
+ const totalScore = completionScore + blockerScore + evalScore + estimateScore;
1051
+ const grade: MilestoneHealthResult['grade'] =
1052
+ totalScore >= 90
1053
+ ? 'A'
1054
+ : totalScore >= 75
1055
+ ? 'B'
1056
+ : totalScore >= 60
1057
+ ? 'C'
1058
+ : totalScore >= 45
1059
+ ? 'D'
1060
+ : 'F';
1061
+
1062
+ const result: MilestoneHealthResult = {
1063
+ milestone,
1064
+ score: totalScore,
1065
+ grade,
1066
+ components: {
1067
+ completion_rate: completionScore,
1068
+ blocker_penalty: blockerPenalty,
1069
+ eval_hit_rate: evalScore,
1070
+ estimate_accuracy: estimateScore,
1071
+ },
1072
+ summary: `${completedPhases}/${totalPhases} phases complete, ${activeBlockerCount} blocker(s), ${evalTargetsMet}/${evalTargetsTotal} eval targets met`,
1073
+ };
1074
+
1075
+ output(result, raw, `Milestone ${milestone} health: ${grade} (${totalScore}/100)`);
1076
+ }
1077
+
1078
+ // ─── CLI: Decision Log Timeline ───────────────────────────────────────────────
1079
+
1080
+ /**
1081
+ * CLI command: Build a chronological timeline of all decisions recorded in
1082
+ * STATE.md and CONTEXT.md files across phases.
1083
+ * @param cwd - Project root
1084
+ * @param raw - Raw output flag
1085
+ */
1086
+ function cmdDecisionTimeline(cwd: string, raw: boolean): void {
1087
+ const decisions: DecisionEntry[] = [];
1088
+
1089
+ // Parse decisions from STATE.md
1090
+ const statePath = path.join(getPlanningDir(cwd), 'STATE.md');
1091
+ const stateContent = safeReadFile(statePath) || '';
1092
+ const decisionSection = stateContent.match(
1093
+ /##\s+(?:Key\s+)?Decisions?\s*\n([\s\S]*?)(?=\n##|$)/i
1094
+ );
1095
+ if (decisionSection) {
1096
+ const lines = decisionSection[1].split('\n');
1097
+ for (const line of lines) {
1098
+ const trimmed = line.trim();
1099
+ if (!trimmed) continue;
1100
+ const datePhaseMatch = trimmed.match(
1101
+ /^[-*]\s+\[?(\d{4}-\d{2}-\d{2})\]?\s+(?:Phase\s+(\d+(?:\.\d+)?)[:\s]+)?(.+)/
1102
+ );
1103
+ const phaseMatch = trimmed.match(/^[-*]\s+(?:Phase\s+(\d+(?:\.\d+)?)[:\s]+)?(.+)/);
1104
+ if (datePhaseMatch) {
1105
+ decisions.push({
1106
+ phase: datePhaseMatch[2] || null,
1107
+ date: datePhaseMatch[1],
1108
+ summary: datePhaseMatch[3].trim(),
1109
+ rationale: '',
1110
+ source: 'STATE.md',
1111
+ });
1112
+ } else if (phaseMatch && trimmed.startsWith('-')) {
1113
+ decisions.push({
1114
+ phase: phaseMatch[1] || null,
1115
+ date: null,
1116
+ summary: phaseMatch[2].trim(),
1117
+ rationale: '',
1118
+ source: 'STATE.md',
1119
+ });
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ // Parse decisions from CONTEXT.md files
1125
+ const phaseDirs = _listPhaseDirs(cwd);
1126
+ for (const phaseDir of phaseDirs) {
1127
+ const phaseName = path.basename(phaseDir);
1128
+ const phaseNumMatch = phaseName.match(/^(\d+(?:\.\d+)?)/);
1129
+ const phaseNum = phaseNumMatch ? phaseNumMatch[1] : phaseName;
1130
+
1131
+ let ctxFiles: string[] = [];
1132
+ try {
1133
+ ctxFiles = fs.readdirSync(phaseDir).filter((f: string) => f.endsWith('-CONTEXT.md'));
1134
+ } catch {
1135
+ /* ignore */
1136
+ }
1137
+
1138
+ for (const ctxFile of ctxFiles) {
1139
+ const content = safeReadFile(path.join(phaseDir, ctxFile));
1140
+ if (!content) continue;
1141
+ const section = content.match(
1142
+ /##\s+(?:Decisions?|Choices?|Resolution)\s*\n([\s\S]*?)(?=\n##|$)/i
1143
+ );
1144
+ if (section) {
1145
+ for (const line of section[1].split('\n')) {
1146
+ const trimmed = line.trim();
1147
+ if (!trimmed || !trimmed.startsWith('-')) continue;
1148
+ decisions.push({
1149
+ phase: phaseNum,
1150
+ date: null,
1151
+ summary: trimmed.replace(/^[-*]\s+/, '').slice(0, 120),
1152
+ rationale: '',
1153
+ source: `${phaseName}/${ctxFile}`,
1154
+ });
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ decisions.sort((a, b) => {
1161
+ if (a.date && b.date) return a.date.localeCompare(b.date);
1162
+ if (a.date) return -1;
1163
+ if (b.date) return 1;
1164
+ return parseFloat(a.phase || '999') - parseFloat(b.phase || '999');
1165
+ });
1166
+
1167
+ output(
1168
+ {
1169
+ total_decisions: decisions.length,
1170
+ decisions_with_dates: decisions.filter((d) => d.date !== null).length,
1171
+ timeline: decisions,
1172
+ },
1173
+ raw
1174
+ );
1175
+ }
1176
+
1177
+ // ─── CLI: Cross-Project Knowledge Import ─────────────────────────────────────
1178
+
1179
+ /**
1180
+ * CLI command: Import research artifacts (LANDSCAPE.md, PAPERS.md, KNOWHOW.md)
1181
+ * from another GRD project into the current project's research directory.
1182
+ * @param cwd - Project root
1183
+ * @param sourcePath - Path to the source project root
1184
+ * @param types - Comma-separated types to import (landscape,papers,knowhow,all)
1185
+ * @param raw - Raw output flag
1186
+ * @param force - If true, overwrite existing files
1187
+ */
1188
+ function cmdImportKnowledge(
1189
+ cwd: string,
1190
+ sourcePath: string,
1191
+ types: string,
1192
+ raw: boolean,
1193
+ force = false,
1194
+ dryRun = false
1195
+ ): void {
1196
+ if (!sourcePath) {
1197
+ error('Source project path required');
1198
+ return;
1199
+ }
1200
+
1201
+ const absoluteSource = path.resolve(cwd, sourcePath);
1202
+ if (!fs.existsSync(absoluteSource)) {
1203
+ output({ error: `Source path does not exist: ${absoluteSource}` }, raw);
1204
+ return;
1205
+ }
1206
+
1207
+ const requestedTypes =
1208
+ !types || types === 'all'
1209
+ ? ['landscape', 'papers', 'knowhow']
1210
+ : types.split(',').map((t: string) => t.trim().toLowerCase());
1211
+
1212
+ const fileMap: Record<string, string> = {
1213
+ landscape: 'LANDSCAPE.md',
1214
+ papers: 'PAPERS.md',
1215
+ knowhow: 'KNOWHOW.md',
1216
+ };
1217
+
1218
+ // Locate source research directory
1219
+ const sourceMilestone = currentMilestone(absoluteSource);
1220
+ const candidateDirs = [
1221
+ path.join(absoluteSource, '.planning', 'milestones', sourceMilestone, 'research'),
1222
+ path.join(absoluteSource, '.planning', 'research'),
1223
+ ];
1224
+ let sourceResearchDir: string | null = null;
1225
+ for (const dir of candidateDirs) {
1226
+ if (fs.existsSync(dir)) {
1227
+ sourceResearchDir = dir;
1228
+ break;
1229
+ }
1230
+ }
1231
+
1232
+ if (!sourceResearchDir) {
1233
+ output({ error: `No research directory found in source project`, tried: candidateDirs }, raw);
1234
+ return;
1235
+ }
1236
+
1237
+ const destResearchDir = getResearchDir(cwd);
1238
+ // Codex r16 P2: defer mkdir to the actual copy site so --dry-run is
1239
+ // truly side-effect-free. If we are not dry-running, create on first
1240
+ // use right before the cpSync call.
1241
+ if (!dryRun) fs.mkdirSync(destResearchDir, { recursive: true });
1242
+
1243
+ const results: ImportResult[] = [];
1244
+
1245
+ for (const type of requestedTypes) {
1246
+ const filename = fileMap[type];
1247
+ if (!filename) {
1248
+ results.push({
1249
+ source: sourcePath,
1250
+ type,
1251
+ destination: '',
1252
+ imported: false,
1253
+ conflict: false,
1254
+ message: `Unknown type: ${type}`,
1255
+ });
1256
+ continue;
1257
+ }
1258
+
1259
+ const srcFile = path.join(sourceResearchDir, filename);
1260
+ const destFile = path.join(destResearchDir, filename);
1261
+
1262
+ if (!fs.existsSync(srcFile)) {
1263
+ results.push({
1264
+ source: srcFile,
1265
+ type,
1266
+ destination: destFile,
1267
+ imported: false,
1268
+ conflict: false,
1269
+ message: `Source file not found: ${filename}`,
1270
+ });
1271
+ continue;
1272
+ }
1273
+
1274
+ const destExists = fs.existsSync(destFile);
1275
+ // Codex r24 P3: dry-run should preview `would overwrite` for
1276
+ // existing files without requiring --force. Skip the hard
1277
+ // conflict only when actually writing.
1278
+ if (destExists && !force && !dryRun) {
1279
+ results.push({
1280
+ source: srcFile,
1281
+ type,
1282
+ destination: destFile,
1283
+ imported: false,
1284
+ conflict: true,
1285
+ message: `${filename} already exists — use --force to overwrite`,
1286
+ });
1287
+ continue;
1288
+ }
1289
+
1290
+ if (dryRun) {
1291
+ results.push({
1292
+ source: srcFile,
1293
+ type,
1294
+ destination: destFile,
1295
+ imported: false,
1296
+ conflict: destExists,
1297
+ message: destExists ? `DRY RUN: would overwrite ${filename}` : `DRY RUN: would import ${filename}`,
1298
+ });
1299
+ continue;
1300
+ }
1301
+
1302
+ try {
1303
+ fs.cpSync(srcFile, destFile);
1304
+ results.push({
1305
+ source: srcFile,
1306
+ type,
1307
+ destination: destFile,
1308
+ imported: true,
1309
+ conflict: destExists,
1310
+ message: destExists ? `Overwrote existing ${filename}` : `Imported ${filename}`,
1311
+ });
1312
+ } catch (err: unknown) {
1313
+ results.push({
1314
+ source: srcFile,
1315
+ type,
1316
+ destination: destFile,
1317
+ imported: false,
1318
+ conflict: false,
1319
+ message: `Copy failed: ${(err as Error).message}`,
1320
+ });
1321
+ }
1322
+ }
1323
+
1324
+ const importedCount = results.filter((r) => r.imported).length;
1325
+ output(
1326
+ {
1327
+ source_project: absoluteSource,
1328
+ destination_project: cwd,
1329
+ imported_count: importedCount,
1330
+ conflict_count: results.filter((r) => r.conflict && !r.imported).length,
1331
+ results,
1332
+ },
1333
+ raw,
1334
+ `${importedCount}/${requestedTypes.length} file(s) imported from ${sourcePath}`
1335
+ );
1336
+ }
1337
+
1338
+ // ─── CLI: Semantic Todo Duplicate Detector ────────────────────────────────────
1339
+
1340
+ /**
1341
+ * CLI command: Find semantically similar todo items using Jaccard word-overlap.
1342
+ * Surfaces near-duplicate pairs above a configurable similarity threshold.
1343
+ * @param cwd - Project root
1344
+ * @param raw - Raw output flag
1345
+ * @param threshold - Similarity threshold 0-1 (default 0.4)
1346
+ */
1347
+ function cmdTodoDuplicates(cwd: string, raw: boolean, threshold = 0.4): void {
1348
+ const todosPath = getTodosDir(cwd);
1349
+ const pendingDir = path.join(todosPath, 'pending');
1350
+
1351
+ let todoFiles: string[];
1352
+ try {
1353
+ todoFiles = fs
1354
+ .readdirSync(pendingDir)
1355
+ .filter((f: string) => f.endsWith('.md') || f.endsWith('.txt'))
1356
+ .map((f: string) => path.join(pendingDir, f));
1357
+ } catch {
1358
+ output({ error: `No pending todos directory found at ${pendingDir}`, duplicates: [] }, raw);
1359
+ return;
1360
+ }
1361
+
1362
+ if (todoFiles.length < 2) {
1363
+ output(
1364
+ { total_todos: todoFiles.length, duplicates: [], note: 'Not enough todos to compare' },
1365
+ raw
1366
+ );
1367
+ return;
1368
+ }
1369
+
1370
+ const todos: Array<{ file: string; title: string; content: string }> = [];
1371
+ for (const f of todoFiles) {
1372
+ const content = safeReadFile(f);
1373
+ if (!content) continue;
1374
+ todos.push({ file: path.basename(f), title: _extractTodoTitle(content), content });
1375
+ }
1376
+
1377
+ const pairs: TodoDuplicatePair[] = [];
1378
+ for (let i = 0; i < todos.length; i++) {
1379
+ for (let j = i + 1; j < todos.length; j++) {
1380
+ const sim = _jaccardSimilarity(
1381
+ todos[i].title + ' ' + todos[i].content.slice(0, 300),
1382
+ todos[j].title + ' ' + todos[j].content.slice(0, 300)
1383
+ );
1384
+ if (sim >= threshold) {
1385
+ pairs.push({
1386
+ a: todos[i].file,
1387
+ b: todos[j].file,
1388
+ similarity: parseFloat(sim.toFixed(3)),
1389
+ a_title: todos[i].title,
1390
+ b_title: todos[j].title,
1391
+ });
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ pairs.sort((a, b) => b.similarity - a.similarity);
1397
+
1398
+ output(
1399
+ {
1400
+ total_todos: todos.length,
1401
+ threshold,
1402
+ duplicate_pairs: pairs.length,
1403
+ duplicates: pairs,
1404
+ unique_todos_at_risk: new Set(pairs.flatMap((p) => [p.a, p.b])).size,
1405
+ },
1406
+ raw,
1407
+ `Found ${pairs.length} potential duplicate pair(s) among ${todos.length} todos (threshold=${threshold})`
1408
+ );
1409
+ }
1410
+
1411
+ // ─── Knowhow List ───────────────────────────────────────────────────────────
1412
+
1413
+ function cmdKnowhowList(cwd: string, raw: boolean, moduleHint?: string, limit?: number): void {
1414
+ const {
1415
+ parseKnowhowEntries,
1416
+ selectTopEntries,
1417
+ }: {
1418
+ parseKnowhowEntries: (content: string) => { pattern_name: string; source: string; applicability: string; code_snippet: string; phase_number: number; created_at: string }[];
1419
+ selectTopEntries: (entries: { pattern_name: string; source: string; applicability: string; code_snippet: string; phase_number: number; created_at: string }[], limit: number, moduleHints?: string[]) => { pattern_name: string; source: string; applicability: string; code_snippet: string; phase_number: number; created_at: string }[];
1420
+ } = require('../knowledge');
1421
+
1422
+ const researchDir = getResearchDir(cwd);
1423
+ const knowhowPath = path.join(researchDir, 'KNOWHOW.md');
1424
+ const content = safeReadFile(knowhowPath);
1425
+ if (!content) {
1426
+ output({ entries: [], total: 0, message: 'No KNOWHOW.md found' }, raw, 'No KNOWHOW.md found');
1427
+ return;
1428
+ }
1429
+
1430
+ const allEntries = parseKnowhowEntries(content);
1431
+ const hints = moduleHint ? moduleHint.split(',').map((h: string) => h.trim()) : undefined;
1432
+ const maxEntries = limit || 20;
1433
+ const selected = hints ? selectTopEntries(allEntries, maxEntries, hints) : allEntries.slice(0, maxEntries);
1434
+
1435
+ output(
1436
+ { total: allEntries.length, showing: selected.length, module_filter: hints || null, entries: selected.map((e) => ({ pattern: e.pattern_name, source: e.source, phase: e.phase_number, created: e.created_at })) },
1437
+ raw,
1438
+ `${selected.length}/${allEntries.length} knowhow entries${hints ? ` (filtered: ${hints.join(', ')})` : ''}`
1439
+ );
1440
+ }
1441
+
1442
+ // ─── Citation Graph ─────────────────────────────────────────────────────────
1443
+
1444
+ function cmdCitationGraph(cwd: string, raw: boolean, unresolvedOnly = false): void {
1445
+ const {
1446
+ buildCitationGraph,
1447
+ findUnresolved,
1448
+ }: {
1449
+ buildCitationGraph: (content: string) => { nodes: { slug: string; title: string; resolved: boolean; priority: string }[]; edges: { from: string; to: string; type: string }[] };
1450
+ findUnresolved: (graph: { nodes: { slug: string; title: string; resolved: boolean; priority: string }[]; edges: { from: string; to: string; type: string }[] }) => { slug: string; title: string; priority: string }[];
1451
+ } = require('../citations');
1452
+
1453
+ const researchDir = getResearchDir(cwd);
1454
+ const papersPath = path.join(researchDir, 'PAPERS.md');
1455
+ const content = safeReadFile(papersPath);
1456
+ if (!content) {
1457
+ output({ nodes: 0, edges: 0, message: 'No PAPERS.md found' }, raw, 'No PAPERS.md found');
1458
+ return;
1459
+ }
1460
+
1461
+ const graph = buildCitationGraph(content);
1462
+ const unresolved = findUnresolved(graph);
1463
+
1464
+ if (unresolvedOnly) {
1465
+ output({ unresolved_count: unresolved.length, unresolved }, raw, `${unresolved.length} unresolved citation(s)`);
1466
+ return;
1467
+ }
1468
+
1469
+ output(
1470
+ { nodes: graph.nodes.length, edges: graph.edges.length, unresolved_count: unresolved.length, papers: graph.nodes.map((n) => ({ slug: n.slug, title: n.title, resolved: n.resolved, priority: n.priority })), unresolved },
1471
+ raw,
1472
+ `Citation graph: ${graph.nodes.length} papers, ${graph.edges.length} edges, ${unresolved.length} unresolved`
1473
+ );
1474
+ }
1475
+
1476
+ // ─── Artifact DAG ───────────────────────────────────────────────────────────
1477
+
1478
+ function cmdArtifactDAG(cwd: string, phase: string, raw: boolean): void {
1479
+ const { buildArtifactDAG, validateArtifactDAG }: { buildArtifactDAG: (plans: import('../types').PlanArtifact[]) => import('../types').ArtifactDAG; validateArtifactDAG: (dag: import('../types').ArtifactDAG, plans: import('../types').PlanArtifact[]) => import('../types').ArtifactDAGValidation } = require('../deps');
1480
+ const { extractFrontmatter }: { extractFrontmatter: (content: string) => Record<string, unknown> } = require('../frontmatter');
1481
+
1482
+ const phasesDir = getPhasesDirPath(cwd);
1483
+ let phaseDir: string | null = null;
1484
+ try {
1485
+ const dirs: string[] = fs.readdirSync(phasesDir);
1486
+ const match = dirs.find((d: string) => d.startsWith(phase + '-') || d === phase);
1487
+ if (match) phaseDir = path.join(phasesDir, match);
1488
+ } catch { /* phases dir may not exist */ }
1489
+
1490
+ if (!phaseDir) { output({ error: `Phase ${phase} not found` }, raw, `Phase ${phase} not found`); return; }
1491
+
1492
+ const files: string[] = fs.readdirSync(phaseDir).filter((f: string) => f.endsWith('-PLAN.md'));
1493
+ const plans: import('../types').PlanArtifact[] = [];
1494
+ for (let i = 0; i < files.length; i++) {
1495
+ const planPath = path.join(phaseDir, files[i]);
1496
+ try {
1497
+ const content = fs.readFileSync(planPath, 'utf-8') as string;
1498
+ const fm = extractFrontmatter(content);
1499
+ plans.push({ objective: (fm.objective as string) || '', files_modified: (fm.files_modified as string[]) || [], phase, plan: i + 1, type: (fm.type as string) || 'implementation', wave: (fm.wave as number) || 1, depends_on: (fm.depends_on as string[]) || [], autonomous: (fm.autonomous as boolean) ?? true, provides: (fm.provides as string[]) || [], requires: (fm.requires as string[]) || [], integration_points: (fm.integration_points as string[]) || [] });
1500
+ } catch { /* skip malformed plans */ }
1501
+ }
1502
+
1503
+ if (plans.length === 0) { output({ error: 'No plans with artifact declarations found' }, raw, 'No plans found'); return; }
1504
+
1505
+ const dag = buildArtifactDAG(plans);
1506
+ const validation = validateArtifactDAG(dag, plans);
1507
+ if (dag.missing_requires.length > 0) {
1508
+ process.stderr.write(`Warning: ${dag.missing_requires.length} unresolvable requires: ${dag.missing_requires.join(', ')}\n`);
1509
+ }
1510
+ output({ phase, plans: plans.length, nodes: dag.nodes.length, edges: dag.edges.length, sorted_plans: dag.sorted_plans, missing_requires: dag.missing_requires, valid: validation.valid, cycles: validation.cycles, missing_deps: validation.missing_deps, warnings: validation.warnings }, raw, `Phase ${phase} DAG: ${dag.nodes.length} nodes, ${dag.edges.length} edges, valid=${validation.valid}${dag.missing_requires.length > 0 ? ` (${dag.missing_requires.length} unresolvable requires)` : ''}`);
1511
+ }
1512
+
1513
+ // ─── Benchmark Report ───────────────────────────────────────────────────────
1514
+
1515
+ function cmdBenchmarkReport(cwd: string, raw: boolean): void {
1516
+ // Codex r44 P1 #5: formatBenchmarkReport requires (results, entries).
1517
+ // The prior call passed entries-only, so the function iterated
1518
+ // undefined.semantic and crashed at runtime. Evaluate each corpus
1519
+ // entry first (with empty observation strings — this surfaces the
1520
+ // category multiplier + zero-baseline so the report is structurally
1521
+ // valid even before an eval run), then pass both arrays.
1522
+ const {
1523
+ loadCorpus,
1524
+ formatBenchmarkReport,
1525
+ evaluateEntry,
1526
+ }: {
1527
+ loadCorpus: (dir: string) => import('../types').BenchmarkEntry[];
1528
+ formatBenchmarkReport: (
1529
+ results: import('../types').BenchmarkResult[],
1530
+ entries: import('../types').BenchmarkEntry[]
1531
+ ) => string;
1532
+ evaluateEntry: (
1533
+ entry: import('../types').BenchmarkEntry,
1534
+ semanticSummary: string,
1535
+ buildOutput: string,
1536
+ runOutput: string,
1537
+ stderr: string,
1538
+ executionTimeMs: number,
1539
+ rubricVersion: string,
1540
+ evaluator: string
1541
+ ) => import('../types').BenchmarkResult;
1542
+ } = require('../benchmark');
1543
+
1544
+ const researchDir = getResearchDir(cwd);
1545
+ const corpusDir = path.join(researchDir, 'benchmarks');
1546
+
1547
+ if (!fs.existsSync(corpusDir)) { output({ entries: 0, message: 'No benchmark corpus found', corpus_dir: corpusDir }, raw, 'No benchmark corpus found'); return; }
1548
+
1549
+ const entries = loadCorpus(corpusDir);
1550
+ if (entries.length === 0) { output({ entries: 0, message: 'Benchmark corpus is empty' }, raw, 'Benchmark corpus is empty'); return; }
1551
+
1552
+ const results = entries.map((e) =>
1553
+ evaluateEntry(e, '', '', '', '', 0, '1.0', 'gd-benchmark-report')
1554
+ );
1555
+ const report = formatBenchmarkReport(results, entries);
1556
+ output({ entries: entries.length, results: results.length, report }, raw, report);
1557
+ }
1558
+
1559
+ // ─── Phase Cost Estimator ─────────────────────────────────────────────────────
1560
+
1561
+ interface PhaseAgentEstimate {
1562
+ agent: string;
1563
+ tasks: number;
1564
+ est_tokens: number;
1565
+ est_cost_usd: number;
1566
+ }
1567
+
1568
+ interface PhaseTokenEstimate {
1569
+ phase: string;
1570
+ agents: PhaseAgentEstimate[];
1571
+ total_tokens: number;
1572
+ total_cost_usd: number;
1573
+ model_tier: string;
1574
+ cheaper_alternative?: string;
1575
+ }
1576
+
1577
+ /** Average tokens per agent task, seeded from empirical observations. */
1578
+ const TOKEN_AVERAGES_BY_TIER: Record<string, number> = {
1579
+ high: 45000,
1580
+ medium: 22000,
1581
+ low: 8000,
1582
+ };
1583
+ const COST_PER_MILLION_BY_TIER: Record<string, number> = {
1584
+ high: 15.0, // e.g. Opus
1585
+ medium: 3.0, // e.g. Sonnet
1586
+ low: 0.25, // e.g. Haiku
1587
+ };
1588
+
1589
+ /**
1590
+ * Estimate token and dollar cost for a phase by counting tasks across plan files,
1591
+ * looking up model tier from config, and applying historical per-agent averages.
1592
+ */
1593
+ function estimatePhaseTokens(cwd: string, phaseNum: string, config: Record<string, unknown>): PhaseTokenEstimate {
1594
+ const phaseDir = _findPhaseDirByNumber(cwd, phaseNum);
1595
+ const modelProfile = (config.model_profile as string) || 'balanced';
1596
+ // Codex r5 P2: model_profile values are quality/balanced/budget;
1597
+ // 'frugal' is a token_profile value and never appears here, so the
1598
+ // prior mapping silently routed `budget` projects to medium-tier
1599
+ // estimates. Map model_profile correctly.
1600
+ const tier =
1601
+ modelProfile === 'quality' ? 'high' : modelProfile === 'budget' ? 'low' : 'medium';
1602
+ const tokensPerTask = TOKEN_AVERAGES_BY_TIER[tier] ?? TOKEN_AVERAGES_BY_TIER.medium;
1603
+ const costPerMillion = COST_PER_MILLION_BY_TIER[tier] ?? COST_PER_MILLION_BY_TIER.medium;
1604
+
1605
+ const agents: PhaseAgentEstimate[] = [];
1606
+
1607
+ if (phaseDir) {
1608
+ const planFiles = _collectPlanFiles(phaseDir);
1609
+ for (const planFile of planFiles) {
1610
+ const content = safeReadFile(planFile);
1611
+ if (!content) continue;
1612
+ // Codex r5 P2: count XML <task> blocks alongside markdown
1613
+ // checkboxes. GRD plans use the XML form; counting only checkboxes
1614
+ // collapsed every multi-task XML plan to the fallback `1`.
1615
+ const taskCount =
1616
+ (content.match(/^[-*]\s+\[/gm) || []).length +
1617
+ (content.match(/<task\b[^>]*>/gi) || []).length || 1;
1618
+ const agentName = path.basename(planFile, '-PLAN.md');
1619
+ const estTokens = taskCount * tokensPerTask;
1620
+ agents.push({
1621
+ agent: agentName,
1622
+ tasks: taskCount,
1623
+ est_tokens: estTokens,
1624
+ est_cost_usd: parseFloat(((estTokens / 1_000_000) * costPerMillion).toFixed(4)),
1625
+ });
1626
+ }
1627
+ }
1628
+
1629
+ const totalTokens = agents.reduce((s, a) => s + a.est_tokens, 0);
1630
+ const totalCost = parseFloat(agents.reduce((s, a) => s + a.est_cost_usd, 0).toFixed(4));
1631
+
1632
+ // Codex r7 P3: model_profile values are quality/balanced/budget;
1633
+ // `frugal` is a token_profile value. Recommend the correct flag
1634
+ // (model_profile=budget) so users don't introduce config drift.
1635
+ const cheaperAlternative =
1636
+ totalCost > 0.5 && tier !== 'low'
1637
+ ? `Use model_profile=budget to reduce cost by ~${Math.round((1 - COST_PER_MILLION_BY_TIER.low / costPerMillion) * 100)}%`
1638
+ : undefined;
1639
+
1640
+ return { phase: phaseNum, agents, total_tokens: totalTokens, total_cost_usd: totalCost, model_tier: tier, cheaper_alternative: cheaperAlternative };
1641
+ }
1642
+
1643
+ /**
1644
+ * CLI command: Estimate token and dollar cost for a phase before execution.
1645
+ * @param cwd - Project root
1646
+ * @param phase - Phase number string
1647
+ * @param raw - Raw output flag
1648
+ */
1649
+ function cmdEstimatePhase(cwd: string, phase: string, raw: boolean): void {
1650
+ if (!phase) {
1651
+ error('Phase number required');
1652
+ return;
1653
+ }
1654
+ const config = require('../utils').loadConfig(cwd) as Record<string, unknown>;
1655
+ const estimate = estimatePhaseTokens(cwd, phase, config);
1656
+ const rawMsg = estimate.agents.length === 0
1657
+ ? `Phase ${phase}: no plan files found`
1658
+ : `Phase ${phase}: ~${estimate.total_tokens.toLocaleString()} tokens / $${estimate.total_cost_usd} (${estimate.model_tier} tier)${estimate.cheaper_alternative ? ' — ' + estimate.cheaper_alternative : ''}`;
1659
+ output(estimate, raw, rawMsg);
1660
+ }
1661
+
1662
+ // ─── Downstream Phase Impact Analyzer ────────────────────────────────────────
1663
+
1664
+ interface ImpactPhaseEntry {
1665
+ phase: string;
1666
+ name: string;
1667
+ status: string;
1668
+ risk: 'LOW' | 'MEDIUM' | 'HIGH';
1669
+ depth: number;
1670
+ }
1671
+
1672
+ interface DownstreamImpactResult {
1673
+ source_phase: string;
1674
+ affected_count: number;
1675
+ high_risk_count: number;
1676
+ affected_phases: ImpactPhaseEntry[];
1677
+ }
1678
+
1679
+ /**
1680
+ * Compute downstream phases that depend (directly or transitively) on the given phase.
1681
+ * Parses phase dependency blocks from ROADMAP.md using a BFS traversal.
1682
+ */
1683
+ function computeDownstreamImpact(cwd: string, phaseNum: string): DownstreamImpactResult {
1684
+ const roadmapPath = path.join(getPlanningDir(cwd), 'ROADMAP.md');
1685
+ const roadmapContent = safeReadFile(roadmapPath) || '';
1686
+ const phasesPath = getPhasesDirPath(cwd);
1687
+
1688
+ // Build dependency graph: phaseN depends on phaseM means M → N edge
1689
+ const deps = new Map<string, string[]>(); // parent → children
1690
+ const phaseNames = new Map<string, string>();
1691
+
1692
+ // Codex r1 P2: roadmap uses `- [x] **Phase 87: name**` form (bold,
1693
+ // checklist marker, comma-separated description); the prior regex
1694
+ // expected a bare `Phase N: name` and matched nothing on real
1695
+ // roadmaps. Accept both the canonical form and the lighter form.
1696
+ for (const m of roadmapContent.matchAll(
1697
+ /^[-*]\s+(?:\[[ x]\]\s+)?(?:\*\*)?(?:Phase\s+)?(\d+(?:\.\d+)?)[:\s]+([^\n*]+)/gim
1698
+ )) {
1699
+ const num = m[1].trim();
1700
+ const name = m[2].replace(/\s*\(.*\)$/, '').trim();
1701
+ phaseNames.set(num, name);
1702
+ if (!deps.has(num)) deps.set(num, []);
1703
+ }
1704
+
1705
+ // Codex r5 P2: most roadmap blocks have `Depends on: Phase N` inside
1706
+ // the *child* phase's section (i.e. the current phase being defined),
1707
+ // so the regex captures only the parent and leaves child undefined.
1708
+ // Walk the roadmap line-by-line tracking the enclosing phase context:
1709
+ // a `#### Phase N: ...` (or comparable heading) sets the current
1710
+ // phase; subsequent `Depends on: Phase M` lines add M → N edges.
1711
+ let currentPhase: string | null = null;
1712
+ const phaseHeadingRe = /^#{2,}\s*Phase\s+(\d+(?:\.\d+)?)\s*:/i;
1713
+ const dependsOnRe =
1714
+ /[Dd]epends?\s+on(?:\*\*)?[:\s]+[Pp]hase\s+(\d+(?:\.\d+)?)(?:[^\n]*?[Pp]hase\s+(\d+(?:\.\d+)?))?/;
1715
+ for (const line of roadmapContent.split('\n')) {
1716
+ const heading = line.match(phaseHeadingRe);
1717
+ if (heading) {
1718
+ currentPhase = heading[1].trim();
1719
+ continue;
1720
+ }
1721
+ const m = line.match(dependsOnRe);
1722
+ if (!m) continue;
1723
+ const parent = m[1].trim();
1724
+ const explicitChild = m[2]?.trim();
1725
+ if (!deps.has(parent)) deps.set(parent, []);
1726
+ // Two-phase pattern wins; single-parent falls back to enclosing
1727
+ // phase context.
1728
+ const child = explicitChild ?? currentPhase;
1729
+ if (child && child !== parent) deps.get(parent)!.push(child);
1730
+ }
1731
+
1732
+ // Codex r13 P2: only infer the sequential N → N+1 edge when the
1733
+ // child has no explicit parent declared. The prior loop always
1734
+ // added the edge, so `gd impact 2` over-reported phase 3 as
1735
+ // affected even when phase 3 declared `Depends on: Phase 1` only.
1736
+ const allPhaseNums = Array.from(phaseNames.keys()).sort(
1737
+ (a, b) => parseFloat(a) - parseFloat(b)
1738
+ );
1739
+ const childrenWithExplicitParent = new Set<string>();
1740
+ for (const [, kids] of deps) {
1741
+ for (const k of kids) childrenWithExplicitParent.add(k);
1742
+ }
1743
+ for (let i = 0; i < allPhaseNums.length - 1; i++) {
1744
+ const parent = allPhaseNums[i];
1745
+ const child = allPhaseNums[i + 1];
1746
+ if (childrenWithExplicitParent.has(child)) continue;
1747
+ const parentChildren = deps.get(parent) || [];
1748
+ if (!parentChildren.includes(child)) parentChildren.push(child);
1749
+ deps.set(parent, parentChildren);
1750
+ }
1751
+
1752
+ // Determine status of each phase
1753
+ function _phaseStatus(num: string): string {
1754
+ const normalized = num.padStart(2, '0');
1755
+ try {
1756
+ const entries: { isDirectory: () => boolean; name: string }[] = require('fs').readdirSync(
1757
+ phasesPath,
1758
+ { withFileTypes: true }
1759
+ );
1760
+ const dir = entries.find(
1761
+ (e) => e.isDirectory() && (e.name.startsWith(normalized + '-') || e.name.startsWith(num + '-'))
1762
+ );
1763
+ if (!dir) return 'not_found';
1764
+ const phaseDir = path.join(phasesPath, dir.name);
1765
+ const files: string[] = require('fs').readdirSync(phaseDir);
1766
+ // Codex r17 P2: include bare PLAN.md / SUMMARY.md so single-plan
1767
+ // phases are not misclassified as `planned` (the rest of this
1768
+ // module's plan discovery already handles both forms).
1769
+ const plans = files.filter(
1770
+ (f: string) => f === 'PLAN.md' || f.endsWith('-PLAN.md')
1771
+ ).length;
1772
+ const summaries = files.filter(
1773
+ (f: string) => f === 'SUMMARY.md' || f.endsWith('-SUMMARY.md')
1774
+ ).length;
1775
+ if (plans === 0) return 'planned';
1776
+ if (summaries >= plans) return 'done';
1777
+ if (summaries > 0) return 'executing';
1778
+ return 'planned';
1779
+ } catch {
1780
+ return 'unknown';
1781
+ }
1782
+ }
1783
+
1784
+ // BFS from phaseNum to find all downstream
1785
+ const visited = new Set<string>();
1786
+ const queue: Array<{ phase: string; depth: number }> = [{ phase: phaseNum, depth: 0 }];
1787
+ const affected: ImpactPhaseEntry[] = [];
1788
+
1789
+ while (queue.length > 0) {
1790
+ const { phase, depth } = queue.shift()!;
1791
+ if (visited.has(phase)) continue;
1792
+ visited.add(phase);
1793
+
1794
+ if (phase !== phaseNum) {
1795
+ const status = _phaseStatus(phase);
1796
+ const risk: 'LOW' | 'MEDIUM' | 'HIGH' =
1797
+ status === 'executing' ? 'HIGH' : status === 'planned' ? 'MEDIUM' : 'LOW';
1798
+ affected.push({ phase, name: phaseNames.get(phase) || phase, status, risk, depth });
1799
+ }
1800
+
1801
+ for (const child of deps.get(phase) || []) {
1802
+ if (!visited.has(child)) queue.push({ phase: child, depth: depth + 1 });
1803
+ }
1804
+ }
1805
+
1806
+ affected.sort((a, b) => a.depth - b.depth || parseFloat(a.phase) - parseFloat(b.phase));
1807
+ const highRisk = affected.filter((p) => p.risk === 'HIGH').length;
1808
+
1809
+ return { source_phase: phaseNum, affected_count: affected.length, high_risk_count: highRisk, affected_phases: affected };
1810
+ }
1811
+
1812
+ /**
1813
+ * CLI command: Show which downstream phases are affected if the given phase changes.
1814
+ * @param cwd - Project root
1815
+ * @param phase - Phase number string
1816
+ * @param raw - Raw output flag
1817
+ */
1818
+ function cmdImpact(cwd: string, phase: string, raw: boolean): void {
1819
+ if (!phase) {
1820
+ error('Phase number required');
1821
+ return;
1822
+ }
1823
+ const result = computeDownstreamImpact(cwd, phase);
1824
+ const rawMsg = result.affected_count === 0
1825
+ ? `Phase ${phase} has no downstream dependencies`
1826
+ : `Phase ${phase} impacts ${result.affected_count} phase(s) (${result.high_risk_count} HIGH risk)`;
1827
+ output(result, raw, rawMsg);
1828
+ }
1829
+
1830
+ // ─── Exports ─────────────────────────────────────────────────────────────────
1831
+
1832
+ module.exports = {
1833
+ cmdPhaseRiskAssessment,
1834
+ cmdCitationBacklinks,
1835
+ cmdEvalRegressionCheck,
1836
+ cmdPhaseTimeBudget,
1837
+ cmdConfigDiff,
1838
+ cmdPhaseReadiness,
1839
+ cmdMilestoneHealth,
1840
+ cmdDecisionTimeline,
1841
+ cmdImportKnowledge,
1842
+ cmdTodoDuplicates,
1843
+ cmdKnowhowList,
1844
+ cmdCitationGraph,
1845
+ cmdArtifactDAG,
1846
+ cmdBenchmarkReport,
1847
+ estimatePhaseTokens,
1848
+ cmdEstimatePhase,
1849
+ computeDownstreamImpact,
1850
+ cmdImpact,
1851
+ };