@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,98 @@
1
+ /**
2
+ * Context Generator Types (v0.16.1)
3
+ *
4
+ * Types for generating agent config files from nax/context.md.
5
+ * Replaces ConstitutionContent from the old constitution generator.
6
+ */
7
+
8
+ /** Auto-injected project metadata */
9
+ export interface ProjectMetadata {
10
+ /** Project name from manifest file */
11
+ name?: string;
12
+ /** Detected language/runtime (e.g. "TypeScript", "Go", "Rust", "Python") */
13
+ language?: string;
14
+ /** Key dependencies (framework, ORM, test runner, etc.) */
15
+ dependencies: string[];
16
+ /** Test command from nax config */
17
+ testCommand?: string;
18
+ /** Lint command from nax config */
19
+ lintCommand?: string;
20
+ /** Typecheck command from nax config */
21
+ typecheckCommand?: string;
22
+ }
23
+
24
+ /** Context content passed to generators */
25
+ export interface ContextContent {
26
+ /** Raw markdown from nax/context.md */
27
+ markdown: string;
28
+ /** Auto-injected project metadata (if enabled) */
29
+ metadata?: ProjectMetadata;
30
+ }
31
+
32
+ /** Agent config generator interface */
33
+ export interface AgentContextGenerator {
34
+ /** Generator name (e.g., 'claude', 'opencode', 'cursor') */
35
+ name: string;
36
+ /** Output filename (e.g., 'CLAUDE.md', '.cursorrules') */
37
+ outputFile: string;
38
+ /** Generate agent-specific config file content from context */
39
+ generate(context: ContextContent): string;
40
+ }
41
+
42
+ /** All available generator types */
43
+ export type AgentType = "claude" | "opencode" | "cursor" | "windsurf" | "aider";
44
+
45
+ /** Generator registry map */
46
+ export type GeneratorMap = Record<AgentType, AgentContextGenerator>;
47
+
48
+ /** A single context element (file content, error, story summary, etc.) */
49
+ export interface ContextElement {
50
+ /** Element type identifier */
51
+ type: string;
52
+ /** Content text */
53
+ content: string;
54
+ /** Estimated token count */
55
+ tokens: number;
56
+ /** Priority (higher = selected first when budgeting) */
57
+ priority: number;
58
+ /** Story ID (for story/dependency elements) */
59
+ storyId?: string;
60
+ /** File path (for file elements) */
61
+ filePath?: string;
62
+ /** Human-readable label (optional) */
63
+ label?: string;
64
+ }
65
+
66
+ /** Token budget for context building */
67
+ export interface ContextBudget {
68
+ /** Total token limit */
69
+ maxTokens: number;
70
+ /** Tokens reserved for instructions/system prompt */
71
+ reservedForInstructions: number;
72
+ /** Tokens available for context elements */
73
+ availableForContext: number;
74
+ }
75
+
76
+ /** Input to the context builder */
77
+ export interface StoryContext {
78
+ /** PRD containing all stories */
79
+ prd: import("../prd/types").PRD;
80
+ /** ID of the current story being worked on */
81
+ currentStoryId: string;
82
+ /** Working directory for file scanning */
83
+ workdir?: string;
84
+ /** nax config (for context settings) */
85
+ config?: import("../config").NaxConfig;
86
+ }
87
+
88
+ /** Output of the context builder */
89
+ export interface BuiltContext {
90
+ /** Selected context elements (within budget) */
91
+ elements: ContextElement[];
92
+ /** Total tokens used */
93
+ totalTokens: number;
94
+ /** Whether some elements were truncated due to budget */
95
+ truncated: boolean;
96
+ /** Human-readable summary */
97
+ summary: string;
98
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Typed Error Classes for nax
3
+ *
4
+ * Replaces process.exit(1) patterns with structured errors that can be caught
5
+ * and handled by the CLI layer or tests.
6
+ */
7
+
8
+ /**
9
+ * Base error class for all nax errors.
10
+ */
11
+ export class NaxError extends Error {
12
+ constructor(
13
+ message: string,
14
+ public readonly code: string,
15
+ public readonly context?: Record<string, unknown>,
16
+ ) {
17
+ super(message);
18
+ this.name = "NaxError";
19
+ Error.captureStackTrace(this, this.constructor);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Agent not found or not installed.
25
+ */
26
+ export class AgentNotFoundError extends NaxError {
27
+ constructor(agentName: string, binary?: string) {
28
+ super(`Agent "${agentName}" not found or not installed`, "AGENT_NOT_FOUND", { agentName, binary });
29
+ this.name = "AgentNotFoundError";
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Agent binary not in PATH.
35
+ */
36
+ export class AgentNotInstalledError extends NaxError {
37
+ constructor(agentName: string, binary: string) {
38
+ super(`Agent "${agentName}" is not installed or not in PATH: ${binary}`, "AGENT_NOT_INSTALLED", {
39
+ agentName,
40
+ binary,
41
+ });
42
+ this.name = "AgentNotInstalledError";
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Feature exceeds story limit.
48
+ */
49
+ export class StoryLimitExceededError extends NaxError {
50
+ constructor(totalStories: number, limit: number) {
51
+ super(`Feature exceeds story limit: ${totalStories} stories (max: ${limit})`, "STORY_LIMIT_EXCEEDED", {
52
+ totalStories,
53
+ limit,
54
+ });
55
+ this.name = "StoryLimitExceededError";
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Another nax process is already running.
61
+ */
62
+ export class LockAcquisitionError extends NaxError {
63
+ constructor(workdir: string) {
64
+ super("Another nax process is already running in this directory", "LOCK_ACQUISITION_FAILED", { workdir });
65
+ this.name = "LockAcquisitionError";
66
+ }
67
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Story batching logic
3
+ *
4
+ * Groups consecutive simple-complexity stories into batches for efficient execution.
5
+ */
6
+
7
+ import type { UserStory } from "../prd";
8
+
9
+ /**
10
+ * Default maximum number of stories per batch.
11
+ *
12
+ * Rationale:
13
+ * - Batch size must balance efficiency vs. blast radius
14
+ * - 4 stories is optimal for most simple tasks (e.g., add 4 similar util functions)
15
+ * - Keeps prompts manageable (~1500 tokens per story = ~6000 tokens total context)
16
+ * - If one story in batch fails, only 3 others retry at next tier (acceptable waste)
17
+ * - Larger batches (8+) increase risk of cascading failures and context overload
18
+ *
19
+ * This default can be overridden via config or function parameter.
20
+ */
21
+ const DEFAULT_MAX_BATCH_SIZE = 4;
22
+
23
+ /**
24
+ * Story batch for grouped execution
25
+ */
26
+ export interface StoryBatch {
27
+ /** Stories in this batch */
28
+ stories: UserStory[];
29
+ /** True if this is a batch of multiple stories, false if single story */
30
+ isBatch: boolean;
31
+ }
32
+
33
+ /**
34
+ * Group consecutive simple-complexity stories into batches (max 4 per batch).
35
+ * Non-simple stories execute individually.
36
+ *
37
+ * @param stories - Array of user stories to batch
38
+ * @param maxBatchSize - Maximum stories per batch (default: 4)
39
+ * @returns Array of story batches
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const stories = [simpleStory1, simpleStory2, complexStory, simpleStory3];
44
+ * const batches = groupStoriesIntoBatches(stories);
45
+ * // Returns: [
46
+ * // { stories: [simpleStory1, simpleStory2], isBatch: true },
47
+ * // { stories: [complexStory], isBatch: false },
48
+ * // { stories: [simpleStory3], isBatch: false }
49
+ * // ]
50
+ * ```
51
+ */
52
+ export function groupStoriesIntoBatches(stories: UserStory[], maxBatchSize = DEFAULT_MAX_BATCH_SIZE): StoryBatch[] {
53
+ const batches: StoryBatch[] = [];
54
+ let currentBatch: UserStory[] = [];
55
+
56
+ for (const story of stories) {
57
+ const isSimple = story.routing?.complexity === "simple";
58
+
59
+ if (isSimple && currentBatch.length < maxBatchSize) {
60
+ // Add to current batch
61
+ currentBatch.push(story);
62
+ } else {
63
+ // Flush current batch if it exists
64
+ if (currentBatch.length > 0) {
65
+ batches.push({
66
+ stories: [...currentBatch],
67
+ isBatch: currentBatch.length > 1,
68
+ });
69
+ currentBatch = [];
70
+ }
71
+
72
+ // Add non-simple story as individual batch
73
+ if (!isSimple) {
74
+ batches.push({
75
+ stories: [story],
76
+ isBatch: false,
77
+ });
78
+ } else {
79
+ // Start new batch with this simple story
80
+ currentBatch.push(story);
81
+ }
82
+ }
83
+ }
84
+
85
+ // Flush remaining batch
86
+ if (currentBatch.length > 0) {
87
+ batches.push({
88
+ stories: [...currentBatch],
89
+ isBatch: currentBatch.length > 1,
90
+ });
91
+ }
92
+
93
+ return batches;
94
+ }
95
+
96
+ /**
97
+ * Precompute the full batch plan from ready stories.
98
+ * This eliminates O(n²) re-checking by computing all batches upfront.
99
+ * Maintains original story order from PRD.
100
+ *
101
+ * @param stories - Array of ready user stories (already filtered for dependencies)
102
+ * @param maxBatchSize - Maximum stories per batch (default: 4)
103
+ * @returns Array of story batches ready for sequential execution
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const readyStories = getAllReadyStories(prd);
108
+ * const batchPlan = precomputeBatchPlan(readyStories);
109
+ * // Iterate through batches sequentially
110
+ * for (const batch of batchPlan) {
111
+ * await executeBatch(batch);
112
+ * }
113
+ * ```
114
+ */
115
+ export function precomputeBatchPlan(stories: UserStory[], maxBatchSize = DEFAULT_MAX_BATCH_SIZE): StoryBatch[] {
116
+ const batches: StoryBatch[] = [];
117
+ let currentBatch: UserStory[] = [];
118
+
119
+ for (const story of stories) {
120
+ const isSimple = story.routing?.complexity === "simple" && story.routing?.testStrategy === "test-after";
121
+
122
+ if (isSimple && currentBatch.length < maxBatchSize) {
123
+ // Add to current batch
124
+ currentBatch.push(story);
125
+ } else {
126
+ // Flush current batch if it exists
127
+ if (currentBatch.length > 0) {
128
+ batches.push({
129
+ stories: [...currentBatch],
130
+ isBatch: currentBatch.length > 1,
131
+ });
132
+ currentBatch = [];
133
+ }
134
+
135
+ // Add non-simple story as individual batch
136
+ if (!isSimple) {
137
+ batches.push({
138
+ stories: [story],
139
+ isBatch: false,
140
+ });
141
+ } else {
142
+ // Start new batch with this simple story
143
+ currentBatch.push(story);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Flush remaining batch
149
+ if (currentBatch.length > 0) {
150
+ batches.push({
151
+ stories: [...currentBatch],
152
+ isBatch: currentBatch.length > 1,
153
+ });
154
+ }
155
+
156
+ return batches;
157
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Crash Recovery — Signal handlers, heartbeat, and exit summary
3
+ *
4
+ * Implements US-007:
5
+ * - SIGTERM/SIGINT/SIGHUP handlers
6
+ * - Uncaught exception handlers
7
+ * - Fatal log + status.json update to "crashed"
8
+ * - Heartbeat every 60s during agent execution
9
+ * - Exit summary entry on normal exit
10
+ */
11
+
12
+ import { getSafeLogger } from "../logger";
13
+ import type { PidRegistry } from "./pid-registry";
14
+ import type { StatusWriter } from "./status-writer";
15
+
16
+ /**
17
+ * Crash recovery context — dependencies injected at setup
18
+ * (BUG-1 fix: use getters to avoid capturing stale closure values)
19
+ */
20
+ export interface CrashRecoveryContext {
21
+ statusWriter: StatusWriter;
22
+ getTotalCost: () => number;
23
+ getIterations: () => number;
24
+ jsonlFilePath?: string;
25
+ pidRegistry?: PidRegistry;
26
+ // BUG-017: Additional context for run.complete event on SIGTERM
27
+ runId?: string;
28
+ feature?: string;
29
+ getStartTime?: () => number;
30
+ getTotalStories?: () => number;
31
+ getStoriesCompleted?: () => number;
32
+ }
33
+
34
+ /**
35
+ * Heartbeat timer handle (for cleanup)
36
+ */
37
+ let heartbeatTimer: Timer | null = null;
38
+
39
+ /**
40
+ * Track whether crash handlers have been installed
41
+ */
42
+ let handlersInstalled = false;
43
+
44
+ /**
45
+ * Write fatal log entry to JSONL file
46
+ */
47
+ async function writeFatalLog(jsonlFilePath: string | undefined, signal: string, error?: Error): Promise<void> {
48
+ if (!jsonlFilePath) return;
49
+
50
+ try {
51
+ const fatalEntry = {
52
+ timestamp: new Date().toISOString(),
53
+ level: "error",
54
+ stage: "crash-recovery",
55
+ message: error ? `Uncaught exception: ${error.message}` : `Process terminated by ${signal}`,
56
+ data: {
57
+ signal,
58
+ ...(error && {
59
+ stack: error.stack,
60
+ name: error.name,
61
+ }),
62
+ },
63
+ };
64
+
65
+ const line = `${JSON.stringify(fatalEntry)}\n`;
66
+ // Use appendFileSync from node:fs to ensure file is created if it doesn't exist
67
+ const { appendFileSync } = await import("node:fs");
68
+ appendFileSync(jsonlFilePath, line, "utf8");
69
+ } catch (err) {
70
+ console.error("[crash-recovery] Failed to write fatal log:", err);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Write run.complete event to JSONL file (BUG-017)
76
+ * Called on SIGTERM to emit final run summary before exit
77
+ */
78
+ async function writeRunComplete(ctx: CrashRecoveryContext, exitReason: string): Promise<void> {
79
+ if (!ctx.jsonlFilePath || !ctx.runId || !ctx.feature) return;
80
+
81
+ const logger = getSafeLogger();
82
+
83
+ try {
84
+ const totalCost = ctx.getTotalCost();
85
+ const iterations = ctx.getIterations();
86
+ const startTime = ctx.getStartTime?.() ?? Date.now();
87
+ const durationMs = Date.now() - startTime;
88
+ const totalStories = ctx.getTotalStories?.() ?? 0;
89
+ const storiesCompleted = ctx.getStoriesCompleted?.() ?? 0;
90
+
91
+ const runCompleteEntry = {
92
+ timestamp: new Date().toISOString(),
93
+ level: "info",
94
+ stage: "run.complete",
95
+ message: "Feature execution terminated",
96
+ data: {
97
+ runId: ctx.runId,
98
+ feature: ctx.feature,
99
+ success: false,
100
+ exitReason,
101
+ totalCost,
102
+ iterations,
103
+ totalStories,
104
+ storiesCompleted,
105
+ durationMs,
106
+ },
107
+ };
108
+
109
+ const line = `${JSON.stringify(runCompleteEntry)}\n`;
110
+ const { appendFileSync } = await import("node:fs");
111
+ appendFileSync(ctx.jsonlFilePath, line, "utf8");
112
+ logger?.debug("crash-recovery", "run.complete event written", { exitReason });
113
+ } catch (err) {
114
+ console.error("[crash-recovery] Failed to write run.complete event:", err);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Update status.json to "crashed" state
120
+ */
121
+ async function updateStatusToCrashed(
122
+ statusWriter: StatusWriter,
123
+ totalCost: number,
124
+ iterations: number,
125
+ signal: string,
126
+ ): Promise<void> {
127
+ try {
128
+ statusWriter.setRunStatus("crashed");
129
+ await statusWriter.update(totalCost, iterations, {
130
+ crashedAt: new Date().toISOString(),
131
+ crashSignal: signal,
132
+ });
133
+ } catch (err) {
134
+ console.error("[crash-recovery] Failed to update status.json:", err);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Install signal handlers for crash recovery
140
+ * (MEM-1 fix: return cleanup function to unregister handlers)
141
+ */
142
+ export function installCrashHandlers(ctx: CrashRecoveryContext): () => void {
143
+ if (handlersInstalled) {
144
+ return () => {}; // Prevent duplicate installations
145
+ }
146
+
147
+ const logger = getSafeLogger();
148
+
149
+ // Signal handler
150
+ const handleSignal = async (signal: NodeJS.Signals) => {
151
+ logger?.error("crash-recovery", `Received ${signal}, shutting down...`, { signal });
152
+
153
+ // Kill all spawned agent processes
154
+ if (ctx.pidRegistry) {
155
+ await ctx.pidRegistry.killAll();
156
+ }
157
+
158
+ // Write fatal log
159
+ await writeFatalLog(ctx.jsonlFilePath, signal);
160
+
161
+ // Write run.complete event (BUG-017)
162
+ await writeRunComplete(ctx, signal.toLowerCase());
163
+
164
+ // Update status.json to crashed
165
+ await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), signal);
166
+
167
+ // Stop heartbeat
168
+ stopHeartbeat();
169
+
170
+ // Exit cleanly
171
+ process.exit(128 + getSignalNumber(signal));
172
+ };
173
+
174
+ const sigtermHandler = () => handleSignal("SIGTERM");
175
+ const sigintHandler = () => handleSignal("SIGINT");
176
+ const sighupHandler = () => handleSignal("SIGHUP");
177
+
178
+ // Install signal handlers
179
+ process.on("SIGTERM", sigtermHandler);
180
+ process.on("SIGINT", sigintHandler);
181
+ process.on("SIGHUP", sighupHandler);
182
+
183
+ // Uncaught exception handler
184
+ const uncaughtExceptionHandler = async (error: Error) => {
185
+ logger?.error("crash-recovery", "Uncaught exception", {
186
+ error: error.message,
187
+ stack: error.stack,
188
+ });
189
+
190
+ // Kill all spawned agent processes
191
+ if (ctx.pidRegistry) {
192
+ await ctx.pidRegistry.killAll();
193
+ }
194
+
195
+ // Write fatal log with stack trace
196
+ await writeFatalLog(ctx.jsonlFilePath, "uncaughtException", error);
197
+
198
+ // Update status.json to crashed
199
+ await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "uncaughtException");
200
+
201
+ // Stop heartbeat
202
+ stopHeartbeat();
203
+
204
+ // Exit with error code
205
+ process.exit(1);
206
+ };
207
+ process.on("uncaughtException", uncaughtExceptionHandler);
208
+
209
+ // Unhandled promise rejection handler
210
+ const unhandledRejectionHandler = async (reason: unknown, promise: Promise<unknown>) => {
211
+ const error = reason instanceof Error ? reason : new Error(String(reason));
212
+ logger?.error("crash-recovery", "Unhandled promise rejection", {
213
+ error: error.message,
214
+ stack: error.stack,
215
+ });
216
+
217
+ // Kill all spawned agent processes
218
+ if (ctx.pidRegistry) {
219
+ await ctx.pidRegistry.killAll();
220
+ }
221
+
222
+ // Write fatal log
223
+ await writeFatalLog(ctx.jsonlFilePath, "unhandledRejection", error);
224
+
225
+ // Update status.json to crashed
226
+ await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "unhandledRejection");
227
+
228
+ // Stop heartbeat
229
+ stopHeartbeat();
230
+
231
+ // Exit with error code
232
+ process.exit(1);
233
+ };
234
+ process.on("unhandledRejection", unhandledRejectionHandler);
235
+
236
+ handlersInstalled = true;
237
+ logger?.debug("crash-recovery", "Crash handlers installed");
238
+
239
+ // Return cleanup function
240
+ return () => {
241
+ process.removeListener("SIGTERM", sigtermHandler);
242
+ process.removeListener("SIGINT", sigintHandler);
243
+ process.removeListener("SIGHUP", sighupHandler);
244
+ process.removeListener("uncaughtException", uncaughtExceptionHandler);
245
+ process.removeListener("unhandledRejection", unhandledRejectionHandler);
246
+ handlersInstalled = false;
247
+ logger?.debug("crash-recovery", "Crash handlers unregistered");
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Start heartbeat timer (60s interval)
253
+ */
254
+ export function startHeartbeat(
255
+ statusWriter: StatusWriter,
256
+ getTotalCost: () => number,
257
+ getIterations: () => number,
258
+ jsonlFilePath?: string,
259
+ ): void {
260
+ const logger = getSafeLogger();
261
+
262
+ // Stop any existing heartbeat first
263
+ stopHeartbeat();
264
+
265
+ heartbeatTimer = setInterval(async () => {
266
+ logger?.debug("crash-recovery", "Heartbeat");
267
+
268
+ // Write heartbeat to JSONL
269
+ if (jsonlFilePath) {
270
+ try {
271
+ const heartbeatEntry = {
272
+ timestamp: new Date().toISOString(),
273
+ level: "debug",
274
+ stage: "heartbeat",
275
+ message: "Process alive",
276
+ data: {
277
+ pid: process.pid,
278
+ memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
279
+ },
280
+ };
281
+ const line = `${JSON.stringify(heartbeatEntry)}\n`;
282
+ const { appendFileSync } = await import("node:fs");
283
+ appendFileSync(jsonlFilePath, line, "utf8");
284
+ } catch (err) {
285
+ logger?.warn("crash-recovery", "Failed to write heartbeat", { error: (err as Error).message });
286
+ }
287
+ }
288
+
289
+ // Update status.json (no-op if nothing changed, but keeps lastHeartbeat fresh)
290
+ try {
291
+ await statusWriter.update(getTotalCost(), getIterations(), {
292
+ lastHeartbeat: new Date().toISOString(),
293
+ });
294
+ } catch (err) {
295
+ logger?.warn("crash-recovery", "Failed to update status during heartbeat", {
296
+ error: (err as Error).message,
297
+ });
298
+ }
299
+ }, 60_000); // 60 seconds
300
+
301
+ logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
302
+ }
303
+
304
+ /**
305
+ * Stop heartbeat timer
306
+ */
307
+ export function stopHeartbeat(): void {
308
+ if (heartbeatTimer) {
309
+ clearInterval(heartbeatTimer);
310
+ heartbeatTimer = null;
311
+ getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Write exit summary entry to JSONL
317
+ */
318
+ export async function writeExitSummary(
319
+ jsonlFilePath: string | undefined,
320
+ totalCost: number,
321
+ iterations: number,
322
+ storiesCompleted: number,
323
+ durationMs: number,
324
+ ): Promise<void> {
325
+ if (!jsonlFilePath) return;
326
+
327
+ const logger = getSafeLogger();
328
+
329
+ try {
330
+ const summaryEntry = {
331
+ timestamp: new Date().toISOString(),
332
+ level: "info",
333
+ stage: "exit-summary",
334
+ message: "Run completed",
335
+ data: {
336
+ totalCost,
337
+ iterations,
338
+ storiesCompleted,
339
+ durationMs,
340
+ exitedCleanly: true,
341
+ },
342
+ };
343
+
344
+ const line = `${JSON.stringify(summaryEntry)}\n`;
345
+ // Use appendFileSync from node:fs to ensure file is created if it doesn't exist
346
+ const { appendFileSync } = await import("node:fs");
347
+ appendFileSync(jsonlFilePath, line, "utf8");
348
+ logger?.debug("crash-recovery", "Exit summary written");
349
+ } catch (err) {
350
+ logger?.warn("crash-recovery", "Failed to write exit summary", { error: (err as Error).message });
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Get numeric signal number for exit code
356
+ */
357
+ function getSignalNumber(signal: NodeJS.Signals): number {
358
+ const signalMap: Record<string, number> = {
359
+ SIGTERM: 15,
360
+ SIGINT: 2,
361
+ SIGHUP: 1,
362
+ };
363
+ return signalMap[signal] ?? 15;
364
+ }
365
+
366
+ /**
367
+ * Reset handlers (for testing)
368
+ * @internal
369
+ */
370
+ export function resetCrashHandlers(): void {
371
+ handlersInstalled = false;
372
+ stopHeartbeat();
373
+ }