@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,1679 @@
1
+ /**
2
+ * Runner Tests — Story Batching + TDD Escalation
3
+ *
4
+ * Tests for grouping consecutive simple stories into batches,
5
+ * and TDD escalation handling (retryAsLite, failure category outcomes).
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { groupStoriesIntoBatches, precomputeBatchPlan } from "../../src/execution/batching";
10
+ import type { StoryBatch } from "../../src/execution/batching";
11
+ import { escalateTier } from "../../src/execution/escalation";
12
+ import { buildBatchPrompt } from "../../src/execution/prompts";
13
+ import { resolveMaxAttemptsOutcome } from "../../src/execution/runner";
14
+ import type { UserStory } from "../../src/prd";
15
+ import type { FailureCategory } from "../../src/tdd/types";
16
+
17
+ describe("buildBatchPrompt", () => {
18
+ test("generates prompt with multiple stories", () => {
19
+ const stories: UserStory[] = [
20
+ {
21
+ id: "US-001",
22
+ title: "Add logging",
23
+ description: "Add debug logging to the service",
24
+ acceptanceCriteria: ["Logs are written", "Logs include timestamps"],
25
+ tags: [],
26
+ dependencies: [],
27
+ status: "pending",
28
+ passes: false,
29
+ escalations: [],
30
+ attempts: 0,
31
+ },
32
+ {
33
+ id: "US-002",
34
+ title: "Update config",
35
+ description: "Update the config schema",
36
+ acceptanceCriteria: ["Schema is valid", "Tests pass"],
37
+ tags: [],
38
+ dependencies: [],
39
+ status: "pending",
40
+ passes: false,
41
+ escalations: [],
42
+ attempts: 0,
43
+ },
44
+ ];
45
+
46
+ const prompt = buildBatchPrompt(stories);
47
+
48
+ expect(prompt).toContain("Batch Task: 2 Stories");
49
+ expect(prompt).toContain("## Story 1: US-001 — Add logging");
50
+ expect(prompt).toContain("## Story 2: US-002 — Update config");
51
+ expect(prompt).toContain("Add debug logging to the service");
52
+ expect(prompt).toContain("Update the config schema");
53
+ expect(prompt).toContain("Commit each story separately");
54
+ });
55
+
56
+ test("includes context markdown when provided", () => {
57
+ const stories: UserStory[] = [
58
+ {
59
+ id: "US-001",
60
+ title: "Add logging",
61
+ description: "Add logging",
62
+ acceptanceCriteria: ["Logs work"],
63
+ tags: [],
64
+ dependencies: [],
65
+ status: "pending",
66
+ passes: false,
67
+ escalations: [],
68
+ attempts: 0,
69
+ },
70
+ ];
71
+
72
+ const contextMarkdown = "## Context\n\nSome context here";
73
+ const prompt = buildBatchPrompt(stories, contextMarkdown);
74
+
75
+ expect(prompt).toContain("---");
76
+ expect(prompt).toContain("## Context");
77
+ expect(prompt).toContain("Some context here");
78
+ });
79
+
80
+ test("numbers stories sequentially", () => {
81
+ const stories: UserStory[] = [
82
+ {
83
+ id: "US-001",
84
+ title: "First",
85
+ description: "First story",
86
+ acceptanceCriteria: ["AC1"],
87
+ tags: [],
88
+ dependencies: [],
89
+ status: "pending",
90
+ passes: false,
91
+ escalations: [],
92
+ attempts: 0,
93
+ },
94
+ {
95
+ id: "US-002",
96
+ title: "Second",
97
+ description: "Second story",
98
+ acceptanceCriteria: ["AC2"],
99
+ tags: [],
100
+ dependencies: [],
101
+ status: "pending",
102
+ passes: false,
103
+ escalations: [],
104
+ attempts: 0,
105
+ },
106
+ {
107
+ id: "US-003",
108
+ title: "Third",
109
+ description: "Third story",
110
+ acceptanceCriteria: ["AC3"],
111
+ tags: [],
112
+ dependencies: [],
113
+ status: "pending",
114
+ passes: false,
115
+ escalations: [],
116
+ attempts: 0,
117
+ },
118
+ ];
119
+
120
+ const prompt = buildBatchPrompt(stories);
121
+
122
+ expect(prompt).toContain("Story 1: US-001");
123
+ expect(prompt).toContain("Story 2: US-002");
124
+ expect(prompt).toContain("Story 3: US-003");
125
+ });
126
+ });
127
+
128
+ describe("groupStoriesIntoBatches", () => {
129
+ test("groups consecutive simple stories into a batch", () => {
130
+ const stories: UserStory[] = [
131
+ {
132
+ id: "US-001",
133
+ title: "Simple 1",
134
+ description: "First simple story",
135
+ acceptanceCriteria: ["AC1"],
136
+ tags: [],
137
+ dependencies: [],
138
+ status: "pending",
139
+ passes: false,
140
+ escalations: [],
141
+ attempts: 0,
142
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
143
+ },
144
+ {
145
+ id: "US-002",
146
+ title: "Simple 2",
147
+ description: "Second simple story",
148
+ acceptanceCriteria: ["AC2"],
149
+ tags: [],
150
+ dependencies: [],
151
+ status: "pending",
152
+ passes: false,
153
+ escalations: [],
154
+ attempts: 0,
155
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
156
+ },
157
+ {
158
+ id: "US-003",
159
+ title: "Simple 3",
160
+ description: "Third simple story",
161
+ acceptanceCriteria: ["AC3"],
162
+ tags: [],
163
+ dependencies: [],
164
+ status: "pending",
165
+ passes: false,
166
+ escalations: [],
167
+ attempts: 0,
168
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
169
+ },
170
+ ];
171
+
172
+ const batches = groupStoriesIntoBatches(stories);
173
+
174
+ expect(batches).toHaveLength(1);
175
+ expect(batches[0].isBatch).toBe(true);
176
+ expect(batches[0].stories).toHaveLength(3);
177
+ expect(batches[0].stories.map((s) => s.id)).toEqual(["US-001", "US-002", "US-003"]);
178
+ });
179
+
180
+ test("enforces max batch size of 4", () => {
181
+ const stories: UserStory[] = Array.from({ length: 6 }, (_, i) => ({
182
+ id: `US-00${i + 1}`,
183
+ title: `Simple ${i + 1}`,
184
+ description: `Story ${i + 1}`,
185
+ acceptanceCriteria: [`AC${i + 1}`],
186
+ tags: [],
187
+ dependencies: [],
188
+ status: "pending" as const,
189
+ passes: false,
190
+ escalations: [],
191
+ attempts: 0,
192
+ routing: {
193
+ complexity: "simple" as const,
194
+ modelTier: "fast" as const,
195
+ testStrategy: "test-after" as const,
196
+ reasoning: "simple",
197
+ },
198
+ }));
199
+
200
+ const batches = groupStoriesIntoBatches(stories);
201
+
202
+ expect(batches).toHaveLength(2);
203
+ expect(batches[0].stories).toHaveLength(4);
204
+ expect(batches[0].isBatch).toBe(true);
205
+ expect(batches[1].stories).toHaveLength(2);
206
+ expect(batches[1].isBatch).toBe(true);
207
+ });
208
+
209
+ test("stops batching at non-simple story", () => {
210
+ const stories: UserStory[] = [
211
+ {
212
+ id: "US-001",
213
+ title: "Simple 1",
214
+ description: "First simple",
215
+ acceptanceCriteria: ["AC1"],
216
+ tags: [],
217
+ dependencies: [],
218
+ status: "pending",
219
+ passes: false,
220
+ escalations: [],
221
+ attempts: 0,
222
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
223
+ },
224
+ {
225
+ id: "US-002",
226
+ title: "Simple 2",
227
+ description: "Second simple",
228
+ acceptanceCriteria: ["AC2"],
229
+ tags: [],
230
+ dependencies: [],
231
+ status: "pending",
232
+ passes: false,
233
+ escalations: [],
234
+ attempts: 0,
235
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
236
+ },
237
+ {
238
+ id: "US-003",
239
+ title: "Complex",
240
+ description: "Complex story",
241
+ acceptanceCriteria: ["AC3"],
242
+ tags: [],
243
+ dependencies: [],
244
+ status: "pending",
245
+ passes: false,
246
+ escalations: [],
247
+ attempts: 0,
248
+ routing: {
249
+ complexity: "complex",
250
+ modelTier: "balanced",
251
+ testStrategy: "three-session-tdd",
252
+ reasoning: "complex",
253
+ },
254
+ },
255
+ {
256
+ id: "US-004",
257
+ title: "Simple 3",
258
+ description: "Third simple",
259
+ acceptanceCriteria: ["AC4"],
260
+ tags: [],
261
+ dependencies: [],
262
+ status: "pending",
263
+ passes: false,
264
+ escalations: [],
265
+ attempts: 0,
266
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
267
+ },
268
+ ];
269
+
270
+ const batches = groupStoriesIntoBatches(stories);
271
+
272
+ expect(batches).toHaveLength(3);
273
+ // First batch: 2 simple stories
274
+ expect(batches[0].stories).toHaveLength(2);
275
+ expect(batches[0].isBatch).toBe(true);
276
+ expect(batches[0].stories.map((s) => s.id)).toEqual(["US-001", "US-002"]);
277
+ // Second batch: 1 complex story (not batched)
278
+ expect(batches[1].stories).toHaveLength(1);
279
+ expect(batches[1].isBatch).toBe(false);
280
+ expect(batches[1].stories[0].id).toBe("US-003");
281
+ // Third batch: 1 simple story (single, marked as batch)
282
+ expect(batches[2].stories).toHaveLength(1);
283
+ expect(batches[2].isBatch).toBe(false);
284
+ expect(batches[2].stories[0].id).toBe("US-004");
285
+ });
286
+
287
+ test("handles single story as non-batch", () => {
288
+ const stories: UserStory[] = [
289
+ {
290
+ id: "US-001",
291
+ title: "Simple",
292
+ description: "Single story",
293
+ acceptanceCriteria: ["AC1"],
294
+ tags: [],
295
+ dependencies: [],
296
+ status: "pending",
297
+ passes: false,
298
+ escalations: [],
299
+ attempts: 0,
300
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
301
+ },
302
+ ];
303
+
304
+ const batches = groupStoriesIntoBatches(stories);
305
+
306
+ expect(batches).toHaveLength(1);
307
+ expect(batches[0].isBatch).toBe(false);
308
+ expect(batches[0].stories).toHaveLength(1);
309
+ });
310
+
311
+ test("handles all non-simple stories", () => {
312
+ const stories: UserStory[] = [
313
+ {
314
+ id: "US-001",
315
+ title: "Complex 1",
316
+ description: "First complex",
317
+ acceptanceCriteria: ["AC1"],
318
+ tags: [],
319
+ dependencies: [],
320
+ status: "pending",
321
+ passes: false,
322
+ escalations: [],
323
+ attempts: 0,
324
+ routing: {
325
+ complexity: "complex",
326
+ modelTier: "balanced",
327
+ testStrategy: "three-session-tdd",
328
+ reasoning: "complex",
329
+ },
330
+ },
331
+ {
332
+ id: "US-002",
333
+ title: "Medium",
334
+ description: "Medium story",
335
+ acceptanceCriteria: ["AC2"],
336
+ tags: [],
337
+ dependencies: [],
338
+ status: "pending",
339
+ passes: false,
340
+ escalations: [],
341
+ attempts: 0,
342
+ routing: { complexity: "medium", modelTier: "fast", testStrategy: "test-after", reasoning: "medium" },
343
+ },
344
+ ];
345
+
346
+ const batches = groupStoriesIntoBatches(stories);
347
+
348
+ expect(batches).toHaveLength(2);
349
+ expect(batches[0].isBatch).toBe(false);
350
+ expect(batches[0].stories).toHaveLength(1);
351
+ expect(batches[1].isBatch).toBe(false);
352
+ expect(batches[1].stories).toHaveLength(1);
353
+ });
354
+
355
+ test("handles empty story list", () => {
356
+ const stories: UserStory[] = [];
357
+ const batches = groupStoriesIntoBatches(stories);
358
+
359
+ expect(batches).toHaveLength(0);
360
+ });
361
+
362
+ test("respects custom max batch size", () => {
363
+ const stories: UserStory[] = Array.from({ length: 5 }, (_, i) => ({
364
+ id: `US-00${i + 1}`,
365
+ title: `Simple ${i + 1}`,
366
+ description: `Story ${i + 1}`,
367
+ acceptanceCriteria: [`AC${i + 1}`],
368
+ tags: [],
369
+ dependencies: [],
370
+ status: "pending" as const,
371
+ passes: false,
372
+ escalations: [],
373
+ attempts: 0,
374
+ routing: {
375
+ complexity: "simple" as const,
376
+ modelTier: "fast" as const,
377
+ testStrategy: "test-after" as const,
378
+ reasoning: "simple",
379
+ },
380
+ }));
381
+
382
+ const batches = groupStoriesIntoBatches(stories, 2);
383
+
384
+ expect(batches).toHaveLength(3);
385
+ expect(batches[0].stories).toHaveLength(2);
386
+ expect(batches[1].stories).toHaveLength(2);
387
+ expect(batches[2].stories).toHaveLength(1);
388
+ });
389
+
390
+ test("handles mixed complexity pattern", () => {
391
+ const stories: UserStory[] = [
392
+ {
393
+ id: "US-001",
394
+ title: "Simple 1",
395
+ description: "Simple",
396
+ acceptanceCriteria: ["AC1"],
397
+ tags: [],
398
+ dependencies: [],
399
+ status: "pending",
400
+ passes: false,
401
+ escalations: [],
402
+ attempts: 0,
403
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
404
+ },
405
+ {
406
+ id: "US-002",
407
+ title: "Complex",
408
+ description: "Complex",
409
+ acceptanceCriteria: ["AC2"],
410
+ tags: [],
411
+ dependencies: [],
412
+ status: "pending",
413
+ passes: false,
414
+ escalations: [],
415
+ attempts: 0,
416
+ routing: {
417
+ complexity: "complex",
418
+ modelTier: "balanced",
419
+ testStrategy: "three-session-tdd",
420
+ reasoning: "complex",
421
+ },
422
+ },
423
+ {
424
+ id: "US-003",
425
+ title: "Simple 2",
426
+ description: "Simple",
427
+ acceptanceCriteria: ["AC3"],
428
+ tags: [],
429
+ dependencies: [],
430
+ status: "pending",
431
+ passes: false,
432
+ escalations: [],
433
+ attempts: 0,
434
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
435
+ },
436
+ {
437
+ id: "US-004",
438
+ title: "Simple 3",
439
+ description: "Simple",
440
+ acceptanceCriteria: ["AC4"],
441
+ tags: [],
442
+ dependencies: [],
443
+ status: "pending",
444
+ passes: false,
445
+ escalations: [],
446
+ attempts: 0,
447
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
448
+ },
449
+ ];
450
+
451
+ const batches = groupStoriesIntoBatches(stories);
452
+
453
+ expect(batches).toHaveLength(3);
454
+ // US-001 alone
455
+ expect(batches[0].isBatch).toBe(false);
456
+ expect(batches[0].stories).toHaveLength(1);
457
+ // US-002 alone
458
+ expect(batches[1].isBatch).toBe(false);
459
+ expect(batches[1].stories).toHaveLength(1);
460
+ // US-003 and US-004 batched
461
+ expect(batches[2].isBatch).toBe(true);
462
+ expect(batches[2].stories).toHaveLength(2);
463
+ });
464
+ });
465
+
466
+ describe("precomputeBatchPlan", () => {
467
+ test("precomputes batch plan from ready stories", () => {
468
+ const stories: UserStory[] = [
469
+ {
470
+ id: "US-001",
471
+ title: "Simple 1",
472
+ description: "First simple",
473
+ acceptanceCriteria: ["AC1"],
474
+ tags: [],
475
+ dependencies: [],
476
+ status: "pending",
477
+ passes: false,
478
+ escalations: [],
479
+ attempts: 0,
480
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
481
+ },
482
+ {
483
+ id: "US-002",
484
+ title: "Simple 2",
485
+ description: "Second simple",
486
+ acceptanceCriteria: ["AC2"],
487
+ tags: [],
488
+ dependencies: [],
489
+ status: "pending",
490
+ passes: false,
491
+ escalations: [],
492
+ attempts: 0,
493
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
494
+ },
495
+ {
496
+ id: "US-003",
497
+ title: "Complex",
498
+ description: "Complex story",
499
+ acceptanceCriteria: ["AC3"],
500
+ tags: [],
501
+ dependencies: [],
502
+ status: "pending",
503
+ passes: false,
504
+ escalations: [],
505
+ attempts: 0,
506
+ routing: {
507
+ complexity: "complex",
508
+ modelTier: "balanced",
509
+ testStrategy: "three-session-tdd",
510
+ reasoning: "complex",
511
+ },
512
+ },
513
+ ];
514
+
515
+ const plan = precomputeBatchPlan(stories);
516
+
517
+ expect(plan).toHaveLength(2);
518
+ // First batch: 2 simple stories
519
+ expect(plan[0].stories).toHaveLength(2);
520
+ expect(plan[0].isBatch).toBe(true);
521
+ expect(plan[0].stories.map((s) => s.id)).toEqual(["US-001", "US-002"]);
522
+ // Second batch: 1 complex story
523
+ expect(plan[1].stories).toHaveLength(1);
524
+ expect(plan[1].isBatch).toBe(false);
525
+ expect(plan[1].stories[0].id).toBe("US-003");
526
+ });
527
+
528
+ test("maintains story order from PRD", () => {
529
+ const stories: UserStory[] = [
530
+ {
531
+ id: "US-001",
532
+ title: "Simple 1",
533
+ description: "First",
534
+ acceptanceCriteria: ["AC1"],
535
+ tags: [],
536
+ dependencies: [],
537
+ status: "pending",
538
+ passes: false,
539
+ escalations: [],
540
+ attempts: 0,
541
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
542
+ },
543
+ {
544
+ id: "US-002",
545
+ title: "Complex",
546
+ description: "Middle",
547
+ acceptanceCriteria: ["AC2"],
548
+ tags: [],
549
+ dependencies: [],
550
+ status: "pending",
551
+ passes: false,
552
+ escalations: [],
553
+ attempts: 0,
554
+ routing: { complexity: "medium", modelTier: "balanced", testStrategy: "test-after", reasoning: "medium" },
555
+ },
556
+ {
557
+ id: "US-003",
558
+ title: "Simple 2",
559
+ description: "Last",
560
+ acceptanceCriteria: ["AC3"],
561
+ tags: [],
562
+ dependencies: [],
563
+ status: "pending",
564
+ passes: false,
565
+ escalations: [],
566
+ attempts: 0,
567
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
568
+ },
569
+ ];
570
+
571
+ const plan = precomputeBatchPlan(stories);
572
+
573
+ // Should maintain order: US-001, US-002, US-003
574
+ expect(plan).toHaveLength(3);
575
+ expect(plan[0].stories[0].id).toBe("US-001");
576
+ expect(plan[1].stories[0].id).toBe("US-002");
577
+ expect(plan[2].stories[0].id).toBe("US-003");
578
+ });
579
+
580
+ test("only batches simple stories with test-after strategy", () => {
581
+ const stories: UserStory[] = [
582
+ {
583
+ id: "US-001",
584
+ title: "Simple TDD",
585
+ description: "Simple but uses TDD",
586
+ acceptanceCriteria: ["AC1"],
587
+ tags: [],
588
+ dependencies: [],
589
+ status: "pending",
590
+ passes: false,
591
+ escalations: [],
592
+ attempts: 0,
593
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "three-session-tdd", reasoning: "simple" },
594
+ },
595
+ {
596
+ id: "US-002",
597
+ title: "Simple test-after",
598
+ description: "Simple with test-after",
599
+ acceptanceCriteria: ["AC2"],
600
+ tags: [],
601
+ dependencies: [],
602
+ status: "pending",
603
+ passes: false,
604
+ escalations: [],
605
+ attempts: 0,
606
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
607
+ },
608
+ ];
609
+
610
+ const plan = precomputeBatchPlan(stories);
611
+
612
+ // US-001 should be individual (TDD), US-002 should be individual (no other simple test-after to batch with)
613
+ expect(plan).toHaveLength(2);
614
+ expect(plan[0].isBatch).toBe(false);
615
+ expect(plan[0].stories[0].id).toBe("US-001");
616
+ expect(plan[1].isBatch).toBe(false);
617
+ expect(plan[1].stories[0].id).toBe("US-002");
618
+ });
619
+
620
+ test("handles empty story list", () => {
621
+ const stories: UserStory[] = [];
622
+ const plan = precomputeBatchPlan(stories);
623
+ expect(plan).toHaveLength(0);
624
+ });
625
+
626
+ test("respects max batch size", () => {
627
+ const stories: UserStory[] = Array.from({ length: 6 }, (_, i) => ({
628
+ id: `US-00${i + 1}`,
629
+ title: `Simple ${i + 1}`,
630
+ description: `Story ${i + 1}`,
631
+ acceptanceCriteria: [`AC${i + 1}`],
632
+ tags: [],
633
+ dependencies: [],
634
+ status: "pending" as const,
635
+ passes: false,
636
+ escalations: [],
637
+ attempts: 0,
638
+ routing: {
639
+ complexity: "simple" as const,
640
+ modelTier: "fast" as const,
641
+ testStrategy: "test-after" as const,
642
+ reasoning: "simple",
643
+ },
644
+ }));
645
+
646
+ const plan = precomputeBatchPlan(stories, 3);
647
+
648
+ // Should create 2 batches of 3
649
+ expect(plan).toHaveLength(2);
650
+ expect(plan[0].stories).toHaveLength(3);
651
+ expect(plan[0].isBatch).toBe(true);
652
+ expect(plan[1].stories).toHaveLength(3);
653
+ expect(plan[1].isBatch).toBe(true);
654
+ });
655
+
656
+ test("handles all stories already passed", () => {
657
+ const stories: UserStory[] = [
658
+ {
659
+ id: "US-001",
660
+ title: "Passed",
661
+ description: "Already done",
662
+ acceptanceCriteria: ["AC1"],
663
+ tags: [],
664
+ dependencies: [],
665
+ status: "passed",
666
+ passes: true,
667
+ escalations: [],
668
+ attempts: 1,
669
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
670
+ },
671
+ ];
672
+
673
+ const plan = precomputeBatchPlan(stories);
674
+
675
+ // Should still include passed story in plan (filtering happens at runtime)
676
+ expect(plan).toHaveLength(1);
677
+ expect(plan[0].stories[0].id).toBe("US-001");
678
+ });
679
+ });
680
+
681
+ describe("Batch Failure Escalation Strategy", () => {
682
+ test("batch failure should escalate only first story, others remain at same tier", () => {
683
+ // Simulate a batch of 4 simple stories at 'fast' tier
684
+ const batchStories: UserStory[] = [
685
+ {
686
+ id: "US-001",
687
+ title: "Simple 1",
688
+ description: "First story in batch",
689
+ acceptanceCriteria: ["AC1"],
690
+ tags: [],
691
+ dependencies: [],
692
+ status: "pending",
693
+ passes: false,
694
+ escalations: [],
695
+ attempts: 0,
696
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
697
+ },
698
+ {
699
+ id: "US-002",
700
+ title: "Simple 2",
701
+ description: "Second story in batch",
702
+ acceptanceCriteria: ["AC2"],
703
+ tags: [],
704
+ dependencies: [],
705
+ status: "pending",
706
+ passes: false,
707
+ escalations: [],
708
+ attempts: 0,
709
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
710
+ },
711
+ {
712
+ id: "US-003",
713
+ title: "Simple 3",
714
+ description: "Third story in batch",
715
+ acceptanceCriteria: ["AC3"],
716
+ tags: [],
717
+ dependencies: [],
718
+ status: "pending",
719
+ passes: false,
720
+ escalations: [],
721
+ attempts: 0,
722
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
723
+ },
724
+ {
725
+ id: "US-004",
726
+ title: "Simple 4",
727
+ description: "Fourth story in batch",
728
+ acceptanceCriteria: ["AC4"],
729
+ tags: [],
730
+ dependencies: [],
731
+ status: "pending",
732
+ passes: false,
733
+ escalations: [],
734
+ attempts: 0,
735
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
736
+ },
737
+ ];
738
+
739
+ // When batch fails at 'fast' tier:
740
+ // 1. First story (US-001) should escalate to 'balanced'
741
+ const firstStory = batchStories[0];
742
+ const currentTier = firstStory.routing!.modelTier;
743
+ const tierOrder = [
744
+ { tier: "fast", attempts: 5 },
745
+ { tier: "balanced", attempts: 3 },
746
+ { tier: "powerful", attempts: 2 },
747
+ ];
748
+ const nextTier = escalateTier(currentTier!, tierOrder);
749
+
750
+ expect(currentTier).toBe("fast");
751
+ expect(nextTier).toBe("balanced");
752
+
753
+ // 2. Remaining stories (US-002, US-003, US-004) should remain at 'fast' tier
754
+ // They will be retried individually at the same tier on next iteration
755
+ const remainingStories = batchStories.slice(1);
756
+ for (const story of remainingStories) {
757
+ expect(story.routing!.modelTier).toBe("fast");
758
+ expect(story.status).toBe("pending");
759
+ }
760
+
761
+ // 3. This tests the documented "Option B" strategy:
762
+ // - Only first story escalates
763
+ // - Others retry individually at same tier first
764
+ // - This minimizes cost and provides better error isolation
765
+ });
766
+
767
+ test("batch failure escalation follows standard escalation chain", () => {
768
+ const tierOrder = [
769
+ { tier: "fast", attempts: 5 },
770
+ { tier: "balanced", attempts: 3 },
771
+ { tier: "powerful", attempts: 2 },
772
+ ];
773
+ const tiers = ["fast", "balanced", "powerful"];
774
+ const expectedNext = ["balanced", "powerful", null];
775
+
776
+ for (let i = 0; i < tiers.length; i++) {
777
+ const nextTier = escalateTier(tiers[i], tierOrder);
778
+ expect(nextTier).toBe(expectedNext[i]);
779
+ }
780
+
781
+ const powerfulTier = escalateTier("powerful", tierOrder);
782
+ expect(powerfulTier).toBeNull();
783
+ });
784
+
785
+ test("batch failure with max attempts should not escalate", () => {
786
+ // When first story in batch has already hit max attempts (e.g., 3),
787
+ // it should be marked as failed instead of escalated
788
+ const story: UserStory = {
789
+ id: "US-001",
790
+ title: "Simple with max attempts",
791
+ description: "Story that has already been retried 3 times",
792
+ acceptanceCriteria: ["AC1"],
793
+ tags: [],
794
+ dependencies: [],
795
+ status: "pending",
796
+ passes: false,
797
+ escalations: [],
798
+ attempts: 3, // Already at max attempts (typical config.autoMode.escalation.maxAttempts = 3)
799
+ routing: { complexity: "simple", modelTier: "balanced", testStrategy: "test-after", reasoning: "simple" },
800
+ };
801
+
802
+ const maxAttempts = 3;
803
+ const escalationEnabled = true;
804
+
805
+ // Should not escalate if attempts >= maxAttempts
806
+ if (escalationEnabled && story.attempts < maxAttempts) {
807
+ // This branch should NOT be taken
808
+ expect(false).toBe(true); // Should not reach here
809
+ } else {
810
+ // Story should be marked as failed (not escalated)
811
+ expect(story.attempts).toBeGreaterThanOrEqual(maxAttempts);
812
+ // In actual runner code, markStoryFailed() would be called here
813
+ }
814
+ });
815
+ });
816
+
817
+ describe("Queue Commands Before Batch Execution", () => {
818
+ test("SKIP command should filter story from batch before execution", () => {
819
+ // Simulate a batch of 3 simple stories: [US-001, US-002, US-003]
820
+ // User issues SKIP US-002 in .queue.txt
821
+ // Expected: Batch should only contain [US-001, US-003]
822
+ const batchStories: UserStory[] = [
823
+ {
824
+ id: "US-001",
825
+ title: "Simple 1",
826
+ description: "First story in batch",
827
+ acceptanceCriteria: ["AC1"],
828
+ tags: [],
829
+ dependencies: [],
830
+ status: "pending",
831
+ passes: false,
832
+ escalations: [],
833
+ attempts: 0,
834
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
835
+ },
836
+ {
837
+ id: "US-002",
838
+ title: "Simple 2",
839
+ description: "Second story in batch (to be skipped)",
840
+ acceptanceCriteria: ["AC2"],
841
+ tags: [],
842
+ dependencies: [],
843
+ status: "pending",
844
+ passes: false,
845
+ escalations: [],
846
+ attempts: 0,
847
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
848
+ },
849
+ {
850
+ id: "US-003",
851
+ title: "Simple 3",
852
+ description: "Third story in batch",
853
+ acceptanceCriteria: ["AC3"],
854
+ tags: [],
855
+ dependencies: [],
856
+ status: "pending",
857
+ passes: false,
858
+ escalations: [],
859
+ attempts: 0,
860
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
861
+ },
862
+ ];
863
+
864
+ // Simulate SKIP command processing
865
+ const skipCommand = { type: "SKIP" as const, storyId: "US-002" };
866
+ const storyIndex = batchStories.findIndex((s) => s.id === skipCommand.storyId);
867
+
868
+ expect(storyIndex).toBe(1);
869
+
870
+ // Remove from batch
871
+ const filteredBatch = batchStories.filter((s) => s.id !== skipCommand.storyId);
872
+
873
+ expect(filteredBatch).toHaveLength(2);
874
+ expect(filteredBatch.map((s) => s.id)).toEqual(["US-001", "US-003"]);
875
+ expect(filteredBatch.every((s) => s.status === "pending")).toBe(true);
876
+ });
877
+
878
+ test("SKIP all stories in batch should result in empty batch and continue to next iteration", () => {
879
+ // Simulate a batch of 2 simple stories: [US-001, US-002]
880
+ // User issues SKIP US-001 and SKIP US-002
881
+ // Expected: Batch becomes empty, runner should continue to next iteration
882
+ const batchStories: UserStory[] = [
883
+ {
884
+ id: "US-001",
885
+ title: "Simple 1",
886
+ description: "First story",
887
+ acceptanceCriteria: ["AC1"],
888
+ tags: [],
889
+ dependencies: [],
890
+ status: "pending",
891
+ passes: false,
892
+ escalations: [],
893
+ attempts: 0,
894
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
895
+ },
896
+ {
897
+ id: "US-002",
898
+ title: "Simple 2",
899
+ description: "Second story",
900
+ acceptanceCriteria: ["AC2"],
901
+ tags: [],
902
+ dependencies: [],
903
+ status: "pending",
904
+ passes: false,
905
+ escalations: [],
906
+ attempts: 0,
907
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
908
+ },
909
+ ];
910
+
911
+ // Simulate SKIP commands
912
+ const skipCommands = [
913
+ { type: "SKIP" as const, storyId: "US-001" },
914
+ { type: "SKIP" as const, storyId: "US-002" },
915
+ ];
916
+
917
+ let filteredBatch = [...batchStories];
918
+ for (const cmd of skipCommands) {
919
+ filteredBatch = filteredBatch.filter((s) => s.id !== cmd.storyId);
920
+ }
921
+
922
+ expect(filteredBatch).toHaveLength(0);
923
+ // When batch is empty, runner should continue to next iteration
924
+ });
925
+
926
+ test("PAUSE command should halt execution before batch starts", () => {
927
+ // When PAUSE is issued before batch execution
928
+ // Expected: Execution stops, no stories are processed
929
+ const pauseCommand = "PAUSE" as const;
930
+ const batchStories: UserStory[] = [
931
+ {
932
+ id: "US-001",
933
+ title: "Simple 1",
934
+ description: "First story",
935
+ acceptanceCriteria: ["AC1"],
936
+ tags: [],
937
+ dependencies: [],
938
+ status: "pending",
939
+ passes: false,
940
+ escalations: [],
941
+ attempts: 0,
942
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
943
+ },
944
+ ];
945
+
946
+ // Simulate PAUSE processing
947
+ let shouldHalt = false;
948
+ if (pauseCommand === "PAUSE") {
949
+ shouldHalt = true;
950
+ }
951
+
952
+ expect(shouldHalt).toBe(true);
953
+ // When halted, no stories should be executed
954
+ expect(batchStories.every((s) => s.status === "pending")).toBe(true);
955
+ });
956
+
957
+ test("SKIP command for story not in batch should still mark it as skipped", () => {
958
+ // Simulate batch [US-001, US-002] but user issues SKIP US-003
959
+ // Expected: US-003 is marked skipped even though not in current batch
960
+ const batchStories: UserStory[] = [
961
+ {
962
+ id: "US-001",
963
+ title: "Simple 1",
964
+ description: "First story",
965
+ acceptanceCriteria: ["AC1"],
966
+ tags: [],
967
+ dependencies: [],
968
+ status: "pending",
969
+ passes: false,
970
+ escalations: [],
971
+ attempts: 0,
972
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
973
+ },
974
+ {
975
+ id: "US-002",
976
+ title: "Simple 2",
977
+ description: "Second story",
978
+ acceptanceCriteria: ["AC2"],
979
+ tags: [],
980
+ dependencies: [],
981
+ status: "pending",
982
+ passes: false,
983
+ escalations: [],
984
+ attempts: 0,
985
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
986
+ },
987
+ ];
988
+
989
+ const skipCommand = { type: "SKIP" as const, storyId: "US-003" };
990
+ const storyIndex = batchStories.findIndex((s) => s.id === skipCommand.storyId);
991
+
992
+ // Story not found in batch
993
+ expect(storyIndex).toBe(-1);
994
+
995
+ // But should still be processed (in actual runner, PRD would be checked)
996
+ // This test validates the logic path exists
997
+ });
998
+
999
+ test("batch size reduction from 4 to 1 should disable batch execution flag", () => {
1000
+ // Simulate batch [US-001, US-002, US-003, US-004]
1001
+ // User issues SKIP US-002, SKIP US-003, SKIP US-004
1002
+ // Expected: Only US-001 remains, isBatchExecution should be false
1003
+ const batchStories: UserStory[] = [
1004
+ {
1005
+ id: "US-001",
1006
+ title: "Simple 1",
1007
+ description: "First story",
1008
+ acceptanceCriteria: ["AC1"],
1009
+ tags: [],
1010
+ dependencies: [],
1011
+ status: "pending",
1012
+ passes: false,
1013
+ escalations: [],
1014
+ attempts: 0,
1015
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1016
+ },
1017
+ {
1018
+ id: "US-002",
1019
+ title: "Simple 2",
1020
+ description: "Second story",
1021
+ acceptanceCriteria: ["AC2"],
1022
+ tags: [],
1023
+ dependencies: [],
1024
+ status: "pending",
1025
+ passes: false,
1026
+ escalations: [],
1027
+ attempts: 0,
1028
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1029
+ },
1030
+ {
1031
+ id: "US-003",
1032
+ title: "Simple 3",
1033
+ description: "Third story",
1034
+ acceptanceCriteria: ["AC3"],
1035
+ tags: [],
1036
+ dependencies: [],
1037
+ status: "pending",
1038
+ passes: false,
1039
+ escalations: [],
1040
+ attempts: 0,
1041
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1042
+ },
1043
+ {
1044
+ id: "US-004",
1045
+ title: "Simple 4",
1046
+ description: "Fourth story",
1047
+ acceptanceCriteria: ["AC4"],
1048
+ tags: [],
1049
+ dependencies: [],
1050
+ status: "pending",
1051
+ passes: false,
1052
+ escalations: [],
1053
+ attempts: 0,
1054
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1055
+ },
1056
+ ];
1057
+
1058
+ let isBatchExecution = true; // Initially true for 4 stories
1059
+ const skipCommands = [
1060
+ { type: "SKIP" as const, storyId: "US-002" },
1061
+ { type: "SKIP" as const, storyId: "US-003" },
1062
+ { type: "SKIP" as const, storyId: "US-004" },
1063
+ ];
1064
+
1065
+ let filteredBatch = [...batchStories];
1066
+ for (const cmd of skipCommands) {
1067
+ filteredBatch = filteredBatch.filter((s) => s.id !== cmd.storyId);
1068
+ }
1069
+
1070
+ // Re-check batch flag
1071
+ if (isBatchExecution && filteredBatch.length === 1) {
1072
+ isBatchExecution = false;
1073
+ }
1074
+
1075
+ expect(filteredBatch).toHaveLength(1);
1076
+ expect(filteredBatch[0].id).toBe("US-001");
1077
+ expect(isBatchExecution).toBe(false);
1078
+ });
1079
+
1080
+ test("ABORT command should mark all pending stories as skipped", () => {
1081
+ // When ABORT is issued
1082
+ // Expected: All pending stories should be marked as skipped
1083
+ const abortCommand = "ABORT" as const;
1084
+ const allStories: UserStory[] = [
1085
+ {
1086
+ id: "US-001",
1087
+ title: "Passed",
1088
+ description: "Already done",
1089
+ acceptanceCriteria: ["AC1"],
1090
+ tags: [],
1091
+ dependencies: [],
1092
+ status: "passed",
1093
+ passes: true,
1094
+ escalations: [],
1095
+ attempts: 1,
1096
+ },
1097
+ {
1098
+ id: "US-002",
1099
+ title: "Pending 1",
1100
+ description: "Not started",
1101
+ acceptanceCriteria: ["AC2"],
1102
+ tags: [],
1103
+ dependencies: [],
1104
+ status: "pending",
1105
+ passes: false,
1106
+ escalations: [],
1107
+ attempts: 0,
1108
+ },
1109
+ {
1110
+ id: "US-003",
1111
+ title: "Pending 2",
1112
+ description: "Not started",
1113
+ acceptanceCriteria: ["AC3"],
1114
+ tags: [],
1115
+ dependencies: [],
1116
+ status: "pending",
1117
+ passes: false,
1118
+ escalations: [],
1119
+ attempts: 0,
1120
+ },
1121
+ ];
1122
+
1123
+ // Simulate ABORT processing
1124
+ let shouldAbort = false;
1125
+ if (abortCommand === "ABORT") {
1126
+ shouldAbort = true;
1127
+ }
1128
+
1129
+ expect(shouldAbort).toBe(true);
1130
+
1131
+ // Filter pending stories that should be skipped
1132
+ const pendingStories = allStories.filter((s) => s.status === "pending");
1133
+ expect(pendingStories).toHaveLength(2);
1134
+ expect(pendingStories.map((s) => s.id)).toEqual(["US-002", "US-003"]);
1135
+
1136
+ // In actual runner, these would be marked as skipped via markStorySkipped()
1137
+ });
1138
+ });
1139
+
1140
+ describe("Configurable Escalation Chain (ADR-003)", () => {
1141
+ const defaultTiers = [
1142
+ { tier: "fast", attempts: 5 },
1143
+ { tier: "balanced", attempts: 3 },
1144
+ { tier: "powerful", attempts: 2 },
1145
+ ];
1146
+
1147
+ test("escalateTier with standard chain", () => {
1148
+ expect(escalateTier("fast", defaultTiers)).toBe("balanced");
1149
+ expect(escalateTier("balanced", defaultTiers)).toBe("powerful");
1150
+ expect(escalateTier("powerful", defaultTiers)).toBeNull();
1151
+ });
1152
+
1153
+ test("escalateTier with custom tierOrder (skip balanced)", () => {
1154
+ const customOrder = [
1155
+ { tier: "fast", attempts: 5 },
1156
+ { tier: "powerful", attempts: 2 },
1157
+ ];
1158
+ expect(escalateTier("fast", customOrder)).toBe("powerful");
1159
+ expect(escalateTier("powerful", customOrder)).toBeNull();
1160
+ expect(escalateTier("balanced", customOrder)).toBeNull();
1161
+ });
1162
+
1163
+ test("escalateTier with single-tier order", () => {
1164
+ const singleTier = [{ tier: "fast", attempts: 10 }];
1165
+ expect(escalateTier("fast", singleTier)).toBeNull();
1166
+ });
1167
+
1168
+ test("escalateTier with reversed order", () => {
1169
+ const reversed = [
1170
+ { tier: "powerful", attempts: 2 },
1171
+ { tier: "balanced", attempts: 3 },
1172
+ { tier: "fast", attempts: 5 },
1173
+ ];
1174
+ expect(escalateTier("powerful", reversed)).toBe("balanced");
1175
+ expect(escalateTier("balanced", reversed)).toBe("fast");
1176
+ expect(escalateTier("fast", reversed)).toBeNull();
1177
+ });
1178
+
1179
+ test("escalateTier with empty tierOrder returns null", () => {
1180
+ expect(escalateTier("fast", [])).toBeNull();
1181
+ });
1182
+
1183
+ test("escalateTier with three-tier standard order", () => {
1184
+ expect(escalateTier("fast", defaultTiers)).toBe("balanced");
1185
+ expect(escalateTier("balanced", defaultTiers)).toBe("powerful");
1186
+ expect(escalateTier("powerful", defaultTiers)).toBeNull();
1187
+ });
1188
+
1189
+ test("escalateTier should return null for unknown tier", () => {
1190
+ expect(escalateTier("unknown", defaultTiers)).toBeNull();
1191
+ });
1192
+
1193
+ test("escalateTier should be idempotent at max tier", () => {
1194
+ expect(escalateTier("powerful", defaultTiers)).toBeNull();
1195
+ // Call again — still null
1196
+ expect(escalateTier("powerful", defaultTiers)).toBeNull();
1197
+ });
1198
+
1199
+ test("calculateMaxIterations sums all tier attempts", () => {
1200
+ const { calculateMaxIterations } = require("../../src/execution/escalation");
1201
+ expect(calculateMaxIterations(defaultTiers)).toBe(10); // 5+3+2
1202
+ expect(calculateMaxIterations([{ tier: "fast", attempts: 1 }])).toBe(1);
1203
+ expect(calculateMaxIterations([])).toBe(0);
1204
+ });
1205
+ });
1206
+
1207
+ describe("Pre-Iteration Escalation (BUG-16, BUG-17)", () => {
1208
+ const defaultTiers = [
1209
+ { tier: "fast", attempts: 5 },
1210
+ { tier: "balanced", attempts: 3 },
1211
+ { tier: "powerful", attempts: 2 },
1212
+ ];
1213
+
1214
+ test("story with attempts >= tier budget should trigger escalation before agent spawn", () => {
1215
+ // Simulate a story at "fast" tier with 5 attempts (budget exhausted)
1216
+ const story: UserStory = {
1217
+ id: "US-001",
1218
+ title: "Test story",
1219
+ description: "Test",
1220
+ acceptanceCriteria: ["AC1"],
1221
+ tags: [],
1222
+ dependencies: [],
1223
+ status: "pending",
1224
+ passes: false,
1225
+ escalations: [],
1226
+ attempts: 5, // Exhausted fast tier budget (5 attempts)
1227
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1228
+ };
1229
+
1230
+ // Get tier config
1231
+ const currentTier = story.routing!.modelTier;
1232
+ const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
1233
+
1234
+ expect(tierCfg).toBeDefined();
1235
+ expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
1236
+
1237
+ // Should escalate to next tier
1238
+ const nextTier = escalateTier(currentTier!, defaultTiers);
1239
+ expect(nextTier).toBe("balanced");
1240
+ });
1241
+
1242
+ test("story at balanced tier with 3 attempts should escalate to powerful", () => {
1243
+ const story: UserStory = {
1244
+ id: "US-002",
1245
+ title: "Test story",
1246
+ description: "Test",
1247
+ acceptanceCriteria: ["AC1"],
1248
+ tags: [],
1249
+ dependencies: [],
1250
+ status: "pending",
1251
+ passes: false,
1252
+ escalations: [],
1253
+ attempts: 3, // Exhausted balanced tier budget (3 attempts)
1254
+ routing: { complexity: "medium", modelTier: "balanced", testStrategy: "test-after", reasoning: "medium" },
1255
+ };
1256
+
1257
+ const currentTier = story.routing!.modelTier;
1258
+ const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
1259
+
1260
+ expect(tierCfg).toBeDefined();
1261
+ expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
1262
+
1263
+ const nextTier = escalateTier(currentTier!, defaultTiers);
1264
+ expect(nextTier).toBe("powerful");
1265
+ });
1266
+
1267
+ test("story at powerful tier with 2 attempts should mark as FAILED (no more tiers)", () => {
1268
+ const story: UserStory = {
1269
+ id: "US-003",
1270
+ title: "Test story",
1271
+ description: "Test",
1272
+ acceptanceCriteria: ["AC1"],
1273
+ tags: [],
1274
+ dependencies: [],
1275
+ status: "pending",
1276
+ passes: false,
1277
+ escalations: [],
1278
+ attempts: 2, // Exhausted powerful tier budget (2 attempts)
1279
+ routing: { complexity: "complex", modelTier: "powerful", testStrategy: "test-after", reasoning: "complex" },
1280
+ };
1281
+
1282
+ const currentTier = story.routing!.modelTier;
1283
+ const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
1284
+
1285
+ expect(tierCfg).toBeDefined();
1286
+ expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
1287
+
1288
+ // No next tier available
1289
+ const nextTier = escalateTier(currentTier!, defaultTiers);
1290
+ expect(nextTier).toBeNull();
1291
+
1292
+ // Story should be marked as FAILED (not retried)
1293
+ // In actual runner code, markStoryFailed() would be called here
1294
+ });
1295
+
1296
+ test("pre-iteration check prevents infinite loop at same tier", () => {
1297
+ // BUG-16: Stories were looping indefinitely at same tier
1298
+ // This test verifies that pre-iteration escalation prevents this
1299
+
1300
+ const story: UserStory = {
1301
+ id: "US-004",
1302
+ title: "ASSET_CHECK failing story",
1303
+ description: "Story with missing files",
1304
+ acceptanceCriteria: ["AC1"],
1305
+ tags: [],
1306
+ dependencies: [],
1307
+ status: "pending",
1308
+ passes: false,
1309
+ escalations: [],
1310
+ attempts: 5, // Budget exhausted
1311
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1312
+ priorErrors: ["ASSET_CHECK_FAILED: Missing file src/test.ts"],
1313
+ };
1314
+
1315
+ // Pre-iteration check should trigger escalation
1316
+ const currentTier = story.routing!.modelTier;
1317
+ const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
1318
+
1319
+ expect(story.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
1320
+
1321
+ // Should escalate instead of retrying at same tier
1322
+ const nextTier = escalateTier(currentTier!, defaultTiers);
1323
+ expect(nextTier).toBe("balanced");
1324
+ });
1325
+
1326
+ test("ASSET_CHECK failure should increment attempts and respect escalation", () => {
1327
+ // BUG-17: ASSET_CHECK failures were reverting to pending without escalation
1328
+ const story: UserStory = {
1329
+ id: "US-005",
1330
+ title: "Story with ASSET_CHECK failure",
1331
+ description: "Test",
1332
+ acceptanceCriteria: ["AC1"],
1333
+ tags: [],
1334
+ dependencies: [],
1335
+ status: "pending",
1336
+ passes: false,
1337
+ escalations: [],
1338
+ attempts: 4, // One attempt left in fast tier
1339
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1340
+ };
1341
+
1342
+ // Simulate ASSET_CHECK failure
1343
+ const updatedStory = {
1344
+ ...story,
1345
+ attempts: story.attempts + 1, // Increment attempts
1346
+ priorErrors: ["ASSET_CHECK_FAILED: Missing file src/finder.ts"],
1347
+ };
1348
+
1349
+ expect(updatedStory.attempts).toBe(5);
1350
+
1351
+ // Now attempts >= tier budget, should escalate on next iteration
1352
+ const tierCfg = defaultTiers.find((t) => t.tier === "fast");
1353
+ expect(updatedStory.attempts).toBeGreaterThanOrEqual(tierCfg!.attempts);
1354
+
1355
+ const nextTier = escalateTier("fast", defaultTiers);
1356
+ expect(nextTier).toBe("balanced");
1357
+ });
1358
+
1359
+ test("story below tier budget should not escalate", () => {
1360
+ const story: UserStory = {
1361
+ id: "US-006",
1362
+ title: "Story with attempts below budget",
1363
+ description: "Test",
1364
+ acceptanceCriteria: ["AC1"],
1365
+ tags: [],
1366
+ dependencies: [],
1367
+ status: "pending",
1368
+ passes: false,
1369
+ escalations: [],
1370
+ attempts: 2, // Below fast tier budget (5 attempts)
1371
+ routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "simple" },
1372
+ };
1373
+
1374
+ const currentTier = story.routing!.modelTier;
1375
+ const tierCfg = defaultTiers.find((t) => t.tier === currentTier);
1376
+
1377
+ expect(tierCfg).toBeDefined();
1378
+ expect(story.attempts).toBeLessThan(tierCfg!.attempts);
1379
+
1380
+ // Should NOT escalate (continue at same tier)
1381
+ });
1382
+ });
1383
+
1384
+ // ─────────────────────────────────────────────────────────────────────────────
1385
+ // T6: resolveMaxAttemptsOutcome — failure category → pause vs fail
1386
+ // ─────────────────────────────────────────────────────────────────────────────
1387
+
1388
+ describe("resolveMaxAttemptsOutcome", () => {
1389
+ describe("categories that require human review → pause", () => {
1390
+ test("isolation-violation → pause", () => {
1391
+ const result = resolveMaxAttemptsOutcome("isolation-violation");
1392
+ expect(result).toBe("pause");
1393
+ });
1394
+
1395
+ test("verifier-rejected → pause", () => {
1396
+ const result = resolveMaxAttemptsOutcome("verifier-rejected");
1397
+ expect(result).toBe("pause");
1398
+ });
1399
+
1400
+ test("greenfield-no-tests → pause", () => {
1401
+ const result = resolveMaxAttemptsOutcome("greenfield-no-tests");
1402
+ expect(result).toBe("pause");
1403
+ });
1404
+ });
1405
+
1406
+ describe("categories that can be failed automatically → fail", () => {
1407
+ test("session-failure → fail", () => {
1408
+ const result = resolveMaxAttemptsOutcome("session-failure");
1409
+ expect(result).toBe("fail");
1410
+ });
1411
+
1412
+ test("tests-failing → fail", () => {
1413
+ const result = resolveMaxAttemptsOutcome("tests-failing");
1414
+ expect(result).toBe("fail");
1415
+ });
1416
+
1417
+ test("undefined (no category) → fail", () => {
1418
+ const result = resolveMaxAttemptsOutcome(undefined);
1419
+ expect(result).toBe("fail");
1420
+ });
1421
+ });
1422
+
1423
+ describe("exhaustive coverage of all FailureCategory values", () => {
1424
+ const pauseCategories: FailureCategory[] = ["isolation-violation", "verifier-rejected", "greenfield-no-tests"];
1425
+ const failCategories: FailureCategory[] = ["session-failure", "tests-failing"];
1426
+
1427
+ for (const cat of pauseCategories) {
1428
+ test(`${cat} always returns pause`, () => {
1429
+ expect(resolveMaxAttemptsOutcome(cat)).toBe("pause");
1430
+ });
1431
+ }
1432
+
1433
+ for (const cat of failCategories) {
1434
+ test(`${cat} always returns fail`, () => {
1435
+ expect(resolveMaxAttemptsOutcome(cat)).toBe("fail");
1436
+ });
1437
+ }
1438
+ });
1439
+ });
1440
+
1441
+ // ─────────────────────────────────────────────────────────────────────────────
1442
+ // T6: retryAsLite routing update logic
1443
+ // ─────────────────────────────────────────────────────────────────────────────
1444
+
1445
+ describe("retryAsLite → testStrategy downgrade", () => {
1446
+ /**
1447
+ * Simulates the routing update logic from the escalate case in runner.ts.
1448
+ * This mirrors the exact transform applied to story.routing when escalating.
1449
+ */
1450
+ function applyEscalationRouting(
1451
+ routing: UserStory["routing"],
1452
+ nextTier: "fast" | "balanced" | "powerful",
1453
+ retryAsLite: boolean,
1454
+ ): UserStory["routing"] {
1455
+ if (!routing) return undefined;
1456
+ return {
1457
+ ...routing,
1458
+ modelTier: nextTier,
1459
+ ...(retryAsLite ? { testStrategy: "three-session-tdd-lite" as const } : {}),
1460
+ };
1461
+ }
1462
+
1463
+ test("retryAsLite=true downgrades testStrategy to three-session-tdd-lite", () => {
1464
+ const routing: UserStory["routing"] = {
1465
+ complexity: "complex",
1466
+ modelTier: "fast",
1467
+ testStrategy: "three-session-tdd",
1468
+ reasoning: "complex",
1469
+ };
1470
+
1471
+ const updated = applyEscalationRouting(routing, "balanced", true);
1472
+
1473
+ expect(updated?.testStrategy).toBe("three-session-tdd-lite");
1474
+ expect(updated?.modelTier).toBe("balanced");
1475
+ expect(updated?.complexity).toBe("complex");
1476
+ });
1477
+
1478
+ test("retryAsLite=false leaves testStrategy unchanged", () => {
1479
+ const routing: UserStory["routing"] = {
1480
+ complexity: "complex",
1481
+ modelTier: "fast",
1482
+ testStrategy: "three-session-tdd",
1483
+ reasoning: "complex",
1484
+ };
1485
+
1486
+ const updated = applyEscalationRouting(routing, "balanced", false);
1487
+
1488
+ expect(updated?.testStrategy).toBe("three-session-tdd");
1489
+ expect(updated?.modelTier).toBe("balanced");
1490
+ });
1491
+
1492
+ test("strategy downgrade happens alongside tier escalation (both applied)", () => {
1493
+ const routing: UserStory["routing"] = {
1494
+ complexity: "complex",
1495
+ modelTier: "fast",
1496
+ testStrategy: "three-session-tdd",
1497
+ reasoning: "complex",
1498
+ };
1499
+
1500
+ const updated = applyEscalationRouting(routing, "powerful", true);
1501
+
1502
+ // Both tier escalation AND strategy downgrade apply simultaneously
1503
+ expect(updated?.modelTier).toBe("powerful");
1504
+ expect(updated?.testStrategy).toBe("three-session-tdd-lite");
1505
+ });
1506
+
1507
+ test("already-lite strategy remains lite after retryAsLite=true", () => {
1508
+ const routing: UserStory["routing"] = {
1509
+ complexity: "complex",
1510
+ modelTier: "fast",
1511
+ testStrategy: "three-session-tdd-lite",
1512
+ reasoning: "complex",
1513
+ };
1514
+
1515
+ const updated = applyEscalationRouting(routing, "balanced", true);
1516
+
1517
+ expect(updated?.testStrategy).toBe("three-session-tdd-lite");
1518
+ });
1519
+
1520
+ test("test-after strategy is not changed by retryAsLite (should not happen, but safe)", () => {
1521
+ const routing: UserStory["routing"] = {
1522
+ complexity: "simple",
1523
+ modelTier: "fast",
1524
+ testStrategy: "test-after",
1525
+ reasoning: "simple",
1526
+ };
1527
+
1528
+ // retryAsLite would only be set for TDD stories, but test correctness:
1529
+ const updated = applyEscalationRouting(routing, "balanced", true);
1530
+
1531
+ // retryAsLite overrides to lite, but this would be a bug in routing
1532
+ // (retryAsLite should only be set when testStrategy is three-session-tdd)
1533
+ expect(updated?.modelTier).toBe("balanced");
1534
+ });
1535
+
1536
+ test("undefined routing returns undefined", () => {
1537
+ const updated = applyEscalationRouting(undefined, "balanced", true);
1538
+ expect(updated).toBeUndefined();
1539
+ });
1540
+ });
1541
+
1542
+ // ─────────────────────────────────────────────────────────────────────────────
1543
+ // T6: TDD Escalation Attempts Counting
1544
+ // ─────────────────────────────────────────────────────────────────────────────
1545
+
1546
+ describe("TDD escalation attempts counting", () => {
1547
+ const defaultTiers = [
1548
+ { tier: "fast", attempts: 5 },
1549
+ { tier: "balanced", attempts: 3 },
1550
+ { tier: "powerful", attempts: 2 },
1551
+ ];
1552
+
1553
+ test("attempts increment on each TDD escalation", () => {
1554
+ // Simulate a TDD story escalating: fast(attempt 1) → balanced(attempt 2) → ...
1555
+ let story: UserStory = {
1556
+ id: "US-001",
1557
+ title: "TDD Story",
1558
+ description: "Complex TDD story",
1559
+ acceptanceCriteria: ["All tests pass"],
1560
+ tags: [],
1561
+ dependencies: [],
1562
+ status: "pending",
1563
+ passes: false,
1564
+ escalations: [],
1565
+ attempts: 0,
1566
+ routing: { complexity: "complex", modelTier: "fast", testStrategy: "three-session-tdd", reasoning: "complex" },
1567
+ };
1568
+
1569
+ // Simulate escalation (what runner does)
1570
+ story = {
1571
+ ...story,
1572
+ attempts: story.attempts + 1,
1573
+ routing: story.routing ? { ...story.routing, modelTier: "balanced" } : undefined,
1574
+ };
1575
+
1576
+ expect(story.attempts).toBe(1);
1577
+ expect(story.routing?.modelTier).toBe("balanced");
1578
+
1579
+ // Second escalation
1580
+ story = {
1581
+ ...story,
1582
+ attempts: story.attempts + 1,
1583
+ routing: story.routing ? { ...story.routing, modelTier: "powerful" } : undefined,
1584
+ };
1585
+
1586
+ expect(story.attempts).toBe(2);
1587
+ expect(story.routing?.modelTier).toBe("powerful");
1588
+ });
1589
+
1590
+ test("TDD story with retryAsLite gets lite strategy on first isolation-violation escalation", () => {
1591
+ let story: UserStory = {
1592
+ id: "US-001",
1593
+ title: "TDD Story",
1594
+ description: "Complex TDD story",
1595
+ acceptanceCriteria: ["All tests pass"],
1596
+ tags: [],
1597
+ dependencies: [],
1598
+ status: "pending",
1599
+ passes: false,
1600
+ escalations: [],
1601
+ attempts: 0,
1602
+ routing: { complexity: "complex", modelTier: "fast", testStrategy: "three-session-tdd", reasoning: "complex" },
1603
+ };
1604
+
1605
+ // First escalation: isolation-violation → retryAsLite=true
1606
+ const retryAsLite = true;
1607
+ const nextTier = "balanced" as const;
1608
+
1609
+ story = {
1610
+ ...story,
1611
+ attempts: story.attempts + 1,
1612
+ routing: story.routing
1613
+ ? {
1614
+ ...story.routing,
1615
+ modelTier: nextTier,
1616
+ ...(retryAsLite ? { testStrategy: "three-session-tdd-lite" as const } : {}),
1617
+ }
1618
+ : undefined,
1619
+ };
1620
+
1621
+ expect(story.attempts).toBe(1);
1622
+ expect(story.routing?.modelTier).toBe("balanced");
1623
+ expect(story.routing?.testStrategy).toBe("three-session-tdd-lite");
1624
+ });
1625
+
1626
+ test("second escalation after retryAsLite does NOT change strategy again", () => {
1627
+ // Story is now in lite mode after first escalation
1628
+ let story: UserStory = {
1629
+ id: "US-001",
1630
+ title: "TDD Story",
1631
+ description: "Complex TDD story",
1632
+ acceptanceCriteria: ["All tests pass"],
1633
+ tags: [],
1634
+ dependencies: [],
1635
+ status: "pending",
1636
+ passes: false,
1637
+ escalations: [],
1638
+ attempts: 1,
1639
+ routing: {
1640
+ complexity: "complex",
1641
+ modelTier: "balanced",
1642
+ testStrategy: "three-session-tdd-lite", // Already downgraded
1643
+ reasoning: "complex",
1644
+ },
1645
+ };
1646
+
1647
+ // Second escalation: lite mode failure → retryAsLite is NOT set (only fires once)
1648
+ const retryAsLite = false; // Not set on subsequent escalations
1649
+ const nextTier = "powerful" as const;
1650
+
1651
+ story = {
1652
+ ...story,
1653
+ attempts: story.attempts + 1,
1654
+ routing: story.routing
1655
+ ? {
1656
+ ...story.routing,
1657
+ modelTier: nextTier,
1658
+ ...(retryAsLite ? { testStrategy: "three-session-tdd-lite" as const } : {}),
1659
+ }
1660
+ : undefined,
1661
+ };
1662
+
1663
+ expect(story.attempts).toBe(2);
1664
+ expect(story.routing?.modelTier).toBe("powerful");
1665
+ // Strategy remains lite (not reset) — retryAsLite only fires once
1666
+ expect(story.routing?.testStrategy).toBe("three-session-tdd-lite");
1667
+ });
1668
+
1669
+ test("max attempts check works correctly for TDD stories using total across tiers", () => {
1670
+ const { calculateMaxIterations } = require("../../src/execution/escalation");
1671
+ const maxAttempts = calculateMaxIterations(defaultTiers);
1672
+
1673
+ // A TDD story at attempt 9 (one below max) should still be escalatable
1674
+ expect(9 < maxAttempts).toBe(true);
1675
+
1676
+ // A TDD story at attempt 10 (= max) should NOT be escalatable
1677
+ expect(10 < maxAttempts).toBe(false);
1678
+ });
1679
+ });