@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,151 @@
1
+ /**
2
+ * Status Cost Metrics
3
+ *
4
+ * Extracted from status.ts: cost metrics display functions for CLI output.
5
+ */
6
+
7
+ import { getLogger } from "../logger";
8
+ import { calculateAggregateMetrics, getLastRun, loadRunMetrics } from "../metrics";
9
+
10
+ /**
11
+ * Display aggregate cost metrics across all runs.
12
+ *
13
+ * @param workdir - Project root directory
14
+ *
15
+ * @example
16
+ * ```bash
17
+ * nax status --cost
18
+ * ```
19
+ */
20
+ export async function displayCostMetrics(workdir: string): Promise<void> {
21
+ const logger = getLogger();
22
+ const runs = await loadRunMetrics(workdir);
23
+
24
+ if (runs.length === 0) {
25
+ logger.info("cli", "No metrics data available yet", { hint: "Run nax run to generate metrics" });
26
+ return;
27
+ }
28
+
29
+ const aggregate = calculateAggregateMetrics(runs);
30
+
31
+ logger.info("cli", "Cost Metrics (All Runs)", {
32
+ totalRuns: aggregate.totalRuns,
33
+ totalStories: aggregate.totalStories,
34
+ totalCost: aggregate.totalCost,
35
+ avgCostPerStory: aggregate.avgCostPerStory,
36
+ avgCostPerFeature: aggregate.avgCostPerFeature,
37
+ firstPassRate: aggregate.firstPassRate,
38
+ escalationRate: aggregate.escalationRate,
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Display metrics from the most recent run.
44
+ *
45
+ * @param workdir - Project root directory
46
+ *
47
+ * @example
48
+ * ```bash
49
+ * nax status --cost --last
50
+ * ```
51
+ */
52
+ export async function displayLastRunMetrics(workdir: string): Promise<void> {
53
+ const logger = getLogger();
54
+ const runs = await loadRunMetrics(workdir);
55
+
56
+ if (runs.length === 0) {
57
+ logger.info("cli", "No metrics data available yet", { hint: "Run nax run to generate metrics" });
58
+ return;
59
+ }
60
+
61
+ const lastRun = getLastRun(runs);
62
+ if (!lastRun) {
63
+ return;
64
+ }
65
+
66
+ logger.info("cli", `Last Run: ${lastRun.feature}`, {
67
+ runId: lastRun.runId,
68
+ startedAt: lastRun.startedAt,
69
+ completedAt: lastRun.completedAt,
70
+ durationMs: lastRun.totalDurationMs,
71
+ totalStories: lastRun.totalStories,
72
+ storiesCompleted: lastRun.storiesCompleted,
73
+ storiesFailed: lastRun.storiesFailed,
74
+ totalCost: lastRun.totalCost,
75
+ avgCostPerStory: lastRun.totalCost / lastRun.totalStories,
76
+ });
77
+
78
+ // Show top 5 most expensive stories
79
+ const sortedStories = [...lastRun.stories].sort((a, b) => b.cost - a.cost);
80
+ const topStories = sortedStories.slice(0, 5);
81
+
82
+ if (topStories.length > 0) {
83
+ logger.info("cli", "Top 5 Most Expensive Stories", {
84
+ stories: topStories.map((s) => ({
85
+ storyId: s.storyId,
86
+ cost: s.cost,
87
+ model: s.modelUsed,
88
+ attempts: s.attempts,
89
+ })),
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Display per-model efficiency metrics.
96
+ *
97
+ * @param workdir - Project root directory
98
+ *
99
+ * @example
100
+ * ```bash
101
+ * nax status --cost --model
102
+ * ```
103
+ */
104
+ export async function displayModelEfficiency(workdir: string): Promise<void> {
105
+ const logger = getLogger();
106
+ const runs = await loadRunMetrics(workdir);
107
+
108
+ if (runs.length === 0) {
109
+ logger.info("cli", "No metrics data available yet", { hint: "Run nax run to generate metrics" });
110
+ return;
111
+ }
112
+
113
+ const aggregate = calculateAggregateMetrics(runs);
114
+
115
+ // Sort models by total cost (descending)
116
+ const sortedModels = Object.entries(aggregate.modelEfficiency).sort(([, a], [, b]) => b.totalCost - a.totalCost);
117
+
118
+ if (sortedModels.length === 0) {
119
+ logger.info("cli", "No model data available");
120
+ return;
121
+ }
122
+
123
+ logger.info("cli", "Model Efficiency", {
124
+ models: sortedModels.map(([modelName, stats]) => ({
125
+ model: modelName,
126
+ attempts: stats.attempts,
127
+ passRate: stats.passRate,
128
+ avgCost: stats.avgCost,
129
+ totalCost: stats.totalCost,
130
+ })),
131
+ });
132
+
133
+ // Show complexity accuracy
134
+ const sortedComplexity = Object.entries(aggregate.complexityAccuracy).sort(
135
+ ([, a], [, b]) => b.predicted - a.predicted,
136
+ );
137
+
138
+ if (sortedComplexity.length === 0) {
139
+ logger.info("cli", "No complexity data available");
140
+ return;
141
+ }
142
+
143
+ logger.info("cli", "Complexity Prediction Accuracy", {
144
+ complexities: sortedComplexity.map(([complexity, stats]) => ({
145
+ complexity,
146
+ predicted: stats.predicted,
147
+ actualTierUsed: stats.actualTierUsed,
148
+ mismatchRate: stats.mismatchRate,
149
+ })),
150
+ });
151
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Status Feature Display
3
+ *
4
+ * Extracted from status.ts: feature status display (all-features table and single-feature details).
5
+ */
6
+
7
+ import { existsSync, readdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import chalk from "chalk";
10
+ import { resolveProject } from "../commands/common";
11
+ import type { NaxStatusFile } from "../execution/status-file";
12
+ import { listPendingInteractions, loadPendingInteraction } from "../interaction";
13
+ import { countStories, loadPRD } from "../prd";
14
+
15
+ /** Options for feature status command */
16
+ export interface FeatureStatusOptions {
17
+ /** Feature name (from -f flag) */
18
+ feature?: string;
19
+ /** Explicit project directory (from -d flag) */
20
+ dir?: string;
21
+ }
22
+
23
+ /** Feature summary for the all-features table */
24
+ interface FeatureSummary {
25
+ name: string;
26
+ done: number;
27
+ failed: number;
28
+ pending: number;
29
+ total: number;
30
+ lastRun?: string;
31
+ cost?: number;
32
+ activeRun?: {
33
+ runId: string;
34
+ pid: number;
35
+ startedAt: string;
36
+ };
37
+ crashedRun?: {
38
+ runId: string;
39
+ pid: number;
40
+ crashedAt?: string;
41
+ };
42
+ }
43
+
44
+ /** Check if a process is alive via PID check */
45
+ function isPidAlive(pid: number): boolean {
46
+ try {
47
+ const result = Bun.spawnSync(["ps", "-p", String(pid)], {
48
+ stdout: "ignore",
49
+ stderr: "ignore",
50
+ });
51
+ return result.exitCode === 0;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /** Load status.json for a feature (if it exists) */
58
+ async function loadStatusFile(featureDir: string): Promise<NaxStatusFile | null> {
59
+ const statusPath = join(featureDir, "status.json");
60
+ if (!existsSync(statusPath)) {
61
+ return null;
62
+ }
63
+
64
+ try {
65
+ const content = Bun.file(statusPath);
66
+ return (await content.json()) as NaxStatusFile;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** Get feature summary from prd.json and optional status.json */
73
+ async function getFeatureSummary(featureName: string, featureDir: string): Promise<FeatureSummary> {
74
+ const prdPath = join(featureDir, "prd.json");
75
+
76
+ // Load PRD for story counts
77
+ const prd = await loadPRD(prdPath);
78
+ const counts = countStories(prd);
79
+
80
+ const summary: FeatureSummary = {
81
+ name: featureName,
82
+ done: counts.passed,
83
+ failed: counts.failed,
84
+ pending: counts.pending,
85
+ total: counts.total,
86
+ };
87
+
88
+ // Load status.json if available
89
+ const status = await loadStatusFile(featureDir);
90
+ if (status) {
91
+ summary.cost = status.cost.spent;
92
+
93
+ // Check if run is active or crashed
94
+ const pidAlive = isPidAlive(status.run.pid);
95
+
96
+ if (status.run.status === "running" && pidAlive) {
97
+ summary.activeRun = {
98
+ runId: status.run.id,
99
+ pid: status.run.pid,
100
+ startedAt: status.run.startedAt,
101
+ };
102
+ } else if (status.run.status === "running" && !pidAlive) {
103
+ // Run is marked "running" but PID is dead — crashed
104
+ summary.crashedRun = {
105
+ runId: status.run.id,
106
+ pid: status.run.pid,
107
+ crashedAt: status.run.crashedAt,
108
+ };
109
+ } else if (status.run.status === "crashed") {
110
+ // Run explicitly marked as crashed
111
+ summary.crashedRun = {
112
+ runId: status.run.id,
113
+ pid: status.run.pid,
114
+ crashedAt: status.run.crashedAt,
115
+ };
116
+ }
117
+ }
118
+
119
+ // Get last run timestamp from runs/ directory
120
+ const runsDir = join(featureDir, "runs");
121
+ if (existsSync(runsDir)) {
122
+ const runs = readdirSync(runsDir, { withFileTypes: true })
123
+ .filter((e) => e.isFile() && e.name.endsWith(".jsonl") && e.name !== "latest.jsonl")
124
+ .map((e) => e.name)
125
+ .sort()
126
+ .reverse();
127
+
128
+ if (runs.length > 0) {
129
+ // Extract timestamp from filename (YYYY-MM-DDTHH-MM-SS.jsonl)
130
+ const latestRun = runs[0].replace(".jsonl", "");
131
+ summary.lastRun = latestRun;
132
+ }
133
+ }
134
+
135
+ return summary;
136
+ }
137
+
138
+ /** Display all features table */
139
+ async function displayAllFeatures(projectDir: string): Promise<void> {
140
+ const featuresDir = join(projectDir, "nax", "features");
141
+
142
+ if (!existsSync(featuresDir)) {
143
+ console.log(chalk.dim("No features found."));
144
+ return;
145
+ }
146
+
147
+ const features = readdirSync(featuresDir, { withFileTypes: true })
148
+ .filter((e) => e.isDirectory())
149
+ .map((e) => e.name)
150
+ .sort();
151
+
152
+ if (features.length === 0) {
153
+ console.log(chalk.dim("No features found."));
154
+ return;
155
+ }
156
+
157
+ // Load summaries for all features
158
+ const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join(featuresDir, name))));
159
+
160
+ console.log(chalk.bold("\n📊 Features\n"));
161
+
162
+ // Print table header
163
+ const header = ` ${"Feature".padEnd(25)} ${"Done".padEnd(6)} ${"Failed".padEnd(8)} ${"Pending".padEnd(9)} ${"Last Run".padEnd(22)} ${"Cost".padEnd(10)} Status`;
164
+ console.log(chalk.dim(header));
165
+ console.log(chalk.dim(` ${"─".repeat(100)}`));
166
+
167
+ // Print each feature row
168
+ for (const summary of summaries) {
169
+ const name = summary.name.padEnd(25);
170
+ const done = chalk.green(String(summary.done).padEnd(6));
171
+ const failed =
172
+ summary.failed > 0 ? chalk.red(String(summary.failed).padEnd(8)) : chalk.dim(String(summary.failed).padEnd(8));
173
+ const pending = chalk.dim(String(summary.pending).padEnd(9));
174
+ const lastRun = summary.lastRun ? summary.lastRun.padEnd(22) : chalk.dim("No runs yet".padEnd(22));
175
+ const cost = summary.cost !== undefined ? `$${summary.cost.toFixed(4)}`.padEnd(10) : chalk.dim("—".padEnd(10));
176
+
177
+ let status = "";
178
+ if (summary.activeRun) {
179
+ status = chalk.yellow("⚡ Running");
180
+ } else if (summary.crashedRun) {
181
+ status = chalk.red("💥 Crashed");
182
+ } else {
183
+ status = chalk.dim("—");
184
+ }
185
+
186
+ console.log(` ${name} ${done} ${failed} ${pending} ${lastRun} ${cost} ${status}`);
187
+ }
188
+
189
+ console.log();
190
+ }
191
+
192
+ /** Display single feature details */
193
+ async function displayFeatureDetails(featureName: string, featureDir: string): Promise<void> {
194
+ const prdPath = join(featureDir, "prd.json");
195
+ const prd = await loadPRD(prdPath);
196
+ const counts = countStories(prd);
197
+
198
+ // Load status.json if available
199
+ const status = await loadStatusFile(featureDir);
200
+
201
+ console.log(chalk.bold(`\n📊 ${prd.feature}\n`));
202
+
203
+ // Check for pending interactions
204
+ const pendingIds = await listPendingInteractions(featureDir);
205
+ if (pendingIds.length > 0) {
206
+ console.log(chalk.cyan(`⏸️ Paused — Waiting for Interaction (${pendingIds.length} pending)\n`));
207
+
208
+ for (const id of pendingIds) {
209
+ const req = await loadPendingInteraction(id, featureDir);
210
+ if (req) {
211
+ const safety = req.metadata?.safety ?? "unknown";
212
+ const safetyIcon = safety === "red" ? "🔴" : safety === "yellow" ? "🟡" : "🟢";
213
+ const timeRemaining = req.timeout ? Math.max(0, req.createdAt + req.timeout - Date.now()) : null;
214
+ const timeoutSec = timeRemaining !== null ? Math.floor(timeRemaining / 1000) : null;
215
+
216
+ console.log(` ${safetyIcon} ${chalk.bold(req.id)}`);
217
+ console.log(chalk.dim(` Type: ${req.type}`));
218
+ console.log(chalk.dim(` Summary: ${req.summary}`));
219
+ console.log(chalk.dim(` Fallback: ${req.fallback}`));
220
+ if (timeoutSec !== null) {
221
+ if (timeoutSec > 0) {
222
+ console.log(chalk.dim(` Timeout: ${timeoutSec}s remaining`));
223
+ } else {
224
+ console.log(chalk.red(" Timeout: EXPIRED"));
225
+ }
226
+ }
227
+ console.log();
228
+ }
229
+ }
230
+
231
+ console.log(chalk.dim(" 💡 Respond with: nax interact respond <id> --action approve|reject|skip|abort"));
232
+ console.log();
233
+ }
234
+
235
+ // Display run status if active or crashed
236
+ if (status) {
237
+ const pidAlive = isPidAlive(status.run.pid);
238
+
239
+ if (status.run.status === "running" && pidAlive) {
240
+ console.log(chalk.yellow("⚡ Active Run:"));
241
+ console.log(chalk.dim(` Run ID: ${status.run.id}`));
242
+ console.log(chalk.dim(` PID: ${status.run.pid}`));
243
+ console.log(chalk.dim(` Started: ${status.run.startedAt}`));
244
+ console.log(chalk.dim(` Progress: ${status.progress.passed}/${status.progress.total} stories`));
245
+ console.log(chalk.dim(` Cost: $${status.cost.spent.toFixed(4)}`));
246
+
247
+ if (status.current) {
248
+ console.log(chalk.dim(` Current: ${status.current.storyId} - ${status.current.title}`));
249
+ }
250
+
251
+ console.log();
252
+ } else if ((status.run.status === "running" && !pidAlive) || status.run.status === "crashed") {
253
+ console.log(chalk.red("💥 Crashed Run Detected:\n"));
254
+ console.log(chalk.dim(` Run ID: ${status.run.id}`));
255
+ console.log(chalk.dim(` PID: ${status.run.pid} (dead)`));
256
+ console.log(chalk.dim(` Started: ${status.run.startedAt}`));
257
+ if (status.run.crashedAt) {
258
+ console.log(chalk.dim(` Crashed: ${status.run.crashedAt}`));
259
+ }
260
+ if (status.run.crashSignal) {
261
+ console.log(chalk.dim(` Signal: ${status.run.crashSignal}`));
262
+ }
263
+ console.log(chalk.dim(` Progress: ${status.progress.passed}/${status.progress.total} stories (at crash)`));
264
+ console.log();
265
+ console.log(chalk.yellow("💡 Recovery Hints:"));
266
+ console.log(chalk.dim(" • Check the latest run log in runs/ directory"));
267
+ console.log(chalk.dim(" • Review status.json for last known state"));
268
+ console.log(chalk.dim(` • Re-run with: nax run -f ${featureName}`));
269
+ console.log();
270
+ }
271
+ } else {
272
+ console.log(chalk.dim("No active run (status.json not found)\n"));
273
+ }
274
+
275
+ // Display story counts
276
+ console.log(chalk.bold("Progress:"));
277
+ console.log(chalk.dim(` Branch: ${prd.branchName}`));
278
+ console.log(chalk.dim(` Updated: ${prd.updatedAt}`));
279
+ console.log(chalk.dim(` Total: ${counts.total}`));
280
+ console.log(chalk.green(` Passed: ${counts.passed}`));
281
+ console.log(chalk.red(` Failed: ${counts.failed}`));
282
+ console.log(chalk.dim(` Pending: ${counts.pending}`));
283
+ if (counts.skipped > 0) {
284
+ console.log(chalk.yellow(` Skipped: ${counts.skipped}`));
285
+ }
286
+ console.log();
287
+
288
+ // Display story table
289
+ console.log(chalk.bold("Stories:\n"));
290
+ for (const story of prd.userStories) {
291
+ const icon = story.passes ? "✅" : story.status === "failed" ? "❌" : story.status === "skipped" ? "⏭️" : "⬜";
292
+ const routing = story.routing
293
+ ? chalk.dim(` [${story.routing.complexity}/${story.routing.modelTier}/${story.routing.testStrategy}]`)
294
+ : "";
295
+ console.log(` ${icon} ${story.id}: ${story.title}${routing}`);
296
+ }
297
+
298
+ console.log();
299
+
300
+ // Display last run info if completed
301
+ if (status && status.run.status !== "running") {
302
+ console.log(chalk.dim(`Last run: ${status.run.id}`));
303
+ console.log(chalk.dim(`Cost: $${status.cost.spent.toFixed(4)}`));
304
+ console.log();
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Display feature status (all features table or single feature details)
310
+ *
311
+ * @param options - Command options
312
+ *
313
+ * @example
314
+ * ```bash
315
+ * # Show all features
316
+ * nax status
317
+ *
318
+ * # Show single feature details
319
+ * nax status -f structured-logging
320
+ * ```
321
+ */
322
+ export async function displayFeatureStatus(options: FeatureStatusOptions = {}): Promise<void> {
323
+ const resolved = resolveProject({
324
+ dir: options.dir,
325
+ feature: options.feature,
326
+ });
327
+
328
+ if (options.feature) {
329
+ // Single feature view
330
+ if (!resolved.featureDir) {
331
+ throw new Error("Feature directory not resolved (this should not happen)");
332
+ }
333
+ await displayFeatureDetails(options.feature, resolved.featureDir);
334
+ } else {
335
+ // All features table
336
+ await displayAllFeatures(resolved.projectDir);
337
+ }
338
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Status CLI Command
3
+ *
4
+ * Re-export barrel for backward compatibility.
5
+ * Cost metrics: ./status-cost
6
+ * Feature display: ./status-features
7
+ */
8
+
9
+ // Cost metrics
10
+ export { displayCostMetrics, displayLastRunMetrics, displayModelEfficiency } from "./status-cost";
11
+
12
+ // Feature display
13
+ export { displayFeatureStatus, type FeatureStatusOptions } from "./status-features";
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Common utilities for CLI commands
3
+ *
4
+ * Provides project resolution logic shared across status, logs, and other commands.
5
+ */
6
+
7
+ import { existsSync, readdirSync, realpathSync } from "node:fs";
8
+ import { join, resolve } from "node:path";
9
+ import { MAX_DIRECTORY_DEPTH } from "../config/path-security";
10
+ import { NaxError } from "../errors";
11
+
12
+ /**
13
+ * Options for project resolution
14
+ */
15
+ export interface ResolveProjectOptions {
16
+ /** Explicit project directory (from -d flag) */
17
+ dir?: string;
18
+ /** Feature name (from -f flag) */
19
+ feature?: string;
20
+ }
21
+
22
+ /**
23
+ * Resolved project paths
24
+ */
25
+ export interface ResolvedProject {
26
+ /** Absolute path to project root directory */
27
+ projectDir: string;
28
+ /** Absolute path to nax config file */
29
+ configPath: string;
30
+ /** Absolute path to feature directory (if feature specified) */
31
+ featureDir?: string;
32
+ }
33
+
34
+ /**
35
+ * Resolves project directory using the following priority:
36
+ * 1. Explicit -d flag path
37
+ * 2. Current working directory (if it contains nax/ directory)
38
+ * 3. Walk up directory tree to find nax/ (up to MAX_DIRECTORY_DEPTH)
39
+ *
40
+ * Validates:
41
+ * - nax/ directory exists
42
+ * - nax/config.json exists
43
+ * - nax/features/<name>/ exists (if feature specified)
44
+ *
45
+ * @param options - Resolution options (dir, feature)
46
+ * @returns Resolved project paths
47
+ * @throws {NaxError} If project cannot be resolved or validation fails
48
+ */
49
+ export function resolveProject(options: ResolveProjectOptions = {}): ResolvedProject {
50
+ const { dir, feature } = options;
51
+
52
+ // Step 1: Determine project root and validate structure
53
+ let projectRoot: string;
54
+ let naxDir: string;
55
+ let configPath: string;
56
+
57
+ if (dir) {
58
+ // Use explicit -d flag path (resolve relative paths and symlinks)
59
+ projectRoot = realpathSync(resolve(dir));
60
+ naxDir = join(projectRoot, "nax");
61
+
62
+ // Validate nax/ directory exists
63
+ if (!existsSync(naxDir)) {
64
+ throw new NaxError(
65
+ `Directory does not contain a nax project: ${projectRoot}\nExpected to find: ${naxDir}`,
66
+ "NAX_DIR_NOT_FOUND",
67
+ { projectRoot, naxDir },
68
+ );
69
+ }
70
+
71
+ // Validate nax/config.json exists
72
+ configPath = join(naxDir, "config.json");
73
+ if (!existsSync(configPath)) {
74
+ throw new NaxError(
75
+ `nax directory found but config.json is missing: ${naxDir}\nExpected to find: ${configPath}`,
76
+ "CONFIG_NOT_FOUND",
77
+ { naxDir, configPath },
78
+ );
79
+ }
80
+ } else {
81
+ // Walk up from CWD to find nax/ directory with config.json
82
+ const found = findProjectRoot(process.cwd());
83
+ if (!found) {
84
+ // Check if CWD has nax/ but missing config.json (for better error message)
85
+ const cwdNaxDir = join(process.cwd(), "nax");
86
+ if (existsSync(cwdNaxDir)) {
87
+ const cwdConfigPath = join(cwdNaxDir, "config.json");
88
+ throw new NaxError(
89
+ `nax directory found but config.json is missing: ${cwdNaxDir}\nExpected to find: ${cwdConfigPath}`,
90
+ "CONFIG_NOT_FOUND",
91
+ { naxDir: cwdNaxDir, configPath: cwdConfigPath },
92
+ );
93
+ }
94
+
95
+ throw new NaxError(
96
+ "No nax project found. Run this command from within a nax project directory, or use -d flag to specify the project path.",
97
+ "PROJECT_NOT_FOUND",
98
+ { cwd: process.cwd() },
99
+ );
100
+ }
101
+ projectRoot = found;
102
+ naxDir = join(projectRoot, "nax");
103
+ configPath = join(naxDir, "config.json");
104
+ }
105
+
106
+ // Step 4: Validate feature directory (if specified)
107
+ let featureDir: string | undefined;
108
+ if (feature) {
109
+ const featuresDir = join(naxDir, "features");
110
+ featureDir = join(featuresDir, feature);
111
+
112
+ if (!existsSync(featureDir)) {
113
+ // List available features for helpful error message
114
+ const availableFeatures = existsSync(featuresDir)
115
+ ? readdirSync(featuresDir, { withFileTypes: true })
116
+ .filter((entry) => entry.isDirectory())
117
+ .map((entry) => entry.name)
118
+ : [];
119
+
120
+ const availableMsg =
121
+ availableFeatures.length > 0
122
+ ? `\n\nAvailable features:\n${availableFeatures.map((f) => ` - ${f}`).join("\n")}`
123
+ : "\n\nNo features found in this project.";
124
+
125
+ throw new NaxError(`Feature not found: ${feature}${availableMsg}`, "FEATURE_NOT_FOUND", {
126
+ feature,
127
+ featuresDir,
128
+ availableFeatures,
129
+ });
130
+ }
131
+ }
132
+
133
+ return {
134
+ projectDir: projectRoot,
135
+ configPath,
136
+ featureDir,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Walks up directory tree to find a nax/ directory with config.json.
142
+ * Stops at filesystem root or MAX_DIRECTORY_DEPTH.
143
+ *
144
+ * @param startDir - Starting directory (typically CWD)
145
+ * @returns Absolute path to project root (with symlinks resolved), or null if not found
146
+ */
147
+ function findProjectRoot(startDir: string): string | null {
148
+ let current = resolve(startDir);
149
+ let depth = 0;
150
+
151
+ while (depth < MAX_DIRECTORY_DEPTH) {
152
+ const naxDir = join(current, "nax");
153
+ const configPath = join(naxDir, "config.json");
154
+
155
+ if (existsSync(configPath)) {
156
+ // Resolve symlinks for consistent path comparison
157
+ return realpathSync(current);
158
+ }
159
+
160
+ const parent = join(current, "..");
161
+ if (parent === current) {
162
+ // Reached filesystem root
163
+ break;
164
+ }
165
+
166
+ current = parent;
167
+ depth++;
168
+ }
169
+
170
+ return null;
171
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Diagnose command wrapper
3
+ *
4
+ * Thin commander.js wrapper for diagnose CLI command.
5
+ */
6
+
7
+ import { diagnoseCommand } from "../cli/diagnose";
8
+ import type { DiagnoseOptions } from "../cli/diagnose";
9
+
10
+ /**
11
+ * Execute diagnose command with commander options
12
+ *
13
+ * @param options - Command options from commander
14
+ */
15
+ export async function diagnose(options: DiagnoseOptions): Promise<void> {
16
+ await diagnoseCommand(options);
17
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Common utilities for CLI commands
3
+ */
4
+
5
+ export { resolveProject, type ResolveProjectOptions, type ResolvedProject } from "./common";
6
+ export { logsCommand, type LogsOptions } from "./logs";
7
+ export { precheckCommand, type PrecheckOptions } from "./precheck";
8
+ export { unlockCommand, type UnlockOptions } from "./unlock";