@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,319 @@
1
+ /**
2
+ * Unit tests for the nax unlock command
3
+ *
4
+ * Covers all acceptance criteria:
5
+ * AC1: No lock file -> prints 'No lock file found', exits 0
6
+ * AC2: Lock PID alive -> prints error, exits 1, lock untouched
7
+ * AC3: Lock PID dead -> prints lock info (PID, age), removes lock, exits 0
8
+ * AC4: --force -> removes lock unconditionally, exits 0
9
+ * AC5: -d <path> -> targets specified directory (not cwd)
10
+ * AC6: Unit coverage of all four scenarios
11
+ */
12
+
13
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
+ import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { unlockCommand } from "../../../src/commands/unlock";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Custom error to intercept process.exit without terminating the test runner
21
+ // ---------------------------------------------------------------------------
22
+
23
+ class ExitError extends Error {
24
+ constructor(public readonly code: number) {
25
+ super(`process.exit(${code})`);
26
+ this.name = "ExitError";
27
+ }
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Test helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ async function writeLock(dir: string, pid: number, ageMs = 0): Promise<void> {
35
+ const lockData = { pid, timestamp: Date.now() - ageMs };
36
+ await Bun.write(join(dir, "nax.lock"), JSON.stringify(lockData));
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Test suite
41
+ // ---------------------------------------------------------------------------
42
+
43
+ describe("unlockCommand", () => {
44
+ let testDir: string;
45
+ let capturedOutput: string[];
46
+ let capturedErrors: string[];
47
+ let exitCode: number | null;
48
+
49
+ const originalLog = console.log;
50
+ const originalError = console.error;
51
+ const originalExit = process.exit;
52
+
53
+ beforeEach(() => {
54
+ const raw = join(tmpdir(), `nax-unlock-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
55
+ mkdirSync(raw, { recursive: true });
56
+ testDir = realpathSync(raw);
57
+
58
+ capturedOutput = [];
59
+ capturedErrors = [];
60
+ exitCode = null;
61
+
62
+ console.log = (...args: unknown[]) => {
63
+ capturedOutput.push(args.join(" "));
64
+ };
65
+ console.error = (...args: unknown[]) => {
66
+ capturedErrors.push(args.join(" "));
67
+ };
68
+
69
+ // Intercept process.exit: record the code and throw so the command stops.
70
+ process.exit = ((code?: number) => {
71
+ exitCode = code ?? 0;
72
+ throw new ExitError(exitCode);
73
+ }) as never;
74
+ });
75
+
76
+ afterEach(() => {
77
+ console.log = originalLog;
78
+ console.error = originalError;
79
+ process.exit = originalExit;
80
+
81
+ if (existsSync(testDir)) {
82
+ rmSync(testDir, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ // Helper: run unlockCommand, absorbing ExitError but re-throwing other errors.
87
+ async function run(options: Parameters<typeof unlockCommand>[0]): Promise<void> {
88
+ try {
89
+ await unlockCommand(options);
90
+ } catch (err) {
91
+ if (!(err instanceof ExitError)) {
92
+ throw err;
93
+ }
94
+ }
95
+ }
96
+
97
+ function allOutput(): string {
98
+ return [...capturedOutput, ...capturedErrors].join("\n");
99
+ }
100
+
101
+ // =========================================================================
102
+ // AC1: No lock file present
103
+ // =========================================================================
104
+
105
+ describe("AC1: no lock file", () => {
106
+ test("prints 'No lock file found' and exits 0", async () => {
107
+ await run({ dir: testDir });
108
+
109
+ expect(allOutput()).toContain("No lock file found");
110
+ // exit 0 means either natural return (exitCode null) or explicit exit(0)
111
+ expect(exitCode === null || exitCode === 0).toBe(true);
112
+ });
113
+
114
+ test("does not create a lock file", async () => {
115
+ await run({ dir: testDir });
116
+
117
+ expect(existsSync(join(testDir, "nax.lock"))).toBe(false);
118
+ });
119
+ });
120
+
121
+ // =========================================================================
122
+ // AC2: Lock PID is alive — refuse to unlock
123
+ // =========================================================================
124
+
125
+ describe("AC2: lock PID is alive", () => {
126
+ test("prints 'nax is still running (PID <n>). Use --force to override.'", async () => {
127
+ // process.pid is the current test-runner process — always alive.
128
+ await writeLock(testDir, process.pid);
129
+
130
+ await run({ dir: testDir });
131
+
132
+ expect(allOutput()).toContain(`nax is still running (PID ${process.pid})`);
133
+ expect(allOutput()).toContain("--force");
134
+ });
135
+
136
+ test("exits with code 1", async () => {
137
+ await writeLock(testDir, process.pid);
138
+
139
+ await run({ dir: testDir });
140
+
141
+ expect(exitCode).toBe(1);
142
+ });
143
+
144
+ test("does NOT delete the lock file", async () => {
145
+ const lockPath = join(testDir, "nax.lock");
146
+ await writeLock(testDir, process.pid);
147
+
148
+ await run({ dir: testDir });
149
+
150
+ expect(existsSync(lockPath)).toBe(true);
151
+ });
152
+ });
153
+
154
+ // =========================================================================
155
+ // AC3: Lock PID is dead — unlock and clean up
156
+ // =========================================================================
157
+
158
+ describe("AC3: lock PID is dead", () => {
159
+ // PID 999999 is astronomically unlikely to exist on any real system.
160
+ const DEAD_PID = 999999;
161
+
162
+ test("prints lock info including PID before removing", async () => {
163
+ await writeLock(testDir, DEAD_PID, 5 * 60 * 1000); // 5 minutes old
164
+
165
+ await run({ dir: testDir });
166
+
167
+ expect(allOutput()).toContain(String(DEAD_PID));
168
+ });
169
+
170
+ test("prints lock age in minutes", async () => {
171
+ await writeLock(testDir, DEAD_PID, 5 * 60 * 1000); // 5 minutes old
172
+
173
+ await run({ dir: testDir });
174
+
175
+ // Output should mention age in minutes (e.g. "5 min" or "5 minutes")
176
+ expect(allOutput()).toMatch(/\d+\s*min/i);
177
+ });
178
+
179
+ test("removes nax.lock", async () => {
180
+ const lockPath = join(testDir, "nax.lock");
181
+ await writeLock(testDir, DEAD_PID);
182
+
183
+ await run({ dir: testDir });
184
+
185
+ expect(existsSync(lockPath)).toBe(false);
186
+ });
187
+
188
+ test("exits 0", async () => {
189
+ await writeLock(testDir, DEAD_PID);
190
+
191
+ await run({ dir: testDir });
192
+
193
+ expect(exitCode === null || exitCode === 0).toBe(true);
194
+ });
195
+ });
196
+
197
+ // =========================================================================
198
+ // AC4: --force flag — unconditional removal
199
+ // =========================================================================
200
+
201
+ describe("AC4: --force flag", () => {
202
+ test("removes lock even when PID is alive", async () => {
203
+ const lockPath = join(testDir, "nax.lock");
204
+ // process.pid is alive
205
+ await writeLock(testDir, process.pid);
206
+
207
+ await run({ dir: testDir, force: true });
208
+
209
+ expect(existsSync(lockPath)).toBe(false);
210
+ });
211
+
212
+ test("exits 0 when lock was held by a live PID", async () => {
213
+ await writeLock(testDir, process.pid);
214
+
215
+ await run({ dir: testDir, force: true });
216
+
217
+ expect(exitCode === null || exitCode === 0).toBe(true);
218
+ });
219
+
220
+ test("exits 0 when there is no lock file at all", async () => {
221
+ // No lock written — --force should still succeed gracefully
222
+ await run({ dir: testDir, force: true });
223
+
224
+ expect(exitCode === null || exitCode === 0).toBe(true);
225
+ });
226
+
227
+ test("does not print the 'still running' refusal message", async () => {
228
+ await writeLock(testDir, process.pid);
229
+
230
+ await run({ dir: testDir, force: true });
231
+
232
+ expect(allOutput()).not.toContain("nax is still running");
233
+ });
234
+ });
235
+
236
+ // =========================================================================
237
+ // AC5: -d <path> flag — target a specific directory
238
+ // =========================================================================
239
+
240
+ describe("AC5: -d <path> targets the specified directory", () => {
241
+ test("reads lock from the specified directory, not cwd", async () => {
242
+ const altDir = realpathSync(
243
+ (() => {
244
+ const d = join(tmpdir(), `nax-unlock-alt-${Date.now()}`);
245
+ mkdirSync(d, { recursive: true });
246
+ return d;
247
+ })(),
248
+ );
249
+
250
+ const DEAD_PID = 999999;
251
+ await writeLock(altDir, DEAD_PID);
252
+
253
+ const altLockPath = join(altDir, "nax.lock");
254
+ const testDirLockPath = join(testDir, "nax.lock");
255
+
256
+ await run({ dir: altDir });
257
+
258
+ // The lock in altDir must be removed
259
+ expect(existsSync(altLockPath)).toBe(false);
260
+ // The (absent) lock in testDir must remain absent
261
+ expect(existsSync(testDirLockPath)).toBe(false);
262
+
263
+ rmSync(altDir, { recursive: true, force: true });
264
+ });
265
+
266
+ test("ignores cwd when -d is provided and cwd has no lock", async () => {
267
+ // Put a lock ONLY in altDir; cwd (testDir) has no lock
268
+ const altDir = realpathSync(
269
+ (() => {
270
+ const d = join(tmpdir(), `nax-unlock-alt2-${Date.now()}`);
271
+ mkdirSync(d, { recursive: true });
272
+ return d;
273
+ })(),
274
+ );
275
+
276
+ // No lock in altDir either — just confirming it reads from altDir
277
+ await run({ dir: altDir });
278
+
279
+ // AC1 behaviour applies for the targeted dir (no lock file)
280
+ expect(allOutput()).toContain("No lock file found");
281
+
282
+ rmSync(altDir, { recursive: true, force: true });
283
+ });
284
+ });
285
+
286
+ // =========================================================================
287
+ // AC6: Scenario matrix — all four core cases covered by unit tests
288
+ //
289
+ // (Verified by the tests above; this block provides explicit proof that
290
+ // each scenario class is addressed.)
291
+ // =========================================================================
292
+
293
+ describe("AC6: all four scenario classes covered", () => {
294
+ const DEAD_PID = 999999;
295
+
296
+ test("scenario: no lock", async () => {
297
+ await run({ dir: testDir });
298
+ expect(allOutput()).toContain("No lock file found");
299
+ });
300
+
301
+ test("scenario: alive PID without --force", async () => {
302
+ await writeLock(testDir, process.pid);
303
+ await run({ dir: testDir });
304
+ expect(exitCode).toBe(1);
305
+ });
306
+
307
+ test("scenario: dead PID without --force", async () => {
308
+ await writeLock(testDir, DEAD_PID);
309
+ await run({ dir: testDir });
310
+ expect(existsSync(join(testDir, "nax.lock"))).toBe(false);
311
+ });
312
+
313
+ test("scenario: --force removes lock regardless of PID state", async () => {
314
+ await writeLock(testDir, process.pid);
315
+ await run({ dir: testDir, force: true });
316
+ expect(existsSync(join(testDir, "nax.lock"))).toBe(false);
317
+ });
318
+ });
319
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Constitution Generators Tests
3
+ *
4
+ * Tests for generating agent-specific config files from constitution.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { aiderGenerator } from "../../src/constitution/generators/aider";
9
+ import { claudeGenerator } from "../../src/constitution/generators/claude";
10
+ import { cursorGenerator } from "../../src/constitution/generators/cursor";
11
+ import { opencodeGenerator } from "../../src/constitution/generators/opencode";
12
+ import type { ConstitutionContent } from "../../src/constitution/generators/types";
13
+ import { windsurfGenerator } from "../../src/constitution/generators/windsurf";
14
+
15
+ const sampleConstitution: ConstitutionContent = {
16
+ markdown: `# Project Constitution
17
+
18
+ ## Coding Standards
19
+ - Follow TypeScript best practices
20
+ - Use strict typing
21
+
22
+ ## Testing Requirements
23
+ - 80% minimum coverage
24
+ - Write tests first (TDD)
25
+
26
+ ## Architecture Rules
27
+ - Single responsibility principle
28
+ - Dependency injection
29
+ `,
30
+ sections: {},
31
+ };
32
+
33
+ describe("Constitution Generators", () => {
34
+ describe("Claude Generator", () => {
35
+ test("should generate CLAUDE.md with correct format", () => {
36
+ const result = claudeGenerator.generate(sampleConstitution);
37
+
38
+ expect(result).toContain("# Project Constitution");
39
+ expect(result).toContain("auto-generated from `nax/constitution.md`");
40
+ expect(result).toContain("DO NOT EDIT MANUALLY");
41
+ expect(result).toContain("## Coding Standards");
42
+ expect(result).toContain("Follow TypeScript best practices");
43
+ });
44
+
45
+ test("should have correct output filename", () => {
46
+ expect(claudeGenerator.outputFile).toBe("CLAUDE.md");
47
+ });
48
+
49
+ test("should have correct generator name", () => {
50
+ expect(claudeGenerator.name).toBe("claude");
51
+ });
52
+ });
53
+
54
+ describe("OpenCode Generator", () => {
55
+ test("should generate AGENTS.md with correct format", () => {
56
+ const result = opencodeGenerator.generate(sampleConstitution);
57
+
58
+ expect(result).toContain("# Agent Instructions");
59
+ expect(result).toContain("auto-generated from `nax/constitution.md`");
60
+ expect(result).toContain("DO NOT EDIT MANUALLY");
61
+ expect(result).toContain("## Coding Standards");
62
+ });
63
+
64
+ test("should have correct output filename", () => {
65
+ expect(opencodeGenerator.outputFile).toBe("AGENTS.md");
66
+ });
67
+
68
+ test("should have correct generator name", () => {
69
+ expect(opencodeGenerator.name).toBe("opencode");
70
+ });
71
+ });
72
+
73
+ describe("Cursor Generator", () => {
74
+ test("should generate .cursorrules with correct format", () => {
75
+ const result = cursorGenerator.generate(sampleConstitution);
76
+
77
+ expect(result).toContain("# Project Rules");
78
+ expect(result).toContain("Auto-generated from nax/constitution.md");
79
+ expect(result).toContain("DO NOT EDIT MANUALLY");
80
+ expect(result).toContain("## Coding Standards");
81
+ });
82
+
83
+ test("should have correct output filename", () => {
84
+ expect(cursorGenerator.outputFile).toBe(".cursorrules");
85
+ });
86
+
87
+ test("should have correct generator name", () => {
88
+ expect(cursorGenerator.name).toBe("cursor");
89
+ });
90
+ });
91
+
92
+ describe("Windsurf Generator", () => {
93
+ test("should generate .windsurfrules with correct format", () => {
94
+ const result = windsurfGenerator.generate(sampleConstitution);
95
+
96
+ expect(result).toContain("# Windsurf Project Rules");
97
+ expect(result).toContain("Auto-generated from nax/constitution.md");
98
+ expect(result).toContain("DO NOT EDIT MANUALLY");
99
+ expect(result).toContain("## Coding Standards");
100
+ });
101
+
102
+ test("should have correct output filename", () => {
103
+ expect(windsurfGenerator.outputFile).toBe(".windsurfrules");
104
+ });
105
+
106
+ test("should have correct generator name", () => {
107
+ expect(windsurfGenerator.name).toBe("windsurf");
108
+ });
109
+ });
110
+
111
+ describe("Aider Generator", () => {
112
+ test("should generate .aider.conf.yml with correct YAML format", () => {
113
+ const result = aiderGenerator.generate(sampleConstitution);
114
+
115
+ expect(result).toContain("# Aider Configuration");
116
+ expect(result).toContain("# Auto-generated from nax/constitution.md");
117
+ expect(result).toContain("# DO NOT EDIT MANUALLY");
118
+ expect(result).toContain("instructions: |");
119
+ // Check YAML indentation
120
+ expect(result).toContain(" # Project Constitution");
121
+ expect(result).toContain(" ## Coding Standards");
122
+ });
123
+
124
+ test("should have correct output filename", () => {
125
+ expect(aiderGenerator.outputFile).toBe(".aider.conf.yml");
126
+ });
127
+
128
+ test("should have correct generator name", () => {
129
+ expect(aiderGenerator.name).toBe("aider");
130
+ });
131
+ });
132
+
133
+ describe("All Generators", () => {
134
+ test("should preserve original constitution content", () => {
135
+ const generators = [claudeGenerator, opencodeGenerator, cursorGenerator, windsurfGenerator, aiderGenerator];
136
+
137
+ for (const generator of generators) {
138
+ const result = generator.generate(sampleConstitution);
139
+ expect(result).toContain("Follow TypeScript best practices");
140
+ expect(result).toContain("80% minimum coverage");
141
+ expect(result).toContain("Single responsibility principle");
142
+ }
143
+ });
144
+
145
+ test("should handle empty constitution", () => {
146
+ const emptyConstitution: ConstitutionContent = {
147
+ markdown: "",
148
+ sections: {},
149
+ };
150
+
151
+ const generators = [claudeGenerator, opencodeGenerator, cursorGenerator, windsurfGenerator, aiderGenerator];
152
+
153
+ for (const generator of generators) {
154
+ const result = generator.generate(emptyConstitution);
155
+ // Should still have header
156
+ expect(result.length).toBeGreaterThan(0);
157
+ }
158
+ });
159
+ });
160
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Constitution system tests
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { estimateTokens, loadConstitution, truncateToTokens } from "../../src/constitution";
9
+ import type { ConstitutionConfig } from "../../src/constitution";
10
+
11
+ const TEST_DIR = join(import.meta.dir, ".tmp-constitution-test");
12
+
13
+ beforeEach(() => {
14
+ if (existsSync(TEST_DIR)) {
15
+ rmSync(TEST_DIR, { recursive: true, force: true });
16
+ }
17
+ mkdirSync(TEST_DIR, { recursive: true });
18
+ });
19
+
20
+ afterEach(() => {
21
+ if (existsSync(TEST_DIR)) {
22
+ rmSync(TEST_DIR, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ describe("estimateTokens", () => {
27
+ test("estimates tokens using 1 token ≈ 3 chars", () => {
28
+ expect(estimateTokens("abc")).toBe(1); // 3 chars = 1 token
29
+ expect(estimateTokens("abcdef")).toBe(2); // 6 chars = 2 tokens
30
+ expect(estimateTokens("a".repeat(100))).toBe(34); // 100 chars = 34 tokens (rounded up)
31
+ });
32
+
33
+ test("handles empty string", () => {
34
+ expect(estimateTokens("")).toBe(0);
35
+ });
36
+
37
+ test("rounds up fractional tokens", () => {
38
+ expect(estimateTokens("ab")).toBe(1); // 2 chars = 0.67 tokens → rounds up to 1
39
+ });
40
+ });
41
+
42
+ describe("truncateToTokens", () => {
43
+ test("returns full text if within token limit", () => {
44
+ const text = "Hello world";
45
+ const result = truncateToTokens(text, 100);
46
+ expect(result).toBe(text);
47
+ });
48
+
49
+ test("truncates at word boundary", () => {
50
+ const text = "The quick brown fox jumps over the lazy dog";
51
+ const result = truncateToTokens(text, 5); // 5 tokens ≈ 15 chars
52
+ expect(result.length).toBeLessThanOrEqual(15);
53
+ expect(result).not.toContain("fox"); // Should stop before "fox"
54
+ // Result should be "The quick" which ends with a word character
55
+ expect(result.trim()).toBe("The quick");
56
+ });
57
+
58
+ test("truncates at newline boundary", () => {
59
+ const text = "Line 1\nLine 2\nLine 3\nLine 4";
60
+ const result = truncateToTokens(text, 3); // 3 tokens ≈ 9 chars
61
+ expect(result).toContain("Line 1");
62
+ expect(result).not.toContain("Line 3");
63
+ });
64
+
65
+ test("hard cuts if no word boundary found", () => {
66
+ const text = "a".repeat(100);
67
+ const result = truncateToTokens(text, 5); // 5 tokens ≈ 15 chars
68
+ expect(result.length).toBe(15);
69
+ });
70
+ });
71
+
72
+ describe("loadConstitution", () => {
73
+ test("returns null if disabled", async () => {
74
+ const config: ConstitutionConfig = {
75
+ enabled: false,
76
+ path: "constitution.md",
77
+ maxTokens: 2000,
78
+ skipGlobal: true,
79
+ };
80
+
81
+ const result = await loadConstitution(TEST_DIR, config);
82
+ expect(result).toBeNull();
83
+ });
84
+
85
+ test("returns null if file doesn't exist", async () => {
86
+ const config: ConstitutionConfig = {
87
+ enabled: true,
88
+ path: "constitution.md",
89
+ maxTokens: 2000,
90
+ skipGlobal: true,
91
+ };
92
+
93
+ const result = await loadConstitution(TEST_DIR, config);
94
+ expect(result).toBeNull();
95
+ });
96
+
97
+ test("returns null if file is empty", async () => {
98
+ const constitutionPath = join(TEST_DIR, "constitution.md");
99
+ await Bun.write(constitutionPath, " \n\n "); // Only whitespace
100
+
101
+ const config: ConstitutionConfig = {
102
+ enabled: true,
103
+ path: "constitution.md",
104
+ maxTokens: 2000,
105
+ skipGlobal: true,
106
+ };
107
+
108
+ const result = await loadConstitution(TEST_DIR, config);
109
+ expect(result).toBeNull();
110
+ });
111
+
112
+ test("loads constitution without truncation", async () => {
113
+ const content = "# Project Constitution\n\nFollow these rules.";
114
+ const constitutionPath = join(TEST_DIR, "constitution.md");
115
+ await Bun.write(constitutionPath, content);
116
+
117
+ const config: ConstitutionConfig = {
118
+ enabled: true,
119
+ path: "constitution.md",
120
+ maxTokens: 2000,
121
+ skipGlobal: true,
122
+ };
123
+
124
+ const result = await loadConstitution(TEST_DIR, config);
125
+ expect(result).not.toBeNull();
126
+ expect(result?.content).toBe(content);
127
+ expect(result?.tokens).toBe(estimateTokens(content));
128
+ expect(result?.truncated).toBe(false);
129
+ expect(result?.originalTokens).toBeUndefined();
130
+ });
131
+
132
+ test("truncates constitution if exceeds maxTokens", async () => {
133
+ const content = "A".repeat(300); // 300 chars = 100 tokens
134
+ const constitutionPath = join(TEST_DIR, "constitution.md");
135
+ await Bun.write(constitutionPath, content);
136
+
137
+ const config: ConstitutionConfig = {
138
+ enabled: true,
139
+ path: "constitution.md",
140
+ maxTokens: 50, // Only allow 50 tokens
141
+ skipGlobal: true,
142
+ };
143
+
144
+ const result = await loadConstitution(TEST_DIR, config);
145
+ expect(result).not.toBeNull();
146
+ expect(result?.truncated).toBe(true);
147
+ expect(result?.tokens).toBeLessThanOrEqual(50);
148
+ expect(result?.originalTokens).toBe(100);
149
+ expect(result?.content.length).toBeLessThan(content.length);
150
+ });
151
+
152
+ test("loads from custom path", async () => {
153
+ const content = "# Custom Constitution";
154
+ const customPath = join(TEST_DIR, "custom-rules.md");
155
+ await Bun.write(customPath, content);
156
+
157
+ const config: ConstitutionConfig = {
158
+ enabled: true,
159
+ path: "custom-rules.md",
160
+ maxTokens: 2000,
161
+ skipGlobal: true,
162
+ };
163
+
164
+ const result = await loadConstitution(TEST_DIR, config);
165
+ expect(result).not.toBeNull();
166
+ expect(result?.content).toBe(content);
167
+ });
168
+
169
+ test("handles large constitution with meaningful content", async () => {
170
+ const content = `# Project Constitution
171
+
172
+ ## Coding Standards
173
+ - Use TypeScript strict mode
174
+ - Follow ESLint rules
175
+ - Write clear variable names
176
+
177
+ ## Testing
178
+ - Write unit tests for all functions
179
+ - Aim for 80%+ coverage
180
+ - Use describe/test/expect pattern
181
+
182
+ ## Architecture
183
+ - Keep functions small (<50 lines)
184
+ - Use dependency injection
185
+ - Follow SOLID principles
186
+
187
+ ## Forbidden Patterns
188
+ - No any types
189
+ - No console.log
190
+ - No hardcoded secrets
191
+ `;
192
+
193
+ const constitutionPath = join(TEST_DIR, "constitution.md");
194
+ await Bun.write(constitutionPath, content);
195
+
196
+ const config: ConstitutionConfig = {
197
+ enabled: true,
198
+ path: "constitution.md",
199
+ maxTokens: 2000,
200
+ skipGlobal: true,
201
+ };
202
+
203
+ const result = await loadConstitution(TEST_DIR, config);
204
+ expect(result).not.toBeNull();
205
+ expect(result?.content).toBe(content);
206
+ expect(result?.truncated).toBe(false);
207
+ expect(result?.tokens).toBeGreaterThan(0);
208
+ });
209
+ });