@nathapp/nax 0.50.2 → 0.51.0

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 (352) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/nax.js +579 -373
  3. package/package.json +1 -3
  4. package/bin/nax.ts +0 -1195
  5. package/src/acceptance/fix-generator.ts +0 -322
  6. package/src/acceptance/generator.ts +0 -423
  7. package/src/acceptance/index.ts +0 -42
  8. package/src/acceptance/refinement.ts +0 -224
  9. package/src/acceptance/templates/cli.ts +0 -47
  10. package/src/acceptance/templates/component.ts +0 -78
  11. package/src/acceptance/templates/e2e.ts +0 -43
  12. package/src/acceptance/templates/index.ts +0 -21
  13. package/src/acceptance/templates/snapshot.ts +0 -50
  14. package/src/acceptance/templates/unit.ts +0 -48
  15. package/src/acceptance/types.ts +0 -135
  16. package/src/agents/acp/adapter.ts +0 -888
  17. package/src/agents/acp/cost.ts +0 -9
  18. package/src/agents/acp/index.ts +0 -7
  19. package/src/agents/acp/interaction-bridge.ts +0 -126
  20. package/src/agents/acp/parser.ts +0 -119
  21. package/src/agents/acp/spawn-client.ts +0 -373
  22. package/src/agents/acp/types.ts +0 -22
  23. package/src/agents/aider/adapter.ts +0 -135
  24. package/src/agents/claude/adapter.ts +0 -258
  25. package/src/agents/claude/complete.ts +0 -80
  26. package/src/agents/claude/cost.ts +0 -16
  27. package/src/agents/claude/execution.ts +0 -215
  28. package/src/agents/claude/index.ts +0 -3
  29. package/src/agents/claude/interactive.ts +0 -77
  30. package/src/agents/claude/plan.ts +0 -179
  31. package/src/agents/codex/adapter.ts +0 -153
  32. package/src/agents/cost/calculate.ts +0 -154
  33. package/src/agents/cost/index.ts +0 -10
  34. package/src/agents/cost/parse.ts +0 -97
  35. package/src/agents/cost/pricing.ts +0 -59
  36. package/src/agents/cost/types.ts +0 -45
  37. package/src/agents/gemini/adapter.ts +0 -177
  38. package/src/agents/index.ts +0 -18
  39. package/src/agents/opencode/adapter.ts +0 -106
  40. package/src/agents/registry.ts +0 -136
  41. package/src/agents/shared/decompose.ts +0 -154
  42. package/src/agents/shared/model-resolution.ts +0 -43
  43. package/src/agents/shared/types-extended.ts +0 -164
  44. package/src/agents/shared/validation.ts +0 -69
  45. package/src/agents/shared/version-detection.ts +0 -109
  46. package/src/agents/types.ts +0 -205
  47. package/src/analyze/classifier.ts +0 -282
  48. package/src/analyze/index.ts +0 -16
  49. package/src/analyze/scanner.ts +0 -171
  50. package/src/analyze/types.ts +0 -51
  51. package/src/cli/accept.ts +0 -108
  52. package/src/cli/agents.ts +0 -87
  53. package/src/cli/analyze-parser.ts +0 -291
  54. package/src/cli/analyze.ts +0 -352
  55. package/src/cli/config-descriptions.ts +0 -218
  56. package/src/cli/config-diff.ts +0 -103
  57. package/src/cli/config-display.ts +0 -285
  58. package/src/cli/config-get.ts +0 -55
  59. package/src/cli/config.ts +0 -14
  60. package/src/cli/constitution.ts +0 -17
  61. package/src/cli/diagnose-analysis.ts +0 -159
  62. package/src/cli/diagnose-formatter.ts +0 -87
  63. package/src/cli/diagnose.ts +0 -203
  64. package/src/cli/generate.ts +0 -250
  65. package/src/cli/index.ts +0 -42
  66. package/src/cli/init-context.ts +0 -405
  67. package/src/cli/init-detect.ts +0 -303
  68. package/src/cli/init.ts +0 -296
  69. package/src/cli/interact.ts +0 -295
  70. package/src/cli/plan.ts +0 -509
  71. package/src/cli/plugins.ts +0 -122
  72. package/src/cli/prompts-export.ts +0 -58
  73. package/src/cli/prompts-init.ts +0 -200
  74. package/src/cli/prompts-main.ts +0 -183
  75. package/src/cli/prompts-shared.ts +0 -70
  76. package/src/cli/prompts-tdd.ts +0 -88
  77. package/src/cli/prompts.ts +0 -17
  78. package/src/cli/runs.ts +0 -174
  79. package/src/cli/status-cost.ts +0 -151
  80. package/src/cli/status-features.ts +0 -405
  81. package/src/cli/status.ts +0 -13
  82. package/src/commands/common.ts +0 -171
  83. package/src/commands/diagnose.ts +0 -17
  84. package/src/commands/index.ts +0 -9
  85. package/src/commands/logs-formatter.ts +0 -201
  86. package/src/commands/logs-reader.ts +0 -171
  87. package/src/commands/logs.ts +0 -103
  88. package/src/commands/precheck.ts +0 -86
  89. package/src/commands/runs.ts +0 -220
  90. package/src/commands/unlock.ts +0 -96
  91. package/src/config/defaults.ts +0 -217
  92. package/src/config/index.ts +0 -22
  93. package/src/config/loader.ts +0 -143
  94. package/src/config/merge.ts +0 -106
  95. package/src/config/merger.ts +0 -147
  96. package/src/config/path-security.ts +0 -121
  97. package/src/config/paths.ts +0 -27
  98. package/src/config/permissions.ts +0 -63
  99. package/src/config/runtime-types.ts +0 -520
  100. package/src/config/schema-types.ts +0 -53
  101. package/src/config/schema.ts +0 -60
  102. package/src/config/schemas.ts +0 -425
  103. package/src/config/test-strategy.ts +0 -71
  104. package/src/config/types.ts +0 -57
  105. package/src/config/validate.ts +0 -103
  106. package/src/constitution/generator.ts +0 -158
  107. package/src/constitution/generators/aider.ts +0 -41
  108. package/src/constitution/generators/claude.ts +0 -35
  109. package/src/constitution/generators/cursor.ts +0 -36
  110. package/src/constitution/generators/opencode.ts +0 -38
  111. package/src/constitution/generators/types.ts +0 -33
  112. package/src/constitution/generators/windsurf.ts +0 -36
  113. package/src/constitution/index.ts +0 -11
  114. package/src/constitution/loader.ts +0 -121
  115. package/src/constitution/types.ts +0 -31
  116. package/src/context/auto-detect.ts +0 -228
  117. package/src/context/builder.ts +0 -299
  118. package/src/context/elements.ts +0 -122
  119. package/src/context/formatter.ts +0 -107
  120. package/src/context/generator.ts +0 -343
  121. package/src/context/generators/aider.ts +0 -34
  122. package/src/context/generators/claude.ts +0 -28
  123. package/src/context/generators/codex.ts +0 -28
  124. package/src/context/generators/cursor.ts +0 -28
  125. package/src/context/generators/gemini.ts +0 -28
  126. package/src/context/generators/opencode.ts +0 -30
  127. package/src/context/generators/windsurf.ts +0 -28
  128. package/src/context/greenfield.ts +0 -114
  129. package/src/context/index.ts +0 -34
  130. package/src/context/injector.ts +0 -279
  131. package/src/context/parent-context.ts +0 -39
  132. package/src/context/test-scanner.ts +0 -370
  133. package/src/context/types.ts +0 -98
  134. package/src/decompose/apply.ts +0 -50
  135. package/src/decompose/builder.ts +0 -181
  136. package/src/decompose/index.ts +0 -8
  137. package/src/decompose/sections/codebase.ts +0 -26
  138. package/src/decompose/sections/constraints.ts +0 -32
  139. package/src/decompose/sections/index.ts +0 -4
  140. package/src/decompose/sections/sibling-stories.ts +0 -25
  141. package/src/decompose/sections/target-story.ts +0 -31
  142. package/src/decompose/types.ts +0 -55
  143. package/src/decompose/validators/complexity.ts +0 -45
  144. package/src/decompose/validators/coverage.ts +0 -134
  145. package/src/decompose/validators/dependency.ts +0 -91
  146. package/src/decompose/validators/index.ts +0 -35
  147. package/src/decompose/validators/overlap.ts +0 -128
  148. package/src/errors.ts +0 -67
  149. package/src/execution/batching.ts +0 -157
  150. package/src/execution/crash-heartbeat.ts +0 -77
  151. package/src/execution/crash-recovery.ts +0 -79
  152. package/src/execution/crash-signals.ts +0 -165
  153. package/src/execution/crash-writer.ts +0 -154
  154. package/src/execution/deferred-review.ts +0 -105
  155. package/src/execution/dry-run.ts +0 -81
  156. package/src/execution/escalation/escalation.ts +0 -46
  157. package/src/execution/escalation/index.ts +0 -13
  158. package/src/execution/escalation/tier-escalation.ts +0 -346
  159. package/src/execution/escalation/tier-outcome.ts +0 -143
  160. package/src/execution/executor-types.ts +0 -73
  161. package/src/execution/helpers.ts +0 -38
  162. package/src/execution/index.ts +0 -27
  163. package/src/execution/iteration-runner.ts +0 -160
  164. package/src/execution/lifecycle/acceptance-loop.ts +0 -280
  165. package/src/execution/lifecycle/headless-formatter.ts +0 -83
  166. package/src/execution/lifecycle/index.ts +0 -11
  167. package/src/execution/lifecycle/parallel-lifecycle.ts +0 -101
  168. package/src/execution/lifecycle/precheck-runner.ts +0 -140
  169. package/src/execution/lifecycle/run-cleanup.ts +0 -81
  170. package/src/execution/lifecycle/run-completion.ts +0 -247
  171. package/src/execution/lifecycle/run-initialization.ts +0 -187
  172. package/src/execution/lifecycle/run-regression.ts +0 -305
  173. package/src/execution/lifecycle/run-setup.ts +0 -240
  174. package/src/execution/lifecycle/story-size-prompts.ts +0 -123
  175. package/src/execution/lock.ts +0 -129
  176. package/src/execution/parallel-coordinator.ts +0 -281
  177. package/src/execution/parallel-executor-rectification-pass.ts +0 -117
  178. package/src/execution/parallel-executor-rectify.ts +0 -136
  179. package/src/execution/parallel-executor.ts +0 -330
  180. package/src/execution/parallel-worker.ts +0 -149
  181. package/src/execution/parallel.ts +0 -13
  182. package/src/execution/pid-registry.ts +0 -275
  183. package/src/execution/pipeline-result-handler.ts +0 -221
  184. package/src/execution/progress.ts +0 -27
  185. package/src/execution/queue-handler.ts +0 -109
  186. package/src/execution/runner-completion.ts +0 -171
  187. package/src/execution/runner-execution.ts +0 -243
  188. package/src/execution/runner-setup.ts +0 -86
  189. package/src/execution/runner.ts +0 -265
  190. package/src/execution/sequential-executor.ts +0 -219
  191. package/src/execution/status-file.ts +0 -264
  192. package/src/execution/status-writer.ts +0 -181
  193. package/src/execution/story-context.ts +0 -266
  194. package/src/execution/story-selector.ts +0 -76
  195. package/src/execution/test-output-parser.ts +0 -14
  196. package/src/execution/timeout-handler.ts +0 -100
  197. package/src/hooks/index.ts +0 -2
  198. package/src/hooks/runner.ts +0 -280
  199. package/src/hooks/types.ts +0 -79
  200. package/src/interaction/chain.ts +0 -170
  201. package/src/interaction/index.ts +0 -61
  202. package/src/interaction/init.ts +0 -84
  203. package/src/interaction/plugins/auto.ts +0 -243
  204. package/src/interaction/plugins/cli.ts +0 -300
  205. package/src/interaction/plugins/telegram.ts +0 -384
  206. package/src/interaction/plugins/webhook.ts +0 -286
  207. package/src/interaction/state.ts +0 -171
  208. package/src/interaction/triggers.ts +0 -250
  209. package/src/interaction/types.ts +0 -170
  210. package/src/logger/formatters.ts +0 -84
  211. package/src/logger/index.ts +0 -16
  212. package/src/logger/logger.ts +0 -296
  213. package/src/logger/types.ts +0 -48
  214. package/src/logging/formatter.ts +0 -355
  215. package/src/logging/index.ts +0 -22
  216. package/src/logging/types.ts +0 -93
  217. package/src/metrics/aggregator.ts +0 -191
  218. package/src/metrics/index.ts +0 -14
  219. package/src/metrics/tracker.ts +0 -200
  220. package/src/metrics/types.ts +0 -115
  221. package/src/optimizer/index.ts +0 -63
  222. package/src/optimizer/noop.optimizer.ts +0 -24
  223. package/src/optimizer/rule-based.optimizer.ts +0 -248
  224. package/src/optimizer/types.ts +0 -53
  225. package/src/pipeline/event-bus.ts +0 -297
  226. package/src/pipeline/events.ts +0 -130
  227. package/src/pipeline/index.ts +0 -19
  228. package/src/pipeline/runner.ts +0 -149
  229. package/src/pipeline/stages/acceptance-setup.ts +0 -140
  230. package/src/pipeline/stages/acceptance.ts +0 -215
  231. package/src/pipeline/stages/autofix.ts +0 -262
  232. package/src/pipeline/stages/completion.ts +0 -110
  233. package/src/pipeline/stages/constitution.ts +0 -63
  234. package/src/pipeline/stages/context.ts +0 -122
  235. package/src/pipeline/stages/execution.ts +0 -359
  236. package/src/pipeline/stages/index.ts +0 -86
  237. package/src/pipeline/stages/optimizer.ts +0 -74
  238. package/src/pipeline/stages/prompt.ts +0 -79
  239. package/src/pipeline/stages/queue-check.ts +0 -103
  240. package/src/pipeline/stages/rectify.ts +0 -101
  241. package/src/pipeline/stages/regression.ts +0 -99
  242. package/src/pipeline/stages/review.ts +0 -94
  243. package/src/pipeline/stages/routing.ts +0 -276
  244. package/src/pipeline/stages/verify.ts +0 -286
  245. package/src/pipeline/subscribers/events-writer.ts +0 -135
  246. package/src/pipeline/subscribers/hooks.ts +0 -179
  247. package/src/pipeline/subscribers/interaction.ts +0 -103
  248. package/src/pipeline/subscribers/registry.ts +0 -73
  249. package/src/pipeline/subscribers/reporters.ts +0 -174
  250. package/src/pipeline/types.ts +0 -220
  251. package/src/plugins/extensions.ts +0 -225
  252. package/src/plugins/index.ts +0 -33
  253. package/src/plugins/loader.ts +0 -352
  254. package/src/plugins/plugin-logger.ts +0 -41
  255. package/src/plugins/registry.ts +0 -168
  256. package/src/plugins/types.ts +0 -206
  257. package/src/plugins/validator.ts +0 -352
  258. package/src/prd/index.ts +0 -220
  259. package/src/prd/schema.ts +0 -268
  260. package/src/prd/types.ts +0 -273
  261. package/src/prd/validate.ts +0 -41
  262. package/src/precheck/checks-agents.ts +0 -63
  263. package/src/precheck/checks-blockers.ts +0 -23
  264. package/src/precheck/checks-cli.ts +0 -68
  265. package/src/precheck/checks-config.ts +0 -102
  266. package/src/precheck/checks-git.ts +0 -117
  267. package/src/precheck/checks-system.ts +0 -101
  268. package/src/precheck/checks-warnings.ts +0 -221
  269. package/src/precheck/checks.ts +0 -36
  270. package/src/precheck/index.ts +0 -374
  271. package/src/precheck/story-size-gate.ts +0 -144
  272. package/src/precheck/types.ts +0 -31
  273. package/src/prompts/builder.ts +0 -166
  274. package/src/prompts/index.ts +0 -2
  275. package/src/prompts/loader.ts +0 -43
  276. package/src/prompts/sections/conventions.ts +0 -19
  277. package/src/prompts/sections/hermetic.ts +0 -41
  278. package/src/prompts/sections/index.ts +0 -12
  279. package/src/prompts/sections/isolation.ts +0 -70
  280. package/src/prompts/sections/role-task.ts +0 -182
  281. package/src/prompts/sections/story.ts +0 -55
  282. package/src/prompts/sections/verdict.ts +0 -70
  283. package/src/prompts/types.ts +0 -21
  284. package/src/queue/index.ts +0 -2
  285. package/src/queue/manager.ts +0 -254
  286. package/src/queue/types.ts +0 -54
  287. package/src/review/index.ts +0 -8
  288. package/src/review/orchestrator.ts +0 -154
  289. package/src/review/runner.ts +0 -303
  290. package/src/review/types.ts +0 -70
  291. package/src/routing/batch-route.ts +0 -35
  292. package/src/routing/builder.ts +0 -81
  293. package/src/routing/chain.ts +0 -75
  294. package/src/routing/content-hash.ts +0 -25
  295. package/src/routing/index.ts +0 -20
  296. package/src/routing/loader.ts +0 -62
  297. package/src/routing/router.ts +0 -305
  298. package/src/routing/strategies/adaptive.ts +0 -215
  299. package/src/routing/strategies/index.ts +0 -8
  300. package/src/routing/strategies/keyword.ts +0 -180
  301. package/src/routing/strategies/llm-prompts.ts +0 -224
  302. package/src/routing/strategies/llm.ts +0 -320
  303. package/src/routing/strategies/manual.ts +0 -50
  304. package/src/routing/strategy.ts +0 -102
  305. package/src/tdd/cleanup.ts +0 -120
  306. package/src/tdd/index.ts +0 -22
  307. package/src/tdd/isolation.ts +0 -117
  308. package/src/tdd/orchestrator.ts +0 -406
  309. package/src/tdd/prompts.ts +0 -40
  310. package/src/tdd/rectification-gate.ts +0 -274
  311. package/src/tdd/session-runner.ts +0 -263
  312. package/src/tdd/types.ts +0 -84
  313. package/src/tdd/verdict-reader.ts +0 -266
  314. package/src/tdd/verdict.ts +0 -152
  315. package/src/tui/App.tsx +0 -265
  316. package/src/tui/components/AgentPanel.tsx +0 -75
  317. package/src/tui/components/CostOverlay.tsx +0 -118
  318. package/src/tui/components/HelpOverlay.tsx +0 -107
  319. package/src/tui/components/StatusBar.tsx +0 -63
  320. package/src/tui/components/StoriesPanel.tsx +0 -177
  321. package/src/tui/hooks/useKeyboard.ts +0 -142
  322. package/src/tui/hooks/useLayout.ts +0 -137
  323. package/src/tui/hooks/usePipelineEvents.ts +0 -183
  324. package/src/tui/hooks/usePty.ts +0 -189
  325. package/src/tui/index.tsx +0 -38
  326. package/src/tui/types.ts +0 -76
  327. package/src/utils/errors.ts +0 -12
  328. package/src/utils/git.ts +0 -245
  329. package/src/utils/json-file.ts +0 -72
  330. package/src/utils/log-test-output.ts +0 -25
  331. package/src/utils/path-security.ts +0 -73
  332. package/src/utils/queue-writer.ts +0 -54
  333. package/src/verification/crash-detector.ts +0 -34
  334. package/src/verification/executor.ts +0 -250
  335. package/src/verification/index.ts +0 -12
  336. package/src/verification/orchestrator-types.ts +0 -154
  337. package/src/verification/orchestrator.ts +0 -76
  338. package/src/verification/parser.ts +0 -220
  339. package/src/verification/rectification-loop.ts +0 -172
  340. package/src/verification/rectification.ts +0 -108
  341. package/src/verification/runners.ts +0 -129
  342. package/src/verification/smart-runner.ts +0 -307
  343. package/src/verification/strategies/acceptance.ts +0 -136
  344. package/src/verification/strategies/regression.ts +0 -90
  345. package/src/verification/strategies/scoped.ts +0 -154
  346. package/src/verification/types.ts +0 -117
  347. package/src/version.ts +0 -40
  348. package/src/worktree/dispatcher.ts +0 -6
  349. package/src/worktree/index.ts +0 -2
  350. package/src/worktree/manager.ts +0 -193
  351. package/src/worktree/merge.ts +0 -302
  352. package/src/worktree/types.ts +0 -4
package/dist/nax.js CHANGED
@@ -3246,6 +3246,8 @@ function resolveTestStrategy(raw) {
3246
3246
  return "test-after";
3247
3247
  if (VALID_TEST_STRATEGIES.includes(raw))
3248
3248
  return raw;
3249
+ if (raw === "none")
3250
+ return "no-test";
3249
3251
  if (raw === "tdd")
3250
3252
  return "tdd-simple";
3251
3253
  if (raw === "three-session")
@@ -3256,6 +3258,9 @@ function resolveTestStrategy(raw) {
3256
3258
  }
3257
3259
  var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guide
3258
3260
 
3261
+ - no-test: Config-only changes, documentation, CI/build files, dependency bumps, pure refactors
3262
+ with NO behavioral change. MUST include noTestJustification explaining why tests are unnecessary.
3263
+ If any user-facing behavior changes, use tdd-simple or higher.
3259
3264
  - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 tdd-simple
3260
3265
  - medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 three-session-tdd-lite
3261
3266
  - complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
@@ -3266,6 +3271,9 @@ var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guid
3266
3271
  Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
3267
3272
  password hashing, access control) must use three-session-tdd regardless of complexity.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
3268
3273
 
3274
+ - no-test: Stories with zero behavioral change \u2014 config files, documentation, CI/build changes,
3275
+ dependency bumps, pure structural refactors. REQUIRES noTestJustification field. If any runtime
3276
+ behavior changes, use tdd-simple or higher. When in doubt, use tdd-simple.
3269
3277
  - tdd-simple: Simple stories (\u226450 LOC). Write failing tests first, then implement to pass them \u2014 all in one session.
3270
3278
  - three-session-tdd-lite: Medium stories, or complex stories involving UI/CLI/integration. 3 sessions: (1) test-writer writes failing tests and may create minimal src/ stubs for imports, (2) implementer makes tests pass and may replace stubs, (3) verifier confirms correctness.
3271
3279
  - three-session-tdd: Complex/expert stories or security-critical code. 3 sessions with strict isolation: (1) test-writer writes failing tests \u2014 no src/ changes allowed, (2) implementer makes them pass without modifying test files, (3) verifier confirms correctness.
@@ -3283,6 +3291,7 @@ password hashing, access control) must use three-session-tdd regardless of compl
3283
3291
  - Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
3284
3292
  var init_test_strategy = __esm(() => {
3285
3293
  VALID_TEST_STRATEGIES = [
3294
+ "no-test",
3286
3295
  "test-after",
3287
3296
  "tdd-simple",
3288
3297
  "three-session-tdd",
@@ -17874,7 +17883,8 @@ var init_schemas3 = __esm(() => {
17874
17883
  refinement: exports_external.boolean().default(true),
17875
17884
  redGate: exports_external.boolean().default(true),
17876
17885
  testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
17877
- testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional()
17886
+ testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional(),
17887
+ timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000)
17878
17888
  });
17879
17889
  TestCoverageConfigSchema = exports_external.object({
17880
17890
  enabled: exports_external.boolean().default(true),
@@ -17966,8 +17976,8 @@ var init_schemas3 = __esm(() => {
17966
17976
  storySizeGate: StorySizeGateConfigSchema
17967
17977
  });
17968
17978
  PromptsConfigSchema = exports_external.object({
17969
- overrides: exports_external.record(exports_external.string().refine((key) => ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
17970
- message: "Role must be one of: test-writer, implementer, verifier, single-session, tdd-simple"
17979
+ overrides: exports_external.record(exports_external.string().refine((key) => ["no-test", "test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
17980
+ message: "Role must be one of: no-test, test-writer, implementer, verifier, single-session, tdd-simple"
17971
17981
  }), exports_external.string().min(1, "Override path must be non-empty")).optional()
17972
17982
  });
17973
17983
  DecomposeConfigSchema = exports_external.object({
@@ -18163,7 +18173,8 @@ var init_defaults = __esm(() => {
18163
18173
  testPath: "acceptance.test.ts",
18164
18174
  model: "fast",
18165
18175
  refinement: true,
18166
- redGate: true
18176
+ redGate: true,
18177
+ timeoutMs: 1800000
18167
18178
  },
18168
18179
  context: {
18169
18180
  fileInjection: "disabled",
@@ -18725,32 +18736,49 @@ async function generateFromPRD(_stories, refinedCriteria, options) {
18725
18736
  }
18726
18737
  const criteriaList = refinedCriteria.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
18727
18738
  `);
18728
- const strategyInstructions = buildStrategyInstructions(options.testStrategy, options.testFramework);
18729
- const prompt = `You are a test engineer. Generate acceptance tests for the "${options.featureName}" feature based on the refined acceptance criteria below.
18739
+ const frameworkOverrideLine = options.testFramework ? `
18740
+ [FRAMEWORK OVERRIDE: Use ${options.testFramework} as the test framework regardless of what you detect.]` : "";
18741
+ const basePrompt = `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${options.featureName}" feature.
18730
18742
 
18731
- CODEBASE CONTEXT:
18732
- ${options.codebaseContext}
18743
+ ## Step 1: Understand and Classify the Acceptance Criteria
18744
+
18745
+ Read each AC below and classify its verification type:
18746
+ - **file-check**: Verify by reading source files (e.g. "no @nestjs/jwt imports", "file exists", "module registered", "uses registerAs pattern")
18747
+ - **runtime-check**: Load and invoke code directly, assert on return values or behavior
18748
+ - **integration-check**: Requires a running service (e.g. HTTP endpoint returns 200, 11th request returns 429, database query succeeds)
18733
18749
 
18734
- ACCEPTANCE CRITERIA (refined):
18750
+ ACCEPTANCE CRITERIA:
18735
18751
  ${criteriaList}
18736
18752
 
18737
- ${strategyInstructions}Generate a complete acceptance.test.ts file using bun:test framework. Each AC maps to exactly one test named "AC-N: <description>".
18753
+ ## Step 2: Explore the Project
18738
18754
 
18739
- Structure example (do NOT wrap in markdown fences \u2014 output raw TypeScript only):
18755
+ Before writing any tests, examine the project to understand:
18756
+ 1. **Language and test framework** \u2014 check dependency manifests (package.json, go.mod, Gemfile, pyproject.toml, Cargo.toml, build.gradle, etc.) to identify the language and test runner
18757
+ 2. **Existing test patterns** \u2014 read 1-2 existing test files to understand import style, describe/test/it conventions, and available helpers
18758
+ 3. **Project structure** \u2014 identify relevant source directories to determine correct import or load paths
18740
18759
 
18741
- import { describe, test, expect } from "bun:test";
18760
+ ${frameworkOverrideLine}
18742
18761
 
18743
- describe("${options.featureName} - Acceptance Tests", () => {
18744
- test("AC-1: <description>", async () => {
18745
- // Test implementation
18746
- });
18747
- });
18762
+ ## Step 3: Generate the Acceptance Test File
18763
+
18764
+ Write the complete acceptance test file using the framework identified in Step 2.
18748
18765
 
18749
- IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\`\`typescript or \`\`\`). Start directly with the import statement.`;
18766
+ Rules:
18767
+ - **One test per AC**, named exactly "AC-N: <description>"
18768
+ - **file-check ACs** \u2192 read source files using the language's standard file I/O, assert with string or regex checks. Do not start the application.
18769
+ - **runtime-check ACs** \u2192 load or import the module directly and invoke it, assert on the return value or observable side effects
18770
+ - **integration-check ACs** \u2192 use the language's HTTP client or existing test helpers; add a clear setup block (beforeAll/setup/TestMain/etc.) explaining what must be running
18771
+ - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18772
+ - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18773
+ - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
18774
+ - **Path anchor (CRITICAL)**: This test file will be saved at \`<repo-root>/nax/features/${options.featureName}/acceptance.test.ts\` and will ALWAYS run from the repo root via \`bun test <absolute-path>\`. The repo root is exactly 3 \`../\` levels above \`__dirname\`: \`join(__dirname, '..', '..', '..')\`. Never use 4 or more \`../\` \u2014 that would escape the repo. For monorepo projects, navigate into packages from root (e.g. \`join(root, 'apps/api/src')\`).`;
18775
+ const prompt = basePrompt;
18750
18776
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
18751
- const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
18777
+ const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
18752
18778
  model: options.modelDef.model,
18753
- config: options.config
18779
+ config: options.config,
18780
+ timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
18781
+ workdir: options.workdir
18754
18782
  });
18755
18783
  const testCode = extractTestCode(rawOutput);
18756
18784
  if (!testCode) {
@@ -18774,40 +18802,6 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
18774
18802
  await _generatorPRDDeps.writeFile(join2(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
18775
18803
  return { testCode, criteria };
18776
18804
  }
18777
- function buildStrategyInstructions(strategy, framework) {
18778
- switch (strategy) {
18779
- case "component": {
18780
- const fw = framework ?? "ink-testing-library";
18781
- if (fw === "react") {
18782
- return `TEST STRATEGY: component (react)
18783
- Import render and screen from @testing-library/react. Render the component and use screen.getByText to assert on output.
18784
-
18785
- `;
18786
- }
18787
- return `TEST STRATEGY: component (ink-testing-library)
18788
- Import render from ink-testing-library. Render the component and use lastFrame() to assert on output.
18789
-
18790
- `;
18791
- }
18792
- case "cli":
18793
- return `TEST STRATEGY: cli
18794
- Use Bun.spawn to run the binary. Read stdout and assert on the text output.
18795
-
18796
- `;
18797
- case "e2e":
18798
- return `TEST STRATEGY: e2e
18799
- Use fetch() against http://localhost to call the running service. Assert on response body using response.text() or response.json().
18800
-
18801
- `;
18802
- case "snapshot":
18803
- return `TEST STRATEGY: snapshot
18804
- Render the component and use toMatchSnapshot() to capture and compare snapshots.
18805
-
18806
- `;
18807
- default:
18808
- return "";
18809
- }
18810
- }
18811
18805
  function parseAcceptanceCriteria(specContent) {
18812
18806
  const criteria = [];
18813
18807
  const lines = specContent.split(`
@@ -18831,46 +18825,39 @@ function parseAcceptanceCriteria(specContent) {
18831
18825
  function buildAcceptanceTestPrompt(criteria, featureName, codebaseContext) {
18832
18826
  const criteriaList = criteria.map((ac) => `${ac.id}: ${ac.text}`).join(`
18833
18827
  `);
18834
- return `You are a test engineer. Generate acceptance tests for the "${featureName}" feature based on the acceptance criteria below.
18828
+ return `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${featureName}" feature.
18835
18829
 
18836
- CODEBASE CONTEXT:
18837
- ${codebaseContext}
18830
+ ## Step 1: Understand and Classify the Acceptance Criteria
18831
+
18832
+ Read each AC below and classify its verification type:
18833
+ - **file-check**: Verify by reading source files (e.g. "no @nestjs/jwt imports", "file exists", "module registered", "uses registerAs pattern")
18834
+ - **runtime-check**: Load and invoke code directly, assert on return values or behavior
18835
+ - **integration-check**: Requires a running service (e.g. HTTP endpoint returns 200, 11th request returns 429, database query succeeds)
18838
18836
 
18839
18837
  ACCEPTANCE CRITERIA:
18840
18838
  ${criteriaList}
18841
18839
 
18842
- Generate a complete acceptance.test.ts file using bun:test framework. Follow these rules:
18840
+ ## Step 2: Explore the Project
18843
18841
 
18844
- 1. **One test per AC**: Each acceptance criterion maps to exactly one test
18845
- 2. **Test observable behavior only**: No implementation details, only user-facing behavior
18846
- 3. **Independent tests**: No shared state between tests
18847
- 4. **Real-implementation**: Tests should use real implementations without mocking (test observable behavior, not internal units)
18848
- 5. **Clear test names**: Use format "AC-N: <description>" for test names
18849
- 6. **Async where needed**: Use async/await for operations that may be asynchronous
18842
+ Before writing any tests, examine the project to understand:
18843
+ 1. **Language and test framework** \u2014 check dependency manifests (package.json, go.mod, Gemfile, pyproject.toml, Cargo.toml, build.gradle, etc.) to identify the language and test runner
18844
+ 2. **Existing test patterns** \u2014 read 1-2 existing test files to understand import style, describe/test/it conventions, and available helpers
18845
+ 3. **Project structure** \u2014 identify relevant source directories to determine correct import or load paths
18850
18846
 
18851
- Use this structure:
18852
18847
 
18853
- \`\`\`typescript
18854
- import { describe, test, expect } from "bun:test";
18848
+ ## Step 3: Generate the Acceptance Test File
18855
18849
 
18856
- describe("${featureName} - Acceptance Tests", () => {
18857
- test("AC-1: <description>", async () => {
18858
- // Test implementation
18859
- });
18850
+ Write the complete acceptance test file using the framework identified in Step 2.
18860
18851
 
18861
- test("AC-2: <description>", async () => {
18862
- // Test implementation
18863
- });
18864
- });
18865
- \`\`\`
18866
-
18867
- **Important**:
18868
- - Import the feature code being tested
18869
- - Set up any necessary test fixtures
18870
- - Use expect() assertions to verify behavior
18871
- - Clean up resources if needed (close connections, delete temp files)
18872
-
18873
- Respond with ONLY the TypeScript test code (no markdown code fences, no explanation).`;
18852
+ Rules:
18853
+ - **One test per AC**, named exactly "AC-N: <description>"
18854
+ - **file-check ACs** \u2192 read source files using the language's standard file I/O, assert with string or regex checks. Do not start the application.
18855
+ - **runtime-check ACs** \u2192 load or import the module directly and invoke it, assert on the return value or observable side effects
18856
+ - **integration-check ACs** \u2192 use the language's HTTP client or existing test helpers; add a clear setup block (beforeAll/setup/TestMain/etc.) explaining what must be running
18857
+ - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18858
+ - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18859
+ - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
18860
+ - **Path anchor (CRITICAL)**: This test file will be saved at \`<repo-root>/nax/features/${featureName}/acceptance.test.ts\` and will ALWAYS run from the repo root via \`bun test <absolute-path>\`. The repo root is exactly 3 \`../\` levels above \`__dirname\`: \`join(__dirname, '..', '..', '..')\`. Never use 4 or more \`../\` \u2014 that would escape the repo. For monorepo projects, navigate into packages from root (e.g. \`join(root, 'apps/api/src')\`).`;
18874
18861
  }
18875
18862
  async function generateAcceptanceTests(adapter, options) {
18876
18863
  const logger = getLogger();
@@ -18887,7 +18874,9 @@ async function generateAcceptanceTests(adapter, options) {
18887
18874
  try {
18888
18875
  const output = await adapter.complete(prompt, {
18889
18876
  model: options.modelDef.model,
18890
- config: options.config
18877
+ config: options.config,
18878
+ timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
18879
+ workdir: options.workdir
18891
18880
  });
18892
18881
  const testCode = extractTestCode(output);
18893
18882
  if (!testCode) {
@@ -18983,7 +18972,34 @@ function findRelatedStories(failedAC, prd) {
18983
18972
  const passedStories = prd.userStories.filter((s) => s.status === "passed").map((s) => s.id);
18984
18973
  return passedStories.slice(0, 5);
18985
18974
  }
18986
- function buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd) {
18975
+ function groupACsByRelatedStories(failedACs, prd) {
18976
+ const groups = new Map;
18977
+ for (const ac of failedACs) {
18978
+ const related = findRelatedStories(ac, prd);
18979
+ const key = [...related].sort().join(",");
18980
+ if (!groups.has(key)) {
18981
+ groups.set(key, { acs: [], relatedStories: related });
18982
+ }
18983
+ groups.get(key)?.acs.push(ac);
18984
+ }
18985
+ const result = Array.from(groups.values());
18986
+ while (result.length > MAX_FIX_STORIES) {
18987
+ result.sort((a, b) => a.acs.length - b.acs.length);
18988
+ const smallest = result.shift();
18989
+ if (!smallest)
18990
+ break;
18991
+ result[0].acs.push(...smallest.acs);
18992
+ for (const s of smallest.relatedStories) {
18993
+ if (!result[0].relatedStories.includes(s)) {
18994
+ result[0].relatedStories.push(s);
18995
+ }
18996
+ }
18997
+ }
18998
+ return result;
18999
+ }
19000
+ function buildFixPrompt(batchedACs, acTextMap, testOutput, relatedStories, prd, testFilePath) {
19001
+ const acList = batchedACs.map((ac) => `${ac}: ${acTextMap[ac] || "No description available"}`).join(`
19002
+ `);
18987
19003
  const relatedStoriesText = relatedStories.map((id) => {
18988
19004
  const story = prd.userStories.find((s) => s.id === id);
18989
19005
  if (!story)
@@ -18993,43 +19009,47 @@ function buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd) {
18993
19009
  }).filter(Boolean).join(`
18994
19010
 
18995
19011
  `);
18996
- return `You are a debugging expert. A feature acceptance test has failed.
18997
-
18998
- FAILED ACCEPTANCE CRITERION:
18999
- ${failedAC}: ${acText}
19012
+ const testFileSection = testFilePath ? `
19013
+ ACCEPTANCE TEST FILE: ${testFilePath}
19014
+ (Read this file first to understand what each test expects)
19015
+ ` : "";
19016
+ return `You are a debugging expert. Feature acceptance tests have failed.${testFileSection}
19017
+ FAILED ACCEPTANCE CRITERIA (${batchedACs.length} total):
19018
+ ${acList}
19000
19019
 
19001
19020
  TEST FAILURE OUTPUT:
19002
- ${testOutput}
19021
+ ${testOutput.slice(0, 2000)}
19003
19022
 
19004
19023
  RELATED STORIES (implemented this functionality):
19005
19024
  ${relatedStoriesText}
19006
19025
 
19007
- Your task: Generate a fix story description that will make the acceptance test pass.
19026
+ Your task: Generate a fix description that will make these acceptance tests pass.
19008
19027
 
19009
19028
  Requirements:
19010
- 1. Analyze the test failure to understand the root cause
19011
- 2. Identify what needs to change in the code
19012
- 3. Write a clear, actionable fix description (2-4 sentences)
19013
- 4. Focus on the specific issue, not general improvements
19029
+ 1. Read the acceptance test file first to understand what each failing test expects
19030
+ 2. Identify the root cause based on the test failure output
19031
+ 3. Find and fix the relevant implementation code (do NOT modify the test file)
19032
+ 4. Write a clear, actionable fix description (2-4 sentences)
19014
19033
  5. Reference the relevant story IDs if needed
19015
19034
 
19016
19035
  Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
19017
19036
  }
19018
19037
  async function generateFixStories(adapter, options) {
19019
- const { failedACs, testOutput, prd, specContent, modelDef } = options;
19020
- const fixStories = [];
19021
- const acTextMap = parseACTextFromSpec(specContent);
19038
+ const { failedACs, testOutput, prd, specContent, modelDef, testFilePath } = options;
19022
19039
  const logger = getLogger();
19023
- for (let i = 0;i < failedACs.length; i++) {
19024
- const failedAC = failedACs[i];
19025
- const acText = acTextMap[failedAC] || "No description available";
19026
- logger.info("acceptance", "Generating fix for failed AC", { failedAC });
19027
- const relatedStories = findRelatedStories(failedAC, prd);
19040
+ const acTextMap = parseACTextFromSpec(specContent);
19041
+ const groups = groupACsByRelatedStories(failedACs, prd);
19042
+ const fixStories = [];
19043
+ for (let i = 0;i < groups.length; i++) {
19044
+ const { acs: batchedACs, relatedStories } = groups[i];
19028
19045
  if (relatedStories.length === 0) {
19029
- logger.warn("acceptance", "\u26A0 No related stories found for failed AC \u2014 skipping", { failedAC });
19046
+ logger.warn("acceptance", "[WARN] No related stories found for AC group \u2014 skipping", { batchedACs });
19030
19047
  continue;
19031
19048
  }
19032
- const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
19049
+ logger.info("acceptance", "Generating fix for AC group", { batchedACs });
19050
+ const prompt = buildFixPrompt(batchedACs, acTextMap, testOutput, relatedStories, prd, testFilePath);
19051
+ const relatedStory = prd.userStories.find((s) => relatedStories.includes(s.id) && s.workdir);
19052
+ const workdir = relatedStory?.workdir;
19033
19053
  try {
19034
19054
  const fixDescription = await adapter.complete(prompt, {
19035
19055
  model: modelDef.model,
@@ -19037,25 +19057,31 @@ async function generateFixStories(adapter, options) {
19037
19057
  });
19038
19058
  fixStories.push({
19039
19059
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
19040
- title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
19041
- failedAC,
19060
+ title: `Fix: ${batchedACs.join(", ")} \u2014 ${(acTextMap[batchedACs[0]] || "").slice(0, 40)}`,
19061
+ failedAC: batchedACs[0],
19062
+ batchedACs,
19042
19063
  testOutput,
19043
19064
  relatedStories,
19044
- description: fixDescription
19065
+ description: fixDescription,
19066
+ testFilePath,
19067
+ workdir
19045
19068
  });
19046
- logger.info("acceptance", "\u2713 Generated fix story", { storyId: fixStories[fixStories.length - 1].id });
19069
+ logger.info("acceptance", "[OK] Generated fix story", { storyId: fixStories[fixStories.length - 1].id });
19047
19070
  } catch (error48) {
19048
- logger.warn("acceptance", "\u26A0 Error generating fix", {
19049
- failedAC,
19071
+ logger.warn("acceptance", "[WARN] Error generating fix", {
19072
+ batchedACs,
19050
19073
  error: error48.message
19051
19074
  });
19052
19075
  fixStories.push({
19053
19076
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
19054
- title: `Fix: ${failedAC}`,
19055
- failedAC,
19077
+ title: `Fix: ${batchedACs.join(", ")}`,
19078
+ failedAC: batchedACs[0],
19079
+ batchedACs,
19056
19080
  testOutput,
19057
19081
  relatedStories,
19058
- description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
19082
+ description: `Fix the implementation to make ${batchedACs.join(", ")} pass. Related stories: ${relatedStories.join(", ")}.`,
19083
+ testFilePath,
19084
+ workdir
19059
19085
  });
19060
19086
  }
19061
19087
  }
@@ -19076,20 +19102,40 @@ function parseACTextFromSpec(specContent) {
19076
19102
  return map2;
19077
19103
  }
19078
19104
  function convertFixStoryToUserStory(fixStory) {
19105
+ const batchedACs = fixStory.batchedACs ?? [fixStory.failedAC];
19106
+ const acList = batchedACs.join(", ");
19107
+ const truncatedOutput = fixStory.testOutput.slice(0, 1000);
19108
+ const testFilePath = fixStory.testFilePath ?? "acceptance.test.ts";
19109
+ const enrichedDescription = [
19110
+ fixStory.description,
19111
+ "",
19112
+ `ACCEPTANCE TEST FILE: ${testFilePath}`,
19113
+ `FAILED ACCEPTANCE CRITERIA: ${acList}`,
19114
+ "",
19115
+ "TEST FAILURE OUTPUT:",
19116
+ truncatedOutput,
19117
+ "",
19118
+ "Instructions: Read the acceptance test file first to understand what each failing test expects.",
19119
+ "Then find the relevant source code and fix the implementation.",
19120
+ "Do NOT modify the test file."
19121
+ ].join(`
19122
+ `);
19079
19123
  return {
19080
19124
  id: fixStory.id,
19081
19125
  title: fixStory.title,
19082
- description: fixStory.description,
19083
- acceptanceCriteria: [`Fix ${fixStory.failedAC}`],
19126
+ description: enrichedDescription,
19127
+ acceptanceCriteria: batchedACs.map((ac) => `Fix ${ac}`),
19084
19128
  tags: ["fix", "acceptance-failure"],
19085
19129
  dependencies: fixStory.relatedStories,
19086
19130
  status: "pending",
19087
19131
  passes: false,
19088
19132
  escalations: [],
19089
19133
  attempts: 0,
19090
- contextFiles: []
19134
+ contextFiles: [],
19135
+ workdir: fixStory.workdir
19091
19136
  };
19092
19137
  }
19138
+ var MAX_FIX_STORIES = 8;
19093
19139
  var init_fix_generator = __esm(() => {
19094
19140
  init_logger2();
19095
19141
  });
@@ -19482,7 +19528,7 @@ async function closeAcpSession(session) {
19482
19528
  }
19483
19529
  }
19484
19530
  function acpSessionsPath(workdir, featureName) {
19485
- return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19531
+ return join3(workdir, ".nax", "features", featureName, "acp-sessions.json");
19486
19532
  }
19487
19533
  function sidecarSessionName(entry) {
19488
19534
  return typeof entry === "string" ? entry : entry.sessionName;
@@ -20993,6 +21039,7 @@ import { join as join6, resolve as resolve4 } from "path";
20993
21039
  function globalConfigDir() {
20994
21040
  return join6(homedir3(), ".nax");
20995
21041
  }
21042
+ var PROJECT_NAX_DIR = ".nax";
20996
21043
  var init_paths = () => {};
20997
21044
 
20998
21045
  // src/config/loader.ts
@@ -21005,7 +21052,7 @@ function findProjectDir(startDir = process.cwd()) {
21005
21052
  let dir = resolve5(startDir);
21006
21053
  let depth = 0;
21007
21054
  while (depth < MAX_DIRECTORY_DEPTH) {
21008
- const candidate = join7(dir, "nax");
21055
+ const candidate = join7(dir, PROJECT_NAX_DIR);
21009
21056
  if (existsSync5(join7(candidate, "config.json"))) {
21010
21057
  return candidate;
21011
21058
  }
@@ -21075,7 +21122,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir) {
21075
21122
  return rootConfig;
21076
21123
  }
21077
21124
  const repoRoot = dirname2(rootNaxDir);
21078
- const packageConfigPath = join7(repoRoot, packageDir, "nax", "config.json");
21125
+ const packageConfigPath = join7(repoRoot, PROJECT_NAX_DIR, "packages", packageDir, "config.json");
21079
21126
  const packageOverride = await loadJsonFile(packageConfigPath, "config");
21080
21127
  if (!packageOverride) {
21081
21128
  return rootConfig;
@@ -22351,7 +22398,7 @@ var package_default;
22351
22398
  var init_package = __esm(() => {
22352
22399
  package_default = {
22353
22400
  name: "@nathapp/nax",
22354
- version: "0.50.2",
22401
+ version: "0.51.0",
22355
22402
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22356
22403
  type: "module",
22357
22404
  bin: {
@@ -22402,8 +22449,6 @@ var init_package = __esm(() => {
22402
22449
  ],
22403
22450
  files: [
22404
22451
  "dist/",
22405
- "src/",
22406
- "bin/",
22407
22452
  "README.md",
22408
22453
  "CHANGELOG.md"
22409
22454
  ],
@@ -22425,8 +22470,8 @@ var init_version = __esm(() => {
22425
22470
  NAX_VERSION = package_default.version;
22426
22471
  NAX_COMMIT = (() => {
22427
22472
  try {
22428
- if (/^[0-9a-f]{6,10}$/.test("c3a5edb"))
22429
- return "c3a5edb";
22473
+ if (/^[0-9a-f]{6,10}$/.test("bcb69c6"))
22474
+ return "bcb69c6";
22430
22475
  } catch {}
22431
22476
  try {
22432
22477
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22575,14 +22620,14 @@ function collectBatchMetrics(ctx, storyStartTime) {
22575
22620
  });
22576
22621
  }
22577
22622
  async function saveRunMetrics(workdir, runMetrics) {
22578
- const metricsPath = path2.join(workdir, "nax", "metrics.json");
22623
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
22579
22624
  const existing = await loadJsonFile(metricsPath, "metrics");
22580
22625
  const allMetrics = Array.isArray(existing) ? existing : [];
22581
22626
  allMetrics.push(runMetrics);
22582
22627
  await saveJsonFile(metricsPath, allMetrics, "metrics");
22583
22628
  }
22584
22629
  async function loadRunMetrics(workdir) {
22585
- const metricsPath = path2.join(workdir, "nax", "metrics.json");
22630
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
22586
22631
  const content = await loadJsonFile(metricsPath, "metrics");
22587
22632
  return Array.isArray(content) ? content : [];
22588
22633
  }
@@ -24180,8 +24225,114 @@ ${stderr}`;
24180
24225
  };
24181
24226
  });
24182
24227
 
24228
+ // src/agents/shared/validation.ts
24229
+ function validateAgentForTier(agent, tier) {
24230
+ return agent.capabilities.supportedTiers.includes(tier);
24231
+ }
24232
+ function validateAgentFeature(agent, feature) {
24233
+ return agent.capabilities.features.has(feature);
24234
+ }
24235
+ function describeAgentCapabilities(agent) {
24236
+ const tiers = agent.capabilities.supportedTiers.join(",");
24237
+ const features = Array.from(agent.capabilities.features).join(",");
24238
+ const maxTokens = agent.capabilities.maxContextTokens;
24239
+ return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
24240
+ }
24241
+
24242
+ // src/agents/shared/version-detection.ts
24243
+ async function getAgentVersion(binaryName) {
24244
+ try {
24245
+ const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
24246
+ stdout: "pipe",
24247
+ stderr: "pipe"
24248
+ });
24249
+ const exitCode = await proc.exited;
24250
+ if (exitCode !== 0) {
24251
+ return null;
24252
+ }
24253
+ const stdout = await new Response(proc.stdout).text();
24254
+ const versionLine = stdout.trim().split(`
24255
+ `)[0];
24256
+ const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
24257
+ if (versionMatch) {
24258
+ return versionMatch[0];
24259
+ }
24260
+ return versionLine || null;
24261
+ } catch {
24262
+ return null;
24263
+ }
24264
+ }
24265
+ async function getAgentVersions() {
24266
+ const agents = await getInstalledAgents();
24267
+ const agentsByName = new Map(agents.map((a) => [a.name, a]));
24268
+ const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
24269
+ const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
24270
+ const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
24271
+ return {
24272
+ name: agent.name,
24273
+ displayName: agent.displayName,
24274
+ version: version2,
24275
+ installed: agentsByName.has(agent.name)
24276
+ };
24277
+ }));
24278
+ return versions2;
24279
+ }
24280
+ var _versionDetectionDeps;
24281
+ var init_version_detection = __esm(() => {
24282
+ init_registry();
24283
+ _versionDetectionDeps = {
24284
+ spawn(cmd, opts) {
24285
+ return Bun.spawn(cmd, opts);
24286
+ }
24287
+ };
24288
+ });
24289
+
24290
+ // src/agents/index.ts
24291
+ var exports_agents = {};
24292
+ __export(exports_agents, {
24293
+ validateAgentForTier: () => validateAgentForTier,
24294
+ validateAgentFeature: () => validateAgentFeature,
24295
+ parseTokenUsage: () => parseTokenUsage,
24296
+ getInstalledAgents: () => getInstalledAgents,
24297
+ getAllAgentNames: () => getAllAgentNames,
24298
+ getAgentVersions: () => getAgentVersions,
24299
+ getAgentVersion: () => getAgentVersion,
24300
+ getAgent: () => getAgent,
24301
+ formatCostWithConfidence: () => formatCostWithConfidence,
24302
+ estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
24303
+ estimateCostFromOutput: () => estimateCostFromOutput,
24304
+ estimateCostByDuration: () => estimateCostByDuration,
24305
+ estimateCost: () => estimateCost,
24306
+ describeAgentCapabilities: () => describeAgentCapabilities,
24307
+ checkAgentHealth: () => checkAgentHealth,
24308
+ MODEL_PRICING: () => MODEL_PRICING,
24309
+ CompleteError: () => CompleteError,
24310
+ ClaudeCodeAdapter: () => ClaudeCodeAdapter,
24311
+ COST_RATES: () => COST_RATES
24312
+ });
24313
+ var init_agents = __esm(() => {
24314
+ init_types2();
24315
+ init_claude();
24316
+ init_registry();
24317
+ init_cost();
24318
+ init_version_detection();
24319
+ });
24320
+
24183
24321
  // src/pipeline/stages/acceptance-setup.ts
24322
+ var exports_acceptance_setup = {};
24323
+ __export(exports_acceptance_setup, {
24324
+ computeACFingerprint: () => computeACFingerprint,
24325
+ acceptanceSetupStage: () => acceptanceSetupStage,
24326
+ _acceptanceSetupDeps: () => _acceptanceSetupDeps
24327
+ });
24184
24328
  import path5 from "path";
24329
+ function computeACFingerprint(criteria) {
24330
+ const sorted = [...criteria].sort().join(`
24331
+ `);
24332
+ const hasher = new Bun.CryptoHasher("sha256");
24333
+ hasher.update(sorted);
24334
+ return `sha256:${hasher.digest("hex")}`;
24335
+ }
24185
24336
  var _acceptanceSetupDeps, acceptanceSetupStage;
24186
24337
  var init_acceptance_setup = __esm(() => {
24187
24338
  init_config();
@@ -24193,6 +24344,27 @@ var init_acceptance_setup = __esm(() => {
24193
24344
  writeFile: async (filePath, content) => {
24194
24345
  await Bun.write(filePath, content);
24195
24346
  },
24347
+ copyFile: async (src, dest) => {
24348
+ const content = await Bun.file(src).text();
24349
+ await Bun.write(dest, content);
24350
+ },
24351
+ deleteFile: async (filePath) => {
24352
+ const { unlink } = await import("fs/promises");
24353
+ await unlink(filePath);
24354
+ },
24355
+ readMeta: async (metaPath) => {
24356
+ const f = Bun.file(metaPath);
24357
+ if (!await f.exists())
24358
+ return null;
24359
+ try {
24360
+ return JSON.parse(await f.text());
24361
+ } catch {
24362
+ return null;
24363
+ }
24364
+ },
24365
+ writeMeta: async (metaPath, meta3) => {
24366
+ await Bun.write(metaPath, JSON.stringify(meta3, null, 2));
24367
+ },
24196
24368
  runTest: async (_testPath, _workdir) => {
24197
24369
  const proc = Bun.spawn(["bun", "test", _testPath], {
24198
24370
  cwd: _workdir,
@@ -24226,12 +24398,25 @@ ${stderr}` };
24226
24398
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
24227
24399
  }
24228
24400
  const testPath = path5.join(ctx.featureDir, "acceptance.test.ts");
24229
- const fileExists = await _acceptanceSetupDeps.fileExists(testPath);
24401
+ const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
24402
+ const allCriteria = ctx.prd.userStories.flatMap((s) => s.acceptanceCriteria);
24230
24403
  let totalCriteria = 0;
24231
24404
  let testableCount = 0;
24232
- if (!fileExists) {
24233
- const allCriteria = ctx.prd.userStories.flatMap((s) => s.acceptanceCriteria);
24405
+ const fileExists = await _acceptanceSetupDeps.fileExists(testPath);
24406
+ let shouldGenerate = !fileExists;
24407
+ if (fileExists) {
24408
+ const fingerprint = computeACFingerprint(allCriteria);
24409
+ const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
24410
+ if (!meta3 || meta3.acFingerprint !== fingerprint) {
24411
+ await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
24412
+ await _acceptanceSetupDeps.deleteFile(testPath);
24413
+ shouldGenerate = true;
24414
+ }
24415
+ }
24416
+ if (shouldGenerate) {
24234
24417
  totalCriteria = allCriteria.length;
24418
+ const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
24419
+ const agent = (ctx.agentGetFn ?? getAgent2)(ctx.config.autoMode.defaultAgent);
24235
24420
  let refinedCriteria;
24236
24421
  if (ctx.config.acceptance.refinement) {
24237
24422
  refinedCriteria = await _acceptanceSetupDeps.refine(allCriteria, {
@@ -24259,9 +24444,18 @@ ${stderr}` };
24259
24444
  modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
24260
24445
  config: ctx.config,
24261
24446
  testStrategy: ctx.config.acceptance.testStrategy,
24262
- testFramework: ctx.config.acceptance.testFramework
24447
+ testFramework: ctx.config.acceptance.testFramework,
24448
+ adapter: agent ?? undefined
24263
24449
  });
24264
24450
  await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
24451
+ const fingerprint = computeACFingerprint(allCriteria);
24452
+ await _acceptanceSetupDeps.writeMeta(metaPath, {
24453
+ generatedAt: new Date().toISOString(),
24454
+ acFingerprint: fingerprint,
24455
+ storyCount: ctx.prd.userStories.length,
24456
+ acCount: totalCriteria,
24457
+ generator: "nax"
24458
+ });
24265
24459
  }
24266
24460
  if (ctx.config.acceptance.redGate === false) {
24267
24461
  ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
@@ -24281,99 +24475,6 @@ ${stderr}` };
24281
24475
  };
24282
24476
  });
24283
24477
 
24284
- // src/agents/shared/validation.ts
24285
- function validateAgentForTier(agent, tier) {
24286
- return agent.capabilities.supportedTiers.includes(tier);
24287
- }
24288
- function validateAgentFeature(agent, feature) {
24289
- return agent.capabilities.features.has(feature);
24290
- }
24291
- function describeAgentCapabilities(agent) {
24292
- const tiers = agent.capabilities.supportedTiers.join(",");
24293
- const features = Array.from(agent.capabilities.features).join(",");
24294
- const maxTokens = agent.capabilities.maxContextTokens;
24295
- return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
24296
- }
24297
-
24298
- // src/agents/shared/version-detection.ts
24299
- async function getAgentVersion(binaryName) {
24300
- try {
24301
- const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
24302
- stdout: "pipe",
24303
- stderr: "pipe"
24304
- });
24305
- const exitCode = await proc.exited;
24306
- if (exitCode !== 0) {
24307
- return null;
24308
- }
24309
- const stdout = await new Response(proc.stdout).text();
24310
- const versionLine = stdout.trim().split(`
24311
- `)[0];
24312
- const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
24313
- if (versionMatch) {
24314
- return versionMatch[0];
24315
- }
24316
- return versionLine || null;
24317
- } catch {
24318
- return null;
24319
- }
24320
- }
24321
- async function getAgentVersions() {
24322
- const agents = await getInstalledAgents();
24323
- const agentsByName = new Map(agents.map((a) => [a.name, a]));
24324
- const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
24325
- const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
24326
- const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
24327
- return {
24328
- name: agent.name,
24329
- displayName: agent.displayName,
24330
- version: version2,
24331
- installed: agentsByName.has(agent.name)
24332
- };
24333
- }));
24334
- return versions2;
24335
- }
24336
- var _versionDetectionDeps;
24337
- var init_version_detection = __esm(() => {
24338
- init_registry();
24339
- _versionDetectionDeps = {
24340
- spawn(cmd, opts) {
24341
- return Bun.spawn(cmd, opts);
24342
- }
24343
- };
24344
- });
24345
-
24346
- // src/agents/index.ts
24347
- var exports_agents = {};
24348
- __export(exports_agents, {
24349
- validateAgentForTier: () => validateAgentForTier,
24350
- validateAgentFeature: () => validateAgentFeature,
24351
- parseTokenUsage: () => parseTokenUsage,
24352
- getInstalledAgents: () => getInstalledAgents,
24353
- getAllAgentNames: () => getAllAgentNames,
24354
- getAgentVersions: () => getAgentVersions,
24355
- getAgentVersion: () => getAgentVersion,
24356
- getAgent: () => getAgent,
24357
- formatCostWithConfidence: () => formatCostWithConfidence,
24358
- estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
24359
- estimateCostFromOutput: () => estimateCostFromOutput,
24360
- estimateCostByDuration: () => estimateCostByDuration,
24361
- estimateCost: () => estimateCost,
24362
- describeAgentCapabilities: () => describeAgentCapabilities,
24363
- checkAgentHealth: () => checkAgentHealth,
24364
- MODEL_PRICING: () => MODEL_PRICING,
24365
- CompleteError: () => CompleteError,
24366
- ClaudeCodeAdapter: () => ClaudeCodeAdapter,
24367
- COST_RATES: () => COST_RATES
24368
- });
24369
- var init_agents = __esm(() => {
24370
- init_types2();
24371
- init_claude();
24372
- init_registry();
24373
- init_cost();
24374
- init_version_detection();
24375
- });
24376
-
24377
24478
  // src/pipeline/event-bus.ts
24378
24479
  class PipelineEventBus {
24379
24480
  subscribers = new Map;
@@ -24458,7 +24559,7 @@ function hasScript(packageJson, scriptName) {
24458
24559
  return false;
24459
24560
  return scriptName in scripts;
24460
24561
  }
24461
- async function resolveCommand(check2, config2, executionConfig, workdir) {
24562
+ async function resolveCommand(check2, config2, executionConfig, workdir, qualityCommands) {
24462
24563
  if (executionConfig) {
24463
24564
  if (check2 === "lint" && executionConfig.lintCommand !== undefined) {
24464
24565
  return executionConfig.lintCommand;
@@ -24470,6 +24571,10 @@ async function resolveCommand(check2, config2, executionConfig, workdir) {
24470
24571
  if (config2.commands[check2]) {
24471
24572
  return config2.commands[check2] ?? null;
24472
24573
  }
24574
+ const qualityCmd = qualityCommands?.[check2];
24575
+ if (qualityCmd) {
24576
+ return qualityCmd;
24577
+ }
24473
24578
  const packageJson = await loadPackageJson(workdir);
24474
24579
  if (hasScript(packageJson, check2)) {
24475
24580
  return `bun run ${check2}`;
@@ -24567,7 +24672,7 @@ async function getUncommittedFilesImpl(workdir) {
24567
24672
  return [];
24568
24673
  }
24569
24674
  }
24570
- async function runReview(config2, workdir, executionConfig) {
24675
+ async function runReview(config2, workdir, executionConfig, qualityCommands) {
24571
24676
  const startTime = Date.now();
24572
24677
  const logger = getSafeLogger();
24573
24678
  const checks3 = [];
@@ -24605,7 +24710,7 @@ Stage and commit these files before running review.`
24605
24710
  };
24606
24711
  }
24607
24712
  for (const checkName of config2.checks) {
24608
- const command = await resolveCommand(checkName, config2, executionConfig, workdir);
24713
+ const command = await resolveCommand(checkName, config2, executionConfig, workdir, qualityCommands);
24609
24714
  if (command === null) {
24610
24715
  getSafeLogger()?.warn("review", `Skipping ${checkName} check (command not configured or disabled)`);
24611
24716
  continue;
@@ -24667,9 +24772,9 @@ async function getChangedFiles(workdir, baseRef) {
24667
24772
  }
24668
24773
 
24669
24774
  class ReviewOrchestrator {
24670
- async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix) {
24775
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands) {
24671
24776
  const logger = getSafeLogger();
24672
- const builtIn = await runReview(reviewConfig, workdir, executionConfig);
24777
+ const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands);
24673
24778
  if (!builtIn.success) {
24674
24779
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
24675
24780
  }
@@ -24759,7 +24864,7 @@ var init_review = __esm(() => {
24759
24864
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
24760
24865
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
24761
24866
  const effectiveWorkdir = ctx.story.workdir ? join17(ctx.workdir, ctx.story.workdir) : ctx.workdir;
24762
- const result = await reviewOrchestrator.review(effectiveConfig.review, effectiveWorkdir, effectiveConfig.execution, ctx.plugins, ctx.storyGitRef, ctx.story.workdir);
24867
+ const result = await reviewOrchestrator.review(effectiveConfig.review, effectiveWorkdir, effectiveConfig.execution, ctx.plugins, ctx.storyGitRef, ctx.story.workdir, effectiveConfig.quality?.commands);
24763
24868
  ctx.reviewResult = result.builtIn;
24764
24869
  if (!result.success) {
24765
24870
  const allFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
@@ -25976,7 +26081,7 @@ function hookCtx(feature, opts) {
25976
26081
  };
25977
26082
  }
25978
26083
  async function loadPackageContextMd(packageWorkdir) {
25979
- const contextPath = `${packageWorkdir}/nax/context.md`;
26084
+ const contextPath = `${packageWorkdir}/.nax/context.md`;
25980
26085
  const file2 = Bun.file(contextPath);
25981
26086
  if (!await file2.exists())
25982
26087
  return null;
@@ -27158,6 +27263,11 @@ function buildIsolationSection(roleOrMode, mode, testCommand) {
27158
27263
  const footer = `
27159
27264
 
27160
27265
  ${buildTestFilterRule(testCmd)}`;
27266
+ if (role === "no-test") {
27267
+ return `${header}
27268
+
27269
+ isolation scope: Implement changes in src/ and other non-test directories. Do NOT create or modify any files in the test/ directory.${footer}`;
27270
+ }
27161
27271
  if (role === "test-writer") {
27162
27272
  const m = mode ?? "strict";
27163
27273
  if (m === "strict") {
@@ -27205,13 +27315,26 @@ function buildTestFrameworkHint(testCommand) {
27205
27315
  return "Use Jest (describe/test/expect)";
27206
27316
  return "Use your project's test framework";
27207
27317
  }
27208
- function buildRoleTaskSection(roleOrVariant, variant, testCommand, isolation) {
27318
+ function buildRoleTaskSection(roleOrVariant, variant, testCommand, isolation, noTestJustification) {
27209
27319
  if ((roleOrVariant === "standard" || roleOrVariant === "lite") && variant === undefined) {
27210
27320
  return buildRoleTaskSection("implementer", roleOrVariant, testCommand, isolation);
27211
27321
  }
27212
27322
  const role = roleOrVariant;
27213
27323
  const testCmd = testCommand ?? DEFAULT_TEST_CMD2;
27214
27324
  const frameworkHint = buildTestFrameworkHint(testCmd);
27325
+ if (role === "no-test") {
27326
+ const justification = noTestJustification ?? "No behavioral changes \u2014 tests not required";
27327
+ return `# Role: Implementer (No Tests)
27328
+
27329
+ Your task: implement the change as described. This story has no behavioral changes and does not require test modifications.
27330
+
27331
+ Instructions:
27332
+ - Implement the change as described in the story
27333
+ - Do NOT create or modify test files
27334
+ - Justification for no tests: ${justification}
27335
+ - When done, stage and commit ALL changed files with: git commit -m 'feat: <description>'
27336
+ - Goal: change implemented, no test files created or modified, all changes committed`;
27337
+ }
27215
27338
  if (role === "implementer") {
27216
27339
  const v = variant ?? "standard";
27217
27340
  if (v === "standard") {
@@ -27486,6 +27609,7 @@ class PromptBuilder {
27486
27609
  _loaderConfig;
27487
27610
  _testCommand;
27488
27611
  _hermeticConfig;
27612
+ _noTestJustification;
27489
27613
  constructor(role, options = {}) {
27490
27614
  this._role = role;
27491
27615
  this._options = options;
@@ -27529,6 +27653,10 @@ class PromptBuilder {
27529
27653
  this._hermeticConfig = config2;
27530
27654
  return this;
27531
27655
  }
27656
+ noTestJustification(justification) {
27657
+ this._noTestJustification = justification;
27658
+ return this;
27659
+ }
27532
27660
  async build() {
27533
27661
  const sections = [];
27534
27662
  if (this._constitution) {
@@ -27588,7 +27716,7 @@ ${this._contextMd}
27588
27716
  }
27589
27717
  const variant = this._options.variant;
27590
27718
  const isolation = this._options.isolation;
27591
- return buildRoleTaskSection(this._role, variant, this._testCommand, isolation);
27719
+ return buildRoleTaskSection(this._role, variant, this._testCommand, isolation, this._noTestJustification);
27592
27720
  }
27593
27721
  }
27594
27722
  var SECTION_SEP2 = `
@@ -28791,8 +28919,8 @@ var init_prompt = __esm(() => {
28791
28919
  const builder = PromptBuilder.for("batch").withLoader(ctx.workdir, ctx.config).stories(ctx.stories).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.quality?.testing);
28792
28920
  prompt = await builder.build();
28793
28921
  } else {
28794
- const role = "tdd-simple";
28795
- const builder = PromptBuilder.for(role).withLoader(ctx.workdir, ctx.config).story(ctx.story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.quality?.testing);
28922
+ const role = ctx.routing.testStrategy === "no-test" ? "no-test" : "tdd-simple";
28923
+ const builder = PromptBuilder.for(role).withLoader(ctx.workdir, ctx.config).story(ctx.story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.quality?.testing).noTestJustification(ctx.story.routing?.noTestJustification);
28796
28924
  prompt = await builder.build();
28797
28925
  }
28798
28926
  ctx.prompt = prompt;
@@ -30425,8 +30553,7 @@ function generatePackageContextTemplate(packagePath) {
30425
30553
  }
30426
30554
  async function initPackage(repoRoot, packagePath, force = false) {
30427
30555
  const logger = getLogger();
30428
- const packageDir = join31(repoRoot, packagePath);
30429
- const naxDir = join31(packageDir, "nax");
30556
+ const naxDir = join31(repoRoot, ".nax", "packages", packagePath);
30430
30557
  const contextPath = join31(naxDir, "context.md");
30431
30558
  if (existsSync20(contextPath) && !force) {
30432
30559
  logger.info("init", "Package context.md already exists (use --force to overwrite)", { path: contextPath });
@@ -30441,7 +30568,7 @@ async function initPackage(repoRoot, packagePath, force = false) {
30441
30568
  }
30442
30569
  async function initContext(projectRoot, options = {}) {
30443
30570
  const logger = getLogger();
30444
- const naxDir = join31(projectRoot, "nax");
30571
+ const naxDir = join31(projectRoot, ".nax");
30445
30572
  const contextPath = join31(naxDir, "context.md");
30446
30573
  if (existsSync20(contextPath) && !options.force) {
30447
30574
  logger.info("init", "context.md already exists, skipping (use --force to overwrite)", { path: contextPath });
@@ -30458,7 +30585,7 @@ async function initContext(projectRoot, options = {}) {
30458
30585
  content = generateContextTemplate(scan);
30459
30586
  }
30460
30587
  await Bun.write(contextPath, content);
30461
- logger.info("init", "Generated nax/context.md template from project scan", { path: contextPath });
30588
+ logger.info("init", "Generated .nax/context.md template from project scan", { path: contextPath });
30462
30589
  }
30463
30590
  var _deps6;
30464
30591
  var init_init_context = __esm(() => {
@@ -31029,18 +31156,18 @@ var NAX_RUNTIME_PATTERNS;
31029
31156
  var init_checks_git = __esm(() => {
31030
31157
  NAX_RUNTIME_PATTERNS = [
31031
31158
  /^.{2} nax\.lock$/,
31032
- /^.{2} nax\/$/,
31033
- /^.{2} nax\/metrics\.json$/,
31034
- /^.{2} nax\/features\/$/,
31035
- /^.{2} nax\/features\/[^/]+\/$/,
31036
- /^.{2} nax\/features\/[^/]+\/status\.json$/,
31037
- /^.{2} nax\/features\/[^/]+\/prd\.json$/,
31038
- /^.{2} nax\/features\/[^/]+\/runs\//,
31039
- /^.{2} nax\/features\/[^/]+\/plan\//,
31040
- /^.{2} nax\/features\/[^/]+\/acp-sessions\.json$/,
31041
- /^.{2} nax\/features\/[^/]+\/interactions\//,
31042
- /^.{2} nax\/features\/[^/]+\/progress\.txt$/,
31043
- /^.{2} nax\/features\/[^/]+\/acceptance-refined\.json$/,
31159
+ /^.{2} \.nax\/$/,
31160
+ /^.{2} \.nax\/metrics\.json$/,
31161
+ /^.{2} \.nax\/features\/$/,
31162
+ /^.{2} \.nax\/features\/[^/]+\/$/,
31163
+ /^.{2} \.nax\/features\/[^/]+\/status\.json$/,
31164
+ /^.{2} \.nax\/features\/[^/]+\/prd\.json$/,
31165
+ /^.{2} \.nax\/features\/[^/]+\/runs\//,
31166
+ /^.{2} \.nax\/features\/[^/]+\/plan\//,
31167
+ /^.{2} \.nax\/features\/[^/]+\/acp-sessions\.json$/,
31168
+ /^.{2} \.nax\/features\/[^/]+\/interactions\//,
31169
+ /^.{2} \.nax\/features\/[^/]+\/progress\.txt$/,
31170
+ /^.{2} \.nax\/features\/[^/]+\/acceptance-refined\.json$/,
31044
31171
  /^.{2} \.nax-verifier-verdict\.json$/,
31045
31172
  /^.{2} \.nax-pids$/,
31046
31173
  /^.{2} \.nax-wt\//
@@ -31351,9 +31478,9 @@ async function checkGitignoreCoversNax(workdir) {
31351
31478
  const content = await file2.text();
31352
31479
  const patterns = [
31353
31480
  "nax.lock",
31354
- "nax/**/runs/",
31355
- "nax/metrics.json",
31356
- "nax/features/*/status.json",
31481
+ ".nax/**/runs/",
31482
+ ".nax/metrics.json",
31483
+ ".nax/features/*/status.json",
31357
31484
  ".nax-pids",
31358
31485
  ".nax-wt/"
31359
31486
  ];
@@ -32201,9 +32328,21 @@ var init_crash_recovery = __esm(() => {
32201
32328
  // src/execution/lifecycle/acceptance-loop.ts
32202
32329
  var exports_acceptance_loop = {};
32203
32330
  __export(exports_acceptance_loop, {
32204
- runAcceptanceLoop: () => runAcceptanceLoop
32331
+ runAcceptanceLoop: () => runAcceptanceLoop,
32332
+ isTestLevelFailure: () => isTestLevelFailure,
32333
+ isStubTestFile: () => isStubTestFile
32205
32334
  });
32206
- import path14 from "path";
32335
+ import path14, { join as join45 } from "path";
32336
+ function isStubTestFile(content) {
32337
+ return /expect\s*\(\s*true\s*\)\s*\.\s*toBe\s*\(\s*(?:false|true)\s*\)/.test(content);
32338
+ }
32339
+ function isTestLevelFailure(failedACs, totalACs) {
32340
+ if (failedACs.includes("AC-ERROR"))
32341
+ return true;
32342
+ if (totalACs === 0)
32343
+ return false;
32344
+ return failedACs.length / totalACs > 0.8;
32345
+ }
32207
32346
  async function loadSpecContent(featureDir) {
32208
32347
  if (!featureDir)
32209
32348
  return "";
@@ -32223,6 +32362,7 @@ async function generateAndAddFixStories(ctx, failures, prd) {
32223
32362
  return null;
32224
32363
  }
32225
32364
  const modelDef = resolveModel(ctx.config.models[ctx.config.analyze.model]);
32365
+ const testFilePath = ctx.featureDir ? path14.join(ctx.featureDir, "acceptance.test.ts") : undefined;
32226
32366
  const fixStories = await generateFixStories(agent, {
32227
32367
  failedACs: failures.failedACs,
32228
32368
  testOutput: failures.testOutput,
@@ -32230,7 +32370,8 @@ async function generateAndAddFixStories(ctx, failures, prd) {
32230
32370
  specContent: await loadSpecContent(ctx.featureDir),
32231
32371
  workdir: ctx.workdir,
32232
32372
  modelDef,
32233
- config: ctx.config
32373
+ config: ctx.config,
32374
+ testFilePath
32234
32375
  });
32235
32376
  if (fixStories.length === 0) {
32236
32377
  logger?.error("acceptance", "Failed to generate fix stories");
@@ -32254,9 +32395,10 @@ async function executeFixStory(ctx, story, prd, iterations) {
32254
32395
  agent: ctx.config.autoMode.defaultAgent,
32255
32396
  iteration: iterations
32256
32397
  }), ctx.workdir);
32398
+ const fixEffectiveConfig = story.workdir ? await loadConfigForWorkdir(join45(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
32257
32399
  const fixContext = {
32258
32400
  config: ctx.config,
32259
- effectiveConfig: ctx.config,
32401
+ effectiveConfig: fixEffectiveConfig,
32260
32402
  prd,
32261
32403
  story,
32262
32404
  stories: [story],
@@ -32276,6 +32418,23 @@ async function executeFixStory(ctx, story, prd, iterations) {
32276
32418
  metrics: result.context.storyMetrics
32277
32419
  };
32278
32420
  }
32421
+ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
32422
+ const logger = getSafeLogger();
32423
+ const bakPath = `${testPath}.bak`;
32424
+ const content = await Bun.file(testPath).text();
32425
+ await Bun.write(bakPath, content);
32426
+ logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
32427
+ const { unlink: unlink3 } = await import("fs/promises");
32428
+ await unlink3(testPath);
32429
+ const { acceptanceSetupStage: acceptanceSetupStage2 } = await Promise.resolve().then(() => (init_acceptance_setup(), exports_acceptance_setup));
32430
+ await acceptanceSetupStage2.execute(acceptanceContext);
32431
+ if (!await Bun.file(testPath).exists()) {
32432
+ logger?.error("acceptance", "Acceptance test regeneration failed \u2014 manual intervention required");
32433
+ return false;
32434
+ }
32435
+ logger?.info("acceptance", "Acceptance test regenerated successfully");
32436
+ return true;
32437
+ }
32279
32438
  async function runAcceptanceLoop(ctx) {
32280
32439
  const logger = getSafeLogger();
32281
32440
  const maxRetries = ctx.config.acceptance.maxRetries;
@@ -32337,6 +32496,39 @@ async function runAcceptanceLoop(ctx) {
32337
32496
  }), ctx.workdir);
32338
32497
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32339
32498
  }
32499
+ if (ctx.featureDir) {
32500
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
32501
+ const testFile = Bun.file(testPath);
32502
+ if (await testFile.exists()) {
32503
+ const testContent = await testFile.text();
32504
+ if (isStubTestFile(testContent)) {
32505
+ logger?.warn("acceptance", "Stub tests detected \u2014 re-generating acceptance tests");
32506
+ const { unlink: unlink3 } = await import("fs/promises");
32507
+ await unlink3(testPath);
32508
+ const { acceptanceSetupStage: acceptanceSetupStage2 } = await Promise.resolve().then(() => (init_acceptance_setup(), exports_acceptance_setup));
32509
+ await acceptanceSetupStage2.execute(acceptanceContext);
32510
+ const newContent = await Bun.file(testPath).text();
32511
+ if (isStubTestFile(newContent)) {
32512
+ logger?.error("acceptance", "Acceptance test generation failed after retry \u2014 manual implementation required");
32513
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32514
+ }
32515
+ continue;
32516
+ }
32517
+ }
32518
+ }
32519
+ const totalACs = prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria).length;
32520
+ if (ctx.featureDir && isTestLevelFailure(failures.failedACs, totalACs)) {
32521
+ logger?.warn("acceptance", `Test-level failure detected (${failures.failedACs.length}/${totalACs} ACs failed) \u2014 regenerating acceptance test`);
32522
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
32523
+ const testFile = Bun.file(testPath);
32524
+ if (await testFile.exists()) {
32525
+ const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
32526
+ if (!regenerated) {
32527
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32528
+ }
32529
+ continue;
32530
+ }
32531
+ }
32340
32532
  logger?.info("acceptance", "Generating fix stories...");
32341
32533
  const fixStories = await generateAndAddFixStories(ctx, failures, prd);
32342
32534
  if (!fixStories) {
@@ -32367,6 +32559,7 @@ async function runAcceptanceLoop(ctx) {
32367
32559
  }
32368
32560
  var init_acceptance_loop = __esm(() => {
32369
32561
  init_acceptance();
32562
+ init_loader2();
32370
32563
  init_schema();
32371
32564
  init_hooks();
32372
32565
  init_logger2();
@@ -32801,12 +32994,12 @@ __export(exports_manager, {
32801
32994
  WorktreeManager: () => WorktreeManager
32802
32995
  });
32803
32996
  import { existsSync as existsSync32, symlinkSync } from "fs";
32804
- import { join as join45 } from "path";
32997
+ import { join as join46 } from "path";
32805
32998
 
32806
32999
  class WorktreeManager {
32807
33000
  async create(projectRoot, storyId) {
32808
33001
  validateStoryId(storyId);
32809
- const worktreePath = join45(projectRoot, ".nax-wt", storyId);
33002
+ const worktreePath = join46(projectRoot, ".nax-wt", storyId);
32810
33003
  const branchName = `nax/${storyId}`;
32811
33004
  try {
32812
33005
  const proc = Bun.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
@@ -32831,9 +33024,9 @@ class WorktreeManager {
32831
33024
  }
32832
33025
  throw new Error(`Failed to create worktree: ${String(error48)}`);
32833
33026
  }
32834
- const nodeModulesSource = join45(projectRoot, "node_modules");
33027
+ const nodeModulesSource = join46(projectRoot, "node_modules");
32835
33028
  if (existsSync32(nodeModulesSource)) {
32836
- const nodeModulesTarget = join45(worktreePath, "node_modules");
33029
+ const nodeModulesTarget = join46(worktreePath, "node_modules");
32837
33030
  try {
32838
33031
  symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
32839
33032
  } catch (error48) {
@@ -32841,9 +33034,9 @@ class WorktreeManager {
32841
33034
  throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
32842
33035
  }
32843
33036
  }
32844
- const envSource = join45(projectRoot, ".env");
33037
+ const envSource = join46(projectRoot, ".env");
32845
33038
  if (existsSync32(envSource)) {
32846
- const envTarget = join45(worktreePath, ".env");
33039
+ const envTarget = join46(worktreePath, ".env");
32847
33040
  try {
32848
33041
  symlinkSync(envSource, envTarget, "file");
32849
33042
  } catch (error48) {
@@ -32854,7 +33047,7 @@ class WorktreeManager {
32854
33047
  }
32855
33048
  async remove(projectRoot, storyId) {
32856
33049
  validateStoryId(storyId);
32857
- const worktreePath = join45(projectRoot, ".nax-wt", storyId);
33050
+ const worktreePath = join46(projectRoot, ".nax-wt", storyId);
32858
33051
  const branchName = `nax/${storyId}`;
32859
33052
  try {
32860
33053
  const proc = Bun.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -33245,7 +33438,7 @@ var init_parallel_worker = __esm(() => {
33245
33438
 
33246
33439
  // src/execution/parallel-coordinator.ts
33247
33440
  import os3 from "os";
33248
- import { join as join46 } from "path";
33441
+ import { join as join47 } from "path";
33249
33442
  function groupStoriesByDependencies(stories) {
33250
33443
  const batches = [];
33251
33444
  const processed = new Set;
@@ -33324,7 +33517,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33324
33517
  };
33325
33518
  const worktreePaths = new Map;
33326
33519
  for (const story of batch) {
33327
- const worktreePath = join46(projectRoot, ".nax-wt", story.id);
33520
+ const worktreePath = join47(projectRoot, ".nax-wt", story.id);
33328
33521
  try {
33329
33522
  await worktreeManager.create(projectRoot, story.id);
33330
33523
  worktreePaths.set(story.id, worktreePath);
@@ -33373,7 +33566,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33373
33566
  });
33374
33567
  logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
33375
33568
  storyId: mergeResult.storyId,
33376
- worktreePath: join46(projectRoot, ".nax-wt", mergeResult.storyId)
33569
+ worktreePath: join47(projectRoot, ".nax-wt", mergeResult.storyId)
33377
33570
  });
33378
33571
  }
33379
33572
  }
@@ -33833,12 +34026,12 @@ var init_parallel_executor = __esm(() => {
33833
34026
  // src/pipeline/subscribers/events-writer.ts
33834
34027
  import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
33835
34028
  import { homedir as homedir7 } from "os";
33836
- import { basename as basename5, join as join47 } from "path";
34029
+ import { basename as basename5, join as join48 } from "path";
33837
34030
  function wireEventsWriter(bus, feature, runId, workdir) {
33838
34031
  const logger = getSafeLogger();
33839
34032
  const project = basename5(workdir);
33840
- const eventsDir = join47(homedir7(), ".nax", "events", project);
33841
- const eventsFile = join47(eventsDir, "events.jsonl");
34033
+ const eventsDir = join48(homedir7(), ".nax", "events", project);
34034
+ const eventsFile = join48(eventsDir, "events.jsonl");
33842
34035
  let dirReady = false;
33843
34036
  const write = (line) => {
33844
34037
  (async () => {
@@ -34012,12 +34205,12 @@ var init_interaction2 = __esm(() => {
34012
34205
  // src/pipeline/subscribers/registry.ts
34013
34206
  import { mkdir as mkdir3, writeFile } from "fs/promises";
34014
34207
  import { homedir as homedir8 } from "os";
34015
- import { basename as basename6, join as join48 } from "path";
34208
+ import { basename as basename6, join as join49 } from "path";
34016
34209
  function wireRegistry(bus, feature, runId, workdir) {
34017
34210
  const logger = getSafeLogger();
34018
34211
  const project = basename6(workdir);
34019
- const runDir = join48(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
34020
- const metaFile = join48(runDir, "meta.json");
34212
+ const runDir = join49(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
34213
+ const metaFile = join49(runDir, "meta.json");
34021
34214
  const unsub = bus.on("run:started", (_ev) => {
34022
34215
  (async () => {
34023
34216
  try {
@@ -34027,8 +34220,8 @@ function wireRegistry(bus, feature, runId, workdir) {
34027
34220
  project,
34028
34221
  feature,
34029
34222
  workdir,
34030
- statusPath: join48(workdir, "nax", "features", feature, "status.json"),
34031
- eventsDir: join48(workdir, "nax", "features", feature, "runs"),
34223
+ statusPath: join49(workdir, ".nax", "features", feature, "status.json"),
34224
+ eventsDir: join49(workdir, ".nax", "features", feature, "runs"),
34032
34225
  registeredAt: new Date().toISOString()
34033
34226
  };
34034
34227
  await writeFile(metaFile, JSON.stringify(meta3, null, 2));
@@ -34455,7 +34648,7 @@ async function handleTierEscalation(ctx) {
34455
34648
  }
34456
34649
  for (const s of storiesToEscalate) {
34457
34650
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
34458
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after";
34651
+ const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
34459
34652
  if (shouldSwitchToTestAfter) {
34460
34653
  logger?.warn("escalation", "Switching strategy to test-after (greenfield-no-tests fallback)", {
34461
34654
  storyId: s.id,
@@ -34479,7 +34672,7 @@ async function handleTierEscalation(ctx) {
34479
34672
  if (!shouldEscalate)
34480
34673
  return s;
34481
34674
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
34482
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after";
34675
+ const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
34483
34676
  const baseRouting = s.routing ?? { ...ctx.routing };
34484
34677
  const updatedRouting = {
34485
34678
  ...baseRouting,
@@ -34681,7 +34874,7 @@ var init_pipeline_result_handler = __esm(() => {
34681
34874
  });
34682
34875
 
34683
34876
  // src/execution/iteration-runner.ts
34684
- import { join as join49 } from "path";
34877
+ import { join as join50 } from "path";
34685
34878
  async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
34686
34879
  const logger = getSafeLogger();
34687
34880
  const { story, storiesToExecute, routing, isBatchExecution } = selection;
@@ -34707,7 +34900,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
34707
34900
  const storyStartTime = Date.now();
34708
34901
  const storyGitRef = await captureGitRef(ctx.workdir);
34709
34902
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
34710
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join49(ctx.workdir, "nax", "config.json"), story.workdir) : ctx.config;
34903
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join50(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
34711
34904
  const pipelineContext = {
34712
34905
  config: ctx.config,
34713
34906
  effectiveConfig,
@@ -35073,7 +35266,7 @@ async function writeStatusFile(filePath, status) {
35073
35266
  var init_status_file = () => {};
35074
35267
 
35075
35268
  // src/execution/status-writer.ts
35076
- import { join as join50 } from "path";
35269
+ import { join as join51 } from "path";
35077
35270
 
35078
35271
  class StatusWriter {
35079
35272
  statusFile;
@@ -35141,7 +35334,7 @@ class StatusWriter {
35141
35334
  if (!this._prd)
35142
35335
  return;
35143
35336
  const safeLogger = getSafeLogger();
35144
- const featureStatusPath = join50(featureDir, "status.json");
35337
+ const featureStatusPath = join51(featureDir, "status.json");
35145
35338
  try {
35146
35339
  const base = this.getSnapshot(totalCost, iterations);
35147
35340
  if (!base) {
@@ -35349,7 +35542,7 @@ __export(exports_run_initialization, {
35349
35542
  initializeRun: () => initializeRun,
35350
35543
  _reconcileDeps: () => _reconcileDeps
35351
35544
  });
35352
- import { join as join51 } from "path";
35545
+ import { join as join52 } from "path";
35353
35546
  async function reconcileState(prd, prdPath, workdir, config2) {
35354
35547
  const logger = getSafeLogger();
35355
35548
  let reconciledCount = 0;
@@ -35361,7 +35554,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
35361
35554
  if (!hasCommits)
35362
35555
  continue;
35363
35556
  if (story.failureStage === "review" || story.failureStage === "autofix") {
35364
- const effectiveWorkdir = story.workdir ? join51(workdir, story.workdir) : workdir;
35557
+ const effectiveWorkdir = story.workdir ? join52(workdir, story.workdir) : workdir;
35365
35558
  try {
35366
35559
  const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
35367
35560
  if (!reviewResult.success) {
@@ -35542,7 +35735,7 @@ async function setupRun(options) {
35542
35735
  }
35543
35736
  try {
35544
35737
  const globalPluginsDir = path18.join(os5.homedir(), ".nax", "plugins");
35545
- const projectPluginsDir = path18.join(workdir, "nax", "plugins");
35738
+ const projectPluginsDir = path18.join(workdir, ".nax", "plugins");
35546
35739
  const configPlugins = config2.plugins || [];
35547
35740
  const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
35548
35741
  logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
@@ -66495,7 +66688,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
66495
66688
  init_source();
66496
66689
  import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
66497
66690
  import { homedir as homedir10 } from "os";
66498
- import { join as join52 } from "path";
66691
+ import { join as join53 } from "path";
66499
66692
 
66500
66693
  // node_modules/commander/esm.mjs
66501
66694
  var import__ = __toESM(require_commander(), 1);
@@ -67191,7 +67384,7 @@ function formatMetadataSection(metadata) {
67191
67384
  // src/context/generators/aider.ts
67192
67385
  function generateAiderConfig(context) {
67193
67386
  const header = `# Aider Configuration
67194
- # Auto-generated from nax/context.md \u2014 run \`nax generate\` to regenerate.
67387
+ # Auto-generated from .nax/context.md \u2014 run \`nax generate\` to regenerate.
67195
67388
  # DO NOT EDIT MANUALLY
67196
67389
 
67197
67390
  # Project instructions
@@ -67215,7 +67408,7 @@ var aiderGenerator = {
67215
67408
  function generateClaudeConfig(context) {
67216
67409
  const header = `# Project Context
67217
67410
 
67218
- This file is auto-generated from \`nax/context.md\`.
67411
+ This file is auto-generated from \`.nax/context.md\`.
67219
67412
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67220
67413
 
67221
67414
  ---
@@ -67234,7 +67427,7 @@ var claudeGenerator = {
67234
67427
  function generateCodexConfig(context) {
67235
67428
  const header = `# Codex Instructions
67236
67429
 
67237
- This file is auto-generated from \`nax/context.md\`.
67430
+ This file is auto-generated from \`.nax/context.md\`.
67238
67431
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67239
67432
 
67240
67433
  ---
@@ -67253,7 +67446,7 @@ var codexGenerator = {
67253
67446
  function generateCursorRules(context) {
67254
67447
  const header = `# Project Rules
67255
67448
 
67256
- Auto-generated from nax/context.md \u2014 run \`nax generate\` to regenerate.
67449
+ Auto-generated from .nax/context.md \u2014 run \`nax generate\` to regenerate.
67257
67450
  DO NOT EDIT MANUALLY
67258
67451
 
67259
67452
  ---
@@ -67272,7 +67465,7 @@ var cursorGenerator = {
67272
67465
  function generateGeminiConfig(context) {
67273
67466
  const header = `# Gemini CLI Context
67274
67467
 
67275
- This file is auto-generated from \`nax/context.md\`.
67468
+ This file is auto-generated from \`.nax/context.md\`.
67276
67469
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67277
67470
 
67278
67471
  ---
@@ -67291,7 +67484,7 @@ var geminiGenerator = {
67291
67484
  function generateOpencodeConfig(context) {
67292
67485
  const header = `# Agent Instructions
67293
67486
 
67294
- This file is auto-generated from \`nax/context.md\`.
67487
+ This file is auto-generated from \`.nax/context.md\`.
67295
67488
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67296
67489
 
67297
67490
  These instructions apply to all AI coding agents in this project.
@@ -67312,7 +67505,7 @@ var opencodeGenerator = {
67312
67505
  function generateWindsurfRules(context) {
67313
67506
  const header = `# Windsurf Project Rules
67314
67507
 
67315
- Auto-generated from nax/context.md \u2014 run \`nax generate\` to regenerate.
67508
+ Auto-generated from .nax/context.md \u2014 run \`nax generate\` to regenerate.
67316
67509
  DO NOT EDIT MANUALLY
67317
67510
 
67318
67511
  ---
@@ -67389,10 +67582,10 @@ async function generateAll(options, config2, agentFilter) {
67389
67582
  async function discoverPackages(repoRoot) {
67390
67583
  const packages = [];
67391
67584
  const seen = new Set;
67392
- for (const pattern of ["*/nax/context.md", "*/*/nax/context.md"]) {
67585
+ for (const pattern of [".nax/packages/*/context.md", ".nax/packages/*/*/context.md"]) {
67393
67586
  const glob = new Bun.Glob(pattern);
67394
- for await (const match of glob.scan(repoRoot)) {
67395
- const pkgRelative = match.replace(/\/nax\/context\.md$/, "");
67587
+ for await (const match of glob.scan({ cwd: repoRoot, dot: true })) {
67588
+ const pkgRelative = match.replace(/^\.nax\/packages\//, "").replace(/\/context\.md$/, "");
67396
67589
  const pkgAbsolute = join11(repoRoot, pkgRelative);
67397
67590
  if (!seen.has(pkgAbsolute)) {
67398
67591
  seen.add(pkgAbsolute);
@@ -67470,7 +67663,7 @@ async function discoverWorkspacePackages(repoRoot) {
67470
67663
  return results.sort();
67471
67664
  }
67472
67665
  async function generateForPackage(packageDir, config2, dryRun = false) {
67473
- const contextPath = join11(packageDir, "nax", "context.md");
67666
+ const contextPath = join11(packageDir, ".nax", "context.md");
67474
67667
  if (!existsSync10(contextPath)) {
67475
67668
  return [
67476
67669
  {
@@ -67587,6 +67780,13 @@ function validateStory(raw, index, allIds) {
67587
67780
  }
67588
67781
  const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
67589
67782
  const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
67783
+ const rawJustification = routing.noTestJustification ?? s.noTestJustification;
67784
+ if (testStrategy === "no-test") {
67785
+ if (!rawJustification || typeof rawJustification !== "string" || rawJustification.trim() === "") {
67786
+ throw new Error(`[schema] story[${index}].routing.noTestJustification is required when testStrategy is "no-test"`);
67787
+ }
67788
+ }
67789
+ const noTestJustification = typeof rawJustification === "string" && rawJustification.trim() !== "" ? rawJustification.trim() : undefined;
67590
67790
  const rawDeps = s.dependencies;
67591
67791
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
67592
67792
  for (const dep of dependencies) {
@@ -67626,7 +67826,8 @@ function validateStory(raw, index, allIds) {
67626
67826
  routing: {
67627
67827
  complexity,
67628
67828
  testStrategy,
67629
- reasoning: "validated from LLM output"
67829
+ reasoning: "validated from LLM output",
67830
+ ...noTestJustification !== undefined ? { noTestJustification } : {}
67630
67831
  },
67631
67832
  ...workdir !== undefined ? { workdir } : {},
67632
67833
  ...contextFiles.length > 0 ? { contextFiles } : {}
@@ -67693,9 +67894,9 @@ var _deps2 = {
67693
67894
  createInteractionBridge: () => createCliInteractionBridge()
67694
67895
  };
67695
67896
  async function planCommand(workdir, config2, options) {
67696
- const naxDir = join12(workdir, "nax");
67897
+ const naxDir = join12(workdir, ".nax");
67697
67898
  if (!existsSync11(naxDir)) {
67698
- throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
67899
+ throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
67699
67900
  }
67700
67901
  const logger = getLogger();
67701
67902
  logger?.info("plan", "Reading spec", { from: options.from });
@@ -68029,7 +68230,7 @@ async function acceptCommand(options) {
68029
68230
  logger.error("cli", "Invalid project directory", { error: err.message });
68030
68231
  throw new NaxError("Invalid project directory", "INVALID_DIRECTORY", { error: err.message });
68031
68232
  }
68032
- const featureDir = path.join(projectDir, "nax", "features", feature);
68233
+ const featureDir = path.join(projectDir, ".nax", "features", feature);
68033
68234
  const prdPath = path.join(featureDir, "prd.json");
68034
68235
  const prdFile = Bun.file(prdPath);
68035
68236
  if (!await prdFile.exists()) {
@@ -68165,29 +68366,29 @@ function resolveProject(options = {}) {
68165
68366
  let configPath;
68166
68367
  if (dir) {
68167
68368
  projectRoot = realpathSync3(resolve6(dir));
68168
- naxDir = join13(projectRoot, "nax");
68369
+ naxDir = join13(projectRoot, ".nax");
68169
68370
  if (!existsSync12(naxDir)) {
68170
68371
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
68171
68372
  Expected to find: ${naxDir}`, "NAX_DIR_NOT_FOUND", { projectRoot, naxDir });
68172
68373
  }
68173
68374
  configPath = join13(naxDir, "config.json");
68174
68375
  if (!existsSync12(configPath)) {
68175
- throw new NaxError(`nax directory found but config.json is missing: ${naxDir}
68376
+ throw new NaxError(`.nax directory found but config.json is missing: ${naxDir}
68176
68377
  Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
68177
68378
  }
68178
68379
  } else {
68179
68380
  const found = findProjectRoot(process.cwd());
68180
68381
  if (!found) {
68181
- const cwdNaxDir = join13(process.cwd(), "nax");
68382
+ const cwdNaxDir = join13(process.cwd(), ".nax");
68182
68383
  if (existsSync12(cwdNaxDir)) {
68183
68384
  const cwdConfigPath = join13(cwdNaxDir, "config.json");
68184
- throw new NaxError(`nax directory found but config.json is missing: ${cwdNaxDir}
68385
+ throw new NaxError(`.nax directory found but config.json is missing: ${cwdNaxDir}
68185
68386
  Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, configPath: cwdConfigPath });
68186
68387
  }
68187
68388
  throw new NaxError("No nax project found. Run this command from within a nax project directory, or use -d flag to specify the project path.", "PROJECT_NOT_FOUND", { cwd: process.cwd() });
68188
68389
  }
68189
68390
  projectRoot = found;
68190
- naxDir = join13(projectRoot, "nax");
68391
+ naxDir = join13(projectRoot, ".nax");
68191
68392
  configPath = join13(naxDir, "config.json");
68192
68393
  }
68193
68394
  let featureDir;
@@ -68220,7 +68421,7 @@ function findProjectRoot(startDir) {
68220
68421
  let current = resolve6(startDir);
68221
68422
  let depth = 0;
68222
68423
  while (depth < MAX_DIRECTORY_DEPTH) {
68223
- const naxDir = join13(current, "nax");
68424
+ const naxDir = join13(current, ".nax");
68224
68425
  const configPath = join13(naxDir, "config.json");
68225
68426
  if (existsSync12(configPath)) {
68226
68427
  return realpathSync3(current);
@@ -68259,7 +68460,7 @@ async function loadStatusFile(featureDir) {
68259
68460
  }
68260
68461
  }
68261
68462
  async function loadProjectStatusFile(projectDir) {
68262
- const statusPath = join15(projectDir, "nax", "status.json");
68463
+ const statusPath = join15(projectDir, ".nax", "status.json");
68263
68464
  if (!existsSync13(statusPath)) {
68264
68465
  return null;
68265
68466
  }
@@ -68325,7 +68526,7 @@ async function getFeatureSummary(featureName, featureDir) {
68325
68526
  return summary;
68326
68527
  }
68327
68528
  async function displayAllFeatures(projectDir) {
68328
- const featuresDir = join15(projectDir, "nax", "features");
68529
+ const featuresDir = join15(projectDir, ".nax", "features");
68329
68530
  if (!existsSync13(featuresDir)) {
68330
68531
  console.log(source_default.dim("No features found."));
68331
68532
  return;
@@ -68530,7 +68731,7 @@ async function parseRunLog(logPath) {
68530
68731
  async function runsListCommand(options) {
68531
68732
  const logger = getLogger();
68532
68733
  const { feature, workdir } = options;
68533
- const runsDir = join16(workdir, "nax", "features", feature, "runs");
68734
+ const runsDir = join16(workdir, ".nax", "features", feature, "runs");
68534
68735
  if (!existsSync14(runsDir)) {
68535
68736
  logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
68536
68737
  return;
@@ -68568,7 +68769,7 @@ async function runsListCommand(options) {
68568
68769
  async function runsShowCommand(options) {
68569
68770
  const logger = getLogger();
68570
68771
  const { runId, feature, workdir } = options;
68571
- const logPath = join16(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
68772
+ const logPath = join16(workdir, ".nax", "features", feature, "runs", `${runId}.jsonl`);
68572
68773
  if (!existsSync14(logPath)) {
68573
68774
  logger.error("cli", "Run not found", { runId, feature, logPath });
68574
68775
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
@@ -68731,9 +68932,9 @@ ${ctx.contextMarkdown}`;
68731
68932
  async function promptsCommand(options) {
68732
68933
  const logger = getLogger();
68733
68934
  const { feature, workdir, config: config2, storyId, outputDir } = options;
68734
- const naxDir = join29(workdir, "nax");
68935
+ const naxDir = join29(workdir, ".nax");
68735
68936
  if (!existsSync18(naxDir)) {
68736
- throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
68937
+ throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
68737
68938
  }
68738
68939
  const featureDir = join29(naxDir, "features", feature);
68739
68940
  const prdPath = join29(featureDir, "prd.json");
@@ -68845,13 +69046,13 @@ var TEMPLATE_HEADER = `<!--
68845
69046
  - Conventions (project coding standards)
68846
69047
 
68847
69048
  To activate overrides, add to your nax/config.json:
68848
- { "prompts": { "overrides": { "<role>": "nax/templates/<role>.md" } } }
69049
+ { "prompts": { "overrides": { "<role>": ".nax/templates/<role>.md" } } }
68849
69050
  -->
68850
69051
 
68851
69052
  `;
68852
69053
  async function promptsInitCommand(options) {
68853
69054
  const { workdir, force = false, autoWireConfig = true } = options;
68854
- const templatesDir = join30(workdir, "nax", "templates");
69055
+ const templatesDir = join30(workdir, ".nax", "templates");
68855
69056
  mkdirSync4(templatesDir, { recursive: true });
68856
69057
  const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync19(join30(templatesDir, f)));
68857
69058
  if (existingFiles.length > 0 && !force) {
@@ -68882,11 +69083,11 @@ async function autoWirePromptsConfig(workdir) {
68882
69083
  const exampleConfig = JSON.stringify({
68883
69084
  prompts: {
68884
69085
  overrides: {
68885
- "test-writer": "nax/templates/test-writer.md",
68886
- implementer: "nax/templates/implementer.md",
68887
- verifier: "nax/templates/verifier.md",
68888
- "single-session": "nax/templates/single-session.md",
68889
- "tdd-simple": "nax/templates/tdd-simple.md"
69086
+ "test-writer": ".nax/templates/test-writer.md",
69087
+ implementer: ".nax/templates/implementer.md",
69088
+ verifier: ".nax/templates/verifier.md",
69089
+ "single-session": ".nax/templates/single-session.md",
69090
+ "tdd-simple": ".nax/templates/tdd-simple.md"
68890
69091
  }
68891
69092
  }
68892
69093
  }, null, 2);
@@ -68904,11 +69105,11 @@ ${exampleConfig}`);
68904
69105
  return;
68905
69106
  }
68906
69107
  const overrides = {
68907
- "test-writer": "nax/templates/test-writer.md",
68908
- implementer: "nax/templates/implementer.md",
68909
- verifier: "nax/templates/verifier.md",
68910
- "single-session": "nax/templates/single-session.md",
68911
- "tdd-simple": "nax/templates/tdd-simple.md"
69108
+ "test-writer": ".nax/templates/test-writer.md",
69109
+ implementer: ".nax/templates/implementer.md",
69110
+ verifier: ".nax/templates/verifier.md",
69111
+ "single-session": ".nax/templates/single-session.md",
69112
+ "tdd-simple": ".nax/templates/tdd-simple.md"
68912
69113
  };
68913
69114
  if (!config2.prompts) {
68914
69115
  config2.prompts = {};
@@ -68980,7 +69181,7 @@ import * as os2 from "os";
68980
69181
  import * as path13 from "path";
68981
69182
  async function pluginsListCommand(config2, workdir, overrideGlobalPluginsDir) {
68982
69183
  const globalPluginsDir = overrideGlobalPluginsDir ?? path13.join(os2.homedir(), ".nax", "plugins");
68983
- const projectPluginsDir = path13.join(workdir, "nax", "plugins");
69184
+ const projectPluginsDir = path13.join(workdir, ".nax", "plugins");
68984
69185
  const configPlugins = config2.plugins || [];
68985
69186
  const registry2 = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
68986
69187
  const plugins = registry2.plugins;
@@ -68989,8 +69190,8 @@ async function pluginsListCommand(config2, workdir, overrideGlobalPluginsDir) {
68989
69190
  console.log(`
68990
69191
  To install plugins:`);
68991
69192
  console.log(" \u2022 Add to global directory: ~/.nax/plugins/");
68992
- console.log(" \u2022 Add to project directory: ./nax/plugins/");
68993
- console.log(" \u2022 Configure in nax/config.json");
69193
+ console.log(" \u2022 Add to project directory: ./.nax/plugins/");
69194
+ console.log(" \u2022 Configure in .nax/config.json");
68994
69195
  console.log(`
68995
69196
  See https://github.com/nax/nax#plugins for more details.`);
68996
69197
  return;
@@ -69241,7 +69442,7 @@ function isProcessAlive2(pid) {
69241
69442
  }
69242
69443
  }
69243
69444
  async function loadStatusFile2(workdir) {
69244
- const statusPath = join34(workdir, "nax", "status.json");
69445
+ const statusPath = join34(workdir, ".nax", "status.json");
69245
69446
  if (!existsSync21(statusPath))
69246
69447
  return null;
69247
69448
  try {
@@ -69288,7 +69489,7 @@ async function diagnoseCommand(options = {}) {
69288
69489
  const workdir = options.workdir ?? process.cwd();
69289
69490
  const naxSubdir = findProjectDir(workdir);
69290
69491
  let projectDir = naxSubdir ? join34(naxSubdir, "..") : null;
69291
- if (!projectDir && existsSync21(join34(workdir, "nax"))) {
69492
+ if (!projectDir && existsSync21(join34(workdir, ".nax"))) {
69292
69493
  projectDir = workdir;
69293
69494
  }
69294
69495
  if (!projectDir)
@@ -69299,7 +69500,7 @@ async function diagnoseCommand(options = {}) {
69299
69500
  if (status2) {
69300
69501
  feature = status2.run.feature;
69301
69502
  } else {
69302
- const featuresDir = join34(projectDir, "nax", "features");
69503
+ const featuresDir = join34(projectDir, ".nax", "features");
69303
69504
  if (!existsSync21(featuresDir))
69304
69505
  throw new Error("No features found in project");
69305
69506
  const features = readdirSync5(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -69309,7 +69510,7 @@ async function diagnoseCommand(options = {}) {
69309
69510
  logger.info("diagnose", "No feature specified, using first found", { feature });
69310
69511
  }
69311
69512
  }
69312
- const featureDir = join34(projectDir, "nax", "features", feature);
69513
+ const featureDir = join34(projectDir, ".nax", "features", feature);
69313
69514
  const prdPath = join34(featureDir, "prd.json");
69314
69515
  if (!existsSync21(prdPath))
69315
69516
  throw new Error(`Feature not found: ${feature}`);
@@ -69368,10 +69569,10 @@ async function generateCommand(options) {
69368
69569
  if (dryRun) {
69369
69570
  console.log(source_default.yellow("\u26A0 Dry run \u2014 no files will be written"));
69370
69571
  }
69371
- console.log(source_default.blue("\u2192 Discovering packages with nax/context.md..."));
69572
+ console.log(source_default.blue("\u2192 Discovering packages with .nax/packages/*/context.md..."));
69372
69573
  const packages = await discoverPackages(workdir);
69373
69574
  if (packages.length === 0) {
69374
- console.log(source_default.yellow(" No packages found (no */nax/context.md or */*/nax/context.md)"));
69575
+ console.log(source_default.yellow(" No packages found (no .nax/packages/*/context.md or .nax/packages/*/*/context.md)"));
69375
69576
  return;
69376
69577
  }
69377
69578
  console.log(source_default.blue(`\u2192 Generating agent files for ${packages.length} package(s)...`));
@@ -69416,12 +69617,12 @@ async function generateCommand(options) {
69416
69617
  process.exit(1);
69417
69618
  return;
69418
69619
  }
69419
- const contextPath = options.context ? join35(workdir, options.context) : join35(workdir, "nax/context.md");
69620
+ const contextPath = options.context ? join35(workdir, options.context) : join35(workdir, ".nax/context.md");
69420
69621
  const outputDir = options.output ? join35(workdir, options.output) : workdir;
69421
69622
  const autoInject = !options.noAutoInject;
69422
69623
  if (!existsSync22(contextPath)) {
69423
69624
  console.error(source_default.red(`\u2717 Context file not found: ${contextPath}`));
69424
- console.error(source_default.yellow(" Create nax/context.md first, or run `nax init` to scaffold it."));
69625
+ console.error(source_default.yellow(" Create .nax/context.md first, or run `nax init` to scaffold it."));
69425
69626
  process.exit(1);
69426
69627
  }
69427
69628
  if (options.agent && !VALID_AGENTS.includes(options.agent)) {
@@ -69487,7 +69688,7 @@ async function generateCommand(options) {
69487
69688
  const packages = await discoverPackages(workdir);
69488
69689
  if (packages.length > 0) {
69489
69690
  console.log(source_default.blue(`
69490
- \u2192 Discovered ${packages.length} package(s) with nax/context.md \u2014 generating agent files...`));
69691
+ \u2192 Discovered ${packages.length} package(s) with context.md \u2014 generating agent files...`));
69491
69692
  let pkgErrorCount = 0;
69492
69693
  for (const pkgDir of packages) {
69493
69694
  const pkgResults = await generateForPackage(pkgDir, config2, dryRun);
@@ -69630,6 +69831,7 @@ var FIELD_DESCRIPTIONS = {
69630
69831
  "acceptance.maxRetries": "Max retry loops for fix stories",
69631
69832
  "acceptance.generateTests": "Generate acceptance tests during analyze",
69632
69833
  "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
69834
+ "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
69633
69835
  context: "Context injection configuration",
69634
69836
  "context.fileInjection": "Mode: 'disabled' (default, MCP-aware agents pull context on-demand) | 'keyword' (legacy git-grep injection for non-MCP agents). Set context.fileInjection in config.",
69635
69837
  "context.testCoverage": "Test coverage context settings",
@@ -70262,7 +70464,7 @@ async function logsCommand(options) {
70262
70464
  return;
70263
70465
  }
70264
70466
  const resolved = resolveProject({ dir: options.dir });
70265
- const naxDir = join40(resolved.projectDir, "nax");
70467
+ const naxDir = join40(resolved.projectDir, ".nax");
70266
70468
  const configPath = resolved.configPath;
70267
70469
  const configFile = Bun.file(configPath);
70268
70470
  const config2 = await configFile.json();
@@ -70312,7 +70514,7 @@ async function precheckCommand(options) {
70312
70514
  process.exit(1);
70313
70515
  }
70314
70516
  }
70315
- const naxDir = join41(resolved.projectDir, "nax");
70517
+ const naxDir = join41(resolved.projectDir, ".nax");
70316
70518
  const featureDir = join41(naxDir, "features", featureName);
70317
70519
  const prdPath = join41(featureDir, "prd.json");
70318
70520
  if (!existsSync31(featureDir)) {
@@ -70628,7 +70830,14 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
70628
70830
  let currentBatch = [];
70629
70831
  for (const story of stories) {
70630
70832
  const isSimple = story.routing?.complexity === "simple" && story.routing?.testStrategy === "test-after";
70631
- if (isSimple && currentBatch.length < maxBatchSize) {
70833
+ const isNoTest = story.routing?.testStrategy === "no-test";
70834
+ const isBatchable = isSimple || isNoTest;
70835
+ if (isBatchable && currentBatch.length < maxBatchSize) {
70836
+ const batchIsNoTest = currentBatch.length > 0 && currentBatch[0]?.routing?.testStrategy === "no-test";
70837
+ if (currentBatch.length > 0 && batchIsNoTest !== isNoTest) {
70838
+ batches.push({ stories: [...currentBatch], isBatch: currentBatch.length > 1 });
70839
+ currentBatch = [];
70840
+ }
70632
70841
  currentBatch.push(story);
70633
70842
  } else {
70634
70843
  if (currentBatch.length > 0) {
@@ -70638,11 +70847,8 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
70638
70847
  });
70639
70848
  currentBatch = [];
70640
70849
  }
70641
- if (!isSimple) {
70642
- batches.push({
70643
- stories: [story],
70644
- isBatch: false
70645
- });
70850
+ if (!isBatchable) {
70851
+ batches.push({ stories: [story], isBatch: false });
70646
70852
  } else {
70647
70853
  currentBatch.push(story);
70648
70854
  }
@@ -78321,15 +78527,15 @@ Next: nax generate --package ${options.package}`));
78321
78527
  }
78322
78528
  return;
78323
78529
  }
78324
- const naxDir = join52(workdir, "nax");
78530
+ const naxDir = join53(workdir, "nax");
78325
78531
  if (existsSync34(naxDir) && !options.force) {
78326
78532
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
78327
78533
  return;
78328
78534
  }
78329
- mkdirSync6(join52(naxDir, "features"), { recursive: true });
78330
- mkdirSync6(join52(naxDir, "hooks"), { recursive: true });
78331
- await Bun.write(join52(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
78332
- await Bun.write(join52(naxDir, "hooks.json"), JSON.stringify({
78535
+ mkdirSync6(join53(naxDir, "features"), { recursive: true });
78536
+ mkdirSync6(join53(naxDir, "hooks"), { recursive: true });
78537
+ await Bun.write(join53(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
78538
+ await Bun.write(join53(naxDir, "hooks.json"), JSON.stringify({
78333
78539
  hooks: {
78334
78540
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
78335
78541
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -78337,12 +78543,12 @@ Next: nax generate --package ${options.package}`));
78337
78543
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
78338
78544
  }
78339
78545
  }, null, 2));
78340
- await Bun.write(join52(naxDir, ".gitignore"), `# nax temp files
78546
+ await Bun.write(join53(naxDir, ".gitignore"), `# nax temp files
78341
78547
  *.tmp
78342
78548
  .paused.json
78343
78549
  .nax-verifier-verdict.json
78344
78550
  `);
78345
- await Bun.write(join52(naxDir, "context.md"), `# Project Context
78551
+ await Bun.write(join53(naxDir, "context.md"), `# Project Context
78346
78552
 
78347
78553
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
78348
78554
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -78468,8 +78674,8 @@ program2.command("run").description("Run the orchestration loop for a feature").
78468
78674
  console.error(source_default.red("nax not initialized. Run: nax init"));
78469
78675
  process.exit(1);
78470
78676
  }
78471
- const featureDir = join52(naxDir, "features", options.feature);
78472
- const prdPath = join52(featureDir, "prd.json");
78677
+ const featureDir = join53(naxDir, "features", options.feature);
78678
+ const prdPath = join53(featureDir, "prd.json");
78473
78679
  if (options.plan && options.from) {
78474
78680
  if (existsSync34(prdPath) && !options.force) {
78475
78681
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
@@ -78491,10 +78697,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78491
78697
  }
78492
78698
  }
78493
78699
  try {
78494
- const planLogDir = join52(featureDir, "plan");
78700
+ const planLogDir = join53(featureDir, "plan");
78495
78701
  mkdirSync6(planLogDir, { recursive: true });
78496
78702
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78497
- const planLogPath = join52(planLogDir, `${planLogId}.jsonl`);
78703
+ const planLogPath = join53(planLogDir, `${planLogId}.jsonl`);
78498
78704
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78499
78705
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78500
78706
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -78532,10 +78738,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78532
78738
  process.exit(1);
78533
78739
  }
78534
78740
  resetLogger();
78535
- const runsDir = join52(featureDir, "runs");
78741
+ const runsDir = join53(featureDir, "runs");
78536
78742
  mkdirSync6(runsDir, { recursive: true });
78537
78743
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78538
- const logFilePath = join52(runsDir, `${runId}.jsonl`);
78744
+ const logFilePath = join53(runsDir, `${runId}.jsonl`);
78539
78745
  const isTTY = process.stdout.isTTY ?? false;
78540
78746
  const headlessFlag = options.headless ?? false;
78541
78747
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -78551,7 +78757,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78551
78757
  config2.autoMode.defaultAgent = options.agent;
78552
78758
  }
78553
78759
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
78554
- const globalNaxDir = join52(homedir10(), ".nax");
78760
+ const globalNaxDir = join53(homedir10(), ".nax");
78555
78761
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
78556
78762
  const eventEmitter = new PipelineEventEmitter;
78557
78763
  let tuiInstance;
@@ -78574,7 +78780,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78574
78780
  } else {
78575
78781
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
78576
78782
  }
78577
- const statusFilePath = join52(workdir, "nax", "status.json");
78783
+ const statusFilePath = join53(workdir, "nax", "status.json");
78578
78784
  let parallel;
78579
78785
  if (options.parallel !== undefined) {
78580
78786
  parallel = Number.parseInt(options.parallel, 10);
@@ -78600,7 +78806,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78600
78806
  headless: useHeadless,
78601
78807
  skipPrecheck: options.skipPrecheck ?? false
78602
78808
  });
78603
- const latestSymlink = join52(runsDir, "latest.jsonl");
78809
+ const latestSymlink = join53(runsDir, "latest.jsonl");
78604
78810
  try {
78605
78811
  if (existsSync34(latestSymlink)) {
78606
78812
  Bun.spawnSync(["rm", latestSymlink]);
@@ -78638,9 +78844,9 @@ features.command("create <name>").description("Create a new feature").option("-d
78638
78844
  console.error(source_default.red("nax not initialized. Run: nax init"));
78639
78845
  process.exit(1);
78640
78846
  }
78641
- const featureDir = join52(naxDir, "features", name);
78847
+ const featureDir = join53(naxDir, "features", name);
78642
78848
  mkdirSync6(featureDir, { recursive: true });
78643
- await Bun.write(join52(featureDir, "spec.md"), `# Feature: ${name}
78849
+ await Bun.write(join53(featureDir, "spec.md"), `# Feature: ${name}
78644
78850
 
78645
78851
  ## Overview
78646
78852
 
@@ -78648,7 +78854,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78648
78854
 
78649
78855
  ## Acceptance Criteria
78650
78856
  `);
78651
- await Bun.write(join52(featureDir, "plan.md"), `# Plan: ${name}
78857
+ await Bun.write(join53(featureDir, "plan.md"), `# Plan: ${name}
78652
78858
 
78653
78859
  ## Architecture
78654
78860
 
@@ -78656,7 +78862,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78656
78862
 
78657
78863
  ## Dependencies
78658
78864
  `);
78659
- await Bun.write(join52(featureDir, "tasks.md"), `# Tasks: ${name}
78865
+ await Bun.write(join53(featureDir, "tasks.md"), `# Tasks: ${name}
78660
78866
 
78661
78867
  ## US-001: [Title]
78662
78868
 
@@ -78665,7 +78871,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78665
78871
  ### Acceptance Criteria
78666
78872
  - [ ] Criterion 1
78667
78873
  `);
78668
- await Bun.write(join52(featureDir, "progress.txt"), `# Progress: ${name}
78874
+ await Bun.write(join53(featureDir, "progress.txt"), `# Progress: ${name}
78669
78875
 
78670
78876
  Created: ${new Date().toISOString()}
78671
78877
 
@@ -78693,7 +78899,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78693
78899
  console.error(source_default.red("nax not initialized."));
78694
78900
  process.exit(1);
78695
78901
  }
78696
- const featuresDir = join52(naxDir, "features");
78902
+ const featuresDir = join53(naxDir, "features");
78697
78903
  if (!existsSync34(featuresDir)) {
78698
78904
  console.log(source_default.dim("No features yet."));
78699
78905
  return;
@@ -78708,7 +78914,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78708
78914
  Features:
78709
78915
  `));
78710
78916
  for (const name of entries) {
78711
- const prdPath = join52(featuresDir, name, "prd.json");
78917
+ const prdPath = join53(featuresDir, name, "prd.json");
78712
78918
  if (existsSync34(prdPath)) {
78713
78919
  const prd = await loadPRD(prdPath);
78714
78920
  const c = countStories(prd);
@@ -78739,10 +78945,10 @@ Use: nax plan -f <feature> --from <spec>`));
78739
78945
  process.exit(1);
78740
78946
  }
78741
78947
  const config2 = await loadConfig(workdir);
78742
- const featureLogDir = join52(naxDir, "features", options.feature, "plan");
78948
+ const featureLogDir = join53(naxDir, "features", options.feature, "plan");
78743
78949
  mkdirSync6(featureLogDir, { recursive: true });
78744
78950
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78745
- const planLogPath = join52(featureLogDir, `${planLogId}.jsonl`);
78951
+ const planLogPath = join53(featureLogDir, `${planLogId}.jsonl`);
78746
78952
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78747
78953
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78748
78954
  try {
@@ -78779,7 +78985,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78779
78985
  console.error(source_default.red("nax not initialized. Run: nax init"));
78780
78986
  process.exit(1);
78781
78987
  }
78782
- const featureDir = join52(naxDir, "features", options.feature);
78988
+ const featureDir = join53(naxDir, "features", options.feature);
78783
78989
  if (!existsSync34(featureDir)) {
78784
78990
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
78785
78991
  process.exit(1);
@@ -78795,7 +79001,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78795
79001
  specPath: options.from,
78796
79002
  reclassify: options.reclassify
78797
79003
  });
78798
- const prdPath = join52(featureDir, "prd.json");
79004
+ const prdPath = join53(featureDir, "prd.json");
78799
79005
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
78800
79006
  const c = countStories(prd);
78801
79007
  console.log(source_default.green(`