@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
package/bin/nax.ts ADDED
@@ -0,0 +1,930 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * nax — AI Coding Agent Orchestrator
4
+ *
5
+ * CLI entry point for the nax orchestration system.
6
+ *
7
+ * Features:
8
+ * - `init`: Initialize nax in a project directory
9
+ * - `run`: Execute the orchestration loop for a feature
10
+ * - `features create/list`: Manage feature definitions
11
+ * - `analyze`: Parse spec.md + tasks.md into prd.json
12
+ * - `agents`: Check available coding agent installations
13
+ * - `status`: Show current feature progress
14
+ *
15
+ * Architecture:
16
+ * - Complexity-based routing to model tiers (fast/balanced/powerful)
17
+ * - Three-session TDD for security-critical and complex stories
18
+ * - Story batching for simple stories to reduce overhead
19
+ * - Lifecycle hooks for custom automation (on-start, on-complete, etc.)
20
+ *
21
+ * @example
22
+ * ```bash
23
+ * # Initialize in project
24
+ * nax init
25
+ *
26
+ * # Create feature
27
+ * nax features create auth-system
28
+ *
29
+ * # Analyze spec/tasks into PRD
30
+ * nax analyze --feature auth-system
31
+ *
32
+ * # Run orchestration
33
+ * nax run --feature auth-system
34
+ *
35
+ * # Check status
36
+ * nax status --feature auth-system
37
+ * ```
38
+ */
39
+
40
+ import { existsSync, mkdirSync } from "node:fs";
41
+ import { homedir } from "node:os";
42
+ import { join, resolve } from "node:path";
43
+ import chalk from "chalk";
44
+ import { Command } from "commander";
45
+
46
+ import { checkAgentHealth, getAllAgentNames } from "../src/agents";
47
+ import {
48
+ acceptCommand,
49
+ analyzeFeature,
50
+ displayCostMetrics,
51
+ displayFeatureStatus,
52
+ displayLastRunMetrics,
53
+ displayModelEfficiency,
54
+ planCommand,
55
+ pluginsListCommand,
56
+ promptsCommand,
57
+ runsListCommand,
58
+ runsShowCommand,
59
+ } from "../src/cli";
60
+ import { configCommand } from "../src/cli/config";
61
+ import { generateCommand } from "../src/cli/generate";
62
+ import { diagnose } from "../src/commands/diagnose";
63
+ import { logsCommand } from "../src/commands/logs";
64
+ import { precheckCommand } from "../src/commands/precheck";
65
+ import { unlockCommand } from "../src/commands/unlock";
66
+ import { DEFAULT_CONFIG, findProjectDir, loadConfig, validateDirectory } from "../src/config";
67
+ import { run } from "../src/execution";
68
+ import { loadHooksConfig } from "../src/hooks";
69
+ import { type LogLevel, initLogger } from "../src/logger";
70
+ import { countStories, loadPRD } from "../src/prd";
71
+ import { PipelineEventEmitter, type StoryDisplayState, renderTui } from "../src/tui";
72
+
73
+ const pkg = await Bun.file(join(import.meta.dir, "..", "package.json")).json();
74
+
75
+ const program = new Command();
76
+
77
+ program.name("nax").description("AI Coding Agent Orchestrator — loops until done").version(pkg.version);
78
+
79
+ // ── init ─────────────────────────────────────────────
80
+ program
81
+ .command("init")
82
+ .description("Initialize nax in the current project")
83
+ .option("-d, --dir <path>", "Project directory", process.cwd())
84
+ .option("-f, --force", "Force overwrite existing files", false)
85
+ .action(async (options) => {
86
+ // Validate directory path
87
+ let workdir: string;
88
+ try {
89
+ workdir = validateDirectory(options.dir);
90
+ } catch (err) {
91
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
92
+ process.exit(1);
93
+ }
94
+
95
+ const naxDir = join(workdir, "nax");
96
+
97
+ if (existsSync(naxDir) && !options.force) {
98
+ console.log(chalk.yellow("nax already initialized. Use --force to overwrite."));
99
+ return;
100
+ }
101
+
102
+ // Create directory structure
103
+ mkdirSync(join(naxDir, "features"), { recursive: true });
104
+ mkdirSync(join(naxDir, "hooks"), { recursive: true });
105
+
106
+ // Write default config
107
+ await Bun.write(join(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
108
+
109
+ // Write default hooks.json
110
+ await Bun.write(
111
+ join(naxDir, "hooks.json"),
112
+ JSON.stringify(
113
+ {
114
+ hooks: {
115
+ "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
116
+ "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
117
+ "on-pause": { command: 'echo "nax paused: $NAX_REASON"', enabled: false },
118
+ "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false },
119
+ },
120
+ },
121
+ null,
122
+ 2,
123
+ ),
124
+ );
125
+
126
+ // Write .gitignore
127
+ await Bun.write(join(naxDir, ".gitignore"), "# nax temp files\n*.tmp\n.paused.json\n.nax-verifier-verdict.json\n");
128
+
129
+ // Write starter context.md
130
+ await Bun.write(
131
+ join(naxDir, "context.md"),
132
+ `# Project Context
133
+
134
+ This document defines coding standards, architectural decisions, and forbidden patterns for this project.
135
+ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
136
+
137
+ > Project metadata (dependencies, commands) is auto-injected by \`nax generate\`.
138
+
139
+ ## Coding Standards
140
+
141
+ - Follow the project's existing code style and conventions
142
+ - Write clear, self-documenting code with meaningful names
143
+ - Keep functions small and focused (single responsibility)
144
+ - Prefer immutability over mutation
145
+ - Use consistent formatting throughout the codebase
146
+
147
+ ## Testing Requirements
148
+
149
+ - All new code must include tests
150
+ - Tests should cover happy paths, edge cases, and error conditions
151
+ - Aim for high test coverage (80%+ recommended)
152
+ - Tests must pass before marking a story as complete
153
+ - Before writing tests, read existing test files to understand what is already covered
154
+ - Do not duplicate test coverage that prior stories already wrote
155
+ - Focus on testing NEW behavior introduced by this story
156
+
157
+ ## Architecture Rules
158
+
159
+ - Follow the project's existing architecture patterns
160
+ - Each module should have a clear, single purpose
161
+ - Avoid tight coupling between modules
162
+ - Use dependency injection where appropriate
163
+ - Document architectural decisions in comments or docs
164
+
165
+ ## Forbidden Patterns
166
+
167
+ - No hardcoded secrets, API keys, or credentials
168
+ - No console.log in production code (use proper logging)
169
+ - No \`any\` types in TypeScript (use proper typing)
170
+ - No commented-out code (use version control instead)
171
+ - No large files (split into smaller, focused modules)
172
+
173
+ ## Commit Standards
174
+
175
+ - Write clear, descriptive commit messages
176
+ - Follow conventional commits format (feat:, fix:, refactor:, etc.)
177
+ - Commit early and often with atomic changes
178
+ - Reference story IDs in commit messages
179
+
180
+ ## Documentation
181
+
182
+ - Add JSDoc comments for public APIs
183
+ - Update README when adding new features
184
+ - Document complex algorithms or business logic
185
+ - Keep documentation up-to-date with code changes
186
+
187
+ ---
188
+
189
+ **Note:** Customize this file to match your project's specific needs.
190
+ `,
191
+ );
192
+
193
+ console.log(chalk.green("✅ Initialized nax"));
194
+ console.log(chalk.dim(` ${naxDir}/`));
195
+ console.log(chalk.dim(" ├── config.json"));
196
+ console.log(chalk.dim(" ├── context.md"));
197
+ console.log(chalk.dim(" ├── hooks.json"));
198
+ console.log(chalk.dim(" ├── features/"));
199
+ console.log(chalk.dim(" └── hooks/"));
200
+ console.log(chalk.dim("\nNext: nax features create <name>"));
201
+ });
202
+
203
+ // ── run ──────────────────────────────────────────────
204
+ program
205
+ .command("run")
206
+ .description("Run the orchestration loop for a feature")
207
+ .requiredOption("-f, --feature <name>", "Feature name")
208
+ .option("-a, --agent <name>", "Force a specific agent")
209
+ .option("-m, --max-iterations <n>", "Max iterations", "20")
210
+ .option("--dry-run", "Show plan without executing", false)
211
+ .option("--no-context", "Disable context builder (skip file context in prompts)")
212
+ .option("--no-batch", "Disable story batching (execute all stories individually)")
213
+ .option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)")
214
+ .option("--headless", "Force headless mode (disable TUI, use pipe mode)", false)
215
+ .option("--verbose", "Enable verbose logging (debug level)", false)
216
+ .option("--quiet", "Quiet mode (warnings and errors only)", false)
217
+ .option("--silent", "Silent mode (errors only)", false)
218
+ .option("--json", "JSON mode (raw JSONL output to stdout)", false)
219
+ .option("-d, --dir <path>", "Working directory", process.cwd())
220
+ .option("--status-file <path>", "Write machine-readable JSON status file (updated during run)")
221
+ .option("--skip-precheck", "Skip precheck validations (advanced users only)", false)
222
+ .action(async (options) => {
223
+ // Validate directory path
224
+ let workdir: string;
225
+ try {
226
+ workdir = validateDirectory(options.dir);
227
+ } catch (err) {
228
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
229
+ process.exit(1);
230
+ }
231
+
232
+ // Determine log level from flags or env var (env var takes precedence)
233
+ let logLevel: LogLevel = "info"; // default
234
+ const envLevel = process.env.NAX_LOG_LEVEL?.toLowerCase();
235
+ if (envLevel && ["error", "warn", "info", "debug"].includes(envLevel)) {
236
+ logLevel = envLevel as LogLevel;
237
+ } else if (options.verbose) {
238
+ logLevel = "debug";
239
+ } else if (options.quiet) {
240
+ logLevel = "warn";
241
+ } else if (options.silent) {
242
+ logLevel = "error";
243
+ }
244
+
245
+ // Determine formatter mode from flags
246
+ let formatterMode: "quiet" | "normal" | "verbose" | "json" = "normal"; // default
247
+ if (options.json) {
248
+ formatterMode = "json";
249
+ } else if (options.verbose) {
250
+ formatterMode = "verbose";
251
+ } else if (options.quiet || options.silent) {
252
+ formatterMode = "quiet";
253
+ }
254
+
255
+ const config = await loadConfig();
256
+ const naxDir = findProjectDir(workdir);
257
+
258
+ if (!naxDir) {
259
+ console.error(chalk.red("nax not initialized. Run: nax init"));
260
+ process.exit(1);
261
+ }
262
+
263
+ const featureDir = join(naxDir, "features", options.feature);
264
+ const prdPath = join(featureDir, "prd.json");
265
+
266
+ if (!existsSync(prdPath)) {
267
+ console.error(chalk.red(`Feature "${options.feature}" not found or missing prd.json`));
268
+ process.exit(1);
269
+ }
270
+
271
+ // Create run directory and JSONL log file path
272
+ const runsDir = join(featureDir, "runs");
273
+ mkdirSync(runsDir, { recursive: true });
274
+
275
+ // Generate run ID from ISO timestamp
276
+ const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
277
+ const logFilePath = join(runsDir, `${runId}.jsonl`);
278
+
279
+ // Determine TUI vs headless mode
280
+ // TUI activates when:
281
+ // 1. stdout is a TTY, AND
282
+ // 2. --headless flag is NOT passed, AND
283
+ // 3. NAX_HEADLESS env var is NOT set
284
+ const isTTY = process.stdout.isTTY ?? false;
285
+ const headlessFlag = options.headless ?? false;
286
+ const headlessEnv = process.env.NAX_HEADLESS === "1";
287
+ const useHeadless = !isTTY || headlessFlag || headlessEnv;
288
+
289
+ // Initialize logger with selected level, file path, and formatter mode
290
+ initLogger({
291
+ level: logLevel,
292
+ filePath: logFilePath,
293
+ useChalk: true,
294
+ formatterMode: useHeadless ? formatterMode : undefined,
295
+ headless: useHeadless,
296
+ });
297
+
298
+ // Override config from CLI
299
+ if (options.agent) {
300
+ config.autoMode.defaultAgent = options.agent;
301
+ }
302
+ config.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
303
+
304
+ const globalNaxDir = join(homedir(), ".nax");
305
+ const hooks = await loadHooksConfig(naxDir, globalNaxDir);
306
+
307
+ // Create event emitter for TUI integration
308
+ const eventEmitter = new PipelineEventEmitter();
309
+
310
+ // Render TUI if not in headless mode
311
+ let tuiInstance: ReturnType<typeof renderTui> | undefined;
312
+ if (!useHeadless) {
313
+ // Load PRD to get initial story states
314
+ const prd = await loadPRD(prdPath);
315
+ const initialStories: StoryDisplayState[] = prd.userStories.map((story) => ({
316
+ story,
317
+ status: story.passes ? "passed" : "pending",
318
+ routing: story.routing,
319
+ cost: 0,
320
+ }));
321
+
322
+ tuiInstance = renderTui({
323
+ feature: options.feature,
324
+ stories: initialStories,
325
+ totalCost: 0,
326
+ elapsedMs: 0,
327
+ events: eventEmitter,
328
+ ptyOptions: null, // TODO: Pass actual PTY spawn options when runner supports it
329
+ });
330
+ } else {
331
+ console.log(chalk.dim(" [Headless mode — pipe output]"));
332
+ }
333
+
334
+ // Resolve --status-file relative to cwd (absolute paths unchanged)
335
+ const statusFilePath = options.statusFile ? resolve(process.cwd(), options.statusFile) : undefined;
336
+
337
+ // Parse --parallel option
338
+ let parallel: number | undefined;
339
+ if (options.parallel !== undefined) {
340
+ parallel = Number.parseInt(options.parallel, 10);
341
+ if (Number.isNaN(parallel) || parallel < 0) {
342
+ console.error(chalk.red("--parallel must be a non-negative integer"));
343
+ process.exit(1);
344
+ }
345
+ }
346
+
347
+ const result = await run({
348
+ prdPath,
349
+ workdir,
350
+ config,
351
+ hooks,
352
+ feature: options.feature,
353
+ featureDir,
354
+ dryRun: options.dryRun,
355
+ useBatch: options.batch ?? true,
356
+ parallel,
357
+ eventEmitter,
358
+ statusFile: statusFilePath,
359
+ logFilePath,
360
+ formatterMode: useHeadless ? formatterMode : undefined,
361
+ headless: useHeadless,
362
+ skipPrecheck: options.skipPrecheck ?? false,
363
+ });
364
+
365
+ // Create/update latest.jsonl symlink
366
+ const latestSymlink = join(runsDir, "latest.jsonl");
367
+ try {
368
+ // Remove existing symlink if present
369
+ if (existsSync(latestSymlink)) {
370
+ Bun.spawnSync(["rm", latestSymlink]);
371
+ }
372
+ // Create new symlink pointing to current run log
373
+ Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
374
+ cwd: runsDir,
375
+ });
376
+ } catch (error) {
377
+ console.error(chalk.yellow(`Warning: Failed to create latest.jsonl symlink: ${error}`));
378
+ }
379
+
380
+ // Cleanup TUI if it was rendered
381
+ if (tuiInstance) {
382
+ tuiInstance.unmount();
383
+ }
384
+
385
+ // Summary (only in headless mode; TUI shows summary itself)
386
+ if (useHeadless) {
387
+ console.log(chalk.dim("\n── Summary ──────────────────────────────────"));
388
+ console.log(chalk.dim(` Iterations: ${result.iterations}`));
389
+ console.log(chalk.dim(` Completed: ${result.storiesCompleted}`));
390
+ console.log(chalk.dim(` Cost: $${result.totalCost.toFixed(4)}`));
391
+ console.log(chalk.dim(` Duration: ${(result.durationMs / 1000 / 60).toFixed(1)} min`));
392
+ }
393
+
394
+ process.exit(result.success ? 0 : 1);
395
+ });
396
+
397
+ // ── features ─────────────────────────────────────────
398
+ const features = program.command("features").description("Manage features");
399
+
400
+ features
401
+ .command("create <name>")
402
+ .description("Create a new feature")
403
+ .option("-d, --dir <path>", "Project directory", process.cwd())
404
+ .action(async (name, options) => {
405
+ // Validate directory path
406
+ let workdir: string;
407
+ try {
408
+ workdir = validateDirectory(options.dir);
409
+ } catch (err) {
410
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
411
+ process.exit(1);
412
+ }
413
+
414
+ const naxDir = findProjectDir(workdir);
415
+ if (!naxDir) {
416
+ console.error(chalk.red("nax not initialized. Run: nax init"));
417
+ process.exit(1);
418
+ }
419
+
420
+ const featureDir = join(naxDir, "features", name);
421
+ mkdirSync(featureDir, { recursive: true });
422
+
423
+ // Create empty templates
424
+ await Bun.write(
425
+ join(featureDir, "spec.md"),
426
+ `# Feature: ${name}\n\n## Overview\n\n## Requirements\n\n## Acceptance Criteria\n`,
427
+ );
428
+ await Bun.write(
429
+ join(featureDir, "plan.md"),
430
+ `# Plan: ${name}\n\n## Architecture\n\n## Phases\n\n## Dependencies\n`,
431
+ );
432
+ await Bun.write(
433
+ join(featureDir, "tasks.md"),
434
+ `# Tasks: ${name}\n\n## US-001: [Title]\n\n### Description\n\n### Acceptance Criteria\n- [ ] Criterion 1\n`,
435
+ );
436
+ await Bun.write(
437
+ join(featureDir, "progress.txt"),
438
+ `# Progress: ${name}\n\nCreated: ${new Date().toISOString()}\n\n---\n`,
439
+ );
440
+
441
+ console.log(chalk.green(`✅ Created feature: ${name}`));
442
+ console.log(chalk.dim(` ${featureDir}/`));
443
+ console.log(chalk.dim(" ├── spec.md"));
444
+ console.log(chalk.dim(" ├── plan.md"));
445
+ console.log(chalk.dim(" ├── tasks.md"));
446
+ console.log(chalk.dim(" └── progress.txt"));
447
+ console.log(chalk.dim(`\nNext: Edit spec.md and tasks.md, then: nax analyze --feature ${name}`));
448
+ });
449
+
450
+ features
451
+ .command("list")
452
+ .description("List all features")
453
+ .option("-d, --dir <path>", "Project directory", process.cwd())
454
+ .action(async (options) => {
455
+ // Validate directory path
456
+ let workdir: string;
457
+ try {
458
+ workdir = validateDirectory(options.dir);
459
+ } catch (err) {
460
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
461
+ process.exit(1);
462
+ }
463
+
464
+ const naxDir = findProjectDir(workdir);
465
+ if (!naxDir) {
466
+ console.error(chalk.red("nax not initialized."));
467
+ process.exit(1);
468
+ }
469
+
470
+ const featuresDir = join(naxDir, "features");
471
+ if (!existsSync(featuresDir)) {
472
+ console.log(chalk.dim("No features yet."));
473
+ return;
474
+ }
475
+
476
+ const { readdirSync } = await import("node:fs");
477
+ const entries = readdirSync(featuresDir, { withFileTypes: true })
478
+ .filter((e) => e.isDirectory())
479
+ .map((e) => e.name);
480
+
481
+ if (entries.length === 0) {
482
+ console.log(chalk.dim("No features yet."));
483
+ return;
484
+ }
485
+
486
+ console.log(chalk.bold("\nFeatures:\n"));
487
+ for (const name of entries) {
488
+ const prdPath = join(featuresDir, name, "prd.json");
489
+ if (existsSync(prdPath)) {
490
+ const prd = await loadPRD(prdPath);
491
+ const c = countStories(prd);
492
+ console.log(` ${name} — ${c.passed}/${c.total} stories done`);
493
+ } else {
494
+ console.log(` ${name} (no prd.json yet)`);
495
+ }
496
+ }
497
+ console.log();
498
+ });
499
+
500
+ // ── plan ─────────────────────────────────────────────
501
+ program
502
+ .command("plan <description>")
503
+ .description("Interactive planning via agent plan mode")
504
+ .option("--from <file>", "Non-interactive mode: read from input file")
505
+ .option("-d, --dir <path>", "Project directory", process.cwd())
506
+ .action(async (description: string, options) => {
507
+ // Validate directory path
508
+ let workdir: string;
509
+ try {
510
+ workdir = validateDirectory(options.dir);
511
+ } catch (err) {
512
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
513
+ process.exit(1);
514
+ }
515
+
516
+ const naxDir = findProjectDir(workdir);
517
+ if (!naxDir) {
518
+ console.error(chalk.red("nax not initialized. Run: nax init"));
519
+ process.exit(1);
520
+ }
521
+
522
+ // Load config
523
+ const config = await loadConfig(workdir);
524
+
525
+ try {
526
+ const specPath = await planCommand(description, workdir, config, {
527
+ interactive: !options.from,
528
+ from: options.from,
529
+ });
530
+
531
+ console.log(chalk.green("\n✅ Planning complete"));
532
+ console.log(chalk.dim(` Spec: ${specPath}`));
533
+ console.log(chalk.dim("\nNext: nax analyze -f <feature-name>"));
534
+ } catch (err) {
535
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
536
+ process.exit(1);
537
+ }
538
+ });
539
+
540
+ // ── analyze ──────────────────────────────────────────
541
+ program
542
+ .command("analyze")
543
+ .description("Parse spec.md into prd.json via agent decompose")
544
+ .requiredOption("-f, --feature <name>", "Feature name")
545
+ .option("-b, --branch <name>", "Branch name", "feat/<feature>")
546
+ .option("--from <path>", "Explicit spec path (overrides default spec.md)")
547
+ .option("--reclassify", "Re-classify existing prd.json without decompose", false)
548
+ .option("-d, --dir <path>", "Project directory", process.cwd())
549
+ .action(async (options) => {
550
+ // Validate directory path
551
+ let workdir: string;
552
+ try {
553
+ workdir = validateDirectory(options.dir);
554
+ } catch (err) {
555
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
556
+ process.exit(1);
557
+ }
558
+
559
+ const naxDir = findProjectDir(workdir);
560
+ if (!naxDir) {
561
+ console.error(chalk.red("nax not initialized. Run: nax init"));
562
+ process.exit(1);
563
+ }
564
+
565
+ const featureDir = join(naxDir, "features", options.feature);
566
+ if (!existsSync(featureDir)) {
567
+ console.error(chalk.red(`Feature "${options.feature}" not found.`));
568
+ process.exit(1);
569
+ }
570
+
571
+ const branchName = options.branch.replace("<feature>", options.feature);
572
+
573
+ // Load config for validation
574
+ const config = await loadConfig(workdir);
575
+
576
+ try {
577
+ const prd = await analyzeFeature({
578
+ featureDir,
579
+ featureName: options.feature,
580
+ branchName,
581
+ config,
582
+ specPath: options.from,
583
+ reclassify: options.reclassify,
584
+ });
585
+
586
+ const prdPath = join(featureDir, "prd.json");
587
+ await Bun.write(prdPath, JSON.stringify(prd, null, 2));
588
+
589
+ const c = countStories(prd);
590
+ console.log(chalk.green(`\n✅ Generated prd.json for ${options.feature}`));
591
+ console.log(chalk.dim(` Stories: ${c.total}`));
592
+ console.log(chalk.dim(` Path: ${prdPath}`));
593
+
594
+ for (const story of prd.userStories) {
595
+ const routing = story.routing ? chalk.dim(` [${story.routing.complexity}]`) : "";
596
+ console.log(chalk.dim(` ${story.id}: ${story.title}${routing}`));
597
+ }
598
+ console.log();
599
+ } catch (err) {
600
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
601
+ process.exit(1);
602
+ }
603
+ });
604
+
605
+ // ── agents ───────────────────────────────────────────
606
+ program
607
+ .command("agents")
608
+ .description("Check available coding agents")
609
+ .action(async () => {
610
+ const health = await checkAgentHealth();
611
+
612
+ console.log(chalk.bold("\nCoding Agents:\n"));
613
+ for (const agent of health) {
614
+ const status = agent.installed ? chalk.green("✅ installed") : chalk.red("❌ not found");
615
+ console.log(` ${agent.displayName.padEnd(15)} ${status}`);
616
+ }
617
+ console.log();
618
+ });
619
+
620
+ // ── config ───────────────────────────────────────────
621
+ program
622
+ .command("config")
623
+ .description("Display effective merged configuration")
624
+ .option("--explain", "Show detailed field descriptions", false)
625
+ .option("--diff", "Show only fields where project overrides global", false)
626
+ .action(async (options) => {
627
+ try {
628
+ const config = await loadConfig();
629
+ await configCommand(config, { explain: options.explain, diff: options.diff });
630
+ } catch (err) {
631
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
632
+ process.exit(1);
633
+ }
634
+ });
635
+
636
+ // ── status ───────────────────────────────────────────
637
+ program
638
+ .command("status")
639
+ .description("Show current run status")
640
+ .option("-f, --feature <name>", "Feature name")
641
+ .option("-d, --dir <path>", "Project directory", process.cwd())
642
+ .option("--cost", "Show cost metrics across all runs", false)
643
+ .option("--last", "Show last run metrics (requires --cost)", false)
644
+ .option("--model", "Show per-model efficiency (requires --cost)", false)
645
+ .action(async (options) => {
646
+ // Validate directory path
647
+ let workdir: string;
648
+ try {
649
+ workdir = validateDirectory(options.dir);
650
+ } catch (err) {
651
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
652
+ process.exit(1);
653
+ }
654
+
655
+ const naxDir = findProjectDir(workdir);
656
+ if (!naxDir) {
657
+ console.error(chalk.red("nax not initialized."));
658
+ process.exit(1);
659
+ }
660
+
661
+ // Handle cost metrics flags
662
+ if (options.cost) {
663
+ if (options.last) {
664
+ await displayLastRunMetrics(workdir);
665
+ } else if (options.model) {
666
+ await displayModelEfficiency(workdir);
667
+ } else {
668
+ await displayCostMetrics(workdir);
669
+ }
670
+ return;
671
+ }
672
+
673
+ // Default status: show feature progress (new implementation with active run detection)
674
+ await displayFeatureStatus({
675
+ feature: options.feature,
676
+ dir: options.dir,
677
+ });
678
+ });
679
+
680
+ // ── logs ─────────────────────────────────────────────
681
+ program
682
+ .command("logs")
683
+ .description("Display run logs with filtering and follow mode")
684
+ .option("-d, --dir <path>", "Project directory", process.cwd())
685
+ .option("-f, --follow", "Follow mode - stream new entries real-time", false)
686
+ .option("-s, --story <id>", "Filter to specific story")
687
+ .option("--level <level>", "Filter by log level (debug|info|warn|error)")
688
+ .option("-l, --list", "List all runs in table format", false)
689
+ .option("-r, --run <timestamp>", "Select specific run by timestamp")
690
+ .option("-j, --json", "Output raw JSONL", false)
691
+ .action(async (options) => {
692
+ let workdir: string;
693
+ try {
694
+ workdir = validateDirectory(options.dir);
695
+ } catch (err) {
696
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
697
+ process.exit(1);
698
+ }
699
+
700
+ try {
701
+ await logsCommand({
702
+ dir: workdir,
703
+ follow: options.follow,
704
+ story: options.story,
705
+ level: options.level,
706
+ list: options.list,
707
+ run: options.run,
708
+ json: options.json,
709
+ });
710
+ } catch (err) {
711
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
712
+ process.exit(1);
713
+ }
714
+ });
715
+
716
+ // ── diagnose ─────────────────────────────────────────
717
+ program
718
+ .command("diagnose")
719
+ .description("Diagnose run failures and generate recommendations")
720
+ .option("-f, --feature <name>", "Feature name (defaults to current feature)")
721
+ .option("-d, --dir <path>", "Working directory", process.cwd())
722
+ .option("--json", "Output machine-readable JSON", false)
723
+ .option("--verbose", "Verbose output with story breakdown", false)
724
+ .action(async (options) => {
725
+ try {
726
+ await diagnose({
727
+ feature: options.feature,
728
+ workdir: options.dir,
729
+ json: options.json,
730
+ verbose: options.verbose,
731
+ });
732
+ } catch (err) {
733
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
734
+ process.exit(1);
735
+ }
736
+ });
737
+
738
+ // ── precheck ─────────────────────────────────────────
739
+ program
740
+ .command("precheck")
741
+ .description("Validate feature readiness before execution")
742
+ .option("-f, --feature <name>", "Feature name")
743
+ .option("-d, --dir <path>", "Project directory", process.cwd())
744
+ .option("--json", "Output machine-readable JSON", false)
745
+ .action(async (options) => {
746
+ try {
747
+ await precheckCommand({
748
+ feature: options.feature,
749
+ dir: options.dir,
750
+ json: options.json,
751
+ });
752
+ } catch (err) {
753
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
754
+ process.exit(1);
755
+ }
756
+ });
757
+
758
+ // ── unlock ───────────────────────────────────────────
759
+ program
760
+ .command("unlock")
761
+ .description("Release stale lock from crashed nax process")
762
+ .option("-d, --dir <path>", "Project directory", process.cwd())
763
+ .option("--force", "Skip liveness check and remove unconditionally", false)
764
+ .action(async (options) => {
765
+ try {
766
+ await unlockCommand({
767
+ dir: options.dir,
768
+ force: options.force,
769
+ });
770
+ } catch (err) {
771
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
772
+ process.exit(1);
773
+ }
774
+ });
775
+
776
+ // ── runs ─────────────────────────────────────────────
777
+ const runs = program.command("runs").description("Manage and view run history");
778
+
779
+ runs
780
+ .command("list")
781
+ .description("List all runs for a feature")
782
+ .requiredOption("-f, --feature <name>", "Feature name")
783
+ .option("-d, --dir <path>", "Project directory", process.cwd())
784
+ .action(async (options) => {
785
+ let workdir: string;
786
+ try {
787
+ workdir = validateDirectory(options.dir);
788
+ } catch (err) {
789
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
790
+ process.exit(1);
791
+ }
792
+
793
+ await runsListCommand({ feature: options.feature, workdir });
794
+ });
795
+
796
+ runs
797
+ .command("show <run-id>")
798
+ .description("Show detailed information for a specific run")
799
+ .requiredOption("-f, --feature <name>", "Feature name")
800
+ .option("-d, --dir <path>", "Project directory", process.cwd())
801
+ .action(async (runId, options) => {
802
+ let workdir: string;
803
+ try {
804
+ workdir = validateDirectory(options.dir);
805
+ } catch (err) {
806
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
807
+ process.exit(1);
808
+ }
809
+
810
+ await runsShowCommand({ runId, feature: options.feature, workdir });
811
+ });
812
+
813
+ // ── accept ───────────────────────────────────────────
814
+ program
815
+ .command("accept")
816
+ .description("Override failed acceptance criteria")
817
+ .requiredOption("-f, --feature <name>", "Feature name")
818
+ .requiredOption("--override <ac-id>", "AC ID to override (e.g., AC-2)")
819
+ .requiredOption("-r, --reason <reason>", "Reason for accepting despite test failure")
820
+ .action(async (options) => {
821
+ try {
822
+ await acceptCommand({
823
+ feature: options.feature,
824
+ override: options.override,
825
+ reason: options.reason,
826
+ });
827
+ } catch (err) {
828
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
829
+ process.exit(1);
830
+ }
831
+ });
832
+
833
+ // ── prompts ──────────────────────────────────────────
834
+ program
835
+ .command("prompts")
836
+ .description("Assemble prompts for stories without executing agents")
837
+ .requiredOption("-f, --feature <name>", "Feature name")
838
+ .option("--story <id>", "Filter to a single story ID (e.g., US-003)")
839
+ .option("--out <dir>", "Output directory for prompt files (default: stdout)")
840
+ .option("-d, --dir <path>", "Project directory", process.cwd())
841
+ .action(async (options) => {
842
+ // Validate directory path
843
+ let workdir: string;
844
+ try {
845
+ workdir = validateDirectory(options.dir);
846
+ } catch (err) {
847
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
848
+ process.exit(1);
849
+ }
850
+
851
+ // Load config
852
+ const config = await loadConfig(workdir);
853
+
854
+ try {
855
+ const processedStories = await promptsCommand({
856
+ feature: options.feature,
857
+ workdir,
858
+ config,
859
+ storyId: options.story,
860
+ outputDir: options.out,
861
+ });
862
+
863
+ if (options.out) {
864
+ console.log(chalk.green(`\n✅ Prompts written to ${options.out}`));
865
+ console.log(chalk.dim(` Processed ${processedStories.length} stories`));
866
+ }
867
+ } catch (err) {
868
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
869
+ process.exit(1);
870
+ }
871
+ });
872
+
873
+ // ── generate ──────────────────────────────────────────
874
+ program
875
+ .command("generate")
876
+ .description("Generate agent config files (CLAUDE.md, AGENTS.md, etc.) from nax/context.md")
877
+ .option("-c, --context <path>", "Context file path (default: nax/context.md)")
878
+ .option("-o, --output <dir>", "Output directory (default: project root)")
879
+ .option("-a, --agent <name>", "Specific agent (claude|opencode|cursor|windsurf|aider)")
880
+ .option("--dry-run", "Preview without writing files", false)
881
+ .option("--no-auto-inject", "Disable auto-injection of project metadata")
882
+ .action(async (options) => {
883
+ try {
884
+ await generateCommand({
885
+ context: options.context,
886
+ output: options.output,
887
+ agent: options.agent,
888
+ dryRun: options.dryRun,
889
+ noAutoInject: !options.autoInject,
890
+ });
891
+ } catch (err) {
892
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
893
+ process.exit(1);
894
+ }
895
+ });
896
+
897
+ // ── plugins ──────────────────────────────────────────
898
+ const plugins = program.command("plugins").description("Manage plugins");
899
+
900
+ plugins
901
+ .command("list")
902
+ .description("List all installed plugins")
903
+ .option("-d, --dir <path>", "Project directory", process.cwd())
904
+ .action(async (options) => {
905
+ // Validate directory path
906
+ let workdir: string;
907
+ try {
908
+ workdir = validateDirectory(options.dir);
909
+ } catch (err) {
910
+ console.error(chalk.red(`Invalid directory: ${(err as Error).message}`));
911
+ process.exit(1);
912
+ }
913
+
914
+ // Load config (or use default if outside project)
915
+ let config = DEFAULT_CONFIG;
916
+ try {
917
+ config = await loadConfig(workdir);
918
+ } catch {
919
+ // Outside project directory, use default config (global plugins only)
920
+ }
921
+
922
+ try {
923
+ await pluginsListCommand(config, workdir);
924
+ } catch (err) {
925
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
926
+ process.exit(1);
927
+ }
928
+ });
929
+
930
+ program.parse();