@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,400 @@
1
+ /**
2
+ * Parallel Execution — Worktree-based concurrent story execution
3
+ *
4
+ * Orchestrates parallel story execution using git worktrees: groups stories
5
+ * by dependencies, creates worktrees, dispatches concurrent pipelines,
6
+ * merges in dependency order, and cleans up worktrees.
7
+ */
8
+
9
+ import os from "node:os";
10
+ import { join } from "node:path";
11
+ import type { NaxConfig } from "../config";
12
+ import type { LoadedHooksConfig } from "../hooks";
13
+ import { getSafeLogger } from "../logger";
14
+ import type { PipelineEventEmitter } from "../pipeline/events";
15
+ import { runPipeline } from "../pipeline/runner";
16
+ import { defaultPipeline } from "../pipeline/stages";
17
+ import type { PipelineContext, RoutingResult } from "../pipeline/types";
18
+ import type { PluginRegistry } from "../plugins/registry";
19
+ import type { PRD, UserStory } from "../prd";
20
+ import { markStoryFailed, markStoryPassed, savePRD } from "../prd";
21
+ import { routeTask } from "../routing";
22
+ import { WorktreeManager } from "../worktree/manager";
23
+ import { MergeEngine, type StoryDependencies } from "../worktree/merge";
24
+
25
+ /**
26
+ * Result from parallel execution of a batch of stories
27
+ */
28
+ export interface ParallelBatchResult {
29
+ /** Stories that completed successfully */
30
+ successfulStories: UserStory[];
31
+ /** Stories that failed */
32
+ failedStories: Array<{ story: UserStory; error: string }>;
33
+ /** Total cost accumulated */
34
+ totalCost: number;
35
+ /** Stories with merge conflicts */
36
+ conflictedStories: Array<{ storyId: string; conflictFiles: string[] }>;
37
+ }
38
+
39
+ /**
40
+ * Group stories into dependency batches; stories in each batch can run in parallel.
41
+ */
42
+ function groupStoriesByDependencies(stories: UserStory[]): UserStory[][] {
43
+ const batches: UserStory[][] = [];
44
+ const processed = new Set<string>();
45
+ const storyMap = new Map(stories.map((s) => [s.id, s]));
46
+
47
+ // Keep processing until all stories are batched
48
+ while (processed.size < stories.length) {
49
+ const batch: UserStory[] = [];
50
+
51
+ for (const story of stories) {
52
+ if (processed.has(story.id)) continue;
53
+
54
+ // Check if all dependencies are satisfied
55
+ const depsCompleted = story.dependencies.every((dep) => processed.has(dep) || !storyMap.has(dep));
56
+
57
+ if (depsCompleted) {
58
+ batch.push(story);
59
+ }
60
+ }
61
+
62
+ if (batch.length === 0) {
63
+ // No stories ready — circular dependency or missing dep
64
+ const remaining = stories.filter((s) => !processed.has(s.id));
65
+ const logger = getSafeLogger();
66
+ logger?.error("parallel", "Cannot resolve story dependencies", {
67
+ remainingStories: remaining.map((s) => s.id),
68
+ });
69
+ throw new Error("Circular dependency or missing dependency detected");
70
+ }
71
+
72
+ // Mark batch stories as processed
73
+ for (const story of batch) {
74
+ processed.add(story.id);
75
+ }
76
+
77
+ batches.push(batch);
78
+ }
79
+
80
+ return batches;
81
+ }
82
+
83
+ /**
84
+ * Build dependency map for merge engine
85
+ */
86
+ function buildDependencyMap(stories: UserStory[]): StoryDependencies {
87
+ const deps: StoryDependencies = {};
88
+ for (const story of stories) {
89
+ deps[story.id] = story.dependencies;
90
+ }
91
+ return deps;
92
+ }
93
+
94
+ /**
95
+ * Execute a single story in its worktree
96
+ */
97
+ async function executeStoryInWorktree(
98
+ story: UserStory,
99
+ worktreePath: string,
100
+ context: Omit<PipelineContext, "story" | "stories" | "workdir" | "routing">,
101
+ routing: RoutingResult,
102
+ eventEmitter?: PipelineEventEmitter,
103
+ ): Promise<{ success: boolean; cost: number; error?: string }> {
104
+ const logger = getSafeLogger();
105
+
106
+ try {
107
+ const pipelineContext: PipelineContext = {
108
+ ...context,
109
+ story,
110
+ stories: [story],
111
+ workdir: worktreePath,
112
+ routing,
113
+ };
114
+
115
+ logger?.debug("parallel", "Executing story in worktree", {
116
+ storyId: story.id,
117
+ worktreePath,
118
+ });
119
+
120
+ const result = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
121
+
122
+ return {
123
+ success: result.success,
124
+ cost: result.context.agentResult?.estimatedCost || 0,
125
+ error: result.success ? undefined : result.reason,
126
+ };
127
+ } catch (error) {
128
+ return {
129
+ success: false,
130
+ cost: 0,
131
+ error: error instanceof Error ? error.message : String(error),
132
+ };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Execute a batch of independent stories in parallel
138
+ */
139
+ async function executeParallelBatch(
140
+ stories: UserStory[],
141
+ projectRoot: string,
142
+ config: NaxConfig,
143
+ prd: PRD,
144
+ context: Omit<PipelineContext, "story" | "stories" | "workdir" | "routing">,
145
+ maxConcurrency: number,
146
+ eventEmitter?: PipelineEventEmitter,
147
+ ): Promise<ParallelBatchResult> {
148
+ const logger = getSafeLogger();
149
+ const worktreeManager = new WorktreeManager();
150
+ const results: ParallelBatchResult = {
151
+ successfulStories: [],
152
+ failedStories: [],
153
+ totalCost: 0,
154
+ conflictedStories: [],
155
+ };
156
+
157
+ // Create worktrees for all stories in batch
158
+ const worktreeSetup: Array<{ story: UserStory; worktreePath: string }> = [];
159
+
160
+ for (const story of stories) {
161
+ const worktreePath = join(projectRoot, ".nax-wt", story.id);
162
+ try {
163
+ await worktreeManager.create(projectRoot, story.id);
164
+ worktreeSetup.push({ story, worktreePath });
165
+
166
+ logger?.info("parallel", "Created worktree for story", {
167
+ storyId: story.id,
168
+ worktreePath,
169
+ });
170
+ } catch (error) {
171
+ results.failedStories.push({
172
+ story,
173
+ error: `Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`,
174
+ });
175
+ logger?.error("parallel", "Failed to create worktree", {
176
+ storyId: story.id,
177
+ error: error instanceof Error ? error.message : String(error),
178
+ });
179
+ }
180
+ }
181
+
182
+ // Execute stories in parallel with concurrency limit
183
+ const executing: Promise<void>[] = [];
184
+ let activeCount = 0;
185
+
186
+ for (const { story, worktreePath } of worktreeSetup) {
187
+ const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config);
188
+
189
+ const executePromise = executeStoryInWorktree(story, worktreePath, context, routing as RoutingResult, eventEmitter)
190
+ .then((result) => {
191
+ results.totalCost += result.cost;
192
+
193
+ if (result.success) {
194
+ results.successfulStories.push(story);
195
+ logger?.info("parallel", "Story execution succeeded", {
196
+ storyId: story.id,
197
+ cost: result.cost,
198
+ });
199
+ } else {
200
+ results.failedStories.push({ story, error: result.error || "Unknown error" });
201
+ logger?.error("parallel", "Story execution failed", {
202
+ storyId: story.id,
203
+ error: result.error,
204
+ });
205
+ }
206
+ })
207
+ .finally(() => {
208
+ activeCount--;
209
+ // BUG-4 fix: Remove completed promise from executing array
210
+ const index = executing.indexOf(executePromise);
211
+ if (index > -1) {
212
+ executing.splice(index, 1);
213
+ }
214
+ });
215
+
216
+ executing.push(executePromise);
217
+ activeCount++;
218
+
219
+ // Wait if we've hit the concurrency limit
220
+ if (activeCount >= maxConcurrency) {
221
+ await Promise.race(executing);
222
+ }
223
+ }
224
+
225
+ // Wait for all remaining executions
226
+ await Promise.all(executing);
227
+
228
+ return results;
229
+ }
230
+
231
+ /**
232
+ * Determine max concurrency from parallel option
233
+ * - undefined: sequential mode (should not call this function)
234
+ * - 0: auto-detect (use CPU count)
235
+ * - N > 0: use N
236
+ */
237
+ function resolveMaxConcurrency(parallel: number): number {
238
+ if (parallel === 0) {
239
+ return os.cpus().length;
240
+ }
241
+ return Math.max(1, parallel);
242
+ }
243
+
244
+ /**
245
+ * Execute stories in parallel using worktree pipeline
246
+ *
247
+ * High-level flow:
248
+ * 1. Group stories by dependencies into batches
249
+ * 2. For each batch:
250
+ * a. Create worktrees for all stories
251
+ * b. Execute pipeline in parallel (respecting maxConcurrency)
252
+ * c. Merge successful branches in topological order
253
+ * d. Clean up worktrees on success, preserve on failure
254
+ * 3. Update PRD with results
255
+ */
256
+ export async function executeParallel(
257
+ stories: UserStory[],
258
+ prdPath: string,
259
+ projectRoot: string,
260
+ config: NaxConfig,
261
+ hooks: LoadedHooksConfig,
262
+ plugins: PluginRegistry,
263
+ prd: PRD,
264
+ featureDir: string | undefined,
265
+ parallel: number,
266
+ eventEmitter?: PipelineEventEmitter,
267
+ ): Promise<{ storiesCompleted: number; totalCost: number; updatedPrd: PRD }> {
268
+ const logger = getSafeLogger();
269
+ const maxConcurrency = resolveMaxConcurrency(parallel);
270
+ const worktreeManager = new WorktreeManager();
271
+ const mergeEngine = new MergeEngine(worktreeManager);
272
+
273
+ logger?.info("parallel", "Starting parallel execution", {
274
+ totalStories: stories.length,
275
+ maxConcurrency,
276
+ });
277
+
278
+ // Group stories by dependencies
279
+ const batches = groupStoriesByDependencies(stories);
280
+ logger?.info("parallel", "Grouped stories into batches", {
281
+ batchCount: batches.length,
282
+ batches: batches.map((b, i) => ({ index: i, storyCount: b.length, storyIds: b.map((s) => s.id) })),
283
+ });
284
+
285
+ let storiesCompleted = 0;
286
+ let totalCost = 0;
287
+ const currentPrd = prd;
288
+
289
+ // Execute each batch sequentially (stories within each batch run in parallel)
290
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
291
+ const batch = batches[batchIndex];
292
+ logger?.info("parallel", `Executing batch ${batchIndex + 1}/${batches.length}`, {
293
+ storyCount: batch.length,
294
+ storyIds: batch.map((s) => s.id),
295
+ });
296
+
297
+ // Build context for this batch (shared across all stories in batch)
298
+ const baseContext = {
299
+ config,
300
+ prd: currentPrd,
301
+ featureDir,
302
+ hooks,
303
+ plugins,
304
+ storyStartTime: new Date().toISOString(),
305
+ };
306
+
307
+ // Execute batch in parallel
308
+ const batchResult = await executeParallelBatch(
309
+ batch,
310
+ projectRoot,
311
+ config,
312
+ currentPrd,
313
+ baseContext,
314
+ maxConcurrency,
315
+ eventEmitter,
316
+ );
317
+
318
+ totalCost += batchResult.totalCost;
319
+
320
+ // Merge successful stories in topological order
321
+ if (batchResult.successfulStories.length > 0) {
322
+ const successfulIds = batchResult.successfulStories.map((s) => s.id);
323
+ const deps = buildDependencyMap(batch);
324
+
325
+ logger?.info("parallel", "Merging successful stories", {
326
+ storyIds: successfulIds,
327
+ });
328
+
329
+ const mergeResults = await mergeEngine.mergeAll(projectRoot, successfulIds, deps);
330
+
331
+ // Process merge results
332
+ for (const mergeResult of mergeResults) {
333
+ if (mergeResult.success) {
334
+ // Update PRD: mark story as passed
335
+ markStoryPassed(currentPrd, mergeResult.storyId);
336
+ storiesCompleted++;
337
+
338
+ logger?.info("parallel", "Story merged successfully", {
339
+ storyId: mergeResult.storyId,
340
+ retryCount: mergeResult.retryCount,
341
+ });
342
+ } else {
343
+ // Merge conflict — mark story as failed
344
+ markStoryFailed(currentPrd, mergeResult.storyId);
345
+ batchResult.conflictedStories.push({
346
+ storyId: mergeResult.storyId,
347
+ conflictFiles: mergeResult.conflictFiles || [],
348
+ });
349
+
350
+ logger?.error("parallel", "Merge conflict", {
351
+ storyId: mergeResult.storyId,
352
+ conflictFiles: mergeResult.conflictFiles,
353
+ });
354
+
355
+ // Keep worktree for manual resolution
356
+ logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
357
+ storyId: mergeResult.storyId,
358
+ worktreePath: join(projectRoot, ".nax-wt", mergeResult.storyId),
359
+ });
360
+ }
361
+ }
362
+ }
363
+
364
+ // Mark failed stories in PRD and clean up their worktrees
365
+ for (const { story, error } of batchResult.failedStories) {
366
+ markStoryFailed(currentPrd, story.id);
367
+
368
+ logger?.error("parallel", "Cleaning up failed story worktree", {
369
+ storyId: story.id,
370
+ error,
371
+ });
372
+
373
+ try {
374
+ await worktreeManager.remove(projectRoot, story.id);
375
+ } catch (cleanupError) {
376
+ logger?.warn("parallel", "Failed to clean up worktree", {
377
+ storyId: story.id,
378
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
379
+ });
380
+ }
381
+ }
382
+
383
+ // Save PRD after each batch
384
+ await savePRD(currentPrd, prdPath);
385
+
386
+ logger?.info("parallel", `Batch ${batchIndex + 1} complete`, {
387
+ successful: batchResult.successfulStories.length,
388
+ failed: batchResult.failedStories.length,
389
+ conflicts: batchResult.conflictedStories.length,
390
+ batchCost: batchResult.totalCost,
391
+ });
392
+ }
393
+
394
+ logger?.info("parallel", "Parallel execution complete", {
395
+ storiesCompleted,
396
+ totalCost,
397
+ });
398
+
399
+ return { storiesCompleted, totalCost, updatedPrd: currentPrd };
400
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * PID Registry — Track and cleanup spawned agent processes
3
+ *
4
+ * Implements BUG-002:
5
+ * - Track PIDs of spawned Claude Code processes
6
+ * - Write .nax-pids file for persistence across crashes
7
+ * - Support killAll() for crash signal handlers
8
+ * - Support cleanupStale() for startup cleanup
9
+ * - Use process groups (setsid) on Linux, direct kill on macOS
10
+ */
11
+
12
+ import { existsSync } from "node:fs";
13
+ import { getSafeLogger } from "../logger";
14
+
15
+ /**
16
+ * PID registry file name
17
+ */
18
+ const PID_REGISTRY_FILE = ".nax-pids";
19
+
20
+ /**
21
+ * PID registry entry
22
+ */
23
+ interface PidEntry {
24
+ pid: number;
25
+ spawnedAt: string;
26
+ workdir: string;
27
+ }
28
+
29
+ /**
30
+ * PID Registry — Track spawned agent processes and cleanup orphans
31
+ *
32
+ * Maintains a .nax-pids file in the workdir to track spawned processes.
33
+ * On crash, signal handlers call killAll() to terminate all tracked PIDs.
34
+ * On startup, runner calls cleanupStale() to kill any orphaned processes from previous runs.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const registry = new PidRegistry("/path/to/project");
39
+ * await registry.register(12345);
40
+ * // ... later, on crash or shutdown
41
+ * await registry.killAll();
42
+ * ```
43
+ */
44
+ export class PidRegistry {
45
+ private readonly workdir: string;
46
+ private readonly pidsFilePath: string;
47
+ private readonly pids: Set<number> = new Set();
48
+ private readonly platform: NodeJS.Platform;
49
+
50
+ /**
51
+ * Create a new PID registry for the given workdir.
52
+ *
53
+ * @param workdir - Working directory where .nax-pids will be stored
54
+ * @param platform - Optional platform override (for testing)
55
+ */
56
+ constructor(workdir: string, platform?: NodeJS.Platform) {
57
+ this.workdir = workdir;
58
+ this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
59
+ this.platform = platform ?? process.platform;
60
+ }
61
+
62
+ /**
63
+ * Register a spawned process PID.
64
+ *
65
+ * Adds the PID to the in-memory set and writes to .nax-pids file.
66
+ *
67
+ * @param pid - Process ID to register
68
+ */
69
+ async register(pid: number): Promise<void> {
70
+ const logger = getSafeLogger();
71
+ this.pids.add(pid);
72
+
73
+ const entry: PidEntry = {
74
+ pid,
75
+ spawnedAt: new Date().toISOString(),
76
+ workdir: this.workdir,
77
+ };
78
+
79
+ try {
80
+ // Read existing content or create empty file
81
+ let existingContent = "";
82
+ if (existsSync(this.pidsFilePath)) {
83
+ existingContent = await Bun.file(this.pidsFilePath).text();
84
+ }
85
+
86
+ // Append to .nax-pids file (one JSON entry per line)
87
+ const line = `${JSON.stringify(entry)}\n`;
88
+ await Bun.write(this.pidsFilePath, existingContent + line);
89
+ logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
90
+ } catch (err) {
91
+ logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
92
+ error: (err as Error).message,
93
+ });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Unregister a process PID (e.g., after clean exit).
99
+ *
100
+ * Removes the PID from the in-memory set and rewrites .nax-pids file.
101
+ *
102
+ * @param pid - Process ID to unregister
103
+ */
104
+ async unregister(pid: number): Promise<void> {
105
+ const logger = getSafeLogger();
106
+ this.pids.delete(pid);
107
+
108
+ try {
109
+ // Rewrite .nax-pids file without the unregistered PID
110
+ await this.writePidsFile();
111
+ logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
112
+ } catch (err) {
113
+ logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
114
+ error: (err as Error).message,
115
+ });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Kill all registered processes.
121
+ *
122
+ * Called by crash signal handlers to cleanup spawned agent processes.
123
+ * Uses process groups (setsid) on Linux, direct kill on macOS.
124
+ *
125
+ * On Linux: kill -TERM -<pid> kills the entire process group
126
+ * On macOS: kill -TERM <pid> kills the process directly
127
+ */
128
+ async killAll(): Promise<void> {
129
+ const logger = getSafeLogger();
130
+ const pids = Array.from(this.pids);
131
+
132
+ if (pids.length === 0) {
133
+ logger?.debug("pid-registry", "No PIDs to kill");
134
+ return;
135
+ }
136
+
137
+ logger?.info("pid-registry", `Killing ${pids.length} registered processes`, { pids });
138
+
139
+ const killPromises = pids.map((pid) => this.killPid(pid));
140
+ await Promise.allSettled(killPromises);
141
+
142
+ // Clear the registry file
143
+ try {
144
+ await Bun.write(this.pidsFilePath, "");
145
+ this.pids.clear();
146
+ logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
147
+ } catch (err) {
148
+ logger?.warn("pid-registry", "Failed to clear registry file", {
149
+ error: (err as Error).message,
150
+ });
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Cleanup stale PIDs from previous runs.
156
+ *
157
+ * Called at runner startup before lock acquisition.
158
+ * Reads .nax-pids file and kills any still-running processes.
159
+ */
160
+ async cleanupStale(): Promise<void> {
161
+ const logger = getSafeLogger();
162
+
163
+ if (!existsSync(this.pidsFilePath)) {
164
+ logger?.debug("pid-registry", "No stale PIDs file found");
165
+ return;
166
+ }
167
+
168
+ try {
169
+ const content = await Bun.file(this.pidsFilePath).text();
170
+ const lines = content
171
+ .split("\n")
172
+ .filter((line) => line.trim())
173
+ .map((line) => {
174
+ try {
175
+ return JSON.parse(line) as PidEntry;
176
+ } catch {
177
+ return null;
178
+ }
179
+ })
180
+ .filter((entry): entry is PidEntry => entry !== null);
181
+
182
+ if (lines.length === 0) {
183
+ logger?.debug("pid-registry", "No stale PIDs to cleanup");
184
+ await Bun.write(this.pidsFilePath, "");
185
+ return;
186
+ }
187
+
188
+ const stalePids = lines.map((entry) => entry.pid);
189
+ logger?.info("pid-registry", `Cleaning up ${stalePids.length} stale PIDs from previous run`, {
190
+ pids: stalePids,
191
+ });
192
+
193
+ const killPromises = stalePids.map((pid) => this.killPid(pid));
194
+ await Promise.allSettled(killPromises);
195
+
196
+ // Clear the registry file after cleanup
197
+ await Bun.write(this.pidsFilePath, "");
198
+ logger?.info("pid-registry", "Stale PIDs cleanup completed");
199
+ } catch (err) {
200
+ logger?.warn("pid-registry", "Failed to cleanup stale PIDs", {
201
+ error: (err as Error).message,
202
+ });
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Kill a single PID.
208
+ *
209
+ * Uses process groups on Linux (kill -TERM -<pid>), direct kill on macOS (kill -TERM <pid>).
210
+ * Ignores ESRCH (process not found) errors.
211
+ *
212
+ * @param pid - Process ID to kill
213
+ */
214
+ private async killPid(pid: number): Promise<void> {
215
+ const logger = getSafeLogger();
216
+
217
+ try {
218
+ // Check if process exists first
219
+ const checkProc = Bun.spawn(["kill", "-0", String(pid)], {
220
+ stdout: "pipe",
221
+ stderr: "pipe",
222
+ });
223
+ const checkCode = await checkProc.exited;
224
+
225
+ if (checkCode !== 0) {
226
+ // Process doesn't exist, skip
227
+ logger?.debug("pid-registry", `PID ${pid} not found (already exited)`, { pid });
228
+ return;
229
+ }
230
+
231
+ // On Linux, use process groups (kill -TERM -<pid>)
232
+ // On macOS, use direct kill (kill -TERM <pid>)
233
+ const killArgs = this.platform === "linux" ? ["kill", "-TERM", `-${pid}`] : ["kill", "-TERM", String(pid)];
234
+
235
+ const killProc = Bun.spawn(killArgs, {
236
+ stdout: "pipe",
237
+ stderr: "pipe",
238
+ });
239
+
240
+ const killCode = await killProc.exited;
241
+
242
+ if (killCode === 0) {
243
+ logger?.debug("pid-registry", `Killed PID ${pid}`, { pid });
244
+ } else {
245
+ const stderr = await new Response(killProc.stderr).text();
246
+ logger?.warn("pid-registry", `Failed to kill PID ${pid}`, {
247
+ pid,
248
+ exitCode: killCode,
249
+ stderr: stderr.trim(),
250
+ });
251
+ }
252
+ } catch (err) {
253
+ logger?.warn("pid-registry", `Error killing PID ${pid}`, {
254
+ pid,
255
+ error: (err as Error).message,
256
+ });
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Rewrite .nax-pids file with current in-memory PIDs.
262
+ */
263
+ private async writePidsFile(): Promise<void> {
264
+ const entries = Array.from(this.pids).map((pid) => ({
265
+ pid,
266
+ spawnedAt: new Date().toISOString(),
267
+ workdir: this.workdir,
268
+ }));
269
+
270
+ const content = entries.map((entry) => JSON.stringify(entry)).join("\n");
271
+ await Bun.write(this.pidsFilePath, content ? `${content}\n` : "");
272
+ }
273
+
274
+ /**
275
+ * Get all registered PIDs (for testing)
276
+ */
277
+ getPids(): number[] {
278
+ return Array.from(this.pids);
279
+ }
280
+ }