@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,388 @@
1
+ /** Sequential Story Executor — main execution loop for story pipeline. */
2
+
3
+ import type { NaxConfig } from "../config";
4
+ import { type LoadedHooksConfig, fireHook } from "../hooks";
5
+ import type { InteractionChain } from "../interaction/chain";
6
+ import { getSafeLogger } from "../logger";
7
+ import type { StoryMetrics } from "../metrics";
8
+ import type { PipelineEventEmitter } from "../pipeline/events";
9
+ import { runPipeline } from "../pipeline/runner";
10
+ import { defaultPipeline } from "../pipeline/stages";
11
+ import type { PipelineContext, RoutingResult } from "../pipeline/types";
12
+ import type { PluginRegistry } from "../plugins";
13
+ import { generateHumanHaltSummary, getNextStory, isComplete, isStalled, loadPRD } from "../prd";
14
+ import type { PRD, UserStory } from "../prd/types";
15
+ import { routeTask } from "../routing";
16
+ import { captureGitRef } from "../utils/git";
17
+ import type { StoryBatch } from "./batching";
18
+ import { startHeartbeat, stopHeartbeat, writeExitSummary } from "./crash-recovery";
19
+ import { preIterationTierCheck } from "./escalation";
20
+ import { hookCtx } from "./helpers";
21
+ import {
22
+ applyCachedRouting,
23
+ handleDryRun,
24
+ handlePipelineFailure,
25
+ handlePipelineSuccess,
26
+ } from "./pipeline-result-handler";
27
+ import type { StatusWriter } from "./status-writer";
28
+
29
+ export interface SequentialExecutionContext {
30
+ prdPath: string;
31
+ workdir: string;
32
+ config: NaxConfig;
33
+ hooks: LoadedHooksConfig;
34
+ feature: string;
35
+ featureDir?: string;
36
+ dryRun: boolean;
37
+ useBatch: boolean;
38
+ pluginRegistry: PluginRegistry;
39
+ eventEmitter?: PipelineEventEmitter;
40
+ statusWriter: StatusWriter;
41
+ logFilePath?: string;
42
+ runId: string;
43
+ startTime: number;
44
+ batchPlan: StoryBatch[];
45
+ interactionChain?: InteractionChain | null;
46
+ }
47
+
48
+ export interface SequentialExecutionResult {
49
+ prd: PRD;
50
+ iterations: number;
51
+ storiesCompleted: number;
52
+ totalCost: number;
53
+ allStoryMetrics: StoryMetrics[];
54
+ timeoutRetryCountMap: Map<string, number>;
55
+ exitReason: "completed" | "cost-limit" | "max-iterations" | "stalled" | "no-stories";
56
+ }
57
+
58
+ /**
59
+ * Execute stories sequentially through the pipeline
60
+ */
61
+ export async function executeSequential(
62
+ ctx: SequentialExecutionContext,
63
+ initialPrd: PRD,
64
+ ): Promise<SequentialExecutionResult> {
65
+ const logger = getSafeLogger();
66
+ let prd = initialPrd;
67
+ let prdDirty = false;
68
+ let iterations = 0;
69
+ let storiesCompleted = 0;
70
+ let totalCost = 0;
71
+ const allStoryMetrics: StoryMetrics[] = [];
72
+ const timeoutRetryCountMap = new Map<string, number>();
73
+ let currentBatchIndex = 0;
74
+ let lastStoryId: string | null = null;
75
+
76
+ const buildResult = (exitReason: SequentialExecutionResult["exitReason"]): SequentialExecutionResult => ({
77
+ prd,
78
+ iterations,
79
+ storiesCompleted,
80
+ totalCost,
81
+ allStoryMetrics,
82
+ timeoutRetryCountMap,
83
+ exitReason,
84
+ });
85
+
86
+ startHeartbeat(
87
+ ctx.statusWriter,
88
+ () => totalCost,
89
+ () => iterations,
90
+ ctx.logFilePath,
91
+ );
92
+
93
+ try {
94
+ // Main execution loop
95
+ while (iterations < ctx.config.execution.maxIterations) {
96
+ iterations++;
97
+
98
+ // MEM-1: Check memory usage (warn if > 1GB heap)
99
+ const memUsage = process.memoryUsage();
100
+ const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
101
+ if (heapUsedMB > 1024) {
102
+ logger?.warn("execution", "High memory usage detected", {
103
+ heapUsedMB,
104
+ suggestion: "Consider pausing (echo PAUSE > .queue.txt) if this continues to grow",
105
+ });
106
+ }
107
+
108
+ // Reload PRD only if dirty (modified since last load)
109
+ if (prdDirty) {
110
+ prd = await loadPRD(ctx.prdPath);
111
+ prdDirty = false;
112
+ }
113
+
114
+ // Check completion
115
+ if (isComplete(prd)) {
116
+ logger?.info("execution", "All stories complete!", {
117
+ feature: ctx.feature,
118
+ totalCost,
119
+ });
120
+ await fireHook(
121
+ ctx.hooks,
122
+ "on-complete",
123
+ hookCtx(ctx.feature, { status: "complete", cost: totalCost }),
124
+ ctx.workdir,
125
+ );
126
+ return buildResult("completed");
127
+ }
128
+
129
+ // Get next story/batch
130
+ let storiesToExecute: UserStory[];
131
+ let isBatchExecution: boolean;
132
+ let story: UserStory;
133
+ let routing: ReturnType<typeof routeTask>;
134
+
135
+ if (ctx.useBatch && currentBatchIndex < ctx.batchPlan.length) {
136
+ // Get next batch from precomputed plan
137
+ const batch = ctx.batchPlan[currentBatchIndex];
138
+ currentBatchIndex++;
139
+
140
+ // Filter out already-completed stories
141
+ storiesToExecute = batch.stories.filter(
142
+ (s) =>
143
+ !s.passes &&
144
+ s.status !== "passed" &&
145
+ s.status !== "skipped" &&
146
+ s.status !== "blocked" &&
147
+ s.status !== "failed" &&
148
+ s.status !== "paused",
149
+ );
150
+ isBatchExecution = batch.isBatch && storiesToExecute.length > 1;
151
+
152
+ if (storiesToExecute.length === 0) {
153
+ // All stories in this batch already completed, move to next batch
154
+ continue;
155
+ }
156
+
157
+ // Use first story as the primary story for routing/context
158
+ story = storiesToExecute[0];
159
+ routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, ctx.config);
160
+ routing = applyCachedRouting(routing, story, ctx.config);
161
+ } else {
162
+ // Fallback to single-story mode (when batching disabled or batch plan exhausted)
163
+ const nextStory = getNextStory(prd, lastStoryId, ctx.config.execution.rectification?.maxRetries ?? 2);
164
+ if (!nextStory) {
165
+ logger?.warn("execution", "No actionable stories (check dependencies)");
166
+ return buildResult("no-stories");
167
+ }
168
+
169
+ story = nextStory;
170
+ lastStoryId = story.id;
171
+ storiesToExecute = [story];
172
+ isBatchExecution = false;
173
+
174
+ routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, ctx.config);
175
+ routing = applyCachedRouting(routing, story, ctx.config);
176
+ }
177
+
178
+ // Pre-iteration tier escalation check
179
+ const tierCheckResult = await preIterationTierCheck(
180
+ story,
181
+ routing,
182
+ ctx.config,
183
+ prd,
184
+ ctx.prdPath,
185
+ ctx.featureDir,
186
+ ctx.hooks,
187
+ ctx.feature,
188
+ totalCost,
189
+ ctx.workdir,
190
+ );
191
+
192
+ if (tierCheckResult.shouldSkipIteration) {
193
+ prd = tierCheckResult.prd;
194
+ prdDirty = tierCheckResult.prdDirty;
195
+ continue;
196
+ }
197
+
198
+ // Check cost limit
199
+ if (totalCost >= ctx.config.execution.costLimit) {
200
+ logger?.warn("execution", "Cost limit reached, pausing", {
201
+ totalCost,
202
+ costLimit: ctx.config.execution.costLimit,
203
+ });
204
+ await fireHook(
205
+ ctx.hooks,
206
+ "on-pause",
207
+ hookCtx(ctx.feature, {
208
+ storyId: story.id,
209
+ reason: `Cost limit reached: $${totalCost.toFixed(2)}`,
210
+ cost: totalCost,
211
+ }),
212
+ ctx.workdir,
213
+ );
214
+ return buildResult("cost-limit");
215
+ }
216
+
217
+ logger?.info("iteration.start", `Starting iteration ${iterations}`, {
218
+ iteration: iterations,
219
+ storyId: story.id,
220
+ storyTitle: story.title,
221
+ isBatch: isBatchExecution,
222
+ batchSize: isBatchExecution ? storiesToExecute.length : 1,
223
+ modelTier: routing.modelTier,
224
+ complexity: routing.complexity,
225
+ ...(isBatchExecution && { batchStoryIds: storiesToExecute.map((s) => s.id) }),
226
+ });
227
+
228
+ // Fire story-start hook
229
+ await fireHook(
230
+ ctx.hooks,
231
+ "on-story-start",
232
+ hookCtx(ctx.feature, {
233
+ storyId: story.id,
234
+ model: routing.modelTier,
235
+ agent: ctx.config.autoMode.defaultAgent,
236
+ iteration: iterations,
237
+ }),
238
+ ctx.workdir,
239
+ );
240
+
241
+ if (ctx.dryRun) {
242
+ const dryRunResult = await handleDryRun({
243
+ prd,
244
+ prdPath: ctx.prdPath,
245
+ storiesToExecute,
246
+ routing,
247
+ statusWriter: ctx.statusWriter,
248
+ pluginRegistry: ctx.pluginRegistry,
249
+ runId: ctx.runId,
250
+ totalCost,
251
+ iterations,
252
+ });
253
+ storiesCompleted += dryRunResult.storiesCompletedDelta;
254
+ prdDirty = dryRunResult.prdDirty;
255
+ continue;
256
+ }
257
+
258
+ // Capture git ref for scoped verification
259
+ const storyGitRef = await captureGitRef(ctx.workdir);
260
+
261
+ // Build pipeline context
262
+ const storyStartTime = new Date().toISOString();
263
+ const pipelineContext: PipelineContext = {
264
+ config: ctx.config,
265
+ prd,
266
+ story,
267
+ stories: storiesToExecute,
268
+ routing: routing as RoutingResult,
269
+ workdir: ctx.workdir,
270
+ featureDir: ctx.featureDir,
271
+ hooks: ctx.hooks,
272
+ plugins: ctx.pluginRegistry,
273
+ storyStartTime,
274
+ interaction: ctx.interactionChain ?? undefined,
275
+ };
276
+
277
+ // Log agent start
278
+ logger?.info("agent.start", "Starting agent execution", {
279
+ storyId: story.id,
280
+ agent: ctx.config.autoMode.defaultAgent,
281
+ modelTier: routing.modelTier,
282
+ testStrategy: routing.testStrategy,
283
+ isBatch: isBatchExecution,
284
+ });
285
+
286
+ // Update status before execution
287
+ ctx.statusWriter.setPrd(prd);
288
+ ctx.statusWriter.setCurrentStory({
289
+ storyId: story.id,
290
+ title: story.title,
291
+ complexity: routing.complexity,
292
+ tddStrategy: routing.testStrategy,
293
+ model: routing.modelTier,
294
+ attempt: (story.attempts ?? 0) + 1,
295
+ phase: "routing",
296
+ });
297
+ await ctx.statusWriter.update(totalCost, iterations);
298
+
299
+ // Run pipeline
300
+ const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, ctx.eventEmitter);
301
+
302
+ // Log agent complete
303
+ logger?.info("agent.complete", "Agent execution completed", {
304
+ storyId: story.id,
305
+ success: pipelineResult.success,
306
+ finalAction: pipelineResult.finalAction,
307
+ estimatedCost: pipelineResult.context.agentResult?.estimatedCost,
308
+ });
309
+
310
+ // Update PRD reference (pipeline may have modified it)
311
+ prd = pipelineResult.context.prd;
312
+
313
+ // Handle pipeline result
314
+ const handlerCtx = {
315
+ config: ctx.config,
316
+ prd,
317
+ prdPath: ctx.prdPath,
318
+ workdir: ctx.workdir,
319
+ featureDir: ctx.featureDir,
320
+ hooks: ctx.hooks,
321
+ feature: ctx.feature,
322
+ totalCost,
323
+ startTime: ctx.startTime,
324
+ runId: ctx.runId,
325
+ pluginRegistry: ctx.pluginRegistry,
326
+ story,
327
+ storiesToExecute,
328
+ routing,
329
+ isBatchExecution,
330
+ allStoryMetrics,
331
+ timeoutRetryCountMap,
332
+ storyGitRef,
333
+ interactionChain: ctx.interactionChain,
334
+ };
335
+
336
+ if (pipelineResult.success) {
337
+ const successResult = await handlePipelineSuccess(handlerCtx, pipelineResult);
338
+ totalCost += successResult.costDelta;
339
+ storiesCompleted += successResult.storiesCompletedDelta;
340
+ prd = successResult.prd;
341
+ prdDirty = successResult.prdDirty;
342
+ } else {
343
+ const failResult = await handlePipelineFailure(handlerCtx, pipelineResult);
344
+ prd = failResult.prd;
345
+ prdDirty = failResult.prdDirty;
346
+ }
347
+
348
+ // Update status after story complete
349
+ if (prdDirty) {
350
+ prd = await loadPRD(ctx.prdPath);
351
+ prdDirty = false;
352
+ }
353
+ ctx.statusWriter.setPrd(prd);
354
+ ctx.statusWriter.setCurrentStory(null);
355
+ await ctx.statusWriter.update(totalCost, iterations);
356
+
357
+ // Stall detection
358
+ if (isStalled(prd)) {
359
+ const summary = generateHumanHaltSummary(prd);
360
+ logger?.error("execution", "Execution stalled", {
361
+ reason: "All remaining stories blocked or dependent on blocked stories",
362
+ summary,
363
+ });
364
+ await fireHook(
365
+ ctx.hooks,
366
+ "on-pause",
367
+ hookCtx(ctx.feature, {
368
+ reason: "All remaining stories blocked or dependent on blocked stories",
369
+ cost: totalCost,
370
+ }),
371
+ ctx.workdir,
372
+ );
373
+ return buildResult("stalled");
374
+ }
375
+
376
+ // Delay between iterations
377
+ if (ctx.config.execution.iterationDelayMs > 0) {
378
+ await Bun.sleep(ctx.config.execution.iterationDelayMs);
379
+ }
380
+ }
381
+
382
+ return buildResult("max-iterations");
383
+ } finally {
384
+ // Stop heartbeat and write exit summary
385
+ stopHeartbeat();
386
+ await writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
387
+ }
388
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Status File — Machine-readable run state for external tooling
3
+ *
4
+ * Writes a JSON status file that external tools (CI/CD, orchestrators,
5
+ * dashboards) can poll to monitor nax runs without parsing logs.
6
+ *
7
+ * Atomic writes: write to <path>.tmp then rename to <path>
8
+ */
9
+
10
+ import { rename, unlink } from "node:fs/promises";
11
+ import { resolve } from "node:path";
12
+ import type { NaxConfig } from "../config";
13
+ import type { PRD, StoryStatus, UserStory } from "../prd";
14
+
15
+ // ============================================================================
16
+ // NaxStatusFile Interface
17
+ // ============================================================================
18
+
19
+ /** Machine-readable status file written during nax runs */
20
+ export interface NaxStatusFile {
21
+ /** Schema version for forward compatibility */
22
+ version: 1;
23
+
24
+ /** Run metadata */
25
+ run: {
26
+ /** Run ID (e.g. "run-2026-02-25T10-00-00-000Z") */
27
+ id: string;
28
+ /** Feature name */
29
+ feature: string;
30
+ /** ISO 8601 start timestamp */
31
+ startedAt: string;
32
+ /** Current run status */
33
+ status: "running" | "completed" | "failed" | "stalled" | "crashed" | "precheck-failed";
34
+ /** Whether this is a dry run */
35
+ dryRun: boolean;
36
+ /** Process ID for crash detection */
37
+ pid: number;
38
+ /** ISO 8601 crash timestamp (present when status is "crashed") */
39
+ crashedAt?: string;
40
+ /** Signal or exception that caused crash (present when status is "crashed") */
41
+ crashSignal?: string;
42
+ };
43
+
44
+ /** Aggregate progress counts */
45
+ progress: {
46
+ /** Total stories in PRD */
47
+ total: number;
48
+ /** Stories that passed */
49
+ passed: number;
50
+ /** Stories that failed */
51
+ failed: number;
52
+ /** Stories that are paused */
53
+ paused: number;
54
+ /** Stories that are blocked */
55
+ blocked: number;
56
+ /** Stories not yet processed (total - passed - failed - paused - blocked) */
57
+ pending: number;
58
+ };
59
+
60
+ /** Cost tracking */
61
+ cost: {
62
+ /** Accumulated cost in USD */
63
+ spent: number;
64
+ /** Cost limit from config (null if not set) */
65
+ limit: number | null;
66
+ };
67
+
68
+ /** Current story being processed (null if between stories or at run boundaries) */
69
+ current: {
70
+ /** Story ID */
71
+ storyId: string;
72
+ /** Story title */
73
+ title: string;
74
+ /** Complexity level */
75
+ complexity: string;
76
+ /** TDD strategy */
77
+ tddStrategy: string;
78
+ /** Resolved model name */
79
+ model: string;
80
+ /** Current attempt number (1-based) */
81
+ attempt: number;
82
+ /** Current phase */
83
+ phase: string;
84
+ } | null;
85
+
86
+ /** Number of loop iterations completed */
87
+ iterations: number;
88
+
89
+ /** ISO 8601 last-updated timestamp */
90
+ updatedAt: string;
91
+
92
+ /** Elapsed duration in milliseconds */
93
+ durationMs: number;
94
+
95
+ /** ISO 8601 last heartbeat timestamp (updated every 60s during execution) */
96
+ lastHeartbeat?: string;
97
+
98
+ /** Parallel execution info (present when --parallel is used) */
99
+ parallel?: {
100
+ /** Whether parallel mode is enabled */
101
+ enabled: boolean;
102
+ /** Max concurrent sessions */
103
+ maxConcurrency: number;
104
+ /** Currently executing stories in parallel */
105
+ activeStories: Array<{
106
+ storyId: string;
107
+ worktreePath: string;
108
+ }>;
109
+ };
110
+ }
111
+
112
+ // ============================================================================
113
+ // Progress Counting
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Derive progress counts from PRD story statuses.
118
+ *
119
+ * Counts each story by its current status. `pending` is computed as
120
+ * everything not in the four explicit terminal/waiting states.
121
+ */
122
+ export function countProgress(prd: PRD): NaxStatusFile["progress"] {
123
+ const stories = prd.userStories;
124
+ const passed = stories.filter((s) => s.status === "passed").length;
125
+ const failed = stories.filter((s) => s.status === "failed").length;
126
+ const paused = stories.filter((s) => s.status === "paused").length;
127
+ const blocked = stories.filter((s) => s.status === "blocked").length;
128
+ const total = stories.length;
129
+ const pending = total - passed - failed - paused - blocked;
130
+
131
+ return { total, passed, failed, paused, blocked, pending };
132
+ }
133
+
134
+ // ============================================================================
135
+ // Run State (for buildStatusSnapshot)
136
+ // ============================================================================
137
+
138
+ /**
139
+ * Snapshot of current run state used to build NaxStatusFile.
140
+ *
141
+ * This is a value-only snapshot — callers pass in what they have at the
142
+ * current write point. The runner constructs this inline from local variables.
143
+ */
144
+ export interface RunStateSnapshot {
145
+ /** Unique run identifier */
146
+ runId: string;
147
+ /** Feature name */
148
+ feature: string;
149
+ /** ISO 8601 start timestamp */
150
+ startedAt: string;
151
+ /** Current run status */
152
+ runStatus: NaxStatusFile["run"]["status"];
153
+ /** Whether this is a dry run */
154
+ dryRun: boolean;
155
+ /** Process ID for crash detection */
156
+ pid: number;
157
+ /** Loaded PRD (for progress counting) */
158
+ prd: PRD;
159
+ /** Accumulated cost in USD */
160
+ totalCost: number;
161
+ /** Cost limit from config (or null) */
162
+ costLimit: number | null;
163
+ /** Currently-executing story info (null between stories) */
164
+ currentStory: {
165
+ storyId: string;
166
+ title: string;
167
+ complexity: string;
168
+ tddStrategy: string;
169
+ model: string;
170
+ attempt: number;
171
+ phase: string;
172
+ } | null;
173
+ /** Number of loop iterations */
174
+ iterations: number;
175
+ /** Run start time as ms epoch (for computing durationMs) */
176
+ startTimeMs: number;
177
+ /** Parallel execution info (optional) */
178
+ parallel?: NaxStatusFile["parallel"];
179
+ /** ISO 8601 crash timestamp (present when status is "crashed") */
180
+ crashedAt?: string;
181
+ /** Signal or exception that caused crash (present when status is "crashed") */
182
+ crashSignal?: string;
183
+ /** ISO 8601 last heartbeat timestamp (updated every 60s during execution) */
184
+ lastHeartbeat?: string;
185
+ }
186
+
187
+ // ============================================================================
188
+ // buildStatusSnapshot
189
+ // ============================================================================
190
+
191
+ /**
192
+ * Build a NaxStatusFile object from current run state.
193
+ *
194
+ * Derives progress from PRD story statuses. Sets updatedAt and durationMs
195
+ * from the current time. Does not write to disk — call writeStatusFile() for that.
196
+ */
197
+ export function buildStatusSnapshot(state: RunStateSnapshot): NaxStatusFile {
198
+ const now = Date.now();
199
+ const snapshot: NaxStatusFile = {
200
+ version: 1,
201
+ run: {
202
+ id: state.runId,
203
+ feature: state.feature,
204
+ startedAt: state.startedAt,
205
+ status: state.runStatus,
206
+ dryRun: state.dryRun,
207
+ pid: state.pid,
208
+ ...(state.crashedAt && { crashedAt: state.crashedAt }),
209
+ ...(state.crashSignal && { crashSignal: state.crashSignal }),
210
+ },
211
+ progress: countProgress(state.prd),
212
+ cost: {
213
+ spent: state.totalCost,
214
+ limit: state.costLimit,
215
+ },
216
+ current: state.currentStory,
217
+ iterations: state.iterations,
218
+ updatedAt: new Date(now).toISOString(),
219
+ durationMs: now - state.startTimeMs,
220
+ ...(state.lastHeartbeat && { lastHeartbeat: state.lastHeartbeat }),
221
+ };
222
+
223
+ if (state.parallel) {
224
+ snapshot.parallel = state.parallel;
225
+ }
226
+
227
+ return snapshot;
228
+ }
229
+
230
+ // ============================================================================
231
+ // Atomic Writer
232
+ // ============================================================================
233
+
234
+ /**
235
+ * Atomically write a NaxStatusFile to disk.
236
+ *
237
+ * Writes to `<path>.tmp` first, then renames to `<path>` to prevent
238
+ * consumers from reading partial JSON during the write.
239
+ *
240
+ * @param filePath - Destination path for the status file
241
+ * @param status - Status file content to write
242
+ * @throws Error if path traversal is detected
243
+ */
244
+ export async function writeStatusFile(filePath: string, status: NaxStatusFile): Promise<void> {
245
+ // SEC-1: Validate path to prevent path traversal attacks
246
+ const resolvedPath = resolve(filePath);
247
+
248
+ // Check for path traversal patterns in the original path
249
+ if (filePath.includes("../") || filePath.includes("..\\")) {
250
+ throw new Error("Invalid status file path: path traversal detected");
251
+ }
252
+
253
+ const tmpPath = `${resolvedPath}.tmp`;
254
+
255
+ // MEM-1: Clean up stale .tmp file if it exists
256
+ try {
257
+ await unlink(tmpPath);
258
+ } catch {
259
+ // Ignore error if file doesn't exist
260
+ }
261
+
262
+ await Bun.write(tmpPath, JSON.stringify(status, null, 2));
263
+ await rename(tmpPath, resolvedPath);
264
+ }