@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,279 @@
1
+ /**
2
+ * Project Metadata Auto-Injector (v0.16.1)
3
+ *
4
+ * Detects project language/stack and injects metadata into agent configs.
5
+ * Supports: Node.js/Bun (package.json), Go (go.mod), Rust (Cargo.toml),
6
+ * Python (pyproject.toml / requirements.txt), PHP (composer.json),
7
+ * Ruby (Gemfile), Java/Kotlin (pom.xml / build.gradle).
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import type { NaxConfig } from "../config";
13
+ import type { ProjectMetadata } from "./types";
14
+
15
+ /** Notable Node.js dependency keywords */
16
+ const NOTABLE_NODE_DEPS = [
17
+ "@nestjs",
18
+ "express",
19
+ "fastify",
20
+ "koa",
21
+ "hono",
22
+ "next",
23
+ "nuxt",
24
+ "react",
25
+ "vue",
26
+ "svelte",
27
+ "solid",
28
+ "prisma",
29
+ "typeorm",
30
+ "mongoose",
31
+ "drizzle",
32
+ "sequelize",
33
+ "jest",
34
+ "vitest",
35
+ "mocha",
36
+ "bun",
37
+ "zod",
38
+ "typescript",
39
+ "graphql",
40
+ "trpc",
41
+ "bull",
42
+ "ioredis",
43
+ ];
44
+
45
+ // ─── Language detectors ──────────────────────────────────────────────────────
46
+
47
+ /** Node.js / Bun: read package.json */
48
+ async function detectNode(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
49
+ const pkgPath = join(workdir, "package.json");
50
+ if (!existsSync(pkgPath)) return null;
51
+
52
+ try {
53
+ const file = Bun.file(pkgPath);
54
+ const pkg = await file.json();
55
+ const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
56
+ const notable = [
57
+ ...new Set(
58
+ Object.keys(allDeps).filter((dep) =>
59
+ NOTABLE_NODE_DEPS.some((kw) => dep === kw || dep.startsWith(`${kw}/`) || dep.includes(kw)),
60
+ ),
61
+ ),
62
+ ].slice(0, 10);
63
+ const lang = pkg.devDependencies?.typescript || pkg.dependencies?.typescript ? "TypeScript" : "JavaScript";
64
+ return { name: pkg.name, lang, dependencies: notable };
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /** Go: read go.mod for module name + direct dependencies */
71
+ function detectGo(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
72
+ const goMod = join(workdir, "go.mod");
73
+ if (!existsSync(goMod)) return null;
74
+
75
+ try {
76
+ const content = readFileSync(goMod, "utf8");
77
+ const moduleMatch = content.match(/^module\s+(\S+)/m);
78
+ const name = moduleMatch?.[1];
79
+
80
+ // Extract require block entries (direct deps, not indirect)
81
+ const requires: string[] = [];
82
+ const requireBlock = content.match(/require\s*\(([^)]+)\)/s)?.[1] ?? "";
83
+ for (const line of requireBlock.split("\n")) {
84
+ const trimmed = line.trim();
85
+ if (trimmed && !trimmed.startsWith("//") && !trimmed.includes("// indirect")) {
86
+ const dep = trimmed.split(/\s+/)[0];
87
+ if (dep) requires.push(dep.split("/").slice(-1)[0]); // last segment only
88
+ }
89
+ }
90
+
91
+ return { name, lang: "Go", dependencies: requires.slice(0, 10) };
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /** Rust: read Cargo.toml for package name + dependencies */
98
+ function detectRust(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
99
+ const cargoPath = join(workdir, "Cargo.toml");
100
+ if (!existsSync(cargoPath)) return null;
101
+
102
+ try {
103
+ const content = readFileSync(cargoPath, "utf8");
104
+ const nameMatch = content.match(/^\[package\][^[]*name\s*=\s*"([^"]+)"/ms);
105
+ const name = nameMatch?.[1];
106
+
107
+ // Extract [dependencies] section keys
108
+ const depsSection = content.match(/^\[dependencies\]([^[]*)/ms)?.[1] ?? "";
109
+ const deps = depsSection
110
+ .split("\n")
111
+ .map((l) => l.split("=")[0].trim())
112
+ .filter((l) => l && !l.startsWith("#"))
113
+ .slice(0, 10);
114
+
115
+ return { name, lang: "Rust", dependencies: deps };
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /** Python: read pyproject.toml or requirements.txt */
122
+ function detectPython(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
123
+ const pyproject = join(workdir, "pyproject.toml");
124
+ const requirements = join(workdir, "requirements.txt");
125
+
126
+ if (!existsSync(pyproject) && !existsSync(requirements)) return null;
127
+
128
+ try {
129
+ if (existsSync(pyproject)) {
130
+ const content = readFileSync(pyproject, "utf8");
131
+ const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
132
+ const depsSection = content.match(/^\[project\][^[]*dependencies\s*=\s*\[([^\]]*)\]/ms)?.[1] ?? "";
133
+ const deps = depsSection
134
+ .split(",")
135
+ .map((d) => d.trim().replace(/["'\s>=<!^~].*/g, ""))
136
+ .filter(Boolean)
137
+ .slice(0, 10);
138
+ return { name: nameMatch?.[1], lang: "Python", dependencies: deps };
139
+ }
140
+
141
+ // Fallback: requirements.txt
142
+ const lines = readFileSync(requirements, "utf8")
143
+ .split("\n")
144
+ .map((l) => l.split(/[>=<!]/)[0].trim())
145
+ .filter((l) => l && !l.startsWith("#"))
146
+ .slice(0, 10);
147
+ return { lang: "Python", dependencies: lines };
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /** PHP: read composer.json */
154
+ async function detectPhp(workdir: string): Promise<{ name?: string; lang: string; dependencies: string[] } | null> {
155
+ const composerPath = join(workdir, "composer.json");
156
+ if (!existsSync(composerPath)) return null;
157
+
158
+ try {
159
+ const file = Bun.file(composerPath);
160
+ const composer = await file.json();
161
+ const deps = Object.keys({ ...(composer.require ?? {}), ...(composer["require-dev"] ?? {}) })
162
+ .filter((d) => d !== "php")
163
+ .map((d) => d.split("/").pop() ?? d)
164
+ .slice(0, 10);
165
+ return { name: composer.name, lang: "PHP", dependencies: deps };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ /** Ruby: read Gemfile */
172
+ function detectRuby(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
173
+ const gemfile = join(workdir, "Gemfile");
174
+ if (!existsSync(gemfile)) return null;
175
+
176
+ try {
177
+ const content = readFileSync(gemfile, "utf8");
178
+ const gems = [...content.matchAll(/^\s*gem\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]).slice(0, 10);
179
+ return { lang: "Ruby", dependencies: gems };
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ /** Java/Kotlin: detect from pom.xml or build.gradle */
186
+ function detectJvm(workdir: string): { name?: string; lang: string; dependencies: string[] } | null {
187
+ const pom = join(workdir, "pom.xml");
188
+ const gradle = join(workdir, "build.gradle");
189
+ const gradleKts = join(workdir, "build.gradle.kts");
190
+
191
+ if (!existsSync(pom) && !existsSync(gradle) && !existsSync(gradleKts)) return null;
192
+
193
+ try {
194
+ if (existsSync(pom)) {
195
+ const content = readFileSync(pom, "utf8");
196
+ const nameMatch = content.match(/<artifactId>([^<]+)<\/artifactId>/);
197
+ const deps = [...content.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)]
198
+ .map((m) => m[1])
199
+ .filter((d) => d !== nameMatch?.[1])
200
+ .slice(0, 10);
201
+ const lang = existsSync(join(workdir, "src/main/kotlin")) ? "Kotlin" : "Java";
202
+ return { name: nameMatch?.[1], lang, dependencies: deps };
203
+ }
204
+
205
+ const gradleFile = existsSync(gradleKts) ? gradleKts : gradle;
206
+ const content = readFileSync(gradleFile, "utf8");
207
+ const lang = gradleFile.endsWith(".kts") ? "Kotlin" : "Java";
208
+ const deps = [...content.matchAll(/implementation[^'"]*['"]([^:'"]+:[^:'"]+)[^'"]*['"]/g)]
209
+ .map((m) => m[1].split(":").pop() ?? m[1])
210
+ .slice(0, 10);
211
+ return { lang, dependencies: deps };
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ // ─── Main ────────────────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * Detect project language and build metadata.
221
+ * Runs all detectors; first match wins (Node checked last to avoid false positives in polyglot repos).
222
+ */
223
+ export async function buildProjectMetadata(workdir: string, config: NaxConfig): Promise<ProjectMetadata> {
224
+ // Priority: Go > Rust > Python > PHP > Ruby > JVM > Node
225
+ const detected =
226
+ detectGo(workdir) ??
227
+ detectRust(workdir) ??
228
+ detectPython(workdir) ??
229
+ (await detectPhp(workdir)) ??
230
+ detectRuby(workdir) ??
231
+ detectJvm(workdir) ??
232
+ (await detectNode(workdir));
233
+
234
+ return {
235
+ name: detected?.name,
236
+ language: detected?.lang,
237
+ dependencies: detected?.dependencies ?? [],
238
+ testCommand: config.execution?.testCommand ?? undefined,
239
+ lintCommand: config.execution?.lintCommand ?? undefined,
240
+ typecheckCommand: config.execution?.typecheckCommand ?? undefined,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Format metadata as a markdown section for injection into agent configs.
246
+ */
247
+ export function formatMetadataSection(metadata: ProjectMetadata): string {
248
+ const lines: string[] = ["## Project Metadata", "", "> Auto-injected by `nax generate`", ""];
249
+
250
+ if (metadata.name) {
251
+ lines.push(`**Project:** \`${metadata.name}\``);
252
+ lines.push("");
253
+ }
254
+
255
+ if (metadata.language) {
256
+ lines.push(`**Language:** ${metadata.language}`);
257
+ lines.push("");
258
+ }
259
+
260
+ if (metadata.dependencies.length > 0) {
261
+ lines.push(`**Key dependencies:** ${metadata.dependencies.join(", ")}`);
262
+ lines.push("");
263
+ }
264
+
265
+ const commands: string[] = [];
266
+ if (metadata.testCommand) commands.push(`test: \`${metadata.testCommand}\``);
267
+ if (metadata.lintCommand) commands.push(`lint: \`${metadata.lintCommand}\``);
268
+ if (metadata.typecheckCommand) commands.push(`typecheck: \`${metadata.typecheckCommand}\``);
269
+
270
+ if (commands.length > 0) {
271
+ lines.push(`**Commands:** ${commands.join(" | ")}`);
272
+ lines.push("");
273
+ }
274
+
275
+ lines.push("---");
276
+ lines.push("");
277
+
278
+ return lines.join("\n");
279
+ }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Test File Scanner (v0.7)
3
+ *
4
+ * Scans test directories and extracts describe/test block names
5
+ * to generate a coverage summary for prompt injection.
6
+ * Prevents test duplication across isolated story sessions.
7
+ */
8
+
9
+ import path from "node:path";
10
+ import { Glob } from "bun";
11
+ import { getLogger } from "../logger";
12
+ import { estimateTokens } from "./builder";
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ /** Detail level for test summary */
19
+ export type TestSummaryDetail = "names-only" | "names-and-counts" | "describe-blocks";
20
+
21
+ /** Options for scanning test files */
22
+ export interface TestScanOptions {
23
+ /** Working directory (base for testDir) */
24
+ workdir: string;
25
+ /** Test directory relative to workdir (default: auto-detect) */
26
+ testDir?: string;
27
+ /** Glob pattern for test files (default: "**\/*.test.{ts,js,tsx,jsx}") */
28
+ testPattern?: string;
29
+ /** Max tokens for the summary (default: 500) */
30
+ maxTokens?: number;
31
+ /** Summary detail level (default: "names-and-counts") */
32
+ detail?: TestSummaryDetail;
33
+ /** Context files to scope test coverage to (default: undefined = scan all) */
34
+ contextFiles?: string[];
35
+ /** Enable scoping to context files (default: true) */
36
+ scopeToStory?: boolean;
37
+ }
38
+
39
+ /** A single describe block extracted from a test file */
40
+ export interface DescribeBlock {
41
+ name: string;
42
+ tests: string[];
43
+ }
44
+
45
+ /** Parsed test file info */
46
+ export interface TestFileInfo {
47
+ /** Relative path from workdir */
48
+ relativePath: string;
49
+ /** Total test count (it/test calls) */
50
+ testCount: number;
51
+ /** Top-level describe blocks with their test names */
52
+ describes: DescribeBlock[];
53
+ }
54
+
55
+ /** Scan result */
56
+ export interface TestScanResult {
57
+ files: TestFileInfo[];
58
+ totalTests: number;
59
+ summary: string;
60
+ tokens: number;
61
+ }
62
+
63
+ // ============================================================================
64
+ // Regex Extraction
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Extract describe and test block names from test file source.
69
+ *
70
+ * Uses regex to find:
71
+ * - `describe("name", ...)` / `describe('name', ...)`
72
+ * - `test("name", ...)` / `it("name", ...)`
73
+ *
74
+ * Only extracts top-level describes (not nested). All test/it calls
75
+ * are associated with the most recent describe block.
76
+ */
77
+ export function extractTestStructure(source: string): { describes: DescribeBlock[]; testCount: number } {
78
+ const describes: DescribeBlock[] = [];
79
+ let currentDescribe: DescribeBlock | null = null;
80
+ let testCount = 0;
81
+
82
+ // Match describe/test/it calls with string arguments (single or double quotes, backticks)
83
+ // Match describe/test/it calls anywhere (not just line-start) to handle single-line files
84
+ const linePattern = /(?:^|\s|;|\{)(describe|test|it)\s*\(\s*(['"`])(.*?)\2/gm;
85
+
86
+ let match: RegExpExecArray | null = linePattern.exec(source);
87
+ while (match !== null) {
88
+ const keyword = match[1];
89
+ const name = match[3];
90
+
91
+ if (keyword === "describe") {
92
+ currentDescribe = { name, tests: [] };
93
+ describes.push(currentDescribe);
94
+ } else {
95
+ // test or it
96
+ testCount++;
97
+ if (currentDescribe) {
98
+ currentDescribe.tests.push(name);
99
+ } else {
100
+ // Top-level test without describe
101
+ if (describes.length === 0 || describes[describes.length - 1].name !== "(top-level)") {
102
+ describes.push({ name: "(top-level)", tests: [] });
103
+ }
104
+ describes[describes.length - 1].tests.push(name);
105
+ }
106
+ }
107
+ match = linePattern.exec(source);
108
+ }
109
+
110
+ return { describes, testCount };
111
+ }
112
+
113
+ // ============================================================================
114
+ // File Scanning
115
+ // ============================================================================
116
+
117
+ /** Common test directory names to auto-detect */
118
+ const COMMON_TEST_DIRS = ["test", "tests", "__tests__", "src/__tests__", "spec"];
119
+
120
+ /**
121
+ * Derive test file patterns from source file paths.
122
+ *
123
+ * Maps source files to their likely test file counterparts:
124
+ * - src/foo.ts → test/foo.test.ts, test/foo.spec.ts
125
+ * - src/bar/baz.service.ts → test/bar/baz.service.test.ts, test/baz.service.test.ts
126
+ *
127
+ * @param contextFiles - Array of source file paths (relative to workdir)
128
+ * @returns Array of test file path patterns (basename patterns for matching)
129
+ */
130
+ export function deriveTestPatterns(contextFiles: string[]): string[] {
131
+ const patterns = new Set<string>();
132
+
133
+ for (const filePath of contextFiles) {
134
+ const basename = path.basename(filePath);
135
+ const basenameNoExt = basename.replace(/\.(ts|js|tsx|jsx)$/, "");
136
+
137
+ // Pattern 1: exact basename match with .test/.spec extension
138
+ // e.g., foo.ts → foo.test.ts, foo.spec.ts
139
+ patterns.add(`${basenameNoExt}.test.ts`);
140
+ patterns.add(`${basenameNoExt}.test.js`);
141
+ patterns.add(`${basenameNoExt}.test.tsx`);
142
+ patterns.add(`${basenameNoExt}.test.jsx`);
143
+ patterns.add(`${basenameNoExt}.spec.ts`);
144
+ patterns.add(`${basenameNoExt}.spec.js`);
145
+ patterns.add(`${basenameNoExt}.spec.tsx`);
146
+ patterns.add(`${basenameNoExt}.spec.jsx`);
147
+
148
+ // Pattern 2: if basename contains .service/.controller/etc, also match without it
149
+ // e.g., foo.service.ts → foo.test.ts
150
+ const simpleBasename = basenameNoExt.replace(
151
+ /\.(service|controller|resolver|module|guard|middleware|util|helper)$/,
152
+ "",
153
+ );
154
+ if (simpleBasename !== basenameNoExt) {
155
+ patterns.add(`${simpleBasename}.test.ts`);
156
+ patterns.add(`${simpleBasename}.test.js`);
157
+ patterns.add(`${simpleBasename}.spec.ts`);
158
+ patterns.add(`${simpleBasename}.spec.js`);
159
+ }
160
+ }
161
+
162
+ return Array.from(patterns);
163
+ }
164
+
165
+ /**
166
+ * Auto-detect test directory by checking common locations.
167
+ */
168
+ async function detectTestDir(workdir: string): Promise<string | null> {
169
+ for (const dir of COMMON_TEST_DIRS) {
170
+ const fullPath = path.join(workdir, dir);
171
+ const file = Bun.file(path.join(fullPath, ".")); // Check directory exists
172
+ try {
173
+ const dirStat = await Bun.file(fullPath).exists();
174
+ // Bun.file().exists() returns false for directories, use different check
175
+ const proc = Bun.spawn(["test", "-d", fullPath], { stdout: "pipe", stderr: "pipe" });
176
+ const exitCode = await proc.exited;
177
+ if (exitCode === 0) return dir;
178
+ } catch {}
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Scan test files and extract structure.
185
+ *
186
+ * @param options - Scan options
187
+ * @returns Array of parsed test file info
188
+ */
189
+ export async function scanTestFiles(options: TestScanOptions): Promise<TestFileInfo[]> {
190
+ const { workdir, testPattern = "**/*.test.{ts,js,tsx,jsx}", contextFiles, scopeToStory = true } = options;
191
+ let testDir = options.testDir;
192
+
193
+ // Auto-detect test directory if not specified
194
+ if (!testDir) {
195
+ testDir = (await detectTestDir(workdir)) || "test";
196
+ }
197
+
198
+ const scanDir = path.join(workdir, testDir);
199
+
200
+ // Check directory exists
201
+ const dirCheck = Bun.spawn(["test", "-d", scanDir], { stdout: "pipe", stderr: "pipe" });
202
+ if ((await dirCheck.exited) !== 0) {
203
+ return [];
204
+ }
205
+
206
+ // Derive test patterns from context files if scoping is enabled
207
+ let allowedBasenames: Set<string> | null = null;
208
+ if (scopeToStory && contextFiles && contextFiles.length > 0) {
209
+ const patterns = deriveTestPatterns(contextFiles);
210
+ allowedBasenames = new Set(patterns);
211
+ }
212
+
213
+ const glob = new Glob(testPattern);
214
+ const files: TestFileInfo[] = [];
215
+
216
+ for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
217
+ // Filter by derived patterns if scoping is enabled
218
+ if (allowedBasenames !== null) {
219
+ const basename = path.basename(filePath);
220
+ if (!allowedBasenames.has(basename)) {
221
+ continue; // Skip test files not matching context files
222
+ }
223
+ }
224
+
225
+ const fullPath = path.join(scanDir, filePath);
226
+ try {
227
+ const source = await Bun.file(fullPath).text();
228
+ const { describes, testCount } = extractTestStructure(source);
229
+
230
+ if (testCount > 0 || describes.length > 0) {
231
+ files.push({
232
+ relativePath: path.join(testDir, filePath),
233
+ testCount,
234
+ describes,
235
+ });
236
+ }
237
+ } catch {}
238
+ }
239
+
240
+ // Sort by path for stable output
241
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
242
+
243
+ return files;
244
+ }
245
+
246
+ // ============================================================================
247
+ // Summary Formatting
248
+ // ============================================================================
249
+
250
+ /**
251
+ * Format test files as a markdown summary at the specified detail level.
252
+ */
253
+ export function formatTestSummary(files: TestFileInfo[], detail: TestSummaryDetail): string {
254
+ if (files.length === 0) {
255
+ return "";
256
+ }
257
+
258
+ const lines: string[] = [];
259
+ const totalTests = files.reduce((sum, f) => sum + f.testCount, 0);
260
+
261
+ lines.push(`## Existing Test Coverage (${totalTests} tests across ${files.length} files)`);
262
+ lines.push("");
263
+ lines.push("The following tests already exist. DO NOT duplicate this coverage.");
264
+ lines.push("Focus only on testing NEW behavior introduced by this story.");
265
+ lines.push("");
266
+
267
+ for (const file of files) {
268
+ switch (detail) {
269
+ case "names-only":
270
+ lines.push(`- **${file.relativePath}** (${file.testCount} tests)`);
271
+ break;
272
+
273
+ case "names-and-counts":
274
+ lines.push(`### ${file.relativePath} (${file.testCount} tests)`);
275
+ for (const desc of file.describes) {
276
+ lines.push(`- ${desc.name} (${desc.tests.length} tests)`);
277
+ }
278
+ lines.push("");
279
+ break;
280
+
281
+ case "describe-blocks":
282
+ lines.push(`### ${file.relativePath} (${file.testCount} tests)`);
283
+ for (const desc of file.describes) {
284
+ lines.push(`- **${desc.name}** (${desc.tests.length} tests)`);
285
+ for (const test of desc.tests) {
286
+ lines.push(` - ${test}`);
287
+ }
288
+ }
289
+ lines.push("");
290
+ break;
291
+ }
292
+ }
293
+
294
+ return lines.join("\n");
295
+ }
296
+
297
+ /**
298
+ * Truncate summary to fit within token budget.
299
+ *
300
+ * Strategy: progressively reduce detail level, then truncate files.
301
+ */
302
+ export function truncateToTokenBudget(
303
+ files: TestFileInfo[],
304
+ maxTokens: number,
305
+ preferredDetail: TestSummaryDetail,
306
+ ): { summary: string; detail: TestSummaryDetail; truncated: boolean } {
307
+ // Try preferred detail level first
308
+ const detailLevels: TestSummaryDetail[] = ["describe-blocks", "names-and-counts", "names-only"];
309
+ const startIndex = detailLevels.indexOf(preferredDetail);
310
+
311
+ for (let i = startIndex; i < detailLevels.length; i++) {
312
+ const detail = detailLevels[i];
313
+ const summary = formatTestSummary(files, detail);
314
+ const tokens = estimateTokens(summary);
315
+
316
+ if (tokens <= maxTokens) {
317
+ return { summary, detail, truncated: i !== startIndex };
318
+ }
319
+ }
320
+
321
+ // Even names-only exceeds budget — truncate files
322
+ let truncatedFiles = [...files];
323
+ while (truncatedFiles.length > 1) {
324
+ truncatedFiles = truncatedFiles.slice(0, truncatedFiles.length - 1);
325
+ const summary = `${formatTestSummary(truncatedFiles, "names-only")}\n... and ${files.length - truncatedFiles.length} more test files`;
326
+ if (estimateTokens(summary) <= maxTokens) {
327
+ return { summary, detail: "names-only", truncated: true };
328
+ }
329
+ }
330
+
331
+ // Last resort: just file count
332
+ const fallback = `## Existing Test Coverage\n\n${files.length} test files with ${files.reduce((s, f) => s + f.testCount, 0)} total tests exist. Review test/ directory before adding new tests.`;
333
+ return { summary: fallback, detail: "names-only", truncated: true };
334
+ }
335
+
336
+ // ============================================================================
337
+ // Main Entry Point
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Scan test files and generate a token-budgeted summary.
342
+ *
343
+ * @param options - Scan and formatting options
344
+ * @returns Scan result with summary, or empty result if no tests found
345
+ */
346
+ export async function generateTestCoverageSummary(options: TestScanOptions): Promise<TestScanResult> {
347
+ const { maxTokens = 500, detail = "names-and-counts", contextFiles, scopeToStory = true } = options;
348
+
349
+ // Log warning if scoping is enabled but no context files provided
350
+ if (scopeToStory && (!contextFiles || contextFiles.length === 0)) {
351
+ try {
352
+ const logger = getLogger();
353
+ logger.warn("context", "scopeToStory=true but no contextFiles provided — falling back to full scan");
354
+ } catch {
355
+ // Logger not initialized (e.g., in tests) — silently skip
356
+ }
357
+ }
358
+
359
+ const files = await scanTestFiles(options);
360
+
361
+ if (files.length === 0) {
362
+ return { files: [], totalTests: 0, summary: "", tokens: 0 };
363
+ }
364
+
365
+ const totalTests = files.reduce((sum, f) => sum + f.testCount, 0);
366
+ const { summary } = truncateToTokenBudget(files, maxTokens, detail);
367
+ const tokens = estimateTokens(summary);
368
+
369
+ return { files, totalTests, summary, tokens };
370
+ }