@nathapp/nax 0.18.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 (459) hide show
  1. package/.gitlab-ci.yml +96 -0
  2. package/BRIEF.md +140 -0
  3. package/CHANGELOG.md +60 -0
  4. package/CLAUDE.md +159 -0
  5. package/README.md +373 -0
  6. package/US-007-IMPLEMENTATION.md +139 -0
  7. package/bin/nax.ts +930 -0
  8. package/biome.json +14 -0
  9. package/bun.lock +168 -0
  10. package/bunfig.toml +11 -0
  11. package/docs/20260216-fix-plan-context-review.md +56 -0
  12. package/docs/20260216-relentless-vs-ngent-comparison.md +208 -0
  13. package/docs/20260216-v02-plan.md +136 -0
  14. package/docs/20260216-v02-review.md +685 -0
  15. package/docs/20260217-dogfood-findings.md +56 -0
  16. package/docs/20260217-p2-plus-plan.md +117 -0
  17. package/docs/20260217-partial-fixes-plan.md +62 -0
  18. package/docs/20260217-plan-analyze-spec.md +117 -0
  19. package/docs/20260217-post-impl-review.md +1137 -0
  20. package/docs/20260217-quick-wins-plan.md +66 -0
  21. package/docs/20260217-split-runner-plan.md +75 -0
  22. package/docs/20260217-v03-impl-plan.md +80 -0
  23. package/docs/20260217-v03-post-impl-review.md +589 -0
  24. package/docs/20260217-v04-impl-plan.md +86 -0
  25. package/docs/20260217-v05-post-impl-review.md +850 -0
  26. package/docs/20260217-v06-post-impl-review.md +817 -0
  27. package/docs/20260218-adr003-port-plan.md +151 -0
  28. package/docs/20260218-review-adr003-verification.md +175 -0
  29. package/docs/20260219-fix-plan-bug16-19.md +79 -0
  30. package/docs/20260219-fix-plan-bug20-22.md +114 -0
  31. package/docs/20260219-plan-llm-routing.md +116 -0
  32. package/docs/20260219-review-bug20-22-fixes.md +135 -0
  33. package/docs/20260219-routing-baseline-keyword.md +63 -0
  34. package/docs/20260220-plan-structured-logging-p1.md +80 -0
  35. package/docs/20260220-plan-structured-logging-p2.md +37 -0
  36. package/docs/20260220-review-llm-routing.md +180 -0
  37. package/docs/20260220-review-post-fix-llm-routing.md +70 -0
  38. package/docs/20260221-fix-plan-relevantfiles-split.md +101 -0
  39. package/docs/20260221-fix-plan-routing-mode.md +125 -0
  40. package/docs/20260221-review-v0.9-implementation.md +379 -0
  41. package/docs/20260222-fix-plan-v091-routing-isolation.md +197 -0
  42. package/docs/20260223-fix-plan-prompt-audit.md +62 -0
  43. package/docs/20260224-nax-roadmap-phases.md +189 -0
  44. package/docs/20260225-phase2-llm-service-layer.md +401 -0
  45. package/docs/20260225-review-v0.10.1.md +187 -0
  46. package/docs/20260303-v010-implementation-plan.md +165 -0
  47. package/docs/CLAUDE.md.bak +191 -0
  48. package/docs/ROADMAP.md +165 -0
  49. package/docs/SPEC-rectification.md +0 -0
  50. package/docs/SPEC.md +324 -0
  51. package/docs/US-001-plugin-loading-verification.md +152 -0
  52. package/docs/architecture-analysis.md +1076 -0
  53. package/docs/bugs/BUG-21-escalation-null-attempts.md +48 -0
  54. package/docs/bugs-from-dogfood-run-c.md +243 -0
  55. package/docs/code-review-20260228.md +612 -0
  56. package/docs/code-review-v0.15.0.md +629 -0
  57. package/docs/hook-lifecycle-test-plan.md +149 -0
  58. package/docs/releases/v0.11.0-and-earlier.md +20 -0
  59. package/docs/releases/v0.12.0.md +15 -0
  60. package/docs/releases/v0.13.0.md +14 -0
  61. package/docs/releases/v0.14.0.md +20 -0
  62. package/docs/releases/v0.14.1.md +36 -0
  63. package/docs/releases/v0.14.2.md +51 -0
  64. package/docs/releases/v0.14.3.md +174 -0
  65. package/docs/releases/v0.14.4.md +94 -0
  66. package/docs/releases/v0.15.0.md +502 -0
  67. package/docs/releases/v0.15.1.md +170 -0
  68. package/docs/releases/v0.15.3.md +193 -0
  69. package/docs/specs/status-file-v0.10.1.md +812 -0
  70. package/docs/v0.10-global-config.md +206 -0
  71. package/docs/v0.10-plugin-system.md +415 -0
  72. package/docs/v0.10-prompt-optimizer.md +234 -0
  73. package/docs/v0.3-spec.md +244 -0
  74. package/docs/v0.4-spec.md +140 -0
  75. package/docs/v0.5-spec.md +237 -0
  76. package/docs/v0.6-spec.md +371 -0
  77. package/docs/v0.7-spec.md +177 -0
  78. package/docs/v0.8-llm-routing.md +206 -0
  79. package/docs/v0.8-structured-logging.md +132 -0
  80. package/docs/v0.9.3-prompt-audit.md +112 -0
  81. package/examples/plugins/console-reporter/index.test.ts +207 -0
  82. package/examples/plugins/console-reporter/index.ts +110 -0
  83. package/nax/config.json +147 -0
  84. package/nax/features/bugfix-v0171/prd.json +52 -0
  85. package/nax/features/config-management/prd.json +108 -0
  86. package/nax/features/config-management/progress.txt +5 -0
  87. package/nax/features/diagnose/acceptance.test.ts +412 -0
  88. package/nax/features/diagnose/prd.json +41 -0
  89. package/nax/features/orchestration-fixes/prd.json +89 -0
  90. package/nax/features/orchestration-fixes/progress.txt +1 -0
  91. package/nax/features/plugin-integration/US-007-VERIFICATION.md +259 -0
  92. package/nax/features/plugin-integration/prd.json +208 -0
  93. package/nax/features/plugin-integration/progress.txt +5 -0
  94. package/nax/features/precheck/prd.json +205 -0
  95. package/nax/features/precheck/progress.txt +15 -0
  96. package/nax/features/structured-logging/prd.json +199 -0
  97. package/nax/features/unlock/prd.json +36 -0
  98. package/package.json +47 -0
  99. package/src/acceptance/fix-generator.ts +348 -0
  100. package/src/acceptance/generator.ts +282 -0
  101. package/src/acceptance/index.ts +30 -0
  102. package/src/acceptance/types.ts +79 -0
  103. package/src/agents/claude-decompose.ts +169 -0
  104. package/src/agents/claude-plan.ts +139 -0
  105. package/src/agents/claude.ts +324 -0
  106. package/src/agents/cost.ts +268 -0
  107. package/src/agents/index.ts +13 -0
  108. package/src/agents/registry.ts +48 -0
  109. package/src/agents/types-extended.ts +133 -0
  110. package/src/agents/types.ts +113 -0
  111. package/src/agents/validation.ts +69 -0
  112. package/src/analyze/classifier.ts +305 -0
  113. package/src/analyze/index.ts +16 -0
  114. package/src/analyze/scanner.ts +175 -0
  115. package/src/analyze/types.ts +51 -0
  116. package/src/cli/accept.ts +108 -0
  117. package/src/cli/analyze-parser.ts +284 -0
  118. package/src/cli/analyze.ts +207 -0
  119. package/src/cli/config.ts +561 -0
  120. package/src/cli/constitution.ts +109 -0
  121. package/src/cli/diagnose-analysis.ts +159 -0
  122. package/src/cli/diagnose-formatter.ts +87 -0
  123. package/src/cli/diagnose.ts +203 -0
  124. package/src/cli/generate.ts +127 -0
  125. package/src/cli/index.ts +37 -0
  126. package/src/cli/init.ts +188 -0
  127. package/src/cli/interact.ts +295 -0
  128. package/src/cli/plan.ts +198 -0
  129. package/src/cli/plugins.ts +111 -0
  130. package/src/cli/prompts.ts +295 -0
  131. package/src/cli/runs.ts +174 -0
  132. package/src/cli/status-cost.ts +151 -0
  133. package/src/cli/status-features.ts +338 -0
  134. package/src/cli/status.ts +13 -0
  135. package/src/commands/common.ts +171 -0
  136. package/src/commands/diagnose.ts +17 -0
  137. package/src/commands/index.ts +8 -0
  138. package/src/commands/logs.ts +384 -0
  139. package/src/commands/precheck.ts +86 -0
  140. package/src/commands/unlock.ts +96 -0
  141. package/src/config/defaults.ts +160 -0
  142. package/src/config/index.ts +22 -0
  143. package/src/config/loader.ts +121 -0
  144. package/src/config/merger.ts +147 -0
  145. package/src/config/path-security.ts +121 -0
  146. package/src/config/paths.ts +27 -0
  147. package/src/config/schema.ts +56 -0
  148. package/src/config/schemas.ts +286 -0
  149. package/src/config/types.ts +423 -0
  150. package/src/config/validate.ts +103 -0
  151. package/src/constitution/generator.ts +191 -0
  152. package/src/constitution/generators/aider.ts +41 -0
  153. package/src/constitution/generators/claude.ts +35 -0
  154. package/src/constitution/generators/cursor.ts +36 -0
  155. package/src/constitution/generators/opencode.ts +38 -0
  156. package/src/constitution/generators/types.ts +33 -0
  157. package/src/constitution/generators/windsurf.ts +36 -0
  158. package/src/constitution/index.ts +10 -0
  159. package/src/constitution/loader.ts +133 -0
  160. package/src/constitution/types.ts +31 -0
  161. package/src/context/auto-detect.ts +227 -0
  162. package/src/context/builder.ts +246 -0
  163. package/src/context/elements.ts +83 -0
  164. package/src/context/formatter.ts +107 -0
  165. package/src/context/generator.ts +129 -0
  166. package/src/context/generators/aider.ts +34 -0
  167. package/src/context/generators/claude.ts +28 -0
  168. package/src/context/generators/cursor.ts +28 -0
  169. package/src/context/generators/opencode.ts +30 -0
  170. package/src/context/generators/windsurf.ts +28 -0
  171. package/src/context/greenfield.ts +114 -0
  172. package/src/context/index.ts +33 -0
  173. package/src/context/injector.ts +279 -0
  174. package/src/context/test-scanner.ts +370 -0
  175. package/src/context/types.ts +98 -0
  176. package/src/errors.ts +67 -0
  177. package/src/execution/batching.ts +157 -0
  178. package/src/execution/crash-recovery.ts +373 -0
  179. package/src/execution/escalation/escalation.ts +44 -0
  180. package/src/execution/escalation/index.ts +13 -0
  181. package/src/execution/escalation/tier-escalation.ts +295 -0
  182. package/src/execution/escalation/tier-outcome.ts +158 -0
  183. package/src/execution/helpers.ts +38 -0
  184. package/src/execution/index.ts +45 -0
  185. package/src/execution/lifecycle/acceptance-loop.ts +272 -0
  186. package/src/execution/lifecycle/headless-formatter.ts +85 -0
  187. package/src/execution/lifecycle/index.ts +12 -0
  188. package/src/execution/lifecycle/parallel-lifecycle.ts +101 -0
  189. package/src/execution/lifecycle/precheck-runner.ts +140 -0
  190. package/src/execution/lifecycle/run-cleanup.ts +81 -0
  191. package/src/execution/lifecycle/run-completion.ts +129 -0
  192. package/src/execution/lifecycle/run-initialization.ts +141 -0
  193. package/src/execution/lifecycle/run-lifecycle.ts +312 -0
  194. package/src/execution/lifecycle/run-setup.ts +204 -0
  195. package/src/execution/lifecycle/story-hooks.ts +38 -0
  196. package/src/execution/lifecycle/story-size-prompts.ts +123 -0
  197. package/src/execution/lock.ts +115 -0
  198. package/src/execution/parallel-executor.ts +216 -0
  199. package/src/execution/parallel.ts +400 -0
  200. package/src/execution/pid-registry.ts +280 -0
  201. package/src/execution/pipeline-result-handler.ts +388 -0
  202. package/src/execution/post-verify-rectification.ts +188 -0
  203. package/src/execution/post-verify.ts +274 -0
  204. package/src/execution/progress.ts +25 -0
  205. package/src/execution/prompts.ts +127 -0
  206. package/src/execution/queue-handler.ts +109 -0
  207. package/src/execution/rectification.ts +13 -0
  208. package/src/execution/runner.ts +377 -0
  209. package/src/execution/sequential-executor.ts +388 -0
  210. package/src/execution/status-file.ts +264 -0
  211. package/src/execution/status-writer.ts +139 -0
  212. package/src/execution/story-context.ts +229 -0
  213. package/src/execution/test-output-parser.ts +14 -0
  214. package/src/execution/verification.ts +72 -0
  215. package/src/hooks/index.ts +2 -0
  216. package/src/hooks/runner.ts +286 -0
  217. package/src/hooks/types.ts +67 -0
  218. package/src/interaction/chain.ts +154 -0
  219. package/src/interaction/index.ts +60 -0
  220. package/src/interaction/init.ts +83 -0
  221. package/src/interaction/plugins/auto.ts +217 -0
  222. package/src/interaction/plugins/cli.ts +300 -0
  223. package/src/interaction/plugins/telegram.ts +384 -0
  224. package/src/interaction/plugins/webhook.ts +258 -0
  225. package/src/interaction/state.ts +171 -0
  226. package/src/interaction/triggers.ts +229 -0
  227. package/src/interaction/types.ts +163 -0
  228. package/src/logger/formatters.ts +84 -0
  229. package/src/logger/index.ts +16 -0
  230. package/src/logger/logger.ts +298 -0
  231. package/src/logger/types.ts +48 -0
  232. package/src/logging/formatter.ts +355 -0
  233. package/src/logging/index.ts +22 -0
  234. package/src/logging/types.ts +93 -0
  235. package/src/metrics/aggregator.ts +190 -0
  236. package/src/metrics/index.ts +14 -0
  237. package/src/metrics/tracker.ts +200 -0
  238. package/src/metrics/types.ts +109 -0
  239. package/src/optimizer/index.ts +62 -0
  240. package/src/optimizer/noop.optimizer.ts +24 -0
  241. package/src/optimizer/rule-based.optimizer.ts +248 -0
  242. package/src/optimizer/types.ts +53 -0
  243. package/src/pipeline/events.ts +130 -0
  244. package/src/pipeline/index.ts +19 -0
  245. package/src/pipeline/runner.ts +161 -0
  246. package/src/pipeline/stages/acceptance.ts +197 -0
  247. package/src/pipeline/stages/completion.ts +99 -0
  248. package/src/pipeline/stages/constitution.ts +63 -0
  249. package/src/pipeline/stages/context.ts +117 -0
  250. package/src/pipeline/stages/execution.ts +194 -0
  251. package/src/pipeline/stages/index.ts +62 -0
  252. package/src/pipeline/stages/optimizer.ts +74 -0
  253. package/src/pipeline/stages/prompt.ts +57 -0
  254. package/src/pipeline/stages/queue-check.ts +103 -0
  255. package/src/pipeline/stages/review.ts +181 -0
  256. package/src/pipeline/stages/routing.ts +81 -0
  257. package/src/pipeline/stages/verify.ts +100 -0
  258. package/src/pipeline/types.ts +167 -0
  259. package/src/plugins/index.ts +31 -0
  260. package/src/plugins/loader.ts +287 -0
  261. package/src/plugins/registry.ts +168 -0
  262. package/src/plugins/types.ts +327 -0
  263. package/src/plugins/validator.ts +352 -0
  264. package/src/prd/index.ts +172 -0
  265. package/src/prd/types.ts +202 -0
  266. package/src/precheck/checks-blockers.ts +391 -0
  267. package/src/precheck/checks-warnings.ts +142 -0
  268. package/src/precheck/checks.ts +30 -0
  269. package/src/precheck/index.ts +247 -0
  270. package/src/precheck/story-size-gate.ts +144 -0
  271. package/src/precheck/types.ts +31 -0
  272. package/src/queue/index.ts +2 -0
  273. package/src/queue/manager.ts +254 -0
  274. package/src/queue/types.ts +54 -0
  275. package/src/review/index.ts +8 -0
  276. package/src/review/runner.ts +172 -0
  277. package/src/review/types.ts +66 -0
  278. package/src/routing/builder.ts +81 -0
  279. package/src/routing/chain.ts +74 -0
  280. package/src/routing/index.ts +16 -0
  281. package/src/routing/loader.ts +58 -0
  282. package/src/routing/router.ts +303 -0
  283. package/src/routing/strategies/adaptive.ts +215 -0
  284. package/src/routing/strategies/index.ts +8 -0
  285. package/src/routing/strategies/keyword.ts +163 -0
  286. package/src/routing/strategies/llm-prompts.ts +209 -0
  287. package/src/routing/strategies/llm.ts +235 -0
  288. package/src/routing/strategies/manual.ts +50 -0
  289. package/src/routing/strategy.ts +99 -0
  290. package/src/tdd/cleanup.ts +111 -0
  291. package/src/tdd/index.ts +23 -0
  292. package/src/tdd/isolation.ts +123 -0
  293. package/src/tdd/orchestrator.ts +383 -0
  294. package/src/tdd/prompts.ts +270 -0
  295. package/src/tdd/rectification-gate.ts +183 -0
  296. package/src/tdd/session-runner.ts +179 -0
  297. package/src/tdd/types.ts +81 -0
  298. package/src/tdd/verdict.ts +271 -0
  299. package/src/tui/App.tsx +265 -0
  300. package/src/tui/components/AgentPanel.tsx +75 -0
  301. package/src/tui/components/CostOverlay.tsx +118 -0
  302. package/src/tui/components/HelpOverlay.tsx +107 -0
  303. package/src/tui/components/StatusBar.tsx +63 -0
  304. package/src/tui/components/StoriesPanel.tsx +177 -0
  305. package/src/tui/hooks/useKeyboard.ts +142 -0
  306. package/src/tui/hooks/useLayout.ts +137 -0
  307. package/src/tui/hooks/usePipelineEvents.ts +183 -0
  308. package/src/tui/hooks/usePty.ts +194 -0
  309. package/src/tui/index.tsx +38 -0
  310. package/src/tui/types.ts +76 -0
  311. package/src/utils/git.ts +83 -0
  312. package/src/utils/queue-writer.ts +54 -0
  313. package/src/verification/executor.ts +235 -0
  314. package/src/verification/gate.ts +207 -0
  315. package/src/verification/index.ts +12 -0
  316. package/src/verification/parser.ts +230 -0
  317. package/src/verification/rectification.ts +108 -0
  318. package/src/verification/types.ts +113 -0
  319. package/src/worktree/dispatcher.ts +65 -0
  320. package/src/worktree/index.ts +2 -0
  321. package/src/worktree/manager.ts +187 -0
  322. package/src/worktree/merge.ts +301 -0
  323. package/src/worktree/types.ts +4 -0
  324. package/test/TEST_COVERAGE_US001.md +217 -0
  325. package/test/TEST_COVERAGE_US003.md +84 -0
  326. package/test/TEST_COVERAGE_US005.md +86 -0
  327. package/test/US-002-orchestrator.test.ts +246 -0
  328. package/test/acceptance/cm-003-default-view.test.ts +194 -0
  329. package/test/execution/pid-registry.test.ts +240 -0
  330. package/test/execution/post-verify.test.ts +224 -0
  331. package/test/helpers/timeout.ts +42 -0
  332. package/test/integration/US-002-TEST-SUMMARY.md +107 -0
  333. package/test/integration/US-003-TEST-SUMMARY.md +149 -0
  334. package/test/integration/US-004-TEST-SUMMARY.md +106 -0
  335. package/test/integration/US-005-TEST-SUMMARY.md +138 -0
  336. package/test/integration/US-007-TEST-SUMMARY.md +100 -0
  337. package/test/integration/agent-validation.test.ts +439 -0
  338. package/test/integration/analyze-integration.test.ts +261 -0
  339. package/test/integration/analyze-scanner.test.ts +131 -0
  340. package/test/integration/cli-config-default-edge-cases.test.ts +222 -0
  341. package/test/integration/cli-config-default-view.test.ts +229 -0
  342. package/test/integration/cli-config-diff.test.ts +460 -0
  343. package/test/integration/cli-config.test.ts +736 -0
  344. package/test/integration/cli-diagnose.test.ts +592 -0
  345. package/test/integration/cli-logs.test.ts +314 -0
  346. package/test/integration/cli-plugins.test.ts +678 -0
  347. package/test/integration/cli-precheck.test.ts +371 -0
  348. package/test/integration/cli-run-headless.test.ts +173 -0
  349. package/test/integration/cli.test.ts +75 -0
  350. package/test/integration/config/merger.test.ts +465 -0
  351. package/test/integration/config/paths.test.ts +51 -0
  352. package/test/integration/config-loader.test.ts +265 -0
  353. package/test/integration/config.test.ts +444 -0
  354. package/test/integration/context-integration.test.ts +702 -0
  355. package/test/integration/context-provider-injection.test.ts +506 -0
  356. package/test/integration/context-verification-integration.test.ts +295 -0
  357. package/test/integration/e2e.test.ts +896 -0
  358. package/test/integration/execution.test.ts +625 -0
  359. package/test/integration/helpers.test.ts +295 -0
  360. package/test/integration/hooks.test.ts +361 -0
  361. package/test/integration/interaction-chain-pipeline.test.ts +464 -0
  362. package/test/integration/isolation.test.ts +143 -0
  363. package/test/integration/logger.test.ts +461 -0
  364. package/test/integration/parallel.test.ts +250 -0
  365. package/test/integration/path-security.test.ts +173 -0
  366. package/test/integration/pipeline-acceptance.test.ts +302 -0
  367. package/test/integration/pipeline-events.test.ts +475 -0
  368. package/test/integration/pipeline.test.ts +658 -0
  369. package/test/integration/plan.test.ts +157 -0
  370. package/test/integration/plugin-routing.test.ts +921 -0
  371. package/test/integration/plugins/config-integration.test.ts +172 -0
  372. package/test/integration/plugins/config-resolution.test.ts +522 -0
  373. package/test/integration/plugins/loader.test.ts +641 -0
  374. package/test/integration/plugins/registry.test.ts +746 -0
  375. package/test/integration/plugins/validator.test.ts +563 -0
  376. package/test/integration/prd-pause.test.ts +205 -0
  377. package/test/integration/prd-resolvers.test.ts +185 -0
  378. package/test/integration/precheck-integration.test.ts +468 -0
  379. package/test/integration/precheck.test.ts +805 -0
  380. package/test/integration/progress.test.ts +34 -0
  381. package/test/integration/rectification-flow.test.ts +512 -0
  382. package/test/integration/reporter-lifecycle.test.ts +860 -0
  383. package/test/integration/review-config-commands.test.ts +319 -0
  384. package/test/integration/review-config-schema.test.ts +116 -0
  385. package/test/integration/review-plugin-integration.test.ts +722 -0
  386. package/test/integration/review.test.ts +149 -0
  387. package/test/integration/routing-stage-bug-021.test.ts +274 -0
  388. package/test/integration/routing-stage-greenfield.test.ts +286 -0
  389. package/test/integration/runner-config-plugins.test.ts +461 -0
  390. package/test/integration/runner-fixes.test.ts +399 -0
  391. package/test/integration/runner-plugin-integration.test.ts +543 -0
  392. package/test/integration/runner.test.ts +1679 -0
  393. package/test/integration/s5-greenfield-fallback.test.ts +297 -0
  394. package/test/integration/status-file-integration.test.ts +325 -0
  395. package/test/integration/status-file.test.ts +379 -0
  396. package/test/integration/status-writer.test.ts +345 -0
  397. package/test/integration/story-id-in-events.test.ts +273 -0
  398. package/test/integration/tdd-cleanup.test.ts +246 -0
  399. package/test/integration/tdd-orchestrator.test.ts +1762 -0
  400. package/test/integration/test-scanner.test.ts +403 -0
  401. package/test/integration/verification-asset-check.test.ts +142 -0
  402. package/test/integration/verify-stage.test.ts +275 -0
  403. package/test/integration/worktree/manager.test.ts +218 -0
  404. package/test/integration/worktree/merge.test.ts +341 -0
  405. package/test/manual/logging-formatter-demo.ts +158 -0
  406. package/test/ui/tui-agent-panel.test.tsx +99 -0
  407. package/test/ui/tui-controls.test.ts +334 -0
  408. package/test/ui/tui-cost-and-pty.test.ts +189 -0
  409. package/test/ui/tui-layout.test.ts +378 -0
  410. package/test/ui/tui-pty-integration.test.tsx +159 -0
  411. package/test/ui/tui-stories.test.ts +332 -0
  412. package/test/unit/acceptance.test.ts +186 -0
  413. package/test/unit/agent-stderr-capture.test.ts +146 -0
  414. package/test/unit/analyze-classifier.test.ts +215 -0
  415. package/test/unit/analyze.test.ts +224 -0
  416. package/test/unit/auto-detect.test.ts +249 -0
  417. package/test/unit/cli-status.test.ts +417 -0
  418. package/test/unit/commands/common.test.ts +320 -0
  419. package/test/unit/commands/logs.test.ts +416 -0
  420. package/test/unit/commands/unlock.test.ts +319 -0
  421. package/test/unit/constitution-generators.test.ts +160 -0
  422. package/test/unit/constitution.test.ts +209 -0
  423. package/test/unit/context.test.ts +1722 -0
  424. package/test/unit/cost.test.ts +231 -0
  425. package/test/unit/crash-recovery.test.ts +308 -0
  426. package/test/unit/escalation.test.ts +126 -0
  427. package/test/unit/execution-logging-stderr.test.ts +156 -0
  428. package/test/unit/execution-stage.test.ts +122 -0
  429. package/test/unit/fix-generator.test.ts +275 -0
  430. package/test/unit/formatters.test.ts +469 -0
  431. package/test/unit/greenfield.test.ts +179 -0
  432. package/test/unit/helpers.test.ts +317 -0
  433. package/test/unit/interaction/human-review-trigger.test.ts +164 -0
  434. package/test/unit/interaction-network-failures.test.ts +389 -0
  435. package/test/unit/interaction-plugins.test.ts +164 -0
  436. package/test/unit/isolation.test.ts +134 -0
  437. package/test/unit/logging/formatter.test.ts +455 -0
  438. package/test/unit/merge.test.ts +268 -0
  439. package/test/unit/metrics.test.ts +276 -0
  440. package/test/unit/optimizer/noop.optimizer.test.ts +125 -0
  441. package/test/unit/optimizer/rule-based.optimizer.test.ts +358 -0
  442. package/test/unit/prd-auto-default.test.ts +290 -0
  443. package/test/unit/prd-failure-category.test.ts +176 -0
  444. package/test/unit/prd-get-next-story.test.ts +186 -0
  445. package/test/unit/precheck-checks.test.ts +840 -0
  446. package/test/unit/precheck-story-size-gate.test.ts +287 -0
  447. package/test/unit/precheck-types.test.ts +142 -0
  448. package/test/unit/prompts.test.ts +475 -0
  449. package/test/unit/queue.test.ts +237 -0
  450. package/test/unit/rectification.test.ts +284 -0
  451. package/test/unit/registry.test.ts +287 -0
  452. package/test/unit/routing.test.ts +937 -0
  453. package/test/unit/run-lifecycle.test.ts +140 -0
  454. package/test/unit/storyid-events.test.ts +224 -0
  455. package/test/unit/tdd-verdict.test.ts +492 -0
  456. package/test/unit/test-output-parser.test.ts +377 -0
  457. package/test/unit/verdict.test.ts +324 -0
  458. package/test/unit/worktree-manager.test.ts +158 -0
  459. package/tsconfig.json +27 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * LLM Routing Prompts & Parsers
3
+ *
4
+ * Extracted from llm.ts: prompt building, response parsing, and validation
5
+ * for LLM-based routing decisions.
6
+ */
7
+
8
+ import type { Complexity, ModelTier, NaxConfig, TestStrategy } from "../../config";
9
+ import type { UserStory } from "../../prd/types";
10
+ import type { RoutingDecision } from "../strategy";
11
+
12
+ /**
13
+ * Build the routing prompt for a single story.
14
+ *
15
+ * @param story - User story to route
16
+ * @param config - nax configuration
17
+ * @returns Formatted prompt string
18
+ */
19
+ export function buildRoutingPrompt(story: UserStory, config: NaxConfig): string {
20
+ const { title, description, acceptanceCriteria, tags } = story;
21
+ const criteria = acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
22
+
23
+ return `You are a code task router. Given a user story, classify its complexity and select the appropriate execution strategy.
24
+
25
+ ## Story
26
+ Title: ${title}
27
+ Description: ${description}
28
+ Acceptance Criteria:
29
+ ${criteria}
30
+ Tags: ${tags.join(", ")}
31
+
32
+ ## Available Tiers
33
+ - fast: Simple changes, typos, config updates, boilerplate. <30 min of coding.
34
+ - balanced: Standard features, moderate logic, straightforward tests. 30-90 min.
35
+ - powerful: Complex architecture, security-critical, multi-file refactors, novel algorithms. >90 min.
36
+
37
+ ## Available Test Strategies
38
+ - test-after: Write implementation first, add tests after. For straightforward work.
39
+ - three-session-tdd: Separate test-writer → implementer → verifier sessions. For complex/critical work where test design matters.
40
+
41
+ ## Rules
42
+ - Default to the CHEAPEST option that will succeed.
43
+ - three-session-tdd ONLY when: (a) security/auth logic, (b) complex algorithms, (c) public API contracts that consumers depend on.
44
+ - Simple barrel exports, re-exports, or index files are ALWAYS test-after + fast, regardless of keywords.
45
+ - A story touching many files doesn't automatically mean complex — copy-paste refactors are simple.
46
+
47
+ Respond with ONLY this JSON (no markdown, no explanation):
48
+ {"complexity":"simple|medium|complex|expert","modelTier":"fast|balanced|powerful","testStrategy":"test-after|three-session-tdd","reasoning":"<one line>"}`;
49
+ }
50
+
51
+ /**
52
+ * Build batch routing prompt for multiple stories.
53
+ *
54
+ * @param stories - Array of user stories to route
55
+ * @param config - nax configuration
56
+ * @returns Formatted batch prompt string
57
+ */
58
+ export function buildBatchPrompt(stories: UserStory[], config: NaxConfig): string {
59
+ const storyBlocks = stories
60
+ .map((story, idx) => {
61
+ const criteria = story.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
62
+ return `${idx + 1}. ${story.id}: ${story.title}
63
+ Description: ${story.description}
64
+ Acceptance Criteria:
65
+ ${criteria}
66
+ Tags: ${story.tags.join(", ")}`;
67
+ })
68
+ .join("\n\n");
69
+
70
+ return `You are a code task router. Given multiple user stories, classify each story's complexity and select the appropriate execution strategy.
71
+
72
+ ## Stories
73
+ ${storyBlocks}
74
+
75
+ ## Available Tiers
76
+ - fast: Simple changes, typos, config updates, boilerplate. <30 min of coding.
77
+ - balanced: Standard features, moderate logic, straightforward tests. 30-90 min.
78
+ - powerful: Complex architecture, security-critical, multi-file refactors, novel algorithms. >90 min.
79
+
80
+ ## Available Test Strategies
81
+ - test-after: Write implementation first, add tests after. For straightforward work.
82
+ - three-session-tdd: Separate test-writer → implementer → verifier sessions. For complex/critical work where test design matters.
83
+
84
+ ## Rules
85
+ - Default to the CHEAPEST option that will succeed.
86
+ - three-session-tdd ONLY when: (a) security/auth logic, (b) complex algorithms, (c) public API contracts that consumers depend on.
87
+ - Simple barrel exports, re-exports, or index files are ALWAYS test-after + fast, regardless of keywords.
88
+ - A story touching many files doesn't automatically mean complex — copy-paste refactors are simple.
89
+
90
+ Respond with ONLY a JSON array (no markdown, no explanation):
91
+ [{"id":"US-001","complexity":"simple|medium|complex|expert","modelTier":"fast|balanced|powerful","testStrategy":"test-after|three-session-tdd","reasoning":"<one line>"}]`;
92
+ }
93
+
94
+ /**
95
+ * Validate a parsed routing object and return a clean RoutingDecision.
96
+ *
97
+ * @param parsed - Parsed JSON object with routing fields
98
+ * @param config - nax configuration (for modelTier validation)
99
+ * @returns Validated routing decision
100
+ * @throws Error if validation fails
101
+ */
102
+ export function validateRoutingDecision(parsed: Record<string, unknown>, config: NaxConfig): RoutingDecision {
103
+ // Validate required fields
104
+ if (!parsed.complexity || !parsed.modelTier || !parsed.testStrategy || !parsed.reasoning) {
105
+ throw new Error(`Missing required fields in LLM response: ${JSON.stringify(parsed)}`);
106
+ }
107
+
108
+ // Validate field values
109
+ const validComplexities: Complexity[] = ["simple", "medium", "complex", "expert"];
110
+ const validTestStrategies: TestStrategy[] = ["test-after", "three-session-tdd"];
111
+
112
+ if (!validComplexities.includes(parsed.complexity as Complexity)) {
113
+ throw new Error(`Invalid complexity: ${parsed.complexity}`);
114
+ }
115
+
116
+ if (!validTestStrategies.includes(parsed.testStrategy as TestStrategy)) {
117
+ throw new Error(`Invalid testStrategy: ${parsed.testStrategy}`);
118
+ }
119
+
120
+ // Validate modelTier exists in config
121
+ if (!config.models[parsed.modelTier as string]) {
122
+ throw new Error(`Invalid modelTier: ${parsed.modelTier} (not in config.models)`);
123
+ }
124
+
125
+ return {
126
+ complexity: parsed.complexity as Complexity,
127
+ modelTier: parsed.modelTier as ModelTier,
128
+ testStrategy: parsed.testStrategy as TestStrategy,
129
+ reasoning: parsed.reasoning as string,
130
+ };
131
+ }
132
+
133
+ /** Strip markdown code fences from LLM output. */
134
+ export function stripCodeFences(text: string): string {
135
+ let result = text.trim();
136
+ if (result.startsWith("```")) {
137
+ const lines = result.split("\n");
138
+ result = lines.slice(1, -1).join("\n").trim();
139
+ }
140
+ if (result.startsWith("json")) {
141
+ result = result.slice(4).trim();
142
+ }
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * Parse and validate LLM routing response.
148
+ *
149
+ * @param output - Raw LLM output text
150
+ * @param story - User story being routed (for error context)
151
+ * @param config - nax configuration
152
+ * @returns Validated routing decision
153
+ * @throws Error if JSON parsing or validation fails
154
+ */
155
+ export function parseRoutingResponse(output: string, story: UserStory, config: NaxConfig): RoutingDecision {
156
+ const jsonText = stripCodeFences(output);
157
+ const parsed = JSON.parse(jsonText);
158
+ return validateRoutingDecision(parsed, config);
159
+ }
160
+
161
+ /**
162
+ * Parse batch LLM response into a map of decisions.
163
+ *
164
+ * @param output - Raw LLM output text (JSON array)
165
+ * @param stories - User stories being routed
166
+ * @param config - nax configuration
167
+ * @returns Map of story ID to routing decision
168
+ * @throws Error if JSON parsing or validation fails
169
+ */
170
+ export function parseBatchResponse(
171
+ output: string,
172
+ stories: UserStory[],
173
+ config: NaxConfig,
174
+ ): Map<string, RoutingDecision> {
175
+ // Strip markdown code blocks if present
176
+ let jsonText = output.trim();
177
+ if (jsonText.startsWith("```")) {
178
+ const lines = jsonText.split("\n");
179
+ jsonText = lines.slice(1, -1).join("\n").trim();
180
+ }
181
+ if (jsonText.startsWith("json")) {
182
+ jsonText = jsonText.slice(4).trim();
183
+ }
184
+
185
+ const parsed = JSON.parse(jsonText);
186
+
187
+ if (!Array.isArray(parsed)) {
188
+ throw new Error("Batch LLM response must be a JSON array");
189
+ }
190
+
191
+ const decisions = new Map<string, RoutingDecision>();
192
+
193
+ for (const entry of parsed) {
194
+ if (!entry.id) {
195
+ throw new Error("Batch entry missing 'id' field");
196
+ }
197
+
198
+ const story = stories.find((s) => s.id === entry.id);
199
+ if (!story) {
200
+ throw new Error(`Batch entry has unknown story ID: ${entry.id}`);
201
+ }
202
+
203
+ // Validate entry directly (no re-serialization needed)
204
+ const decision = validateRoutingDecision(entry, config);
205
+ decisions.set(entry.id, decision);
206
+ }
207
+
208
+ return decisions;
209
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * LLM-Based Routing Strategy
3
+ *
4
+ * Routes stories using an LLM to perform semantic analysis of story requirements.
5
+ * Falls back to keyword strategy on failure. Supports batch mode for efficiency.
6
+ */
7
+
8
+ import type { NaxConfig } from "../../config";
9
+ import { resolveModel } from "../../config";
10
+ import { getLogger } from "../../logger";
11
+ import type { UserStory } from "../../prd/types";
12
+ import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../strategy";
13
+ import { keywordStrategy } from "./keyword";
14
+ import { buildBatchPrompt, buildRoutingPrompt, parseBatchResponse, parseRoutingResponse } from "./llm-prompts";
15
+
16
+ // Re-export for backward compatibility
17
+ export {
18
+ buildRoutingPrompt,
19
+ buildBatchPrompt,
20
+ validateRoutingDecision,
21
+ stripCodeFences,
22
+ parseRoutingResponse,
23
+ } from "./llm-prompts";
24
+
25
+ /** Module-level cache for routing decisions (PERF-1 fix: max 100 entries LRU) */
26
+ const cachedDecisions = new Map<string, RoutingDecision>();
27
+ const MAX_CACHE_SIZE = 100;
28
+
29
+ /** Clear the routing cache (for testing or new runs) */
30
+ export function clearCache(): void {
31
+ cachedDecisions.clear();
32
+ }
33
+
34
+ /** Get the current cache size (for testing) */
35
+ export function getCacheSize(): number {
36
+ return cachedDecisions.size;
37
+ }
38
+
39
+ /** Evict oldest entry when cache is full (LRU) */
40
+ function evictOldest(): void {
41
+ const firstKey = cachedDecisions.keys().next().value;
42
+ if (firstKey !== undefined) {
43
+ cachedDecisions.delete(firstKey);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Call LLM via claude CLI with timeout.
49
+ *
50
+ * @param modelTier - Model tier to use for routing call
51
+ * @param prompt - Prompt to send to LLM
52
+ * @param config - nax configuration
53
+ * @returns LLM response text
54
+ * @throws Error on timeout or spawn failure
55
+ */
56
+ async function callLlm(modelTier: string, prompt: string, config: NaxConfig): Promise<string> {
57
+ const llmConfig = config.routing.llm;
58
+ const timeoutMs = llmConfig?.timeoutMs ?? 15000;
59
+
60
+ // Resolve model tier to actual model identifier
61
+ const modelEntry = config.models[modelTier];
62
+ if (!modelEntry) {
63
+ throw new Error(`Model tier "${modelTier}" not found in config.models`);
64
+ }
65
+
66
+ const modelDef = resolveModel(modelEntry);
67
+ const modelArg = modelDef.model;
68
+
69
+ // Spawn claude CLI with timeout
70
+ const proc = Bun.spawn(["claude", "-p", prompt, "--model", modelArg], {
71
+ stdout: "pipe",
72
+ stderr: "pipe",
73
+ });
74
+
75
+ // Race between completion and timeout, ensuring cleanup on either path
76
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
77
+
78
+ const timeoutPromise = new Promise<never>((_, reject) => {
79
+ timeoutId = setTimeout(() => {
80
+ reject(new Error(`LLM call timeout after ${timeoutMs}ms`));
81
+ }, timeoutMs);
82
+ });
83
+
84
+ const outputPromise = (async () => {
85
+ const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
86
+
87
+ const exitCode = await proc.exited;
88
+ if (exitCode !== 0) {
89
+ throw new Error(`claude CLI failed with exit code ${exitCode}: ${stderr}`);
90
+ }
91
+
92
+ return stdout.trim();
93
+ })();
94
+
95
+ try {
96
+ const result = await Promise.race([outputPromise, timeoutPromise]);
97
+ clearTimeout(timeoutId);
98
+ return result;
99
+ } catch (err) {
100
+ clearTimeout(timeoutId);
101
+ proc.kill();
102
+ throw err;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Route multiple stories in a single batch LLM call.
108
+ *
109
+ * This function pre-populates the cache with routing decisions for all stories.
110
+ * Individual route() calls will then hit the cache.
111
+ *
112
+ * @param stories - Array of user stories to route
113
+ * @param context - Routing context
114
+ * @returns Map of story ID to routing decision
115
+ */
116
+ export async function routeBatch(stories: UserStory[], context: RoutingContext): Promise<Map<string, RoutingDecision>> {
117
+ const config = context.config;
118
+ const llmConfig = config.routing.llm;
119
+
120
+ if (!llmConfig) {
121
+ throw new Error("LLM routing config not found");
122
+ }
123
+
124
+ const modelTier = llmConfig.model ?? "fast";
125
+ const prompt = buildBatchPrompt(stories, config);
126
+
127
+ try {
128
+ const output = await callLlm(modelTier, prompt, config);
129
+ const decisions = parseBatchResponse(output, stories, config);
130
+
131
+ // Populate cache (PERF-1 fix: evict oldest if full)
132
+ if (llmConfig.cacheDecisions) {
133
+ for (const [storyId, decision] of decisions.entries()) {
134
+ if (cachedDecisions.size >= MAX_CACHE_SIZE) {
135
+ evictOldest();
136
+ }
137
+ cachedDecisions.set(storyId, decision);
138
+ }
139
+ }
140
+
141
+ return decisions;
142
+ } catch (err) {
143
+ throw new Error(`Batch LLM routing failed: ${(err as Error).message}`);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * LLM-based routing strategy.
149
+ *
150
+ * This strategy:
151
+ * - Checks cache first (if enabled)
152
+ * - Calls LLM with story context to classify complexity
153
+ * - Parses structured JSON response
154
+ * - Maps complexity to model tier and test strategy
155
+ * - Falls back to null (keyword fallback) on any failure
156
+ */
157
+ export const llmStrategy: RoutingStrategy = {
158
+ name: "llm",
159
+
160
+ async route(story: UserStory, context: RoutingContext): Promise<RoutingDecision | null> {
161
+ const config = context.config;
162
+ const llmConfig = config.routing.llm;
163
+
164
+ if (!llmConfig) {
165
+ return null; // LLM routing not configured
166
+ }
167
+
168
+ const mode = llmConfig.mode ?? "hybrid";
169
+
170
+ // Check cache first
171
+ if (llmConfig.cacheDecisions && cachedDecisions.has(story.id)) {
172
+ const cached = cachedDecisions.get(story.id);
173
+ if (!cached) {
174
+ throw new Error(`Cached decision not found for story: ${story.id}`);
175
+ }
176
+ const logger = getLogger();
177
+ logger.debug("routing", "LLM cache hit", {
178
+ storyId: story.id,
179
+ complexity: cached.complexity,
180
+ modelTier: cached.modelTier,
181
+ testStrategy: cached.testStrategy,
182
+ });
183
+ return cached;
184
+ }
185
+
186
+ // One-shot mode: cache miss -> keyword fallback without new LLM call
187
+ if (mode === "one-shot") {
188
+ const logger = getLogger();
189
+ logger.info("routing", "One-shot mode cache miss, falling back to keyword", {
190
+ storyId: story.id,
191
+ });
192
+ return keywordStrategy.route(story, context);
193
+ }
194
+
195
+ try {
196
+ const modelTier = llmConfig.model ?? "fast";
197
+ const prompt = buildRoutingPrompt(story, config);
198
+ const output = await callLlm(modelTier, prompt, config);
199
+ const decision = parseRoutingResponse(output, story, config);
200
+
201
+ // Cache decision (PERF-1 fix: evict oldest if full)
202
+ if (llmConfig.cacheDecisions) {
203
+ if (cachedDecisions.size >= MAX_CACHE_SIZE) {
204
+ evictOldest();
205
+ }
206
+ cachedDecisions.set(story.id, decision);
207
+ }
208
+
209
+ // Log decision
210
+ const logger = getLogger();
211
+ logger.info("routing", "LLM classified story", {
212
+ storyId: story.id,
213
+ complexity: decision.complexity,
214
+ modelTier: decision.modelTier,
215
+ testStrategy: decision.testStrategy,
216
+ reasoning: decision.reasoning,
217
+ });
218
+
219
+ return decision;
220
+ } catch (err) {
221
+ const logger = getLogger();
222
+ const errorMsg = (err as Error).message;
223
+ logger.warn("routing", "LLM routing failed", { storyId: story.id, error: errorMsg });
224
+
225
+ // Fall back to keyword strategy if configured
226
+ if (llmConfig.fallbackToKeywords) {
227
+ logger.info("routing", "Falling back to keyword strategy", { storyId: story.id });
228
+ return null; // Delegate to next strategy (keyword)
229
+ }
230
+
231
+ // Re-throw if no fallback
232
+ throw err;
233
+ }
234
+ },
235
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Manual Routing Strategy
3
+ *
4
+ * Reads routing decision from story.routing metadata in prd.json.
5
+ * Users can manually specify complexity, modelTier, and testStrategy per story.
6
+ */
7
+
8
+ import type { UserStory } from "../../prd/types";
9
+ import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../strategy";
10
+
11
+ /**
12
+ * Manual routing strategy.
13
+ *
14
+ * If story.routing is present in prd.json, uses that data directly.
15
+ * Otherwise returns null (delegates to next strategy).
16
+ *
17
+ * Use case: Override routing for specific stories that need manual control.
18
+ *
19
+ * @example
20
+ * ```json
21
+ * {
22
+ * "id": "US-001",
23
+ * "title": "Critical database migration",
24
+ * "routing": {
25
+ * "complexity": "expert",
26
+ * "modelTier": "powerful",
27
+ * "testStrategy": "three-session-tdd",
28
+ * "reasoning": "Manually specified: critical migration"
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export const manualStrategy: RoutingStrategy = {
34
+ name: "manual",
35
+
36
+ route(story: UserStory, _context: RoutingContext): RoutingDecision | null {
37
+ // If story has routing metadata with all required fields, use it
38
+ if (story.routing?.complexity && story.routing.modelTier && story.routing.testStrategy) {
39
+ return {
40
+ complexity: story.routing.complexity,
41
+ modelTier: story.routing.modelTier,
42
+ testStrategy: story.routing.testStrategy,
43
+ reasoning: story.routing.reasoning || "Manual routing from prd.json",
44
+ };
45
+ }
46
+
47
+ // No manual routing specified, delegate to next strategy
48
+ return null;
49
+ },
50
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Routing Strategy Interface
3
+ *
4
+ * Pluggable routing system that allows custom model tier selection logic.
5
+ * Strategies can return null to delegate to the next strategy in the chain.
6
+ */
7
+
8
+ import type { Complexity, ModelTier, NaxConfig, TestStrategy } from "../config";
9
+ import type { UserStory } from "../prd/types";
10
+
11
+ /** Aggregate metrics (v0.5 Phase 1 — not yet implemented) */
12
+ export interface AggregateMetrics {
13
+ totalRuns: number;
14
+ totalCost: number;
15
+ totalStories: number;
16
+ firstPassRate: number;
17
+ escalationRate: number;
18
+ avgCostPerStory: number;
19
+ avgCostPerFeature: number;
20
+ modelEfficiency: Record<
21
+ string,
22
+ {
23
+ attempts: number;
24
+ successes: number;
25
+ passRate: number;
26
+ avgCost: number;
27
+ totalCost: number;
28
+ }
29
+ >;
30
+ complexityAccuracy: Record<
31
+ string,
32
+ {
33
+ predicted: number;
34
+ actualTierUsed: string;
35
+ mismatchRate: number;
36
+ }
37
+ >;
38
+ }
39
+
40
+ /** Context passed to routing strategies */
41
+ export interface RoutingContext {
42
+ /** Full configuration */
43
+ config: NaxConfig;
44
+ /** Optional codebase context summary */
45
+ codebaseContext?: string;
46
+ /** Optional historical metrics (v0.5 Phase 1) */
47
+ metrics?: AggregateMetrics;
48
+ }
49
+
50
+ /** Routing decision returned by strategies */
51
+ export interface RoutingDecision {
52
+ /** Classified complexity */
53
+ complexity: Complexity;
54
+ /** Model tier to use */
55
+ modelTier: ModelTier;
56
+ /** Test strategy */
57
+ testStrategy: TestStrategy;
58
+ /** Reasoning for the decision */
59
+ reasoning: string;
60
+ }
61
+
62
+ /**
63
+ * Routing strategy interface.
64
+ *
65
+ * Strategies can return:
66
+ * - A RoutingDecision if they can route the story
67
+ * - null to delegate to the next strategy in the chain
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const myStrategy: RoutingStrategy = {
72
+ * name: "domain-specific",
73
+ * route(story, context) {
74
+ * if (story.tags.includes("migration")) {
75
+ * return {
76
+ * complexity: "expert",
77
+ * modelTier: "powerful",
78
+ * testStrategy: "three-session-tdd",
79
+ * reasoning: "Database migrations require expert model",
80
+ * };
81
+ * }
82
+ * return null; // Delegate to next strategy
83
+ * },
84
+ * };
85
+ * ```
86
+ */
87
+ export interface RoutingStrategy {
88
+ /** Strategy name (for logging) */
89
+ readonly name: string;
90
+
91
+ /**
92
+ * Route a user story.
93
+ *
94
+ * @param story - The user story to route
95
+ * @param context - Routing context (config, metrics, codebase)
96
+ * @returns RoutingDecision if strategy can route, null to delegate (sync or async)
97
+ */
98
+ route(story: UserStory, context: RoutingContext): RoutingDecision | null | Promise<RoutingDecision | null>;
99
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Process tree cleanup utilities for TDD session management.
3
+ *
4
+ * Handles cleanup of orphaned child processes when agent sessions fail.
5
+ * Prevents zombie processes from consuming CPU after agent crashes.
6
+ */
7
+
8
+ import { getLogger } from "../logger";
9
+
10
+ /**
11
+ * Get process group ID (PGID) for a given process ID.
12
+ *
13
+ * @param pid - Process ID to get PGID for
14
+ * @returns PGID if found, null if process doesn't exist or has no PGID
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const pgid = await getPgid(12345);
19
+ * if (pgid) {
20
+ * console.log(`Process 12345 belongs to group ${pgid}`);
21
+ * }
22
+ * ```
23
+ */
24
+ export async function getPgid(pid: number): Promise<number | null> {
25
+ try {
26
+ // Use ps to get PGID for the process
27
+ const proc = Bun.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
28
+ stdout: "pipe",
29
+ stderr: "pipe",
30
+ });
31
+
32
+ const exitCode = await proc.exited;
33
+ if (exitCode !== 0) {
34
+ return null;
35
+ }
36
+
37
+ const output = await new Response(proc.stdout).text();
38
+ const pgid = Number.parseInt(output.trim(), 10);
39
+
40
+ return Number.isNaN(pgid) ? null : pgid;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Clean up an entire process tree by killing all processes in the process group.
48
+ *
49
+ * Uses SIGTERM first (graceful shutdown), then SIGKILL after a delay if processes persist.
50
+ * Handles the case where the process is already dead gracefully.
51
+ *
52
+ * @param pid - Root process ID whose process group should be cleaned up
53
+ * @param gracePeriodMs - Time to wait between SIGTERM and SIGKILL (default: 3000ms)
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * // After agent session fails
58
+ * if (!result.success && result.pid) {
59
+ * await cleanupProcessTree(result.pid);
60
+ * }
61
+ * ```
62
+ */
63
+ export async function cleanupProcessTree(pid: number, gracePeriodMs = 3000): Promise<void> {
64
+ try {
65
+ // Get the process group ID
66
+ const pgid = await getPgid(pid);
67
+
68
+ if (!pgid) {
69
+ // Process already dead or has no PGID — nothing to clean up
70
+ return;
71
+ }
72
+
73
+ // Send SIGTERM to all processes in the group (negative PGID)
74
+ try {
75
+ process.kill(-pgid, "SIGTERM");
76
+ } catch (error) {
77
+ // ESRCH means no such process — already dead
78
+ const err = error as NodeJS.ErrnoException;
79
+ if (err.code !== "ESRCH") {
80
+ throw error;
81
+ }
82
+ return;
83
+ }
84
+
85
+ // Wait for graceful shutdown
86
+ await Bun.sleep(gracePeriodMs);
87
+
88
+ // Re-check PGID before SIGKILL to prevent race condition
89
+ // If the original process exited and a new process inherited its PID,
90
+ // we don't want to kill the wrong process group
91
+ const pgidAfterWait = await getPgid(pid);
92
+
93
+ // Only send SIGKILL if:
94
+ // 1. Process still exists (pgidAfterWait is not null)
95
+ // 2. PGID hasn't changed (still the same process group)
96
+ if (pgidAfterWait && pgidAfterWait === pgid) {
97
+ try {
98
+ process.kill(-pgid, "SIGKILL");
99
+ } catch {
100
+ // Ignore errors — processes may have exited during the wait
101
+ }
102
+ }
103
+ } catch (error) {
104
+ // Log but don't throw — cleanup is best-effort
105
+ const logger = getLogger();
106
+ logger.warn("tdd", "Failed to cleanup process tree", {
107
+ pid,
108
+ error: (error as Error).message,
109
+ });
110
+ }
111
+ }