@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,1722 @@
1
+ /**
2
+ * Tests for context builder module
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import fs from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import {
10
+ buildContext,
11
+ createDependencyContext,
12
+ createErrorContext,
13
+ createFileContext,
14
+ createProgressContext,
15
+ createStoryContext,
16
+ estimateTokens,
17
+ formatContextAsMarkdown,
18
+ sortContextElements,
19
+ } from "../../src/context/builder";
20
+ import type { ContextBudget, ContextElement, StoryContext } from "../../src/context/types";
21
+ import type { PRD, UserStory } from "../../src/prd";
22
+
23
+ // Helper to create test PRD
24
+ const createTestPRD = (stories: Partial<UserStory>[]): PRD => ({
25
+ project: "test-project",
26
+ feature: "test-feature",
27
+ branchName: "test-branch",
28
+ createdAt: new Date().toISOString(),
29
+ updatedAt: new Date().toISOString(),
30
+ userStories: stories.map((s, i) => ({
31
+ id: s.id || `US-${String(i + 1).padStart(3, "0")}`,
32
+ title: s.title || "Test Story",
33
+ description: s.description || "Test description",
34
+ acceptanceCriteria: s.acceptanceCriteria || ["AC1"],
35
+ dependencies: s.dependencies || [],
36
+ tags: s.tags || [],
37
+ status: s.status || "pending",
38
+ passes: s.passes ?? false,
39
+ escalations: s.escalations || [],
40
+ attempts: s.attempts || 0,
41
+ routing: s.routing,
42
+ priorErrors: s.priorErrors,
43
+ relevantFiles: s.relevantFiles,
44
+ contextFiles: s.contextFiles,
45
+ expectedFiles: s.expectedFiles,
46
+ })),
47
+ });
48
+
49
+ describe("Context Builder", () => {
50
+ describe("estimateTokens", () => {
51
+ test("should estimate tokens correctly", () => {
52
+ expect(estimateTokens("test")).toBe(2); // 4 chars = 2 tokens (1 token ≈ 3 chars)
53
+ expect(estimateTokens("hello world")).toBe(4); // 11 chars = 4 tokens
54
+ expect(estimateTokens("")).toBe(0);
55
+ });
56
+ });
57
+
58
+ describe("defensive checks", () => {
59
+ test("should handle story with null acceptanceCriteria", async () => {
60
+ // Create PRD directly to bypass helper defaults
61
+ const prd: PRD = {
62
+ project: "test-project",
63
+ feature: "test-feature",
64
+ branchName: "test-branch",
65
+ createdAt: new Date().toISOString(),
66
+ updatedAt: new Date().toISOString(),
67
+ userStories: [
68
+ {
69
+ id: "US-001",
70
+ title: "Malformed Story",
71
+ description: "Test",
72
+ acceptanceCriteria: null as any, // Simulate malformed data
73
+ dependencies: [],
74
+ tags: [],
75
+ status: "pending",
76
+ passes: false,
77
+ escalations: [],
78
+ attempts: 0,
79
+ },
80
+ ],
81
+ };
82
+
83
+ const context: StoryContext = {
84
+ prd,
85
+ currentStoryId: "US-001",
86
+ };
87
+
88
+ const budget: ContextBudget = {
89
+ maxTokens: 10000,
90
+ reservedForInstructions: 1000,
91
+ availableForContext: 9000,
92
+ };
93
+
94
+ const built = await buildContext(context, budget);
95
+ expect(built.elements.length).toBeGreaterThan(0);
96
+ const storyElement = built.elements.find((e) => e.type === "story");
97
+ expect(storyElement?.content).toContain("(No acceptance criteria defined)");
98
+ });
99
+
100
+ test("should handle story with undefined acceptanceCriteria", async () => {
101
+ // Create PRD directly to bypass helper defaults
102
+ const prd: PRD = {
103
+ project: "test-project",
104
+ feature: "test-feature",
105
+ branchName: "test-branch",
106
+ createdAt: new Date().toISOString(),
107
+ updatedAt: new Date().toISOString(),
108
+ userStories: [
109
+ {
110
+ id: "US-001",
111
+ title: "Malformed Story",
112
+ description: "Test",
113
+ acceptanceCriteria: undefined as any, // Simulate malformed data
114
+ dependencies: [],
115
+ tags: [],
116
+ status: "pending",
117
+ passes: false,
118
+ escalations: [],
119
+ attempts: 0,
120
+ },
121
+ ],
122
+ };
123
+
124
+ const context: StoryContext = {
125
+ prd,
126
+ currentStoryId: "US-001",
127
+ };
128
+
129
+ const budget: ContextBudget = {
130
+ maxTokens: 10000,
131
+ reservedForInstructions: 1000,
132
+ availableForContext: 9000,
133
+ };
134
+
135
+ const built = await buildContext(context, budget);
136
+ expect(built.elements.length).toBeGreaterThan(0);
137
+ const storyElement = built.elements.find((e) => e.type === "story");
138
+ expect(storyElement?.content).toContain("(No acceptance criteria defined)");
139
+ });
140
+
141
+ test("should log warning for missing dependency story", async () => {
142
+ const prd = createTestPRD([
143
+ {
144
+ id: "US-001",
145
+ title: "Story with Missing Dependency",
146
+ description: "Test",
147
+ acceptanceCriteria: ["AC1"],
148
+ dependencies: ["US-999"], // Non-existent dependency
149
+ },
150
+ ]);
151
+
152
+ const context: StoryContext = {
153
+ prd,
154
+ currentStoryId: "US-001",
155
+ };
156
+
157
+ const budget: ContextBudget = {
158
+ maxTokens: 10000,
159
+ reservedForInstructions: 1000,
160
+ availableForContext: 9000,
161
+ };
162
+
163
+ const built = await buildContext(context, budget);
164
+
165
+ // Should not include the missing dependency in the context
166
+ expect(built.elements.find((e) => e.type === "dependency")).toBeUndefined();
167
+ });
168
+
169
+ test("should handle story with non-array priorErrors", async () => {
170
+ const prd = createTestPRD([
171
+ {
172
+ id: "US-001",
173
+ title: "Story with Malformed Errors",
174
+ description: "Test",
175
+ acceptanceCriteria: ["AC1"],
176
+ priorErrors: "not an array" as any, // Malformed data
177
+ },
178
+ ]);
179
+
180
+ const context: StoryContext = {
181
+ prd,
182
+ currentStoryId: "US-001",
183
+ };
184
+
185
+ const budget: ContextBudget = {
186
+ maxTokens: 10000,
187
+ reservedForInstructions: 1000,
188
+ availableForContext: 9000,
189
+ };
190
+
191
+ const built = await buildContext(context, budget);
192
+ expect(built.elements.find((e) => e.type === "error")).toBeUndefined();
193
+ });
194
+ });
195
+
196
+ describe("createStoryContext", () => {
197
+ test("should create story context element", () => {
198
+ const story: UserStory = {
199
+ id: "US-001",
200
+ title: "Test Story",
201
+ description: "Test description",
202
+ acceptanceCriteria: ["AC1", "AC2"],
203
+ dependencies: [],
204
+ tags: ["feature"],
205
+ status: "pending",
206
+ passes: false,
207
+ escalations: [],
208
+ attempts: 0,
209
+ };
210
+
211
+ const element = createStoryContext(story, 80);
212
+
213
+ expect(element.type).toBe("story");
214
+ expect(element.storyId).toBe("US-001");
215
+ expect(element.priority).toBe(80);
216
+ expect(element.content).toContain("US-001: Test Story");
217
+ expect(element.content).toContain("Test description");
218
+ expect(element.content).toContain("AC1");
219
+ expect(element.content).toContain("AC2");
220
+ expect(element.tokens).toBeGreaterThan(0);
221
+ });
222
+ });
223
+
224
+ describe("createDependencyContext", () => {
225
+ test("should create dependency context element", () => {
226
+ const story: UserStory = {
227
+ id: "US-002",
228
+ title: "Dependency Story",
229
+ description: "Dependency description",
230
+ acceptanceCriteria: ["AC1"],
231
+ dependencies: [],
232
+ tags: [],
233
+ status: "passed",
234
+ passes: true,
235
+ escalations: [],
236
+ attempts: 0,
237
+ };
238
+
239
+ const element = createDependencyContext(story, 50);
240
+
241
+ expect(element.type).toBe("dependency");
242
+ expect(element.storyId).toBe("US-002");
243
+ expect(element.priority).toBe(50);
244
+ expect(element.content).toContain("US-002: Dependency Story");
245
+ expect(element.tokens).toBeGreaterThan(0);
246
+ });
247
+ });
248
+
249
+ describe("createErrorContext", () => {
250
+ test("should create error context element", () => {
251
+ const error = "TypeError: Cannot read property";
252
+ const element = createErrorContext(error, 90);
253
+
254
+ expect(element.type).toBe("error");
255
+ expect(element.content).toBe(error);
256
+ expect(element.priority).toBe(90);
257
+ expect(element.tokens).toBeGreaterThan(0);
258
+ });
259
+ });
260
+
261
+ describe("createProgressContext", () => {
262
+ test("should create progress context element", () => {
263
+ const progress = "Progress: 5/12 stories complete (4 passed, 1 failed)";
264
+ const element = createProgressContext(progress, 100);
265
+
266
+ expect(element.type).toBe("progress");
267
+ expect(element.content).toBe(progress);
268
+ expect(element.priority).toBe(100);
269
+ expect(element.tokens).toBeGreaterThan(0);
270
+ });
271
+ });
272
+
273
+ describe("createFileContext", () => {
274
+ test("should create file context element", () => {
275
+ const filePath = "src/utils/helper.ts";
276
+ const content = 'export function helper() { return "test"; }';
277
+ const element = createFileContext(filePath, content, 60);
278
+
279
+ expect(element.type).toBe("file");
280
+ expect(element.filePath).toBe(filePath);
281
+ expect(element.content).toBe(content);
282
+ expect(element.priority).toBe(60);
283
+ expect(element.tokens).toBeGreaterThan(0);
284
+ });
285
+ });
286
+
287
+ describe("sortContextElements", () => {
288
+ test("should sort by priority descending", () => {
289
+ const elements: ContextElement[] = [
290
+ createErrorContext("error", 10),
291
+ createProgressContext("progress", 100),
292
+ createErrorContext("error2", 50),
293
+ ];
294
+
295
+ const sorted = sortContextElements(elements);
296
+
297
+ expect(sorted[0].priority).toBe(100);
298
+ expect(sorted[1].priority).toBe(50);
299
+ expect(sorted[2].priority).toBe(10);
300
+ });
301
+
302
+ test("should sort by tokens ascending for same priority", () => {
303
+ const elements: ContextElement[] = [
304
+ createErrorContext("this is a much longer error message with lots of text", 50),
305
+ createErrorContext("short", 50),
306
+ createErrorContext("medium length message", 50),
307
+ ];
308
+
309
+ const sorted = sortContextElements(elements);
310
+
311
+ expect(sorted[0].tokens).toBeLessThan(sorted[1].tokens);
312
+ expect(sorted[1].tokens).toBeLessThan(sorted[2].tokens);
313
+ });
314
+
315
+ test("should not mutate original array", () => {
316
+ const elements: ContextElement[] = [createErrorContext("a", 10), createErrorContext("b", 20)];
317
+
318
+ const original = [...elements];
319
+ sortContextElements(elements);
320
+
321
+ expect(elements).toEqual(original);
322
+ });
323
+ });
324
+
325
+ describe("buildContext", () => {
326
+ test("should extract current story from PRD", async () => {
327
+ const prd = createTestPRD([
328
+ {
329
+ id: "US-001",
330
+ title: "First Story",
331
+ description: "First description",
332
+ acceptanceCriteria: ["AC1"],
333
+ },
334
+ {
335
+ id: "US-002",
336
+ title: "Second Story",
337
+ description: "Second description",
338
+ acceptanceCriteria: ["AC2"],
339
+ },
340
+ ]);
341
+
342
+ const storyContext: StoryContext = {
343
+ prd,
344
+ currentStoryId: "US-001",
345
+ };
346
+
347
+ const budget: ContextBudget = {
348
+ maxTokens: 10000,
349
+ reservedForInstructions: 1000,
350
+ availableForContext: 9000,
351
+ };
352
+
353
+ const built = await buildContext(storyContext, budget);
354
+
355
+ // Should have progress + current story
356
+ expect(built.elements.length).toBe(2);
357
+ expect(built.elements.some((e) => e.type === "progress")).toBe(true);
358
+ expect(built.elements.some((e) => e.type === "story" && e.storyId === "US-001")).toBe(true);
359
+ expect(built.totalTokens).toBeLessThanOrEqual(9000);
360
+ });
361
+
362
+ test("should include dependency stories", async () => {
363
+ const prd = createTestPRD([
364
+ {
365
+ id: "US-001",
366
+ title: "Dependency Story",
367
+ description: "Dependency description",
368
+ acceptanceCriteria: ["AC1"],
369
+ status: "passed",
370
+ passes: true,
371
+ },
372
+ {
373
+ id: "US-002",
374
+ title: "Current Story",
375
+ description: "Current description",
376
+ acceptanceCriteria: ["AC2"],
377
+ dependencies: ["US-001"],
378
+ },
379
+ ]);
380
+
381
+ const storyContext: StoryContext = {
382
+ prd,
383
+ currentStoryId: "US-002",
384
+ };
385
+
386
+ const budget: ContextBudget = {
387
+ maxTokens: 10000,
388
+ reservedForInstructions: 1000,
389
+ availableForContext: 9000,
390
+ };
391
+
392
+ const built = await buildContext(storyContext, budget);
393
+
394
+ // Should have progress + current story + dependency
395
+ expect(built.elements.length).toBe(3);
396
+ expect(built.elements.some((e) => e.type === "progress")).toBe(true);
397
+ expect(built.elements.some((e) => e.type === "story" && e.storyId === "US-002")).toBe(true);
398
+ expect(built.elements.some((e) => e.type === "dependency" && e.storyId === "US-001")).toBe(true);
399
+ });
400
+
401
+ test("should include prior errors", async () => {
402
+ const prd = createTestPRD([
403
+ {
404
+ id: "US-001",
405
+ title: "Failed Story",
406
+ description: "Story with errors",
407
+ acceptanceCriteria: ["AC1"],
408
+ priorErrors: ["Error 1", "Error 2"],
409
+ },
410
+ ]);
411
+
412
+ const storyContext: StoryContext = {
413
+ prd,
414
+ currentStoryId: "US-001",
415
+ };
416
+
417
+ const budget: ContextBudget = {
418
+ maxTokens: 10000,
419
+ reservedForInstructions: 1000,
420
+ availableForContext: 9000,
421
+ };
422
+
423
+ const built = await buildContext(storyContext, budget);
424
+
425
+ const errorElements = built.elements.filter((e) => e.type === "error");
426
+ expect(errorElements.length).toBe(2);
427
+ expect(built.summary).toContain("2 errors");
428
+ });
429
+
430
+ test("should generate progress summary", async () => {
431
+ const prd = createTestPRD([
432
+ { id: "US-001", status: "passed", passes: true },
433
+ { id: "US-002", status: "passed", passes: true },
434
+ { id: "US-003", status: "failed", passes: false },
435
+ { id: "US-004", status: "pending", passes: false },
436
+ ]);
437
+
438
+ const storyContext: StoryContext = {
439
+ prd,
440
+ currentStoryId: "US-004",
441
+ };
442
+
443
+ const budget: ContextBudget = {
444
+ maxTokens: 10000,
445
+ reservedForInstructions: 1000,
446
+ availableForContext: 9000,
447
+ };
448
+
449
+ const built = await buildContext(storyContext, budget);
450
+
451
+ const progressElement = built.elements.find((e) => e.type === "progress");
452
+ expect(progressElement).toBeDefined();
453
+ expect(progressElement!.content).toContain("3/4 stories complete");
454
+ expect(progressElement!.content).toContain("2 passed");
455
+ expect(progressElement!.content).toContain("1 failed");
456
+ });
457
+
458
+ test("should truncate when exceeding budget", async () => {
459
+ const prd = createTestPRD([
460
+ {
461
+ id: "US-001",
462
+ title: "Story with many dependencies",
463
+ description: "x".repeat(1000),
464
+ acceptanceCriteria: ["AC1"],
465
+ dependencies: ["US-002", "US-003", "US-004", "US-005"],
466
+ },
467
+ { id: "US-002", description: "x".repeat(1000), acceptanceCriteria: ["AC2"] },
468
+ { id: "US-003", description: "x".repeat(1000), acceptanceCriteria: ["AC3"] },
469
+ { id: "US-004", description: "x".repeat(1000), acceptanceCriteria: ["AC4"] },
470
+ { id: "US-005", description: "x".repeat(1000), acceptanceCriteria: ["AC5"] },
471
+ ]);
472
+
473
+ const storyContext: StoryContext = {
474
+ prd,
475
+ currentStoryId: "US-001",
476
+ };
477
+
478
+ const budget: ContextBudget = {
479
+ maxTokens: 1000,
480
+ reservedForInstructions: 500,
481
+ availableForContext: 500, // Small budget
482
+ };
483
+
484
+ const built = await buildContext(storyContext, budget);
485
+
486
+ expect(built.truncated).toBe(true);
487
+ expect(built.totalTokens).toBeLessThanOrEqual(500);
488
+ expect(built.summary).toContain("[TRUNCATED]");
489
+ // Progress should always be included (highest priority)
490
+ expect(built.elements.some((e) => e.type === "progress")).toBe(true);
491
+ });
492
+
493
+ test("should throw error for non-existent story", async () => {
494
+ const prd = createTestPRD([{ id: "US-001", title: "Story" }]);
495
+
496
+ const storyContext: StoryContext = {
497
+ prd,
498
+ currentStoryId: "US-999", // Non-existent
499
+ };
500
+
501
+ const budget: ContextBudget = {
502
+ maxTokens: 10000,
503
+ reservedForInstructions: 1000,
504
+ availableForContext: 9000,
505
+ };
506
+
507
+ await expect(buildContext(storyContext, budget)).rejects.toThrow("Story US-999 not found in PRD");
508
+ });
509
+
510
+ test("should load files from contextFiles when present", async () => {
511
+ // Create temp directory and files
512
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
513
+ const testFile1 = path.join(tempDir, "helper.ts");
514
+ const testFile2 = path.join(tempDir, "utils.ts");
515
+
516
+ await fs.writeFile(testFile1, 'export function helper() { return "test"; }');
517
+ await fs.writeFile(testFile2, 'export function utils() { return "util"; }');
518
+
519
+ try {
520
+ const prd = createTestPRD([
521
+ {
522
+ id: "US-001",
523
+ title: "Story with Files",
524
+ description: "Test",
525
+ acceptanceCriteria: ["AC1"],
526
+ contextFiles: ["helper.ts", "utils.ts"],
527
+ },
528
+ ]);
529
+
530
+ const storyContext: StoryContext = {
531
+ prd,
532
+ currentStoryId: "US-001",
533
+ workdir: tempDir,
534
+ };
535
+
536
+ const budget: ContextBudget = {
537
+ maxTokens: 10000,
538
+ reservedForInstructions: 1000,
539
+ availableForContext: 9000,
540
+ };
541
+
542
+ const built = await buildContext(storyContext, budget);
543
+
544
+ const fileElements = built.elements.filter((e) => e.type === "file");
545
+ expect(fileElements.length).toBe(2);
546
+ expect(fileElements[0].filePath).toBe("helper.ts");
547
+ expect(fileElements[1].filePath).toBe("utils.ts");
548
+ expect(fileElements[0].content).toContain("helper()");
549
+ expect(fileElements[1].content).toContain("utils()");
550
+ expect(built.summary).toContain("2 files");
551
+ } finally {
552
+ // Cleanup
553
+ await fs.rm(tempDir, { recursive: true, force: true });
554
+ }
555
+ });
556
+
557
+ test("should fall back to relevantFiles for file loading when contextFiles not present", async () => {
558
+ // Create temp directory and files
559
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
560
+ const testFile = path.join(tempDir, "legacy.ts");
561
+
562
+ await fs.writeFile(testFile, 'export function legacy() { return "old"; }');
563
+
564
+ try {
565
+ const prd = createTestPRD([
566
+ {
567
+ id: "US-001",
568
+ title: "Legacy Story with relevantFiles",
569
+ description: "Test backward compatibility",
570
+ acceptanceCriteria: ["AC1"],
571
+ relevantFiles: ["legacy.ts"],
572
+ },
573
+ ]);
574
+
575
+ const storyContext: StoryContext = {
576
+ prd,
577
+ currentStoryId: "US-001",
578
+ workdir: tempDir,
579
+ };
580
+
581
+ const budget: ContextBudget = {
582
+ maxTokens: 10000,
583
+ reservedForInstructions: 1000,
584
+ availableForContext: 9000,
585
+ };
586
+
587
+ const built = await buildContext(storyContext, budget);
588
+
589
+ const fileElements = built.elements.filter((e) => e.type === "file");
590
+ expect(fileElements.length).toBe(1);
591
+ expect(fileElements[0].filePath).toBe("legacy.ts");
592
+ expect(fileElements[0].content).toContain("legacy()");
593
+ } finally {
594
+ await fs.rm(tempDir, { recursive: true, force: true });
595
+ }
596
+ });
597
+
598
+ test("should prefer contextFiles over relevantFiles for file loading", async () => {
599
+ // Create temp directory and files
600
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
601
+ const newFile = path.join(tempDir, "new.ts");
602
+ const oldFile = path.join(tempDir, "old.ts");
603
+
604
+ await fs.writeFile(newFile, "export function newFunc() {}");
605
+ await fs.writeFile(oldFile, "export function oldFunc() {}");
606
+
607
+ try {
608
+ const prd = createTestPRD([
609
+ {
610
+ id: "US-001",
611
+ title: "Story with both contextFiles and relevantFiles",
612
+ description: "Test precedence",
613
+ acceptanceCriteria: ["AC1"],
614
+ contextFiles: ["new.ts"],
615
+ relevantFiles: ["old.ts"],
616
+ },
617
+ ]);
618
+
619
+ const storyContext: StoryContext = {
620
+ prd,
621
+ currentStoryId: "US-001",
622
+ workdir: tempDir,
623
+ };
624
+
625
+ const budget: ContextBudget = {
626
+ maxTokens: 10000,
627
+ reservedForInstructions: 1000,
628
+ availableForContext: 9000,
629
+ };
630
+
631
+ const built = await buildContext(storyContext, budget);
632
+
633
+ const fileElements = built.elements.filter((e) => e.type === "file");
634
+ expect(fileElements.length).toBe(1);
635
+ expect(fileElements[0].filePath).toBe("new.ts");
636
+ expect(fileElements[0].content).toContain("newFunc()");
637
+ // Should NOT load old.ts
638
+ expect(fileElements.find((e) => e.filePath === "old.ts")).toBeUndefined();
639
+ } finally {
640
+ await fs.rm(tempDir, { recursive: true, force: true });
641
+ }
642
+ });
643
+
644
+ test("should respect max 5 files limit", async () => {
645
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
646
+
647
+ try {
648
+ // Create 10 test files
649
+ const files: string[] = [];
650
+ for (let i = 0; i < 10; i++) {
651
+ const filename = `file${i}.ts`;
652
+ files.push(filename);
653
+ await fs.writeFile(path.join(tempDir, filename), `export const file${i} = ${i};`);
654
+ }
655
+
656
+ const prd = createTestPRD([
657
+ {
658
+ id: "US-001",
659
+ title: "Story with Many Files",
660
+ description: "Test",
661
+ acceptanceCriteria: ["AC1"],
662
+ contextFiles: files,
663
+ },
664
+ ]);
665
+
666
+ const storyContext: StoryContext = {
667
+ prd,
668
+ currentStoryId: "US-001",
669
+ workdir: tempDir,
670
+ };
671
+
672
+ const budget: ContextBudget = {
673
+ maxTokens: 10000,
674
+ reservedForInstructions: 1000,
675
+ availableForContext: 9000,
676
+ };
677
+
678
+ const built = await buildContext(storyContext, budget);
679
+
680
+ const fileElements = built.elements.filter((e) => e.type === "file");
681
+ expect(fileElements.length).toBe(5); // Max 5 files
682
+ } finally {
683
+ await fs.rm(tempDir, { recursive: true, force: true });
684
+ }
685
+ });
686
+
687
+ test("should skip files larger than 10KB", async () => {
688
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
689
+
690
+ try {
691
+ const smallFile = path.join(tempDir, "small.ts");
692
+ const largeFile = path.join(tempDir, "large.ts");
693
+
694
+ await fs.writeFile(smallFile, 'export const small = "ok";');
695
+ await fs.writeFile(largeFile, "x".repeat(11 * 1024)); // 11KB
696
+
697
+ const prd = createTestPRD([
698
+ {
699
+ id: "US-001",
700
+ title: "Story with Large File",
701
+ description: "Test",
702
+ acceptanceCriteria: ["AC1"],
703
+ contextFiles: ["small.ts", "large.ts"],
704
+ },
705
+ ]);
706
+
707
+ const storyContext: StoryContext = {
708
+ prd,
709
+ currentStoryId: "US-001",
710
+ workdir: tempDir,
711
+ };
712
+
713
+ const budget: ContextBudget = {
714
+ maxTokens: 20000,
715
+ reservedForInstructions: 1000,
716
+ availableForContext: 19000,
717
+ };
718
+
719
+ // Capture warnings
720
+ const originalWarn = console.warn;
721
+ const warnings: string[] = [];
722
+ console.warn = (msg: string) => warnings.push(msg);
723
+
724
+ const built = await buildContext(storyContext, budget);
725
+
726
+ console.warn = originalWarn;
727
+
728
+ const fileElements = built.elements.filter((e) => e.type === "file");
729
+ expect(fileElements.length).toBe(1); // Only small file loaded
730
+ expect(fileElements[0].filePath).toBe("small.ts");
731
+ // Large file should be skipped (warning logged via structured logger)
732
+ } finally {
733
+ await fs.rm(tempDir, { recursive: true, force: true });
734
+ }
735
+ });
736
+
737
+ test("should warn on missing files", async () => {
738
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
739
+
740
+ try {
741
+ const prd = createTestPRD([
742
+ {
743
+ id: "US-001",
744
+ title: "Story with Missing File",
745
+ description: "Test",
746
+ acceptanceCriteria: ["AC1"],
747
+ contextFiles: ["nonexistent.ts"],
748
+ },
749
+ ]);
750
+
751
+ const storyContext: StoryContext = {
752
+ prd,
753
+ currentStoryId: "US-001",
754
+ workdir: tempDir,
755
+ };
756
+
757
+ const budget: ContextBudget = {
758
+ maxTokens: 10000,
759
+ reservedForInstructions: 1000,
760
+ availableForContext: 9000,
761
+ };
762
+
763
+ const built = await buildContext(storyContext, budget);
764
+
765
+ const fileElements = built.elements.filter((e) => e.type === "file");
766
+ expect(fileElements.length).toBe(0);
767
+ // Missing file should be skipped (warning logged via structured logger)
768
+ } finally {
769
+ await fs.rm(tempDir, { recursive: true, force: true });
770
+ }
771
+ });
772
+
773
+ test("should handle empty contextFiles array", async () => {
774
+ const prd = createTestPRD([
775
+ {
776
+ id: "US-001",
777
+ title: "Story with Empty Files",
778
+ description: "Test",
779
+ acceptanceCriteria: ["AC1"],
780
+ contextFiles: [],
781
+ },
782
+ ]);
783
+
784
+ const storyContext: StoryContext = {
785
+ prd,
786
+ currentStoryId: "US-001",
787
+ };
788
+
789
+ const budget: ContextBudget = {
790
+ maxTokens: 10000,
791
+ reservedForInstructions: 1000,
792
+ availableForContext: 9000,
793
+ };
794
+
795
+ const built = await buildContext(storyContext, budget);
796
+
797
+ const fileElements = built.elements.filter((e) => e.type === "file");
798
+ expect(fileElements.length).toBe(0);
799
+ });
800
+
801
+ test("should respect token budget when loading files", async () => {
802
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
803
+
804
+ try {
805
+ // Create files with substantial content
806
+ await fs.writeFile(path.join(tempDir, "file1.ts"), "x".repeat(5000));
807
+ await fs.writeFile(path.join(tempDir, "file2.ts"), "x".repeat(5000));
808
+
809
+ const prd = createTestPRD([
810
+ {
811
+ id: "US-001",
812
+ title: "Story",
813
+ description: "x".repeat(1000),
814
+ acceptanceCriteria: ["AC1"],
815
+ contextFiles: ["file1.ts", "file2.ts"],
816
+ },
817
+ ]);
818
+
819
+ const storyContext: StoryContext = {
820
+ prd,
821
+ currentStoryId: "US-001",
822
+ workdir: tempDir,
823
+ };
824
+
825
+ const budget: ContextBudget = {
826
+ maxTokens: 2000,
827
+ reservedForInstructions: 500,
828
+ availableForContext: 1500, // Small budget
829
+ };
830
+
831
+ const built = await buildContext(storyContext, budget);
832
+
833
+ expect(built.totalTokens).toBeLessThanOrEqual(1500);
834
+ // Files have lower priority (60) than story (80), so story should be included
835
+ expect(built.elements.some((e) => e.type === "story")).toBe(true);
836
+ } finally {
837
+ await fs.rm(tempDir, { recursive: true, force: true });
838
+ }
839
+ });
840
+ });
841
+
842
+ describe("formatContextAsMarkdown", () => {
843
+ test("should format context with all element types", async () => {
844
+ const prd = createTestPRD([
845
+ {
846
+ id: "US-001",
847
+ title: "Dependency",
848
+ description: "Dep description",
849
+ acceptanceCriteria: ["AC1"],
850
+ status: "passed",
851
+ passes: true,
852
+ },
853
+ {
854
+ id: "US-002",
855
+ title: "Current",
856
+ description: "Current description",
857
+ acceptanceCriteria: ["AC2"],
858
+ dependencies: ["US-001"],
859
+ priorErrors: ["Test error"],
860
+ },
861
+ ]);
862
+
863
+ const storyContext: StoryContext = {
864
+ prd,
865
+ currentStoryId: "US-002",
866
+ };
867
+
868
+ const budget: ContextBudget = {
869
+ maxTokens: 10000,
870
+ reservedForInstructions: 1000,
871
+ availableForContext: 9000,
872
+ };
873
+
874
+ const built = await buildContext(storyContext, budget);
875
+ const markdown = formatContextAsMarkdown(built);
876
+
877
+ expect(markdown).toContain("# Story Context");
878
+ expect(markdown).toContain("## Progress");
879
+ expect(markdown).toContain("## Prior Errors");
880
+ expect(markdown).toContain("## Current Story");
881
+ expect(markdown).toContain("## Dependency Stories");
882
+ expect(markdown).toContain("US-001");
883
+ expect(markdown).toContain("US-002");
884
+ expect(markdown).toContain("Test error");
885
+ });
886
+
887
+ test("should include summary with token count", async () => {
888
+ const prd = createTestPRD([{ id: "US-001", title: "Story" }]);
889
+
890
+ const storyContext: StoryContext = {
891
+ prd,
892
+ currentStoryId: "US-001",
893
+ };
894
+
895
+ const budget: ContextBudget = {
896
+ maxTokens: 10000,
897
+ reservedForInstructions: 1000,
898
+ availableForContext: 9000,
899
+ };
900
+
901
+ const built = await buildContext(storyContext, budget);
902
+ const markdown = formatContextAsMarkdown(built);
903
+
904
+ expect(markdown).toContain("Context:");
905
+ expect(markdown).toContain("tokens");
906
+ expect(markdown).toContain(built.totalTokens.toString());
907
+ });
908
+
909
+ test("should show truncation indicator", async () => {
910
+ const prd = createTestPRD([
911
+ {
912
+ id: "US-001",
913
+ description: "x".repeat(2000),
914
+ dependencies: ["US-002", "US-003"],
915
+ },
916
+ { id: "US-002", description: "x".repeat(2000) },
917
+ { id: "US-003", description: "x".repeat(2000) },
918
+ ]);
919
+
920
+ const storyContext: StoryContext = {
921
+ prd,
922
+ currentStoryId: "US-001",
923
+ };
924
+
925
+ const budget: ContextBudget = {
926
+ maxTokens: 500,
927
+ reservedForInstructions: 250,
928
+ availableForContext: 250,
929
+ };
930
+
931
+ const built = await buildContext(storyContext, budget);
932
+ const markdown = formatContextAsMarkdown(built);
933
+
934
+ expect(markdown).toContain("[TRUNCATED]");
935
+ });
936
+
937
+ test("should format context with file elements", async () => {
938
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
939
+
940
+ try {
941
+ await fs.writeFile(path.join(tempDir, "helper.ts"), "export function helper() {}");
942
+
943
+ const prd = createTestPRD([
944
+ {
945
+ id: "US-001",
946
+ title: "Story with File",
947
+ description: "Test",
948
+ acceptanceCriteria: ["AC1"],
949
+ contextFiles: ["helper.ts"],
950
+ },
951
+ ]);
952
+
953
+ const storyContext: StoryContext = {
954
+ prd,
955
+ currentStoryId: "US-001",
956
+ workdir: tempDir,
957
+ };
958
+
959
+ const budget: ContextBudget = {
960
+ maxTokens: 10000,
961
+ reservedForInstructions: 1000,
962
+ availableForContext: 9000,
963
+ };
964
+
965
+ const built = await buildContext(storyContext, budget);
966
+ const markdown = formatContextAsMarkdown(built);
967
+
968
+ expect(markdown).toContain("# Story Context");
969
+ expect(markdown).toContain("## Relevant Source Files");
970
+ expect(markdown).toContain("helper.ts");
971
+ expect(markdown).toContain("helper()");
972
+ } finally {
973
+ await fs.rm(tempDir, { recursive: true, force: true });
974
+ }
975
+ });
976
+
977
+ test("should format ASSET_CHECK_FAILED errors as mandatory instructions", async () => {
978
+ const prd = createTestPRD([
979
+ {
980
+ id: "US-001",
981
+ title: "Story with asset check failure",
982
+ description: "Test description",
983
+ acceptanceCriteria: ["AC1"],
984
+ priorErrors: [
985
+ "ASSET_CHECK_FAILED: Missing files: [src/finder.ts, test/finder.test.ts]\nAction: Create the missing files before tests can run.",
986
+ ],
987
+ },
988
+ ]);
989
+
990
+ const storyContext: StoryContext = {
991
+ prd,
992
+ currentStoryId: "US-001",
993
+ };
994
+
995
+ const budget: ContextBudget = {
996
+ maxTokens: 10000,
997
+ reservedForInstructions: 1000,
998
+ availableForContext: 9000,
999
+ };
1000
+
1001
+ const built = await buildContext(storyContext, budget);
1002
+ const markdown = formatContextAsMarkdown(built);
1003
+
1004
+ // Verify ASSET_CHECK errors are formatted prominently
1005
+ expect(markdown).toContain("⚠️ MANDATORY: Missing Files from Previous Attempts");
1006
+ expect(markdown).toContain("CRITICAL");
1007
+ expect(markdown).toContain("You MUST create these exact files");
1008
+ expect(markdown).toContain("Do NOT use alternative filenames");
1009
+ expect(markdown).toContain("**Required files:**");
1010
+ expect(markdown).toContain("`src/finder.ts`");
1011
+ expect(markdown).toContain("`test/finder.test.ts`");
1012
+
1013
+ // Verify it's NOT in the generic "Prior Errors" section
1014
+ expect(markdown).not.toContain("## Prior Errors");
1015
+ });
1016
+
1017
+ test("should format mixed ASSET_CHECK and other errors separately", async () => {
1018
+ const prd = createTestPRD([
1019
+ {
1020
+ id: "US-001",
1021
+ title: "Story with multiple error types",
1022
+ description: "Test description",
1023
+ acceptanceCriteria: ["AC1"],
1024
+ priorErrors: [
1025
+ "ASSET_CHECK_FAILED: Missing files: [src/utils.ts]\nAction: Create the missing files before tests can run.",
1026
+ 'TypeError: Cannot read property "foo" of undefined',
1027
+ "Test execution failed",
1028
+ ],
1029
+ },
1030
+ ]);
1031
+
1032
+ const storyContext: StoryContext = {
1033
+ prd,
1034
+ currentStoryId: "US-001",
1035
+ };
1036
+
1037
+ const budget: ContextBudget = {
1038
+ maxTokens: 10000,
1039
+ reservedForInstructions: 1000,
1040
+ availableForContext: 9000,
1041
+ };
1042
+
1043
+ const built = await buildContext(storyContext, budget);
1044
+ const markdown = formatContextAsMarkdown(built);
1045
+
1046
+ // Verify ASSET_CHECK errors section exists
1047
+ expect(markdown).toContain("⚠️ MANDATORY: Missing Files from Previous Attempts");
1048
+ expect(markdown).toContain("`src/utils.ts`");
1049
+
1050
+ // Verify other errors are in separate section
1051
+ expect(markdown).toContain("## Prior Errors");
1052
+ expect(markdown).toContain('TypeError: Cannot read property "foo" of undefined');
1053
+ expect(markdown).toContain("Test execution failed");
1054
+ });
1055
+
1056
+ test("should handle non-ASSET_CHECK errors normally", async () => {
1057
+ const prd = createTestPRD([
1058
+ {
1059
+ id: "US-001",
1060
+ title: "Story with regular errors",
1061
+ description: "Test description",
1062
+ acceptanceCriteria: ["AC1"],
1063
+ priorErrors: ['TypeError: Cannot read property "foo" of undefined', "Test execution failed"],
1064
+ },
1065
+ ]);
1066
+
1067
+ const storyContext: StoryContext = {
1068
+ prd,
1069
+ currentStoryId: "US-001",
1070
+ };
1071
+
1072
+ const budget: ContextBudget = {
1073
+ maxTokens: 10000,
1074
+ reservedForInstructions: 1000,
1075
+ availableForContext: 9000,
1076
+ };
1077
+
1078
+ const built = await buildContext(storyContext, budget);
1079
+ const markdown = formatContextAsMarkdown(built);
1080
+
1081
+ // Verify only "Prior Errors" section exists (no MANDATORY section)
1082
+ expect(markdown).toContain("## Prior Errors");
1083
+ expect(markdown).toContain('TypeError: Cannot read property "foo" of undefined');
1084
+ expect(markdown).toContain("Test execution failed");
1085
+ expect(markdown).not.toContain("⚠️ MANDATORY");
1086
+ });
1087
+ });
1088
+
1089
+ describe("test coverage scoping", () => {
1090
+ test("should scope test coverage to story contextFiles", async () => {
1091
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1092
+
1093
+ try {
1094
+ // Create test directory and files
1095
+ const testDir = path.join(tempDir, "test");
1096
+ await fs.mkdir(testDir);
1097
+
1098
+ // Create multiple test files
1099
+ await fs.writeFile(
1100
+ path.join(testDir, "health.service.test.ts"),
1101
+ 'describe("Health Service", () => { test("checks health", () => {}); });',
1102
+ );
1103
+ await fs.writeFile(
1104
+ path.join(testDir, "auth.service.test.ts"),
1105
+ 'describe("Auth Service", () => { test("authenticates", () => {}); });',
1106
+ );
1107
+ await fs.writeFile(
1108
+ path.join(testDir, "db.connection.test.ts"),
1109
+ 'describe("DB Connection", () => { test("connects", () => {}); });',
1110
+ );
1111
+
1112
+ const prd = createTestPRD([
1113
+ {
1114
+ id: "US-001",
1115
+ title: "Implement health service",
1116
+ description: "Create health check service",
1117
+ acceptanceCriteria: ["Service works"],
1118
+ contextFiles: ["src/health.service.ts"], // Only health service
1119
+ },
1120
+ ]);
1121
+
1122
+ const storyContext: StoryContext = {
1123
+ prd,
1124
+ currentStoryId: "US-001",
1125
+ workdir: tempDir,
1126
+ config: {
1127
+ context: {
1128
+ testCoverage: {
1129
+ enabled: true,
1130
+ scopeToStory: true, // Enable scoping
1131
+ },
1132
+ },
1133
+ } as any,
1134
+ };
1135
+
1136
+ const budget: ContextBudget = {
1137
+ maxTokens: 10000,
1138
+ reservedForInstructions: 1000,
1139
+ availableForContext: 9000,
1140
+ };
1141
+
1142
+ const built = await buildContext(storyContext, budget);
1143
+ const markdown = formatContextAsMarkdown(built);
1144
+
1145
+ // Should include test coverage element
1146
+ expect(built.elements.some((e) => e.type === "test-coverage")).toBe(true);
1147
+
1148
+ // Should only mention health.service.test.ts, not auth or db tests
1149
+ expect(markdown).toContain("health.service.test.ts");
1150
+ expect(markdown).not.toContain("auth.service.test.ts");
1151
+ expect(markdown).not.toContain("db.connection.test.ts");
1152
+ } finally {
1153
+ await fs.rm(tempDir, { recursive: true, force: true });
1154
+ }
1155
+ });
1156
+
1157
+ test("should scan all tests when scopeToStory=false", async () => {
1158
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1159
+
1160
+ try {
1161
+ const testDir = path.join(tempDir, "test");
1162
+ await fs.mkdir(testDir);
1163
+
1164
+ await fs.writeFile(
1165
+ path.join(testDir, "health.test.ts"),
1166
+ 'describe("Health", () => { test("works", () => {}); });',
1167
+ );
1168
+ await fs.writeFile(path.join(testDir, "auth.test.ts"), 'describe("Auth", () => { test("works", () => {}); });');
1169
+
1170
+ const prd = createTestPRD([
1171
+ {
1172
+ id: "US-001",
1173
+ title: "Story",
1174
+ description: "Test",
1175
+ acceptanceCriteria: ["AC1"],
1176
+ contextFiles: ["src/health.ts"],
1177
+ },
1178
+ ]);
1179
+
1180
+ const storyContext: StoryContext = {
1181
+ prd,
1182
+ currentStoryId: "US-001",
1183
+ workdir: tempDir,
1184
+ config: {
1185
+ context: {
1186
+ testCoverage: {
1187
+ enabled: true,
1188
+ scopeToStory: false, // Disabled - should scan all
1189
+ },
1190
+ },
1191
+ } as any,
1192
+ };
1193
+
1194
+ const budget: ContextBudget = {
1195
+ maxTokens: 10000,
1196
+ reservedForInstructions: 1000,
1197
+ availableForContext: 9000,
1198
+ };
1199
+
1200
+ const built = await buildContext(storyContext, budget);
1201
+ const markdown = formatContextAsMarkdown(built);
1202
+
1203
+ // Should include both test files
1204
+ expect(markdown).toContain("health.test.ts");
1205
+ expect(markdown).toContain("auth.test.ts");
1206
+ } finally {
1207
+ await fs.rm(tempDir, { recursive: true, force: true });
1208
+ }
1209
+ });
1210
+
1211
+ test("should fall back to full scan when no contextFiles provided", async () => {
1212
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1213
+
1214
+ try {
1215
+ const testDir = path.join(tempDir, "test");
1216
+ await fs.mkdir(testDir);
1217
+
1218
+ await fs.writeFile(
1219
+ path.join(testDir, "test1.test.ts"),
1220
+ 'describe("Test1", () => { test("works", () => {}); });',
1221
+ );
1222
+ await fs.writeFile(
1223
+ path.join(testDir, "test2.test.ts"),
1224
+ 'describe("Test2", () => { test("works", () => {}); });',
1225
+ );
1226
+
1227
+ const prd = createTestPRD([
1228
+ {
1229
+ id: "US-001",
1230
+ title: "Story without contextFiles",
1231
+ description: "Test",
1232
+ acceptanceCriteria: ["AC1"],
1233
+ // No contextFiles
1234
+ },
1235
+ ]);
1236
+
1237
+ const storyContext: StoryContext = {
1238
+ prd,
1239
+ currentStoryId: "US-001",
1240
+ workdir: tempDir,
1241
+ config: {
1242
+ context: {
1243
+ testCoverage: {
1244
+ enabled: true,
1245
+ scopeToStory: true, // true but no contextFiles
1246
+ },
1247
+ },
1248
+ } as any,
1249
+ };
1250
+
1251
+ const budget: ContextBudget = {
1252
+ maxTokens: 10000,
1253
+ reservedForInstructions: 1000,
1254
+ availableForContext: 9000,
1255
+ };
1256
+
1257
+ const built = await buildContext(storyContext, budget);
1258
+ const markdown = formatContextAsMarkdown(built);
1259
+
1260
+ // Should fall back to scanning all files
1261
+ expect(markdown).toContain("test1.test.ts");
1262
+ expect(markdown).toContain("test2.test.ts");
1263
+ } finally {
1264
+ await fs.rm(tempDir, { recursive: true, force: true });
1265
+ }
1266
+ });
1267
+ });
1268
+
1269
+ describe("context isolation", () => {
1270
+ test("should only include current story and declared dependencies — no other stories", async () => {
1271
+ const prd = createTestPRD([
1272
+ {
1273
+ id: "US-001",
1274
+ title: "Define core interfaces",
1275
+ description: "Create base interfaces for the module",
1276
+ acceptanceCriteria: ["Interface exported", "Types documented"],
1277
+ dependencies: [],
1278
+ status: "passed" as any,
1279
+ passes: true,
1280
+ },
1281
+ {
1282
+ id: "US-002",
1283
+ title: "Implement health service",
1284
+ description: "Service that aggregates indicators",
1285
+ acceptanceCriteria: ["Service injectable", "Aggregates results"],
1286
+ dependencies: [],
1287
+ status: "passed" as any,
1288
+ passes: true,
1289
+ },
1290
+ {
1291
+ id: "US-003",
1292
+ title: "Add HTTP indicator",
1293
+ description: "HTTP health check indicator",
1294
+ acceptanceCriteria: ["Pings endpoint", "Returns status"],
1295
+ dependencies: ["US-001"],
1296
+ },
1297
+ {
1298
+ id: "US-004",
1299
+ title: "Add database indicator",
1300
+ description: "Database connectivity check",
1301
+ acceptanceCriteria: ["Checks DB connection", "Timeout support"],
1302
+ dependencies: ["US-001"],
1303
+ },
1304
+ {
1305
+ id: "US-005",
1306
+ title: "REST endpoint",
1307
+ description: "Expose health check via REST API",
1308
+ acceptanceCriteria: ["GET /health returns JSON", "Includes all indicators"],
1309
+ dependencies: ["US-002", "US-003"],
1310
+ },
1311
+ ]);
1312
+
1313
+ // Build context for US-003 which depends only on US-001
1314
+ const storyContext: StoryContext = {
1315
+ prd,
1316
+ currentStoryId: "US-003",
1317
+ };
1318
+
1319
+ const budget: ContextBudget = {
1320
+ maxTokens: 50000,
1321
+ reservedForInstructions: 5000,
1322
+ availableForContext: 45000,
1323
+ };
1324
+
1325
+ const built = await buildContext(storyContext, budget);
1326
+ const markdown = formatContextAsMarkdown(built);
1327
+
1328
+ // Current story IS present
1329
+ expect(markdown).toContain("US-003");
1330
+ expect(markdown).toContain("Add HTTP indicator");
1331
+
1332
+ // Declared dependency IS present
1333
+ expect(markdown).toContain("US-001");
1334
+ expect(markdown).toContain("Define core interfaces");
1335
+
1336
+ // Non-dependency stories are NOT present
1337
+ expect(markdown).not.toContain("US-002");
1338
+ expect(markdown).not.toContain("Implement health service");
1339
+ expect(markdown).not.toContain("US-004");
1340
+ expect(markdown).not.toContain("Add database indicator");
1341
+ expect(markdown).not.toContain("US-005");
1342
+ expect(markdown).not.toContain("REST endpoint");
1343
+
1344
+ // Acceptance criteria from other stories are NOT leaked
1345
+ expect(markdown).not.toContain("Aggregates results");
1346
+ expect(markdown).not.toContain("Checks DB connection");
1347
+ expect(markdown).not.toContain("Includes all indicators");
1348
+ });
1349
+
1350
+ test("progress summary contains only aggregate counts, not story titles or IDs", async () => {
1351
+ const prd = createTestPRD([
1352
+ {
1353
+ id: "US-001",
1354
+ title: "Secret Story Alpha",
1355
+ description: "Should not appear in progress",
1356
+ acceptanceCriteria: ["AC1"],
1357
+ dependencies: [],
1358
+ status: "passed" as any,
1359
+ passes: true,
1360
+ },
1361
+ {
1362
+ id: "US-002",
1363
+ title: "Secret Story Beta",
1364
+ description: "Also should not appear",
1365
+ acceptanceCriteria: ["AC1"],
1366
+ dependencies: [],
1367
+ status: "failed" as any,
1368
+ passes: false,
1369
+ },
1370
+ {
1371
+ id: "US-003",
1372
+ title: "Current Story",
1373
+ description: "The one being built",
1374
+ acceptanceCriteria: ["AC1"],
1375
+ dependencies: [],
1376
+ },
1377
+ ]);
1378
+
1379
+ const storyContext: StoryContext = {
1380
+ prd,
1381
+ currentStoryId: "US-003",
1382
+ };
1383
+
1384
+ const budget: ContextBudget = {
1385
+ maxTokens: 50000,
1386
+ reservedForInstructions: 5000,
1387
+ availableForContext: 45000,
1388
+ };
1389
+
1390
+ const built = await buildContext(storyContext, budget);
1391
+ const progressElement = built.elements.find((e) => e.type === "progress");
1392
+
1393
+ expect(progressElement).toBeDefined();
1394
+ // Progress shows counts only
1395
+ expect(progressElement!.content).toContain("2/3");
1396
+ // Does NOT contain other story titles
1397
+ expect(progressElement!.content).not.toContain("Secret Story Alpha");
1398
+ expect(progressElement!.content).not.toContain("Secret Story Beta");
1399
+ expect(progressElement!.content).not.toContain("US-001");
1400
+ expect(progressElement!.content).not.toContain("US-002");
1401
+ });
1402
+
1403
+ test("prior errors from other stories do not leak into current story context", async () => {
1404
+ const prd = createTestPRD([
1405
+ {
1406
+ id: "US-001",
1407
+ title: "Story with errors",
1408
+ description: "Has prior errors",
1409
+ acceptanceCriteria: ["AC1"],
1410
+ dependencies: [],
1411
+ priorErrors: ["LEAKED_ERROR: something broke in US-001"],
1412
+ },
1413
+ {
1414
+ id: "US-002",
1415
+ title: "Clean story",
1416
+ description: "No errors here",
1417
+ acceptanceCriteria: ["AC1"],
1418
+ dependencies: [],
1419
+ },
1420
+ ]);
1421
+
1422
+ const storyContext: StoryContext = {
1423
+ prd,
1424
+ currentStoryId: "US-002",
1425
+ };
1426
+
1427
+ const budget: ContextBudget = {
1428
+ maxTokens: 50000,
1429
+ reservedForInstructions: 5000,
1430
+ availableForContext: 45000,
1431
+ };
1432
+
1433
+ const built = await buildContext(storyContext, budget);
1434
+ const markdown = formatContextAsMarkdown(built);
1435
+
1436
+ expect(markdown).not.toContain("LEAKED_ERROR");
1437
+ expect(markdown).not.toContain("US-001");
1438
+ // No error section at all
1439
+ expect(markdown).not.toContain("## Prior Errors");
1440
+ });
1441
+
1442
+ test("context elements only contain expected types for a story with no deps/errors", async () => {
1443
+ const prd = createTestPRD([
1444
+ {
1445
+ id: "US-001",
1446
+ title: "Solo story",
1447
+ description: "No dependencies",
1448
+ acceptanceCriteria: ["AC1"],
1449
+ dependencies: [],
1450
+ },
1451
+ ]);
1452
+
1453
+ const storyContext: StoryContext = {
1454
+ prd,
1455
+ currentStoryId: "US-001",
1456
+ };
1457
+
1458
+ const budget: ContextBudget = {
1459
+ maxTokens: 50000,
1460
+ reservedForInstructions: 5000,
1461
+ availableForContext: 45000,
1462
+ };
1463
+
1464
+ const built = await buildContext(storyContext, budget);
1465
+
1466
+ // Should only have progress + current story (no deps, no errors, no files, no test-coverage without workdir)
1467
+ const types = built.elements.map((e) => e.type);
1468
+ expect(types).toContain("progress");
1469
+ expect(types).toContain("story");
1470
+ expect(types).not.toContain("dependency");
1471
+ expect(types).not.toContain("error");
1472
+
1473
+ // All story elements reference only US-001
1474
+ const storyElements = built.elements.filter((e) => e.storyId);
1475
+ for (const el of storyElements) {
1476
+ expect(el.storyId).toBe("US-001");
1477
+ }
1478
+ });
1479
+ });
1480
+
1481
+ describe("context auto-detection (BUG-006)", () => {
1482
+ test("should auto-detect files when contextFiles is empty", async () => {
1483
+ // Create temp git repo
1484
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1485
+
1486
+ try {
1487
+ // Initialize git
1488
+ await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
1489
+ await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
1490
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
1491
+
1492
+ // Create files matching story keywords
1493
+ await fs.mkdir(path.join(tempDir, "src/routing"), { recursive: true });
1494
+ await fs.writeFile(path.join(tempDir, "src/routing/router.ts"), "export class Router { /* routing logic */ }");
1495
+ await fs.writeFile(
1496
+ path.join(tempDir, "src/routing/chain.ts"),
1497
+ "export class RouterChain { /* chain logic */ }",
1498
+ );
1499
+
1500
+ // Commit so git grep can find them
1501
+ await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
1502
+ await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
1503
+
1504
+ const prd = createTestPRD([
1505
+ {
1506
+ id: "US-001",
1507
+ title: "Fix routing chain bug",
1508
+ description: "Fix issue in router chain",
1509
+ acceptanceCriteria: ["Router chain works correctly"],
1510
+ // No contextFiles - should auto-detect
1511
+ },
1512
+ ]);
1513
+
1514
+ const storyContext: StoryContext = {
1515
+ prd,
1516
+ currentStoryId: "US-001",
1517
+ workdir: tempDir,
1518
+ config: {
1519
+ context: {
1520
+ autoDetect: {
1521
+ enabled: true,
1522
+ maxFiles: 5,
1523
+ traceImports: false,
1524
+ },
1525
+ testCoverage: {
1526
+ enabled: false, // Disable to isolate auto-detect test
1527
+ },
1528
+ },
1529
+ } as any,
1530
+ };
1531
+
1532
+ const budget: ContextBudget = {
1533
+ maxTokens: 10000,
1534
+ reservedForInstructions: 1000,
1535
+ availableForContext: 9000,
1536
+ };
1537
+
1538
+ const built = await buildContext(storyContext, budget);
1539
+ const fileElements = built.elements.filter((e) => e.type === "file");
1540
+
1541
+ // Should auto-detect routing files
1542
+ expect(fileElements.length).toBeGreaterThan(0);
1543
+ const filePaths = fileElements.map((e) => e.filePath);
1544
+ expect(filePaths).toContain("src/routing/router.ts");
1545
+ expect(filePaths).toContain("src/routing/chain.ts");
1546
+ } finally {
1547
+ await fs.rm(tempDir, { recursive: true, force: true });
1548
+ }
1549
+ });
1550
+
1551
+ test("should skip auto-detection when contextFiles is provided", async () => {
1552
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1553
+
1554
+ try {
1555
+ await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
1556
+ await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
1557
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
1558
+
1559
+ await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
1560
+ await fs.writeFile(path.join(tempDir, "src/explicit.ts"), "export const explicit = true;");
1561
+ await fs.writeFile(path.join(tempDir, "src/routing.ts"), "export const routing = true;");
1562
+
1563
+ await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
1564
+ await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
1565
+
1566
+ const prd = createTestPRD([
1567
+ {
1568
+ id: "US-001",
1569
+ title: "Fix routing bug",
1570
+ description: "Fix routing",
1571
+ acceptanceCriteria: ["Works"],
1572
+ contextFiles: ["src/explicit.ts"], // Explicit file provided
1573
+ },
1574
+ ]);
1575
+
1576
+ const storyContext: StoryContext = {
1577
+ prd,
1578
+ currentStoryId: "US-001",
1579
+ workdir: tempDir,
1580
+ config: {
1581
+ context: {
1582
+ autoDetect: {
1583
+ enabled: true,
1584
+ maxFiles: 5,
1585
+ traceImports: false,
1586
+ },
1587
+ testCoverage: {
1588
+ enabled: false,
1589
+ },
1590
+ },
1591
+ } as any,
1592
+ };
1593
+
1594
+ const budget: ContextBudget = {
1595
+ maxTokens: 10000,
1596
+ reservedForInstructions: 1000,
1597
+ availableForContext: 9000,
1598
+ };
1599
+
1600
+ const built = await buildContext(storyContext, budget);
1601
+ const fileElements = built.elements.filter((e) => e.type === "file");
1602
+
1603
+ // Should only load explicit file, NOT auto-detect
1604
+ expect(fileElements.length).toBe(1);
1605
+ expect(fileElements[0].filePath).toBe("src/explicit.ts");
1606
+ } finally {
1607
+ await fs.rm(tempDir, { recursive: true, force: true });
1608
+ }
1609
+ });
1610
+
1611
+ test("should skip auto-detection when disabled in config", async () => {
1612
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1613
+
1614
+ try {
1615
+ await Bun.spawn(["git", "init"], { cwd: tempDir }).exited;
1616
+ await Bun.spawn(["git", "config", "user.email", "test@test.com"], { cwd: tempDir }).exited;
1617
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: tempDir }).exited;
1618
+
1619
+ await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
1620
+ await fs.writeFile(path.join(tempDir, "src/routing.ts"), "export const routing = true;");
1621
+
1622
+ await Bun.spawn(["git", "add", "."], { cwd: tempDir }).exited;
1623
+ await Bun.spawn(["git", "commit", "-m", "initial"], { cwd: tempDir }).exited;
1624
+
1625
+ const prd = createTestPRD([
1626
+ {
1627
+ id: "US-001",
1628
+ title: "Fix routing bug",
1629
+ description: "Fix routing",
1630
+ acceptanceCriteria: ["Works"],
1631
+ // No contextFiles
1632
+ },
1633
+ ]);
1634
+
1635
+ const storyContext: StoryContext = {
1636
+ prd,
1637
+ currentStoryId: "US-001",
1638
+ workdir: tempDir,
1639
+ config: {
1640
+ context: {
1641
+ autoDetect: {
1642
+ enabled: false, // Disabled
1643
+ maxFiles: 5,
1644
+ traceImports: false,
1645
+ },
1646
+ testCoverage: {
1647
+ enabled: false,
1648
+ },
1649
+ },
1650
+ } as any,
1651
+ };
1652
+
1653
+ const budget: ContextBudget = {
1654
+ maxTokens: 10000,
1655
+ reservedForInstructions: 1000,
1656
+ availableForContext: 9000,
1657
+ };
1658
+
1659
+ const built = await buildContext(storyContext, budget);
1660
+ const fileElements = built.elements.filter((e) => e.type === "file");
1661
+
1662
+ // Should NOT auto-detect when disabled
1663
+ expect(fileElements.length).toBe(0);
1664
+ } finally {
1665
+ await fs.rm(tempDir, { recursive: true, force: true });
1666
+ }
1667
+ });
1668
+
1669
+ test("should handle auto-detection failure gracefully", async () => {
1670
+ // Non-git directory - git grep will fail
1671
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nax-test-"));
1672
+
1673
+ try {
1674
+ await fs.mkdir(path.join(tempDir, "src"), { recursive: true });
1675
+ await fs.writeFile(path.join(tempDir, "src/file.ts"), "export const test = true;");
1676
+
1677
+ const prd = createTestPRD([
1678
+ {
1679
+ id: "US-001",
1680
+ title: "Fix test bug",
1681
+ description: "Fix test",
1682
+ acceptanceCriteria: ["Works"],
1683
+ // No contextFiles
1684
+ },
1685
+ ]);
1686
+
1687
+ const storyContext: StoryContext = {
1688
+ prd,
1689
+ currentStoryId: "US-001",
1690
+ workdir: tempDir,
1691
+ config: {
1692
+ context: {
1693
+ autoDetect: {
1694
+ enabled: true,
1695
+ maxFiles: 5,
1696
+ traceImports: false,
1697
+ },
1698
+ testCoverage: {
1699
+ enabled: false,
1700
+ },
1701
+ },
1702
+ } as any,
1703
+ };
1704
+
1705
+ const budget: ContextBudget = {
1706
+ maxTokens: 10000,
1707
+ reservedForInstructions: 1000,
1708
+ availableForContext: 9000,
1709
+ };
1710
+
1711
+ // Should not throw, just log warning and continue
1712
+ const built = await buildContext(storyContext, budget);
1713
+ const fileElements = built.elements.filter((e) => e.type === "file");
1714
+
1715
+ // No files loaded (graceful failure)
1716
+ expect(fileElements.length).toBe(0);
1717
+ } finally {
1718
+ await fs.rm(tempDir, { recursive: true, force: true });
1719
+ }
1720
+ });
1721
+ });
1722
+ });