@nathapp/nax 0.50.3 → 0.51.2

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 (353) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +177 -104
  3. package/dist/nax.js +417 -213
  4. package/package.json +1 -3
  5. package/bin/nax.ts +0 -1195
  6. package/src/acceptance/fix-generator.ts +0 -322
  7. package/src/acceptance/generator.ts +0 -415
  8. package/src/acceptance/index.ts +0 -42
  9. package/src/acceptance/refinement.ts +0 -224
  10. package/src/acceptance/templates/cli.ts +0 -47
  11. package/src/acceptance/templates/component.ts +0 -78
  12. package/src/acceptance/templates/e2e.ts +0 -43
  13. package/src/acceptance/templates/index.ts +0 -21
  14. package/src/acceptance/templates/snapshot.ts +0 -50
  15. package/src/acceptance/templates/unit.ts +0 -48
  16. package/src/acceptance/types.ts +0 -138
  17. package/src/agents/acp/adapter.ts +0 -888
  18. package/src/agents/acp/cost.ts +0 -9
  19. package/src/agents/acp/index.ts +0 -7
  20. package/src/agents/acp/interaction-bridge.ts +0 -126
  21. package/src/agents/acp/parser.ts +0 -119
  22. package/src/agents/acp/spawn-client.ts +0 -373
  23. package/src/agents/acp/types.ts +0 -22
  24. package/src/agents/aider/adapter.ts +0 -135
  25. package/src/agents/claude/adapter.ts +0 -258
  26. package/src/agents/claude/complete.ts +0 -80
  27. package/src/agents/claude/cost.ts +0 -16
  28. package/src/agents/claude/execution.ts +0 -215
  29. package/src/agents/claude/index.ts +0 -3
  30. package/src/agents/claude/interactive.ts +0 -77
  31. package/src/agents/claude/plan.ts +0 -179
  32. package/src/agents/codex/adapter.ts +0 -153
  33. package/src/agents/cost/calculate.ts +0 -154
  34. package/src/agents/cost/index.ts +0 -10
  35. package/src/agents/cost/parse.ts +0 -97
  36. package/src/agents/cost/pricing.ts +0 -59
  37. package/src/agents/cost/types.ts +0 -45
  38. package/src/agents/gemini/adapter.ts +0 -177
  39. package/src/agents/index.ts +0 -18
  40. package/src/agents/opencode/adapter.ts +0 -106
  41. package/src/agents/registry.ts +0 -136
  42. package/src/agents/shared/decompose.ts +0 -154
  43. package/src/agents/shared/model-resolution.ts +0 -43
  44. package/src/agents/shared/types-extended.ts +0 -164
  45. package/src/agents/shared/validation.ts +0 -69
  46. package/src/agents/shared/version-detection.ts +0 -109
  47. package/src/agents/types.ts +0 -205
  48. package/src/analyze/classifier.ts +0 -282
  49. package/src/analyze/index.ts +0 -16
  50. package/src/analyze/scanner.ts +0 -171
  51. package/src/analyze/types.ts +0 -51
  52. package/src/cli/accept.ts +0 -108
  53. package/src/cli/agents.ts +0 -87
  54. package/src/cli/analyze-parser.ts +0 -291
  55. package/src/cli/analyze.ts +0 -352
  56. package/src/cli/config-descriptions.ts +0 -219
  57. package/src/cli/config-diff.ts +0 -103
  58. package/src/cli/config-display.ts +0 -285
  59. package/src/cli/config-get.ts +0 -55
  60. package/src/cli/config.ts +0 -14
  61. package/src/cli/constitution.ts +0 -17
  62. package/src/cli/diagnose-analysis.ts +0 -159
  63. package/src/cli/diagnose-formatter.ts +0 -87
  64. package/src/cli/diagnose.ts +0 -203
  65. package/src/cli/generate.ts +0 -250
  66. package/src/cli/index.ts +0 -42
  67. package/src/cli/init-context.ts +0 -405
  68. package/src/cli/init-detect.ts +0 -303
  69. package/src/cli/init.ts +0 -296
  70. package/src/cli/interact.ts +0 -295
  71. package/src/cli/plan.ts +0 -509
  72. package/src/cli/plugins.ts +0 -122
  73. package/src/cli/prompts-export.ts +0 -58
  74. package/src/cli/prompts-init.ts +0 -200
  75. package/src/cli/prompts-main.ts +0 -183
  76. package/src/cli/prompts-shared.ts +0 -70
  77. package/src/cli/prompts-tdd.ts +0 -88
  78. package/src/cli/prompts.ts +0 -17
  79. package/src/cli/runs.ts +0 -174
  80. package/src/cli/status-cost.ts +0 -151
  81. package/src/cli/status-features.ts +0 -405
  82. package/src/cli/status.ts +0 -13
  83. package/src/commands/common.ts +0 -171
  84. package/src/commands/diagnose.ts +0 -17
  85. package/src/commands/index.ts +0 -9
  86. package/src/commands/logs-formatter.ts +0 -201
  87. package/src/commands/logs-reader.ts +0 -171
  88. package/src/commands/logs.ts +0 -103
  89. package/src/commands/precheck.ts +0 -86
  90. package/src/commands/runs.ts +0 -220
  91. package/src/commands/unlock.ts +0 -96
  92. package/src/config/defaults.ts +0 -218
  93. package/src/config/index.ts +0 -22
  94. package/src/config/loader.ts +0 -143
  95. package/src/config/merge.ts +0 -106
  96. package/src/config/merger.ts +0 -147
  97. package/src/config/path-security.ts +0 -121
  98. package/src/config/paths.ts +0 -27
  99. package/src/config/permissions.ts +0 -63
  100. package/src/config/runtime-types.ts +0 -522
  101. package/src/config/schema-types.ts +0 -53
  102. package/src/config/schema.ts +0 -60
  103. package/src/config/schemas.ts +0 -426
  104. package/src/config/test-strategy.ts +0 -71
  105. package/src/config/types.ts +0 -57
  106. package/src/config/validate.ts +0 -103
  107. package/src/constitution/generator.ts +0 -158
  108. package/src/constitution/generators/aider.ts +0 -41
  109. package/src/constitution/generators/claude.ts +0 -35
  110. package/src/constitution/generators/cursor.ts +0 -36
  111. package/src/constitution/generators/opencode.ts +0 -38
  112. package/src/constitution/generators/types.ts +0 -33
  113. package/src/constitution/generators/windsurf.ts +0 -36
  114. package/src/constitution/index.ts +0 -11
  115. package/src/constitution/loader.ts +0 -121
  116. package/src/constitution/types.ts +0 -31
  117. package/src/context/auto-detect.ts +0 -228
  118. package/src/context/builder.ts +0 -299
  119. package/src/context/elements.ts +0 -122
  120. package/src/context/formatter.ts +0 -107
  121. package/src/context/generator.ts +0 -343
  122. package/src/context/generators/aider.ts +0 -34
  123. package/src/context/generators/claude.ts +0 -28
  124. package/src/context/generators/codex.ts +0 -28
  125. package/src/context/generators/cursor.ts +0 -28
  126. package/src/context/generators/gemini.ts +0 -28
  127. package/src/context/generators/opencode.ts +0 -30
  128. package/src/context/generators/windsurf.ts +0 -28
  129. package/src/context/greenfield.ts +0 -114
  130. package/src/context/index.ts +0 -34
  131. package/src/context/injector.ts +0 -279
  132. package/src/context/parent-context.ts +0 -39
  133. package/src/context/test-scanner.ts +0 -370
  134. package/src/context/types.ts +0 -98
  135. package/src/decompose/apply.ts +0 -50
  136. package/src/decompose/builder.ts +0 -181
  137. package/src/decompose/index.ts +0 -8
  138. package/src/decompose/sections/codebase.ts +0 -26
  139. package/src/decompose/sections/constraints.ts +0 -32
  140. package/src/decompose/sections/index.ts +0 -4
  141. package/src/decompose/sections/sibling-stories.ts +0 -25
  142. package/src/decompose/sections/target-story.ts +0 -31
  143. package/src/decompose/types.ts +0 -55
  144. package/src/decompose/validators/complexity.ts +0 -45
  145. package/src/decompose/validators/coverage.ts +0 -134
  146. package/src/decompose/validators/dependency.ts +0 -91
  147. package/src/decompose/validators/index.ts +0 -35
  148. package/src/decompose/validators/overlap.ts +0 -128
  149. package/src/errors.ts +0 -67
  150. package/src/execution/batching.ts +0 -157
  151. package/src/execution/crash-heartbeat.ts +0 -77
  152. package/src/execution/crash-recovery.ts +0 -79
  153. package/src/execution/crash-signals.ts +0 -165
  154. package/src/execution/crash-writer.ts +0 -154
  155. package/src/execution/deferred-review.ts +0 -105
  156. package/src/execution/dry-run.ts +0 -81
  157. package/src/execution/escalation/escalation.ts +0 -46
  158. package/src/execution/escalation/index.ts +0 -13
  159. package/src/execution/escalation/tier-escalation.ts +0 -346
  160. package/src/execution/escalation/tier-outcome.ts +0 -143
  161. package/src/execution/executor-types.ts +0 -73
  162. package/src/execution/helpers.ts +0 -38
  163. package/src/execution/index.ts +0 -27
  164. package/src/execution/iteration-runner.ts +0 -160
  165. package/src/execution/lifecycle/acceptance-loop.ts +0 -309
  166. package/src/execution/lifecycle/headless-formatter.ts +0 -83
  167. package/src/execution/lifecycle/index.ts +0 -11
  168. package/src/execution/lifecycle/parallel-lifecycle.ts +0 -101
  169. package/src/execution/lifecycle/precheck-runner.ts +0 -140
  170. package/src/execution/lifecycle/run-cleanup.ts +0 -81
  171. package/src/execution/lifecycle/run-completion.ts +0 -247
  172. package/src/execution/lifecycle/run-initialization.ts +0 -187
  173. package/src/execution/lifecycle/run-regression.ts +0 -305
  174. package/src/execution/lifecycle/run-setup.ts +0 -240
  175. package/src/execution/lifecycle/story-size-prompts.ts +0 -123
  176. package/src/execution/lock.ts +0 -129
  177. package/src/execution/parallel-coordinator.ts +0 -281
  178. package/src/execution/parallel-executor-rectification-pass.ts +0 -117
  179. package/src/execution/parallel-executor-rectify.ts +0 -136
  180. package/src/execution/parallel-executor.ts +0 -330
  181. package/src/execution/parallel-worker.ts +0 -149
  182. package/src/execution/parallel.ts +0 -13
  183. package/src/execution/pid-registry.ts +0 -275
  184. package/src/execution/pipeline-result-handler.ts +0 -221
  185. package/src/execution/progress.ts +0 -27
  186. package/src/execution/queue-handler.ts +0 -109
  187. package/src/execution/runner-completion.ts +0 -171
  188. package/src/execution/runner-execution.ts +0 -243
  189. package/src/execution/runner-setup.ts +0 -86
  190. package/src/execution/runner.ts +0 -265
  191. package/src/execution/sequential-executor.ts +0 -219
  192. package/src/execution/status-file.ts +0 -264
  193. package/src/execution/status-writer.ts +0 -181
  194. package/src/execution/story-context.ts +0 -266
  195. package/src/execution/story-selector.ts +0 -76
  196. package/src/execution/test-output-parser.ts +0 -14
  197. package/src/execution/timeout-handler.ts +0 -100
  198. package/src/hooks/index.ts +0 -2
  199. package/src/hooks/runner.ts +0 -280
  200. package/src/hooks/types.ts +0 -79
  201. package/src/interaction/chain.ts +0 -170
  202. package/src/interaction/index.ts +0 -61
  203. package/src/interaction/init.ts +0 -84
  204. package/src/interaction/plugins/auto.ts +0 -243
  205. package/src/interaction/plugins/cli.ts +0 -300
  206. package/src/interaction/plugins/telegram.ts +0 -384
  207. package/src/interaction/plugins/webhook.ts +0 -286
  208. package/src/interaction/state.ts +0 -171
  209. package/src/interaction/triggers.ts +0 -250
  210. package/src/interaction/types.ts +0 -170
  211. package/src/logger/formatters.ts +0 -84
  212. package/src/logger/index.ts +0 -16
  213. package/src/logger/logger.ts +0 -296
  214. package/src/logger/types.ts +0 -48
  215. package/src/logging/formatter.ts +0 -355
  216. package/src/logging/index.ts +0 -22
  217. package/src/logging/types.ts +0 -93
  218. package/src/metrics/aggregator.ts +0 -191
  219. package/src/metrics/index.ts +0 -14
  220. package/src/metrics/tracker.ts +0 -200
  221. package/src/metrics/types.ts +0 -115
  222. package/src/optimizer/index.ts +0 -63
  223. package/src/optimizer/noop.optimizer.ts +0 -24
  224. package/src/optimizer/rule-based.optimizer.ts +0 -248
  225. package/src/optimizer/types.ts +0 -53
  226. package/src/pipeline/event-bus.ts +0 -297
  227. package/src/pipeline/events.ts +0 -130
  228. package/src/pipeline/index.ts +0 -19
  229. package/src/pipeline/runner.ts +0 -149
  230. package/src/pipeline/stages/acceptance-setup.ts +0 -144
  231. package/src/pipeline/stages/acceptance.ts +0 -215
  232. package/src/pipeline/stages/autofix.ts +0 -262
  233. package/src/pipeline/stages/completion.ts +0 -110
  234. package/src/pipeline/stages/constitution.ts +0 -63
  235. package/src/pipeline/stages/context.ts +0 -122
  236. package/src/pipeline/stages/execution.ts +0 -359
  237. package/src/pipeline/stages/index.ts +0 -86
  238. package/src/pipeline/stages/optimizer.ts +0 -74
  239. package/src/pipeline/stages/prompt.ts +0 -79
  240. package/src/pipeline/stages/queue-check.ts +0 -103
  241. package/src/pipeline/stages/rectify.ts +0 -101
  242. package/src/pipeline/stages/regression.ts +0 -99
  243. package/src/pipeline/stages/review.ts +0 -94
  244. package/src/pipeline/stages/routing.ts +0 -276
  245. package/src/pipeline/stages/verify.ts +0 -286
  246. package/src/pipeline/subscribers/events-writer.ts +0 -135
  247. package/src/pipeline/subscribers/hooks.ts +0 -179
  248. package/src/pipeline/subscribers/interaction.ts +0 -103
  249. package/src/pipeline/subscribers/registry.ts +0 -73
  250. package/src/pipeline/subscribers/reporters.ts +0 -174
  251. package/src/pipeline/types.ts +0 -220
  252. package/src/plugins/extensions.ts +0 -225
  253. package/src/plugins/index.ts +0 -33
  254. package/src/plugins/loader.ts +0 -352
  255. package/src/plugins/plugin-logger.ts +0 -41
  256. package/src/plugins/registry.ts +0 -168
  257. package/src/plugins/types.ts +0 -206
  258. package/src/plugins/validator.ts +0 -352
  259. package/src/prd/index.ts +0 -220
  260. package/src/prd/schema.ts +0 -268
  261. package/src/prd/types.ts +0 -273
  262. package/src/prd/validate.ts +0 -41
  263. package/src/precheck/checks-agents.ts +0 -63
  264. package/src/precheck/checks-blockers.ts +0 -23
  265. package/src/precheck/checks-cli.ts +0 -68
  266. package/src/precheck/checks-config.ts +0 -102
  267. package/src/precheck/checks-git.ts +0 -117
  268. package/src/precheck/checks-system.ts +0 -101
  269. package/src/precheck/checks-warnings.ts +0 -221
  270. package/src/precheck/checks.ts +0 -36
  271. package/src/precheck/index.ts +0 -374
  272. package/src/precheck/story-size-gate.ts +0 -144
  273. package/src/precheck/types.ts +0 -31
  274. package/src/prompts/builder.ts +0 -166
  275. package/src/prompts/index.ts +0 -2
  276. package/src/prompts/loader.ts +0 -43
  277. package/src/prompts/sections/conventions.ts +0 -19
  278. package/src/prompts/sections/hermetic.ts +0 -41
  279. package/src/prompts/sections/index.ts +0 -12
  280. package/src/prompts/sections/isolation.ts +0 -70
  281. package/src/prompts/sections/role-task.ts +0 -182
  282. package/src/prompts/sections/story.ts +0 -55
  283. package/src/prompts/sections/verdict.ts +0 -70
  284. package/src/prompts/types.ts +0 -21
  285. package/src/queue/index.ts +0 -2
  286. package/src/queue/manager.ts +0 -254
  287. package/src/queue/types.ts +0 -54
  288. package/src/review/index.ts +0 -8
  289. package/src/review/orchestrator.ts +0 -154
  290. package/src/review/runner.ts +0 -303
  291. package/src/review/types.ts +0 -70
  292. package/src/routing/batch-route.ts +0 -35
  293. package/src/routing/builder.ts +0 -81
  294. package/src/routing/chain.ts +0 -75
  295. package/src/routing/content-hash.ts +0 -25
  296. package/src/routing/index.ts +0 -20
  297. package/src/routing/loader.ts +0 -62
  298. package/src/routing/router.ts +0 -305
  299. package/src/routing/strategies/adaptive.ts +0 -215
  300. package/src/routing/strategies/index.ts +0 -8
  301. package/src/routing/strategies/keyword.ts +0 -180
  302. package/src/routing/strategies/llm-prompts.ts +0 -224
  303. package/src/routing/strategies/llm.ts +0 -320
  304. package/src/routing/strategies/manual.ts +0 -50
  305. package/src/routing/strategy.ts +0 -102
  306. package/src/tdd/cleanup.ts +0 -120
  307. package/src/tdd/index.ts +0 -22
  308. package/src/tdd/isolation.ts +0 -117
  309. package/src/tdd/orchestrator.ts +0 -406
  310. package/src/tdd/prompts.ts +0 -40
  311. package/src/tdd/rectification-gate.ts +0 -274
  312. package/src/tdd/session-runner.ts +0 -263
  313. package/src/tdd/types.ts +0 -84
  314. package/src/tdd/verdict-reader.ts +0 -266
  315. package/src/tdd/verdict.ts +0 -152
  316. package/src/tui/App.tsx +0 -265
  317. package/src/tui/components/AgentPanel.tsx +0 -75
  318. package/src/tui/components/CostOverlay.tsx +0 -118
  319. package/src/tui/components/HelpOverlay.tsx +0 -107
  320. package/src/tui/components/StatusBar.tsx +0 -63
  321. package/src/tui/components/StoriesPanel.tsx +0 -177
  322. package/src/tui/hooks/useKeyboard.ts +0 -142
  323. package/src/tui/hooks/useLayout.ts +0 -137
  324. package/src/tui/hooks/usePipelineEvents.ts +0 -183
  325. package/src/tui/hooks/usePty.ts +0 -189
  326. package/src/tui/index.tsx +0 -38
  327. package/src/tui/types.ts +0 -76
  328. package/src/utils/errors.ts +0 -12
  329. package/src/utils/git.ts +0 -245
  330. package/src/utils/json-file.ts +0 -72
  331. package/src/utils/log-test-output.ts +0 -25
  332. package/src/utils/path-security.ts +0 -73
  333. package/src/utils/queue-writer.ts +0 -54
  334. package/src/verification/crash-detector.ts +0 -34
  335. package/src/verification/executor.ts +0 -250
  336. package/src/verification/index.ts +0 -12
  337. package/src/verification/orchestrator-types.ts +0 -154
  338. package/src/verification/orchestrator.ts +0 -76
  339. package/src/verification/parser.ts +0 -220
  340. package/src/verification/rectification-loop.ts +0 -172
  341. package/src/verification/rectification.ts +0 -108
  342. package/src/verification/runners.ts +0 -129
  343. package/src/verification/smart-runner.ts +0 -307
  344. package/src/verification/strategies/acceptance.ts +0 -136
  345. package/src/verification/strategies/regression.ts +0 -90
  346. package/src/verification/strategies/scoped.ts +0 -154
  347. package/src/verification/types.ts +0 -117
  348. package/src/version.ts +0 -40
  349. package/src/worktree/dispatcher.ts +0 -6
  350. package/src/worktree/index.ts +0 -2
  351. package/src/worktree/manager.ts +0 -193
  352. package/src/worktree/merge.ts +0 -302
  353. 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",
@@ -3312,7 +3321,8 @@ Decompose this spec into user stories. For each story, provide:
3312
3321
  9. reasoning: Why this complexity level
3313
3322
  10. estimatedLOC: Estimated lines of code to change
3314
3323
  11. risks: Array of implementation risks
3315
- 12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
3324
+ 12. testStrategy: "no-test" | "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
3325
+ 13. noTestJustification: string (REQUIRED when testStrategy is "no-test" \u2014 explain why tests are unnecessary)
3316
3326
 
3317
3327
  ${COMPLEXITY_GUIDE}
3318
3328
 
@@ -17967,8 +17977,8 @@ var init_schemas3 = __esm(() => {
17967
17977
  storySizeGate: StorySizeGateConfigSchema
17968
17978
  });
17969
17979
  PromptsConfigSchema = exports_external.object({
17970
- overrides: exports_external.record(exports_external.string().refine((key) => ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
17971
- message: "Role must be one of: test-writer, implementer, verifier, single-session, tdd-simple"
17980
+ overrides: exports_external.record(exports_external.string().refine((key) => ["no-test", "test-writer", "implementer", "verifier", "single-session", "tdd-simple"].includes(key), {
17981
+ message: "Role must be one of: no-test, test-writer, implementer, verifier, single-session, tdd-simple"
17972
17982
  }), exports_external.string().min(1, "Override path must be non-empty")).optional()
17973
17983
  });
17974
17984
  DecomposeConfigSchema = exports_external.object({
@@ -18761,7 +18771,8 @@ Rules:
18761
18771
  - **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
18762
18772
  - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18763
18773
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18764
- - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration`;
18774
+ - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
18775
+ - **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')\`).`;
18765
18776
  const prompt = basePrompt;
18766
18777
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
18767
18778
  const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
@@ -18846,7 +18857,8 @@ Rules:
18846
18857
  - **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
18847
18858
  - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18848
18859
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18849
- - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration`;
18860
+ - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
18861
+ - **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')\`).`;
18850
18862
  }
18851
18863
  async function generateAcceptanceTests(adapter, options) {
18852
18864
  const logger = getLogger();
@@ -18961,7 +18973,34 @@ function findRelatedStories(failedAC, prd) {
18961
18973
  const passedStories = prd.userStories.filter((s) => s.status === "passed").map((s) => s.id);
18962
18974
  return passedStories.slice(0, 5);
18963
18975
  }
18964
- function buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd) {
18976
+ function groupACsByRelatedStories(failedACs, prd) {
18977
+ const groups = new Map;
18978
+ for (const ac of failedACs) {
18979
+ const related = findRelatedStories(ac, prd);
18980
+ const key = [...related].sort().join(",");
18981
+ if (!groups.has(key)) {
18982
+ groups.set(key, { acs: [], relatedStories: related });
18983
+ }
18984
+ groups.get(key)?.acs.push(ac);
18985
+ }
18986
+ const result = Array.from(groups.values());
18987
+ while (result.length > MAX_FIX_STORIES) {
18988
+ result.sort((a, b) => a.acs.length - b.acs.length);
18989
+ const smallest = result.shift();
18990
+ if (!smallest)
18991
+ break;
18992
+ result[0].acs.push(...smallest.acs);
18993
+ for (const s of smallest.relatedStories) {
18994
+ if (!result[0].relatedStories.includes(s)) {
18995
+ result[0].relatedStories.push(s);
18996
+ }
18997
+ }
18998
+ }
18999
+ return result;
19000
+ }
19001
+ function buildFixPrompt(batchedACs, acTextMap, testOutput, relatedStories, prd, testFilePath) {
19002
+ const acList = batchedACs.map((ac) => `${ac}: ${acTextMap[ac] || "No description available"}`).join(`
19003
+ `);
18965
19004
  const relatedStoriesText = relatedStories.map((id) => {
18966
19005
  const story = prd.userStories.find((s) => s.id === id);
18967
19006
  if (!story)
@@ -18971,43 +19010,47 @@ function buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd) {
18971
19010
  }).filter(Boolean).join(`
18972
19011
 
18973
19012
  `);
18974
- return `You are a debugging expert. A feature acceptance test has failed.
18975
-
18976
- FAILED ACCEPTANCE CRITERION:
18977
- ${failedAC}: ${acText}
19013
+ const testFileSection = testFilePath ? `
19014
+ ACCEPTANCE TEST FILE: ${testFilePath}
19015
+ (Read this file first to understand what each test expects)
19016
+ ` : "";
19017
+ return `You are a debugging expert. Feature acceptance tests have failed.${testFileSection}
19018
+ FAILED ACCEPTANCE CRITERIA (${batchedACs.length} total):
19019
+ ${acList}
18978
19020
 
18979
19021
  TEST FAILURE OUTPUT:
18980
- ${testOutput}
19022
+ ${testOutput.slice(0, 2000)}
18981
19023
 
18982
19024
  RELATED STORIES (implemented this functionality):
18983
19025
  ${relatedStoriesText}
18984
19026
 
18985
- Your task: Generate a fix story description that will make the acceptance test pass.
19027
+ Your task: Generate a fix description that will make these acceptance tests pass.
18986
19028
 
18987
19029
  Requirements:
18988
- 1. Analyze the test failure to understand the root cause
18989
- 2. Identify what needs to change in the code
18990
- 3. Write a clear, actionable fix description (2-4 sentences)
18991
- 4. Focus on the specific issue, not general improvements
19030
+ 1. Read the acceptance test file first to understand what each failing test expects
19031
+ 2. Identify the root cause based on the test failure output
19032
+ 3. Find and fix the relevant implementation code (do NOT modify the test file)
19033
+ 4. Write a clear, actionable fix description (2-4 sentences)
18992
19034
  5. Reference the relevant story IDs if needed
18993
19035
 
18994
19036
  Respond with ONLY the fix description (no JSON, no markdown, just the description text).`;
18995
19037
  }
18996
19038
  async function generateFixStories(adapter, options) {
18997
- const { failedACs, testOutput, prd, specContent, modelDef } = options;
18998
- const fixStories = [];
18999
- const acTextMap = parseACTextFromSpec(specContent);
19039
+ const { failedACs, testOutput, prd, specContent, modelDef, testFilePath } = options;
19000
19040
  const logger = getLogger();
19001
- for (let i = 0;i < failedACs.length; i++) {
19002
- const failedAC = failedACs[i];
19003
- const acText = acTextMap[failedAC] || "No description available";
19004
- logger.info("acceptance", "Generating fix for failed AC", { failedAC });
19005
- const relatedStories = findRelatedStories(failedAC, prd);
19041
+ const acTextMap = parseACTextFromSpec(specContent);
19042
+ const groups = groupACsByRelatedStories(failedACs, prd);
19043
+ const fixStories = [];
19044
+ for (let i = 0;i < groups.length; i++) {
19045
+ const { acs: batchedACs, relatedStories } = groups[i];
19006
19046
  if (relatedStories.length === 0) {
19007
- logger.warn("acceptance", "\u26A0 No related stories found for failed AC \u2014 skipping", { failedAC });
19047
+ logger.warn("acceptance", "[WARN] No related stories found for AC group \u2014 skipping", { batchedACs });
19008
19048
  continue;
19009
19049
  }
19010
- const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
19050
+ logger.info("acceptance", "Generating fix for AC group", { batchedACs });
19051
+ const prompt = buildFixPrompt(batchedACs, acTextMap, testOutput, relatedStories, prd, testFilePath);
19052
+ const relatedStory = prd.userStories.find((s) => relatedStories.includes(s.id) && s.workdir);
19053
+ const workdir = relatedStory?.workdir;
19011
19054
  try {
19012
19055
  const fixDescription = await adapter.complete(prompt, {
19013
19056
  model: modelDef.model,
@@ -19015,25 +19058,31 @@ async function generateFixStories(adapter, options) {
19015
19058
  });
19016
19059
  fixStories.push({
19017
19060
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
19018
- title: `Fix: ${failedAC} \u2014 ${acText.slice(0, 50)}`,
19019
- failedAC,
19061
+ title: `Fix: ${batchedACs.join(", ")} \u2014 ${(acTextMap[batchedACs[0]] || "").slice(0, 40)}`,
19062
+ failedAC: batchedACs[0],
19063
+ batchedACs,
19020
19064
  testOutput,
19021
19065
  relatedStories,
19022
- description: fixDescription
19066
+ description: fixDescription,
19067
+ testFilePath,
19068
+ workdir
19023
19069
  });
19024
- logger.info("acceptance", "\u2713 Generated fix story", { storyId: fixStories[fixStories.length - 1].id });
19070
+ logger.info("acceptance", "[OK] Generated fix story", { storyId: fixStories[fixStories.length - 1].id });
19025
19071
  } catch (error48) {
19026
- logger.warn("acceptance", "\u26A0 Error generating fix", {
19027
- failedAC,
19072
+ logger.warn("acceptance", "[WARN] Error generating fix", {
19073
+ batchedACs,
19028
19074
  error: error48.message
19029
19075
  });
19030
19076
  fixStories.push({
19031
19077
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
19032
- title: `Fix: ${failedAC}`,
19033
- failedAC,
19078
+ title: `Fix: ${batchedACs.join(", ")}`,
19079
+ failedAC: batchedACs[0],
19080
+ batchedACs,
19034
19081
  testOutput,
19035
19082
  relatedStories,
19036
- description: `Fix the implementation to make ${failedAC} pass. Related stories: ${relatedStories.join(", ")}.`
19083
+ description: `Fix the implementation to make ${batchedACs.join(", ")} pass. Related stories: ${relatedStories.join(", ")}.`,
19084
+ testFilePath,
19085
+ workdir
19037
19086
  });
19038
19087
  }
19039
19088
  }
@@ -19054,20 +19103,40 @@ function parseACTextFromSpec(specContent) {
19054
19103
  return map2;
19055
19104
  }
19056
19105
  function convertFixStoryToUserStory(fixStory) {
19106
+ const batchedACs = fixStory.batchedACs ?? [fixStory.failedAC];
19107
+ const acList = batchedACs.join(", ");
19108
+ const truncatedOutput = fixStory.testOutput.slice(0, 1000);
19109
+ const testFilePath = fixStory.testFilePath ?? "acceptance.test.ts";
19110
+ const enrichedDescription = [
19111
+ fixStory.description,
19112
+ "",
19113
+ `ACCEPTANCE TEST FILE: ${testFilePath}`,
19114
+ `FAILED ACCEPTANCE CRITERIA: ${acList}`,
19115
+ "",
19116
+ "TEST FAILURE OUTPUT:",
19117
+ truncatedOutput,
19118
+ "",
19119
+ "Instructions: Read the acceptance test file first to understand what each failing test expects.",
19120
+ "Then find the relevant source code and fix the implementation.",
19121
+ "Do NOT modify the test file."
19122
+ ].join(`
19123
+ `);
19057
19124
  return {
19058
19125
  id: fixStory.id,
19059
19126
  title: fixStory.title,
19060
- description: fixStory.description,
19061
- acceptanceCriteria: [`Fix ${fixStory.failedAC}`],
19127
+ description: enrichedDescription,
19128
+ acceptanceCriteria: batchedACs.map((ac) => `Fix ${ac}`),
19062
19129
  tags: ["fix", "acceptance-failure"],
19063
19130
  dependencies: fixStory.relatedStories,
19064
19131
  status: "pending",
19065
19132
  passes: false,
19066
19133
  escalations: [],
19067
19134
  attempts: 0,
19068
- contextFiles: []
19135
+ contextFiles: [],
19136
+ workdir: fixStory.workdir
19069
19137
  };
19070
19138
  }
19139
+ var MAX_FIX_STORIES = 8;
19071
19140
  var init_fix_generator = __esm(() => {
19072
19141
  init_logger2();
19073
19142
  });
@@ -19460,7 +19529,7 @@ async function closeAcpSession(session) {
19460
19529
  }
19461
19530
  }
19462
19531
  function acpSessionsPath(workdir, featureName) {
19463
- return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19532
+ return join3(workdir, ".nax", "features", featureName, "acp-sessions.json");
19464
19533
  }
19465
19534
  function sidecarSessionName(entry) {
19466
19535
  return typeof entry === "string" ? entry : entry.sessionName;
@@ -20971,6 +21040,7 @@ import { join as join6, resolve as resolve4 } from "path";
20971
21040
  function globalConfigDir() {
20972
21041
  return join6(homedir3(), ".nax");
20973
21042
  }
21043
+ var PROJECT_NAX_DIR = ".nax";
20974
21044
  var init_paths = () => {};
20975
21045
 
20976
21046
  // src/config/loader.ts
@@ -20983,7 +21053,7 @@ function findProjectDir(startDir = process.cwd()) {
20983
21053
  let dir = resolve5(startDir);
20984
21054
  let depth = 0;
20985
21055
  while (depth < MAX_DIRECTORY_DEPTH) {
20986
- const candidate = join7(dir, "nax");
21056
+ const candidate = join7(dir, PROJECT_NAX_DIR);
20987
21057
  if (existsSync5(join7(candidate, "config.json"))) {
20988
21058
  return candidate;
20989
21059
  }
@@ -21053,7 +21123,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir) {
21053
21123
  return rootConfig;
21054
21124
  }
21055
21125
  const repoRoot = dirname2(rootNaxDir);
21056
- const packageConfigPath = join7(repoRoot, packageDir, "nax", "config.json");
21126
+ const packageConfigPath = join7(repoRoot, PROJECT_NAX_DIR, "mono", packageDir, "config.json");
21057
21127
  const packageOverride = await loadJsonFile(packageConfigPath, "config");
21058
21128
  if (!packageOverride) {
21059
21129
  return rootConfig;
@@ -22329,7 +22399,7 @@ var package_default;
22329
22399
  var init_package = __esm(() => {
22330
22400
  package_default = {
22331
22401
  name: "@nathapp/nax",
22332
- version: "0.50.3",
22402
+ version: "0.51.2",
22333
22403
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22334
22404
  type: "module",
22335
22405
  bin: {
@@ -22380,8 +22450,6 @@ var init_package = __esm(() => {
22380
22450
  ],
22381
22451
  files: [
22382
22452
  "dist/",
22383
- "src/",
22384
- "bin/",
22385
22453
  "README.md",
22386
22454
  "CHANGELOG.md"
22387
22455
  ],
@@ -22403,8 +22471,8 @@ var init_version = __esm(() => {
22403
22471
  NAX_VERSION = package_default.version;
22404
22472
  NAX_COMMIT = (() => {
22405
22473
  try {
22406
- if (/^[0-9a-f]{6,10}$/.test("684b48b"))
22407
- return "684b48b";
22474
+ if (/^[0-9a-f]{6,10}$/.test("7f71f43"))
22475
+ return "7f71f43";
22408
22476
  } catch {}
22409
22477
  try {
22410
22478
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22553,14 +22621,14 @@ function collectBatchMetrics(ctx, storyStartTime) {
22553
22621
  });
22554
22622
  }
22555
22623
  async function saveRunMetrics(workdir, runMetrics) {
22556
- const metricsPath = path2.join(workdir, "nax", "metrics.json");
22624
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
22557
22625
  const existing = await loadJsonFile(metricsPath, "metrics");
22558
22626
  const allMetrics = Array.isArray(existing) ? existing : [];
22559
22627
  allMetrics.push(runMetrics);
22560
22628
  await saveJsonFile(metricsPath, allMetrics, "metrics");
22561
22629
  }
22562
22630
  async function loadRunMetrics(workdir) {
22563
- const metricsPath = path2.join(workdir, "nax", "metrics.json");
22631
+ const metricsPath = path2.join(workdir, ".nax", "metrics.json");
22564
22632
  const content = await loadJsonFile(metricsPath, "metrics");
22565
22633
  return Array.isArray(content) ? content : [];
22566
22634
  }
@@ -24254,10 +24322,18 @@ var init_agents = __esm(() => {
24254
24322
  // src/pipeline/stages/acceptance-setup.ts
24255
24323
  var exports_acceptance_setup = {};
24256
24324
  __export(exports_acceptance_setup, {
24325
+ computeACFingerprint: () => computeACFingerprint,
24257
24326
  acceptanceSetupStage: () => acceptanceSetupStage,
24258
24327
  _acceptanceSetupDeps: () => _acceptanceSetupDeps
24259
24328
  });
24260
24329
  import path5 from "path";
24330
+ function computeACFingerprint(criteria) {
24331
+ const sorted = [...criteria].sort().join(`
24332
+ `);
24333
+ const hasher = new Bun.CryptoHasher("sha256");
24334
+ hasher.update(sorted);
24335
+ return `sha256:${hasher.digest("hex")}`;
24336
+ }
24261
24337
  var _acceptanceSetupDeps, acceptanceSetupStage;
24262
24338
  var init_acceptance_setup = __esm(() => {
24263
24339
  init_config();
@@ -24269,6 +24345,27 @@ var init_acceptance_setup = __esm(() => {
24269
24345
  writeFile: async (filePath, content) => {
24270
24346
  await Bun.write(filePath, content);
24271
24347
  },
24348
+ copyFile: async (src, dest) => {
24349
+ const content = await Bun.file(src).text();
24350
+ await Bun.write(dest, content);
24351
+ },
24352
+ deleteFile: async (filePath) => {
24353
+ const { unlink } = await import("fs/promises");
24354
+ await unlink(filePath);
24355
+ },
24356
+ readMeta: async (metaPath) => {
24357
+ const f = Bun.file(metaPath);
24358
+ if (!await f.exists())
24359
+ return null;
24360
+ try {
24361
+ return JSON.parse(await f.text());
24362
+ } catch {
24363
+ return null;
24364
+ }
24365
+ },
24366
+ writeMeta: async (metaPath, meta3) => {
24367
+ await Bun.write(metaPath, JSON.stringify(meta3, null, 2));
24368
+ },
24272
24369
  runTest: async (_testPath, _workdir) => {
24273
24370
  const proc = Bun.spawn(["bun", "test", _testPath], {
24274
24371
  cwd: _workdir,
@@ -24302,11 +24399,22 @@ ${stderr}` };
24302
24399
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
24303
24400
  }
24304
24401
  const testPath = path5.join(ctx.featureDir, "acceptance.test.ts");
24305
- const fileExists = await _acceptanceSetupDeps.fileExists(testPath);
24402
+ const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
24403
+ const allCriteria = ctx.prd.userStories.flatMap((s) => s.acceptanceCriteria);
24306
24404
  let totalCriteria = 0;
24307
24405
  let testableCount = 0;
24308
- if (!fileExists) {
24309
- const allCriteria = ctx.prd.userStories.flatMap((s) => s.acceptanceCriteria);
24406
+ const fileExists = await _acceptanceSetupDeps.fileExists(testPath);
24407
+ let shouldGenerate = !fileExists;
24408
+ if (fileExists) {
24409
+ const fingerprint = computeACFingerprint(allCriteria);
24410
+ const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
24411
+ if (!meta3 || meta3.acFingerprint !== fingerprint) {
24412
+ await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
24413
+ await _acceptanceSetupDeps.deleteFile(testPath);
24414
+ shouldGenerate = true;
24415
+ }
24416
+ }
24417
+ if (shouldGenerate) {
24310
24418
  totalCriteria = allCriteria.length;
24311
24419
  const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
24312
24420
  const agent = (ctx.agentGetFn ?? getAgent2)(ctx.config.autoMode.defaultAgent);
@@ -24341,6 +24449,14 @@ ${stderr}` };
24341
24449
  adapter: agent ?? undefined
24342
24450
  });
24343
24451
  await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
24452
+ const fingerprint = computeACFingerprint(allCriteria);
24453
+ await _acceptanceSetupDeps.writeMeta(metaPath, {
24454
+ generatedAt: new Date().toISOString(),
24455
+ acFingerprint: fingerprint,
24456
+ storyCount: ctx.prd.userStories.length,
24457
+ acCount: totalCriteria,
24458
+ generator: "nax"
24459
+ });
24344
24460
  }
24345
24461
  if (ctx.config.acceptance.redGate === false) {
24346
24462
  ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
@@ -24444,7 +24560,7 @@ function hasScript(packageJson, scriptName) {
24444
24560
  return false;
24445
24561
  return scriptName in scripts;
24446
24562
  }
24447
- async function resolveCommand(check2, config2, executionConfig, workdir) {
24563
+ async function resolveCommand(check2, config2, executionConfig, workdir, qualityCommands) {
24448
24564
  if (executionConfig) {
24449
24565
  if (check2 === "lint" && executionConfig.lintCommand !== undefined) {
24450
24566
  return executionConfig.lintCommand;
@@ -24456,6 +24572,10 @@ async function resolveCommand(check2, config2, executionConfig, workdir) {
24456
24572
  if (config2.commands[check2]) {
24457
24573
  return config2.commands[check2] ?? null;
24458
24574
  }
24575
+ const qualityCmd = qualityCommands?.[check2];
24576
+ if (qualityCmd) {
24577
+ return qualityCmd;
24578
+ }
24459
24579
  const packageJson = await loadPackageJson(workdir);
24460
24580
  if (hasScript(packageJson, check2)) {
24461
24581
  return `bun run ${check2}`;
@@ -24553,7 +24673,7 @@ async function getUncommittedFilesImpl(workdir) {
24553
24673
  return [];
24554
24674
  }
24555
24675
  }
24556
- async function runReview(config2, workdir, executionConfig) {
24676
+ async function runReview(config2, workdir, executionConfig, qualityCommands) {
24557
24677
  const startTime = Date.now();
24558
24678
  const logger = getSafeLogger();
24559
24679
  const checks3 = [];
@@ -24591,7 +24711,7 @@ Stage and commit these files before running review.`
24591
24711
  };
24592
24712
  }
24593
24713
  for (const checkName of config2.checks) {
24594
- const command = await resolveCommand(checkName, config2, executionConfig, workdir);
24714
+ const command = await resolveCommand(checkName, config2, executionConfig, workdir, qualityCommands);
24595
24715
  if (command === null) {
24596
24716
  getSafeLogger()?.warn("review", `Skipping ${checkName} check (command not configured or disabled)`);
24597
24717
  continue;
@@ -24653,9 +24773,9 @@ async function getChangedFiles(workdir, baseRef) {
24653
24773
  }
24654
24774
 
24655
24775
  class ReviewOrchestrator {
24656
- async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix) {
24776
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands) {
24657
24777
  const logger = getSafeLogger();
24658
- const builtIn = await runReview(reviewConfig, workdir, executionConfig);
24778
+ const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands);
24659
24779
  if (!builtIn.success) {
24660
24780
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
24661
24781
  }
@@ -24745,7 +24865,7 @@ var init_review = __esm(() => {
24745
24865
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
24746
24866
  logger.info("review", "Running review phase", { storyId: ctx.story.id });
24747
24867
  const effectiveWorkdir = ctx.story.workdir ? join17(ctx.workdir, ctx.story.workdir) : ctx.workdir;
24748
- const result = await reviewOrchestrator.review(effectiveConfig.review, effectiveWorkdir, effectiveConfig.execution, ctx.plugins, ctx.storyGitRef, ctx.story.workdir);
24868
+ const result = await reviewOrchestrator.review(effectiveConfig.review, effectiveWorkdir, effectiveConfig.execution, ctx.plugins, ctx.storyGitRef, ctx.story.workdir, effectiveConfig.quality?.commands);
24749
24869
  ctx.reviewResult = result.builtIn;
24750
24870
  if (!result.success) {
24751
24871
  const allFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
@@ -25962,7 +26082,7 @@ function hookCtx(feature, opts) {
25962
26082
  };
25963
26083
  }
25964
26084
  async function loadPackageContextMd(packageWorkdir) {
25965
- const contextPath = `${packageWorkdir}/nax/context.md`;
26085
+ const contextPath = `${packageWorkdir}/.nax/context.md`;
25966
26086
  const file2 = Bun.file(contextPath);
25967
26087
  if (!await file2.exists())
25968
26088
  return null;
@@ -27144,6 +27264,11 @@ function buildIsolationSection(roleOrMode, mode, testCommand) {
27144
27264
  const footer = `
27145
27265
 
27146
27266
  ${buildTestFilterRule(testCmd)}`;
27267
+ if (role === "no-test") {
27268
+ return `${header}
27269
+
27270
+ isolation scope: Implement changes in src/ and other non-test directories. Do NOT create or modify any files in the test/ directory.${footer}`;
27271
+ }
27147
27272
  if (role === "test-writer") {
27148
27273
  const m = mode ?? "strict";
27149
27274
  if (m === "strict") {
@@ -27191,13 +27316,26 @@ function buildTestFrameworkHint(testCommand) {
27191
27316
  return "Use Jest (describe/test/expect)";
27192
27317
  return "Use your project's test framework";
27193
27318
  }
27194
- function buildRoleTaskSection(roleOrVariant, variant, testCommand, isolation) {
27319
+ function buildRoleTaskSection(roleOrVariant, variant, testCommand, isolation, noTestJustification) {
27195
27320
  if ((roleOrVariant === "standard" || roleOrVariant === "lite") && variant === undefined) {
27196
27321
  return buildRoleTaskSection("implementer", roleOrVariant, testCommand, isolation);
27197
27322
  }
27198
27323
  const role = roleOrVariant;
27199
27324
  const testCmd = testCommand ?? DEFAULT_TEST_CMD2;
27200
27325
  const frameworkHint = buildTestFrameworkHint(testCmd);
27326
+ if (role === "no-test") {
27327
+ const justification = noTestJustification ?? "No behavioral changes \u2014 tests not required";
27328
+ return `# Role: Implementer (No Tests)
27329
+
27330
+ Your task: implement the change as described. This story has no behavioral changes and does not require test modifications.
27331
+
27332
+ Instructions:
27333
+ - Implement the change as described in the story
27334
+ - Do NOT create or modify test files
27335
+ - Justification for no tests: ${justification}
27336
+ - When done, stage and commit ALL changed files with: git commit -m 'feat: <description>'
27337
+ - Goal: change implemented, no test files created or modified, all changes committed`;
27338
+ }
27201
27339
  if (role === "implementer") {
27202
27340
  const v = variant ?? "standard";
27203
27341
  if (v === "standard") {
@@ -27472,6 +27610,7 @@ class PromptBuilder {
27472
27610
  _loaderConfig;
27473
27611
  _testCommand;
27474
27612
  _hermeticConfig;
27613
+ _noTestJustification;
27475
27614
  constructor(role, options = {}) {
27476
27615
  this._role = role;
27477
27616
  this._options = options;
@@ -27515,6 +27654,10 @@ class PromptBuilder {
27515
27654
  this._hermeticConfig = config2;
27516
27655
  return this;
27517
27656
  }
27657
+ noTestJustification(justification) {
27658
+ this._noTestJustification = justification;
27659
+ return this;
27660
+ }
27518
27661
  async build() {
27519
27662
  const sections = [];
27520
27663
  if (this._constitution) {
@@ -27574,7 +27717,7 @@ ${this._contextMd}
27574
27717
  }
27575
27718
  const variant = this._options.variant;
27576
27719
  const isolation = this._options.isolation;
27577
- return buildRoleTaskSection(this._role, variant, this._testCommand, isolation);
27720
+ return buildRoleTaskSection(this._role, variant, this._testCommand, isolation, this._noTestJustification);
27578
27721
  }
27579
27722
  }
27580
27723
  var SECTION_SEP2 = `
@@ -28777,8 +28920,8 @@ var init_prompt = __esm(() => {
28777
28920
  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);
28778
28921
  prompt = await builder.build();
28779
28922
  } else {
28780
- const role = "tdd-simple";
28781
- 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);
28923
+ const role = ctx.routing.testStrategy === "no-test" ? "no-test" : "tdd-simple";
28924
+ 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);
28782
28925
  prompt = await builder.build();
28783
28926
  }
28784
28927
  ctx.prompt = prompt;
@@ -30411,8 +30554,7 @@ function generatePackageContextTemplate(packagePath) {
30411
30554
  }
30412
30555
  async function initPackage(repoRoot, packagePath, force = false) {
30413
30556
  const logger = getLogger();
30414
- const packageDir = join31(repoRoot, packagePath);
30415
- const naxDir = join31(packageDir, "nax");
30557
+ const naxDir = join31(repoRoot, ".nax", "mono", packagePath);
30416
30558
  const contextPath = join31(naxDir, "context.md");
30417
30559
  if (existsSync20(contextPath) && !force) {
30418
30560
  logger.info("init", "Package context.md already exists (use --force to overwrite)", { path: contextPath });
@@ -30427,7 +30569,7 @@ async function initPackage(repoRoot, packagePath, force = false) {
30427
30569
  }
30428
30570
  async function initContext(projectRoot, options = {}) {
30429
30571
  const logger = getLogger();
30430
- const naxDir = join31(projectRoot, "nax");
30572
+ const naxDir = join31(projectRoot, ".nax");
30431
30573
  const contextPath = join31(naxDir, "context.md");
30432
30574
  if (existsSync20(contextPath) && !options.force) {
30433
30575
  logger.info("init", "context.md already exists, skipping (use --force to overwrite)", { path: contextPath });
@@ -30444,7 +30586,7 @@ async function initContext(projectRoot, options = {}) {
30444
30586
  content = generateContextTemplate(scan);
30445
30587
  }
30446
30588
  await Bun.write(contextPath, content);
30447
- logger.info("init", "Generated nax/context.md template from project scan", { path: contextPath });
30589
+ logger.info("init", "Generated .nax/context.md template from project scan", { path: contextPath });
30448
30590
  }
30449
30591
  var _deps6;
30450
30592
  var init_init_context = __esm(() => {
@@ -31015,18 +31157,18 @@ var NAX_RUNTIME_PATTERNS;
31015
31157
  var init_checks_git = __esm(() => {
31016
31158
  NAX_RUNTIME_PATTERNS = [
31017
31159
  /^.{2} nax\.lock$/,
31018
- /^.{2} nax\/$/,
31019
- /^.{2} nax\/metrics\.json$/,
31020
- /^.{2} nax\/features\/$/,
31021
- /^.{2} nax\/features\/[^/]+\/$/,
31022
- /^.{2} nax\/features\/[^/]+\/status\.json$/,
31023
- /^.{2} nax\/features\/[^/]+\/prd\.json$/,
31024
- /^.{2} nax\/features\/[^/]+\/runs\//,
31025
- /^.{2} nax\/features\/[^/]+\/plan\//,
31026
- /^.{2} nax\/features\/[^/]+\/acp-sessions\.json$/,
31027
- /^.{2} nax\/features\/[^/]+\/interactions\//,
31028
- /^.{2} nax\/features\/[^/]+\/progress\.txt$/,
31029
- /^.{2} nax\/features\/[^/]+\/acceptance-refined\.json$/,
31160
+ /^.{2} \.nax\/$/,
31161
+ /^.{2} \.nax\/metrics\.json$/,
31162
+ /^.{2} \.nax\/features\/$/,
31163
+ /^.{2} \.nax\/features\/[^/]+\/$/,
31164
+ /^.{2} \.nax\/features\/[^/]+\/status\.json$/,
31165
+ /^.{2} \.nax\/features\/[^/]+\/prd\.json$/,
31166
+ /^.{2} \.nax\/features\/[^/]+\/runs\//,
31167
+ /^.{2} \.nax\/features\/[^/]+\/plan\//,
31168
+ /^.{2} \.nax\/features\/[^/]+\/acp-sessions\.json$/,
31169
+ /^.{2} \.nax\/features\/[^/]+\/interactions\//,
31170
+ /^.{2} \.nax\/features\/[^/]+\/progress\.txt$/,
31171
+ /^.{2} \.nax\/features\/[^/]+\/acceptance-refined\.json$/,
31030
31172
  /^.{2} \.nax-verifier-verdict\.json$/,
31031
31173
  /^.{2} \.nax-pids$/,
31032
31174
  /^.{2} \.nax-wt\//
@@ -31337,9 +31479,9 @@ async function checkGitignoreCoversNax(workdir) {
31337
31479
  const content = await file2.text();
31338
31480
  const patterns = [
31339
31481
  "nax.lock",
31340
- "nax/**/runs/",
31341
- "nax/metrics.json",
31342
- "nax/features/*/status.json",
31482
+ ".nax/**/runs/",
31483
+ ".nax/metrics.json",
31484
+ ".nax/features/*/status.json",
31343
31485
  ".nax-pids",
31344
31486
  ".nax-wt/"
31345
31487
  ];
@@ -32188,12 +32330,20 @@ var init_crash_recovery = __esm(() => {
32188
32330
  var exports_acceptance_loop = {};
32189
32331
  __export(exports_acceptance_loop, {
32190
32332
  runAcceptanceLoop: () => runAcceptanceLoop,
32333
+ isTestLevelFailure: () => isTestLevelFailure,
32191
32334
  isStubTestFile: () => isStubTestFile
32192
32335
  });
32193
- import path14 from "path";
32336
+ import path14, { join as join45 } from "path";
32194
32337
  function isStubTestFile(content) {
32195
32338
  return /expect\s*\(\s*true\s*\)\s*\.\s*toBe\s*\(\s*(?:false|true)\s*\)/.test(content);
32196
32339
  }
32340
+ function isTestLevelFailure(failedACs, totalACs) {
32341
+ if (failedACs.includes("AC-ERROR"))
32342
+ return true;
32343
+ if (totalACs === 0)
32344
+ return false;
32345
+ return failedACs.length / totalACs > 0.8;
32346
+ }
32197
32347
  async function loadSpecContent(featureDir) {
32198
32348
  if (!featureDir)
32199
32349
  return "";
@@ -32213,6 +32363,7 @@ async function generateAndAddFixStories(ctx, failures, prd) {
32213
32363
  return null;
32214
32364
  }
32215
32365
  const modelDef = resolveModel(ctx.config.models[ctx.config.analyze.model]);
32366
+ const testFilePath = ctx.featureDir ? path14.join(ctx.featureDir, "acceptance.test.ts") : undefined;
32216
32367
  const fixStories = await generateFixStories(agent, {
32217
32368
  failedACs: failures.failedACs,
32218
32369
  testOutput: failures.testOutput,
@@ -32220,7 +32371,8 @@ async function generateAndAddFixStories(ctx, failures, prd) {
32220
32371
  specContent: await loadSpecContent(ctx.featureDir),
32221
32372
  workdir: ctx.workdir,
32222
32373
  modelDef,
32223
- config: ctx.config
32374
+ config: ctx.config,
32375
+ testFilePath
32224
32376
  });
32225
32377
  if (fixStories.length === 0) {
32226
32378
  logger?.error("acceptance", "Failed to generate fix stories");
@@ -32244,9 +32396,10 @@ async function executeFixStory(ctx, story, prd, iterations) {
32244
32396
  agent: ctx.config.autoMode.defaultAgent,
32245
32397
  iteration: iterations
32246
32398
  }), ctx.workdir);
32399
+ const fixEffectiveConfig = story.workdir ? await loadConfigForWorkdir(join45(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
32247
32400
  const fixContext = {
32248
32401
  config: ctx.config,
32249
- effectiveConfig: ctx.config,
32402
+ effectiveConfig: fixEffectiveConfig,
32250
32403
  prd,
32251
32404
  story,
32252
32405
  stories: [story],
@@ -32266,6 +32419,23 @@ async function executeFixStory(ctx, story, prd, iterations) {
32266
32419
  metrics: result.context.storyMetrics
32267
32420
  };
32268
32421
  }
32422
+ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
32423
+ const logger = getSafeLogger();
32424
+ const bakPath = `${testPath}.bak`;
32425
+ const content = await Bun.file(testPath).text();
32426
+ await Bun.write(bakPath, content);
32427
+ logger?.info("acceptance", `Backed up acceptance test -> ${bakPath}`);
32428
+ const { unlink: unlink3 } = await import("fs/promises");
32429
+ await unlink3(testPath);
32430
+ const { acceptanceSetupStage: acceptanceSetupStage2 } = await Promise.resolve().then(() => (init_acceptance_setup(), exports_acceptance_setup));
32431
+ await acceptanceSetupStage2.execute(acceptanceContext);
32432
+ if (!await Bun.file(testPath).exists()) {
32433
+ logger?.error("acceptance", "Acceptance test regeneration failed \u2014 manual intervention required");
32434
+ return false;
32435
+ }
32436
+ logger?.info("acceptance", "Acceptance test regenerated successfully");
32437
+ return true;
32438
+ }
32269
32439
  async function runAcceptanceLoop(ctx) {
32270
32440
  const logger = getSafeLogger();
32271
32441
  const maxRetries = ctx.config.acceptance.maxRetries;
@@ -32343,9 +32513,23 @@ async function runAcceptanceLoop(ctx) {
32343
32513
  logger?.error("acceptance", "Acceptance test generation failed after retry \u2014 manual implementation required");
32344
32514
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32345
32515
  }
32516
+ continue;
32346
32517
  }
32347
32518
  }
32348
32519
  }
32520
+ const totalACs = prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria).length;
32521
+ if (ctx.featureDir && isTestLevelFailure(failures.failedACs, totalACs)) {
32522
+ logger?.warn("acceptance", `Test-level failure detected (${failures.failedACs.length}/${totalACs} ACs failed) \u2014 regenerating acceptance test`);
32523
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
32524
+ const testFile = Bun.file(testPath);
32525
+ if (await testFile.exists()) {
32526
+ const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
32527
+ if (!regenerated) {
32528
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
32529
+ }
32530
+ continue;
32531
+ }
32532
+ }
32349
32533
  logger?.info("acceptance", "Generating fix stories...");
32350
32534
  const fixStories = await generateAndAddFixStories(ctx, failures, prd);
32351
32535
  if (!fixStories) {
@@ -32376,6 +32560,7 @@ async function runAcceptanceLoop(ctx) {
32376
32560
  }
32377
32561
  var init_acceptance_loop = __esm(() => {
32378
32562
  init_acceptance();
32563
+ init_loader2();
32379
32564
  init_schema();
32380
32565
  init_hooks();
32381
32566
  init_logger2();
@@ -32810,12 +32995,12 @@ __export(exports_manager, {
32810
32995
  WorktreeManager: () => WorktreeManager
32811
32996
  });
32812
32997
  import { existsSync as existsSync32, symlinkSync } from "fs";
32813
- import { join as join45 } from "path";
32998
+ import { join as join46 } from "path";
32814
32999
 
32815
33000
  class WorktreeManager {
32816
33001
  async create(projectRoot, storyId) {
32817
33002
  validateStoryId(storyId);
32818
- const worktreePath = join45(projectRoot, ".nax-wt", storyId);
33003
+ const worktreePath = join46(projectRoot, ".nax-wt", storyId);
32819
33004
  const branchName = `nax/${storyId}`;
32820
33005
  try {
32821
33006
  const proc = Bun.spawn(["git", "worktree", "add", worktreePath, "-b", branchName], {
@@ -32840,9 +33025,9 @@ class WorktreeManager {
32840
33025
  }
32841
33026
  throw new Error(`Failed to create worktree: ${String(error48)}`);
32842
33027
  }
32843
- const nodeModulesSource = join45(projectRoot, "node_modules");
33028
+ const nodeModulesSource = join46(projectRoot, "node_modules");
32844
33029
  if (existsSync32(nodeModulesSource)) {
32845
- const nodeModulesTarget = join45(worktreePath, "node_modules");
33030
+ const nodeModulesTarget = join46(worktreePath, "node_modules");
32846
33031
  try {
32847
33032
  symlinkSync(nodeModulesSource, nodeModulesTarget, "dir");
32848
33033
  } catch (error48) {
@@ -32850,9 +33035,9 @@ class WorktreeManager {
32850
33035
  throw new Error(`Failed to symlink node_modules: ${errorMessage(error48)}`);
32851
33036
  }
32852
33037
  }
32853
- const envSource = join45(projectRoot, ".env");
33038
+ const envSource = join46(projectRoot, ".env");
32854
33039
  if (existsSync32(envSource)) {
32855
- const envTarget = join45(worktreePath, ".env");
33040
+ const envTarget = join46(worktreePath, ".env");
32856
33041
  try {
32857
33042
  symlinkSync(envSource, envTarget, "file");
32858
33043
  } catch (error48) {
@@ -32863,7 +33048,7 @@ class WorktreeManager {
32863
33048
  }
32864
33049
  async remove(projectRoot, storyId) {
32865
33050
  validateStoryId(storyId);
32866
- const worktreePath = join45(projectRoot, ".nax-wt", storyId);
33051
+ const worktreePath = join46(projectRoot, ".nax-wt", storyId);
32867
33052
  const branchName = `nax/${storyId}`;
32868
33053
  try {
32869
33054
  const proc = Bun.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -33254,7 +33439,7 @@ var init_parallel_worker = __esm(() => {
33254
33439
 
33255
33440
  // src/execution/parallel-coordinator.ts
33256
33441
  import os3 from "os";
33257
- import { join as join46 } from "path";
33442
+ import { join as join47 } from "path";
33258
33443
  function groupStoriesByDependencies(stories) {
33259
33444
  const batches = [];
33260
33445
  const processed = new Set;
@@ -33333,7 +33518,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33333
33518
  };
33334
33519
  const worktreePaths = new Map;
33335
33520
  for (const story of batch) {
33336
- const worktreePath = join46(projectRoot, ".nax-wt", story.id);
33521
+ const worktreePath = join47(projectRoot, ".nax-wt", story.id);
33337
33522
  try {
33338
33523
  await worktreeManager.create(projectRoot, story.id);
33339
33524
  worktreePaths.set(story.id, worktreePath);
@@ -33382,7 +33567,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33382
33567
  });
33383
33568
  logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
33384
33569
  storyId: mergeResult.storyId,
33385
- worktreePath: join46(projectRoot, ".nax-wt", mergeResult.storyId)
33570
+ worktreePath: join47(projectRoot, ".nax-wt", mergeResult.storyId)
33386
33571
  });
33387
33572
  }
33388
33573
  }
@@ -33842,12 +34027,12 @@ var init_parallel_executor = __esm(() => {
33842
34027
  // src/pipeline/subscribers/events-writer.ts
33843
34028
  import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
33844
34029
  import { homedir as homedir7 } from "os";
33845
- import { basename as basename5, join as join47 } from "path";
34030
+ import { basename as basename5, join as join48 } from "path";
33846
34031
  function wireEventsWriter(bus, feature, runId, workdir) {
33847
34032
  const logger = getSafeLogger();
33848
34033
  const project = basename5(workdir);
33849
- const eventsDir = join47(homedir7(), ".nax", "events", project);
33850
- const eventsFile = join47(eventsDir, "events.jsonl");
34034
+ const eventsDir = join48(homedir7(), ".nax", "events", project);
34035
+ const eventsFile = join48(eventsDir, "events.jsonl");
33851
34036
  let dirReady = false;
33852
34037
  const write = (line) => {
33853
34038
  (async () => {
@@ -34021,12 +34206,12 @@ var init_interaction2 = __esm(() => {
34021
34206
  // src/pipeline/subscribers/registry.ts
34022
34207
  import { mkdir as mkdir3, writeFile } from "fs/promises";
34023
34208
  import { homedir as homedir8 } from "os";
34024
- import { basename as basename6, join as join48 } from "path";
34209
+ import { basename as basename6, join as join49 } from "path";
34025
34210
  function wireRegistry(bus, feature, runId, workdir) {
34026
34211
  const logger = getSafeLogger();
34027
34212
  const project = basename6(workdir);
34028
- const runDir = join48(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
34029
- const metaFile = join48(runDir, "meta.json");
34213
+ const runDir = join49(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
34214
+ const metaFile = join49(runDir, "meta.json");
34030
34215
  const unsub = bus.on("run:started", (_ev) => {
34031
34216
  (async () => {
34032
34217
  try {
@@ -34036,8 +34221,8 @@ function wireRegistry(bus, feature, runId, workdir) {
34036
34221
  project,
34037
34222
  feature,
34038
34223
  workdir,
34039
- statusPath: join48(workdir, "nax", "features", feature, "status.json"),
34040
- eventsDir: join48(workdir, "nax", "features", feature, "runs"),
34224
+ statusPath: join49(workdir, ".nax", "features", feature, "status.json"),
34225
+ eventsDir: join49(workdir, ".nax", "features", feature, "runs"),
34041
34226
  registeredAt: new Date().toISOString()
34042
34227
  };
34043
34228
  await writeFile(metaFile, JSON.stringify(meta3, null, 2));
@@ -34464,7 +34649,7 @@ async function handleTierEscalation(ctx) {
34464
34649
  }
34465
34650
  for (const s of storiesToEscalate) {
34466
34651
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
34467
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after";
34652
+ const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
34468
34653
  if (shouldSwitchToTestAfter) {
34469
34654
  logger?.warn("escalation", "Switching strategy to test-after (greenfield-no-tests fallback)", {
34470
34655
  storyId: s.id,
@@ -34488,7 +34673,7 @@ async function handleTierEscalation(ctx) {
34488
34673
  if (!shouldEscalate)
34489
34674
  return s;
34490
34675
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
34491
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after";
34676
+ const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
34492
34677
  const baseRouting = s.routing ?? { ...ctx.routing };
34493
34678
  const updatedRouting = {
34494
34679
  ...baseRouting,
@@ -34690,7 +34875,7 @@ var init_pipeline_result_handler = __esm(() => {
34690
34875
  });
34691
34876
 
34692
34877
  // src/execution/iteration-runner.ts
34693
- import { join as join49 } from "path";
34878
+ import { join as join50 } from "path";
34694
34879
  async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
34695
34880
  const logger = getSafeLogger();
34696
34881
  const { story, storiesToExecute, routing, isBatchExecution } = selection;
@@ -34716,7 +34901,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
34716
34901
  const storyStartTime = Date.now();
34717
34902
  const storyGitRef = await captureGitRef(ctx.workdir);
34718
34903
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
34719
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join49(ctx.workdir, "nax", "config.json"), story.workdir) : ctx.config;
34904
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join50(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
34720
34905
  const pipelineContext = {
34721
34906
  config: ctx.config,
34722
34907
  effectiveConfig,
@@ -35082,7 +35267,7 @@ async function writeStatusFile(filePath, status) {
35082
35267
  var init_status_file = () => {};
35083
35268
 
35084
35269
  // src/execution/status-writer.ts
35085
- import { join as join50 } from "path";
35270
+ import { join as join51 } from "path";
35086
35271
 
35087
35272
  class StatusWriter {
35088
35273
  statusFile;
@@ -35150,7 +35335,7 @@ class StatusWriter {
35150
35335
  if (!this._prd)
35151
35336
  return;
35152
35337
  const safeLogger = getSafeLogger();
35153
- const featureStatusPath = join50(featureDir, "status.json");
35338
+ const featureStatusPath = join51(featureDir, "status.json");
35154
35339
  try {
35155
35340
  const base = this.getSnapshot(totalCost, iterations);
35156
35341
  if (!base) {
@@ -35358,7 +35543,7 @@ __export(exports_run_initialization, {
35358
35543
  initializeRun: () => initializeRun,
35359
35544
  _reconcileDeps: () => _reconcileDeps
35360
35545
  });
35361
- import { join as join51 } from "path";
35546
+ import { join as join52 } from "path";
35362
35547
  async function reconcileState(prd, prdPath, workdir, config2) {
35363
35548
  const logger = getSafeLogger();
35364
35549
  let reconciledCount = 0;
@@ -35370,7 +35555,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
35370
35555
  if (!hasCommits)
35371
35556
  continue;
35372
35557
  if (story.failureStage === "review" || story.failureStage === "autofix") {
35373
- const effectiveWorkdir = story.workdir ? join51(workdir, story.workdir) : workdir;
35558
+ const effectiveWorkdir = story.workdir ? join52(workdir, story.workdir) : workdir;
35374
35559
  try {
35375
35560
  const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
35376
35561
  if (!reviewResult.success) {
@@ -35551,7 +35736,7 @@ async function setupRun(options) {
35551
35736
  }
35552
35737
  try {
35553
35738
  const globalPluginsDir = path18.join(os5.homedir(), ".nax", "plugins");
35554
- const projectPluginsDir = path18.join(workdir, "nax", "plugins");
35739
+ const projectPluginsDir = path18.join(workdir, ".nax", "plugins");
35555
35740
  const configPlugins = config2.plugins || [];
35556
35741
  const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
35557
35742
  logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
@@ -66504,7 +66689,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
66504
66689
  init_source();
66505
66690
  import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
66506
66691
  import { homedir as homedir10 } from "os";
66507
- import { join as join52 } from "path";
66692
+ import { join as join53 } from "path";
66508
66693
 
66509
66694
  // node_modules/commander/esm.mjs
66510
66695
  var import__ = __toESM(require_commander(), 1);
@@ -67200,7 +67385,7 @@ function formatMetadataSection(metadata) {
67200
67385
  // src/context/generators/aider.ts
67201
67386
  function generateAiderConfig(context) {
67202
67387
  const header = `# Aider Configuration
67203
- # Auto-generated from nax/context.md \u2014 run \`nax generate\` to regenerate.
67388
+ # Auto-generated from .nax/context.md \u2014 run \`nax generate\` to regenerate.
67204
67389
  # DO NOT EDIT MANUALLY
67205
67390
 
67206
67391
  # Project instructions
@@ -67224,7 +67409,7 @@ var aiderGenerator = {
67224
67409
  function generateClaudeConfig(context) {
67225
67410
  const header = `# Project Context
67226
67411
 
67227
- This file is auto-generated from \`nax/context.md\`.
67412
+ This file is auto-generated from \`.nax/context.md\`.
67228
67413
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67229
67414
 
67230
67415
  ---
@@ -67243,7 +67428,7 @@ var claudeGenerator = {
67243
67428
  function generateCodexConfig(context) {
67244
67429
  const header = `# Codex Instructions
67245
67430
 
67246
- This file is auto-generated from \`nax/context.md\`.
67431
+ This file is auto-generated from \`.nax/context.md\`.
67247
67432
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67248
67433
 
67249
67434
  ---
@@ -67262,7 +67447,7 @@ var codexGenerator = {
67262
67447
  function generateCursorRules(context) {
67263
67448
  const header = `# Project Rules
67264
67449
 
67265
- Auto-generated from nax/context.md \u2014 run \`nax generate\` to regenerate.
67450
+ Auto-generated from .nax/context.md \u2014 run \`nax generate\` to regenerate.
67266
67451
  DO NOT EDIT MANUALLY
67267
67452
 
67268
67453
  ---
@@ -67281,7 +67466,7 @@ var cursorGenerator = {
67281
67466
  function generateGeminiConfig(context) {
67282
67467
  const header = `# Gemini CLI Context
67283
67468
 
67284
- This file is auto-generated from \`nax/context.md\`.
67469
+ This file is auto-generated from \`.nax/context.md\`.
67285
67470
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67286
67471
 
67287
67472
  ---
@@ -67300,7 +67485,7 @@ var geminiGenerator = {
67300
67485
  function generateOpencodeConfig(context) {
67301
67486
  const header = `# Agent Instructions
67302
67487
 
67303
- This file is auto-generated from \`nax/context.md\`.
67488
+ This file is auto-generated from \`.nax/context.md\`.
67304
67489
  DO NOT EDIT MANUALLY \u2014 run \`nax generate\` to regenerate.
67305
67490
 
67306
67491
  These instructions apply to all AI coding agents in this project.
@@ -67321,7 +67506,7 @@ var opencodeGenerator = {
67321
67506
  function generateWindsurfRules(context) {
67322
67507
  const header = `# Windsurf Project Rules
67323
67508
 
67324
- Auto-generated from nax/context.md \u2014 run \`nax generate\` to regenerate.
67509
+ Auto-generated from .nax/context.md \u2014 run \`nax generate\` to regenerate.
67325
67510
  DO NOT EDIT MANUALLY
67326
67511
 
67327
67512
  ---
@@ -67398,10 +67583,10 @@ async function generateAll(options, config2, agentFilter) {
67398
67583
  async function discoverPackages(repoRoot) {
67399
67584
  const packages = [];
67400
67585
  const seen = new Set;
67401
- for (const pattern of ["*/nax/context.md", "*/*/nax/context.md"]) {
67586
+ for (const pattern of [".nax/mono/*/context.md", ".nax/mono/*/*/context.md"]) {
67402
67587
  const glob = new Bun.Glob(pattern);
67403
- for await (const match of glob.scan(repoRoot)) {
67404
- const pkgRelative = match.replace(/\/nax\/context\.md$/, "");
67588
+ for await (const match of glob.scan({ cwd: repoRoot, dot: true })) {
67589
+ const pkgRelative = match.replace(/^\.nax\/mono\//, "").replace(/\/context\.md$/, "");
67405
67590
  const pkgAbsolute = join11(repoRoot, pkgRelative);
67406
67591
  if (!seen.has(pkgAbsolute)) {
67407
67592
  seen.add(pkgAbsolute);
@@ -67479,7 +67664,7 @@ async function discoverWorkspacePackages(repoRoot) {
67479
67664
  return results.sort();
67480
67665
  }
67481
67666
  async function generateForPackage(packageDir, config2, dryRun = false) {
67482
- const contextPath = join11(packageDir, "nax", "context.md");
67667
+ const contextPath = join11(packageDir, ".nax", "context.md");
67483
67668
  if (!existsSync10(contextPath)) {
67484
67669
  return [
67485
67670
  {
@@ -67596,6 +67781,13 @@ function validateStory(raw, index, allIds) {
67596
67781
  }
67597
67782
  const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
67598
67783
  const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
67784
+ const rawJustification = routing.noTestJustification ?? s.noTestJustification;
67785
+ if (testStrategy === "no-test") {
67786
+ if (!rawJustification || typeof rawJustification !== "string" || rawJustification.trim() === "") {
67787
+ throw new Error(`[schema] story[${index}].routing.noTestJustification is required when testStrategy is "no-test"`);
67788
+ }
67789
+ }
67790
+ const noTestJustification = typeof rawJustification === "string" && rawJustification.trim() !== "" ? rawJustification.trim() : undefined;
67599
67791
  const rawDeps = s.dependencies;
67600
67792
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
67601
67793
  for (const dep of dependencies) {
@@ -67635,7 +67827,8 @@ function validateStory(raw, index, allIds) {
67635
67827
  routing: {
67636
67828
  complexity,
67637
67829
  testStrategy,
67638
- reasoning: "validated from LLM output"
67830
+ reasoning: "validated from LLM output",
67831
+ ...noTestJustification !== undefined ? { noTestJustification } : {}
67639
67832
  },
67640
67833
  ...workdir !== undefined ? { workdir } : {},
67641
67834
  ...contextFiles.length > 0 ? { contextFiles } : {}
@@ -67702,9 +67895,9 @@ var _deps2 = {
67702
67895
  createInteractionBridge: () => createCliInteractionBridge()
67703
67896
  };
67704
67897
  async function planCommand(workdir, config2, options) {
67705
- const naxDir = join12(workdir, "nax");
67898
+ const naxDir = join12(workdir, ".nax");
67706
67899
  if (!existsSync11(naxDir)) {
67707
- throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
67900
+ throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
67708
67901
  }
67709
67902
  const logger = getLogger();
67710
67903
  logger?.info("plan", "Reading spec", { from: options.from });
@@ -68000,7 +68193,8 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
68000
68193
  "passes": false,
68001
68194
  "routing": {
68002
68195
  "complexity": "simple | medium | complex | expert",
68003
- "testStrategy": "tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
68196
+ "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
68197
+ "noTestJustification": "string \u2014 REQUIRED when testStrategy is no-test, explains why tests are unnecessary",
68004
68198
  "reasoning": "string \u2014 brief classification rationale"
68005
68199
  },
68006
68200
  "escalations": [],
@@ -68038,7 +68232,7 @@ async function acceptCommand(options) {
68038
68232
  logger.error("cli", "Invalid project directory", { error: err.message });
68039
68233
  throw new NaxError("Invalid project directory", "INVALID_DIRECTORY", { error: err.message });
68040
68234
  }
68041
- const featureDir = path.join(projectDir, "nax", "features", feature);
68235
+ const featureDir = path.join(projectDir, ".nax", "features", feature);
68042
68236
  const prdPath = path.join(featureDir, "prd.json");
68043
68237
  const prdFile = Bun.file(prdPath);
68044
68238
  if (!await prdFile.exists()) {
@@ -68174,29 +68368,29 @@ function resolveProject(options = {}) {
68174
68368
  let configPath;
68175
68369
  if (dir) {
68176
68370
  projectRoot = realpathSync3(resolve6(dir));
68177
- naxDir = join13(projectRoot, "nax");
68371
+ naxDir = join13(projectRoot, ".nax");
68178
68372
  if (!existsSync12(naxDir)) {
68179
68373
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
68180
68374
  Expected to find: ${naxDir}`, "NAX_DIR_NOT_FOUND", { projectRoot, naxDir });
68181
68375
  }
68182
68376
  configPath = join13(naxDir, "config.json");
68183
68377
  if (!existsSync12(configPath)) {
68184
- throw new NaxError(`nax directory found but config.json is missing: ${naxDir}
68378
+ throw new NaxError(`.nax directory found but config.json is missing: ${naxDir}
68185
68379
  Expected to find: ${configPath}`, "CONFIG_NOT_FOUND", { naxDir, configPath });
68186
68380
  }
68187
68381
  } else {
68188
68382
  const found = findProjectRoot(process.cwd());
68189
68383
  if (!found) {
68190
- const cwdNaxDir = join13(process.cwd(), "nax");
68384
+ const cwdNaxDir = join13(process.cwd(), ".nax");
68191
68385
  if (existsSync12(cwdNaxDir)) {
68192
68386
  const cwdConfigPath = join13(cwdNaxDir, "config.json");
68193
- throw new NaxError(`nax directory found but config.json is missing: ${cwdNaxDir}
68387
+ throw new NaxError(`.nax directory found but config.json is missing: ${cwdNaxDir}
68194
68388
  Expected to find: ${cwdConfigPath}`, "CONFIG_NOT_FOUND", { naxDir: cwdNaxDir, configPath: cwdConfigPath });
68195
68389
  }
68196
68390
  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() });
68197
68391
  }
68198
68392
  projectRoot = found;
68199
- naxDir = join13(projectRoot, "nax");
68393
+ naxDir = join13(projectRoot, ".nax");
68200
68394
  configPath = join13(naxDir, "config.json");
68201
68395
  }
68202
68396
  let featureDir;
@@ -68229,7 +68423,7 @@ function findProjectRoot(startDir) {
68229
68423
  let current = resolve6(startDir);
68230
68424
  let depth = 0;
68231
68425
  while (depth < MAX_DIRECTORY_DEPTH) {
68232
- const naxDir = join13(current, "nax");
68426
+ const naxDir = join13(current, ".nax");
68233
68427
  const configPath = join13(naxDir, "config.json");
68234
68428
  if (existsSync12(configPath)) {
68235
68429
  return realpathSync3(current);
@@ -68268,7 +68462,7 @@ async function loadStatusFile(featureDir) {
68268
68462
  }
68269
68463
  }
68270
68464
  async function loadProjectStatusFile(projectDir) {
68271
- const statusPath = join15(projectDir, "nax", "status.json");
68465
+ const statusPath = join15(projectDir, ".nax", "status.json");
68272
68466
  if (!existsSync13(statusPath)) {
68273
68467
  return null;
68274
68468
  }
@@ -68334,7 +68528,7 @@ async function getFeatureSummary(featureName, featureDir) {
68334
68528
  return summary;
68335
68529
  }
68336
68530
  async function displayAllFeatures(projectDir) {
68337
- const featuresDir = join15(projectDir, "nax", "features");
68531
+ const featuresDir = join15(projectDir, ".nax", "features");
68338
68532
  if (!existsSync13(featuresDir)) {
68339
68533
  console.log(source_default.dim("No features found."));
68340
68534
  return;
@@ -68539,7 +68733,7 @@ async function parseRunLog(logPath) {
68539
68733
  async function runsListCommand(options) {
68540
68734
  const logger = getLogger();
68541
68735
  const { feature, workdir } = options;
68542
- const runsDir = join16(workdir, "nax", "features", feature, "runs");
68736
+ const runsDir = join16(workdir, ".nax", "features", feature, "runs");
68543
68737
  if (!existsSync14(runsDir)) {
68544
68738
  logger.info("cli", "No runs found for feature", { feature, hint: `Directory not found: ${runsDir}` });
68545
68739
  return;
@@ -68577,7 +68771,7 @@ async function runsListCommand(options) {
68577
68771
  async function runsShowCommand(options) {
68578
68772
  const logger = getLogger();
68579
68773
  const { runId, feature, workdir } = options;
68580
- const logPath = join16(workdir, "nax", "features", feature, "runs", `${runId}.jsonl`);
68774
+ const logPath = join16(workdir, ".nax", "features", feature, "runs", `${runId}.jsonl`);
68581
68775
  if (!existsSync14(logPath)) {
68582
68776
  logger.error("cli", "Run not found", { runId, feature, logPath });
68583
68777
  throw new NaxError("Run not found", "RUN_NOT_FOUND", { runId, feature, logPath });
@@ -68740,9 +68934,9 @@ ${ctx.contextMarkdown}`;
68740
68934
  async function promptsCommand(options) {
68741
68935
  const logger = getLogger();
68742
68936
  const { feature, workdir, config: config2, storyId, outputDir } = options;
68743
- const naxDir = join29(workdir, "nax");
68937
+ const naxDir = join29(workdir, ".nax");
68744
68938
  if (!existsSync18(naxDir)) {
68745
- throw new Error(`nax directory not found. Run 'nax init' first in ${workdir}`);
68939
+ throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
68746
68940
  }
68747
68941
  const featureDir = join29(naxDir, "features", feature);
68748
68942
  const prdPath = join29(featureDir, "prd.json");
@@ -68854,13 +69048,13 @@ var TEMPLATE_HEADER = `<!--
68854
69048
  - Conventions (project coding standards)
68855
69049
 
68856
69050
  To activate overrides, add to your nax/config.json:
68857
- { "prompts": { "overrides": { "<role>": "nax/templates/<role>.md" } } }
69051
+ { "prompts": { "overrides": { "<role>": ".nax/templates/<role>.md" } } }
68858
69052
  -->
68859
69053
 
68860
69054
  `;
68861
69055
  async function promptsInitCommand(options) {
68862
69056
  const { workdir, force = false, autoWireConfig = true } = options;
68863
- const templatesDir = join30(workdir, "nax", "templates");
69057
+ const templatesDir = join30(workdir, ".nax", "templates");
68864
69058
  mkdirSync4(templatesDir, { recursive: true });
68865
69059
  const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync19(join30(templatesDir, f)));
68866
69060
  if (existingFiles.length > 0 && !force) {
@@ -68891,11 +69085,11 @@ async function autoWirePromptsConfig(workdir) {
68891
69085
  const exampleConfig = JSON.stringify({
68892
69086
  prompts: {
68893
69087
  overrides: {
68894
- "test-writer": "nax/templates/test-writer.md",
68895
- implementer: "nax/templates/implementer.md",
68896
- verifier: "nax/templates/verifier.md",
68897
- "single-session": "nax/templates/single-session.md",
68898
- "tdd-simple": "nax/templates/tdd-simple.md"
69088
+ "test-writer": ".nax/templates/test-writer.md",
69089
+ implementer: ".nax/templates/implementer.md",
69090
+ verifier: ".nax/templates/verifier.md",
69091
+ "single-session": ".nax/templates/single-session.md",
69092
+ "tdd-simple": ".nax/templates/tdd-simple.md"
68899
69093
  }
68900
69094
  }
68901
69095
  }, null, 2);
@@ -68913,11 +69107,11 @@ ${exampleConfig}`);
68913
69107
  return;
68914
69108
  }
68915
69109
  const overrides = {
68916
- "test-writer": "nax/templates/test-writer.md",
68917
- implementer: "nax/templates/implementer.md",
68918
- verifier: "nax/templates/verifier.md",
68919
- "single-session": "nax/templates/single-session.md",
68920
- "tdd-simple": "nax/templates/tdd-simple.md"
69110
+ "test-writer": ".nax/templates/test-writer.md",
69111
+ implementer: ".nax/templates/implementer.md",
69112
+ verifier: ".nax/templates/verifier.md",
69113
+ "single-session": ".nax/templates/single-session.md",
69114
+ "tdd-simple": ".nax/templates/tdd-simple.md"
68921
69115
  };
68922
69116
  if (!config2.prompts) {
68923
69117
  config2.prompts = {};
@@ -68989,7 +69183,7 @@ import * as os2 from "os";
68989
69183
  import * as path13 from "path";
68990
69184
  async function pluginsListCommand(config2, workdir, overrideGlobalPluginsDir) {
68991
69185
  const globalPluginsDir = overrideGlobalPluginsDir ?? path13.join(os2.homedir(), ".nax", "plugins");
68992
- const projectPluginsDir = path13.join(workdir, "nax", "plugins");
69186
+ const projectPluginsDir = path13.join(workdir, ".nax", "plugins");
68993
69187
  const configPlugins = config2.plugins || [];
68994
69188
  const registry2 = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins);
68995
69189
  const plugins = registry2.plugins;
@@ -68998,8 +69192,8 @@ async function pluginsListCommand(config2, workdir, overrideGlobalPluginsDir) {
68998
69192
  console.log(`
68999
69193
  To install plugins:`);
69000
69194
  console.log(" \u2022 Add to global directory: ~/.nax/plugins/");
69001
- console.log(" \u2022 Add to project directory: ./nax/plugins/");
69002
- console.log(" \u2022 Configure in nax/config.json");
69195
+ console.log(" \u2022 Add to project directory: ./.nax/plugins/");
69196
+ console.log(" \u2022 Configure in .nax/config.json");
69003
69197
  console.log(`
69004
69198
  See https://github.com/nax/nax#plugins for more details.`);
69005
69199
  return;
@@ -69250,7 +69444,7 @@ function isProcessAlive2(pid) {
69250
69444
  }
69251
69445
  }
69252
69446
  async function loadStatusFile2(workdir) {
69253
- const statusPath = join34(workdir, "nax", "status.json");
69447
+ const statusPath = join34(workdir, ".nax", "status.json");
69254
69448
  if (!existsSync21(statusPath))
69255
69449
  return null;
69256
69450
  try {
@@ -69297,7 +69491,7 @@ async function diagnoseCommand(options = {}) {
69297
69491
  const workdir = options.workdir ?? process.cwd();
69298
69492
  const naxSubdir = findProjectDir(workdir);
69299
69493
  let projectDir = naxSubdir ? join34(naxSubdir, "..") : null;
69300
- if (!projectDir && existsSync21(join34(workdir, "nax"))) {
69494
+ if (!projectDir && existsSync21(join34(workdir, ".nax"))) {
69301
69495
  projectDir = workdir;
69302
69496
  }
69303
69497
  if (!projectDir)
@@ -69308,7 +69502,7 @@ async function diagnoseCommand(options = {}) {
69308
69502
  if (status2) {
69309
69503
  feature = status2.run.feature;
69310
69504
  } else {
69311
- const featuresDir = join34(projectDir, "nax", "features");
69505
+ const featuresDir = join34(projectDir, ".nax", "features");
69312
69506
  if (!existsSync21(featuresDir))
69313
69507
  throw new Error("No features found in project");
69314
69508
  const features = readdirSync5(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -69318,7 +69512,7 @@ async function diagnoseCommand(options = {}) {
69318
69512
  logger.info("diagnose", "No feature specified, using first found", { feature });
69319
69513
  }
69320
69514
  }
69321
- const featureDir = join34(projectDir, "nax", "features", feature);
69515
+ const featureDir = join34(projectDir, ".nax", "features", feature);
69322
69516
  const prdPath = join34(featureDir, "prd.json");
69323
69517
  if (!existsSync21(prdPath))
69324
69518
  throw new Error(`Feature not found: ${feature}`);
@@ -69377,10 +69571,10 @@ async function generateCommand(options) {
69377
69571
  if (dryRun) {
69378
69572
  console.log(source_default.yellow("\u26A0 Dry run \u2014 no files will be written"));
69379
69573
  }
69380
- console.log(source_default.blue("\u2192 Discovering packages with nax/context.md..."));
69574
+ console.log(source_default.blue("\u2192 Discovering packages with .nax/mono/*/context.md..."));
69381
69575
  const packages = await discoverPackages(workdir);
69382
69576
  if (packages.length === 0) {
69383
- console.log(source_default.yellow(" No packages found (no */nax/context.md or */*/nax/context.md)"));
69577
+ console.log(source_default.yellow(" No packages found (no .nax/mono/*/context.md or .nax/mono/*/*/context.md)"));
69384
69578
  return;
69385
69579
  }
69386
69580
  console.log(source_default.blue(`\u2192 Generating agent files for ${packages.length} package(s)...`));
@@ -69425,12 +69619,12 @@ async function generateCommand(options) {
69425
69619
  process.exit(1);
69426
69620
  return;
69427
69621
  }
69428
- const contextPath = options.context ? join35(workdir, options.context) : join35(workdir, "nax/context.md");
69622
+ const contextPath = options.context ? join35(workdir, options.context) : join35(workdir, ".nax/context.md");
69429
69623
  const outputDir = options.output ? join35(workdir, options.output) : workdir;
69430
69624
  const autoInject = !options.noAutoInject;
69431
69625
  if (!existsSync22(contextPath)) {
69432
69626
  console.error(source_default.red(`\u2717 Context file not found: ${contextPath}`));
69433
- console.error(source_default.yellow(" Create nax/context.md first, or run `nax init` to scaffold it."));
69627
+ console.error(source_default.yellow(" Create .nax/context.md first, or run `nax init` to scaffold it."));
69434
69628
  process.exit(1);
69435
69629
  }
69436
69630
  if (options.agent && !VALID_AGENTS.includes(options.agent)) {
@@ -69496,7 +69690,7 @@ async function generateCommand(options) {
69496
69690
  const packages = await discoverPackages(workdir);
69497
69691
  if (packages.length > 0) {
69498
69692
  console.log(source_default.blue(`
69499
- \u2192 Discovered ${packages.length} package(s) with nax/context.md \u2014 generating agent files...`));
69693
+ \u2192 Discovered ${packages.length} package(s) with context.md \u2014 generating agent files...`));
69500
69694
  let pkgErrorCount = 0;
69501
69695
  for (const pkgDir of packages) {
69502
69696
  const pkgResults = await generateForPackage(pkgDir, config2, dryRun);
@@ -70272,7 +70466,7 @@ async function logsCommand(options) {
70272
70466
  return;
70273
70467
  }
70274
70468
  const resolved = resolveProject({ dir: options.dir });
70275
- const naxDir = join40(resolved.projectDir, "nax");
70469
+ const naxDir = join40(resolved.projectDir, ".nax");
70276
70470
  const configPath = resolved.configPath;
70277
70471
  const configFile = Bun.file(configPath);
70278
70472
  const config2 = await configFile.json();
@@ -70322,7 +70516,7 @@ async function precheckCommand(options) {
70322
70516
  process.exit(1);
70323
70517
  }
70324
70518
  }
70325
- const naxDir = join41(resolved.projectDir, "nax");
70519
+ const naxDir = join41(resolved.projectDir, ".nax");
70326
70520
  const featureDir = join41(naxDir, "features", featureName);
70327
70521
  const prdPath = join41(featureDir, "prd.json");
70328
70522
  if (!existsSync31(featureDir)) {
@@ -70638,7 +70832,14 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
70638
70832
  let currentBatch = [];
70639
70833
  for (const story of stories) {
70640
70834
  const isSimple = story.routing?.complexity === "simple" && story.routing?.testStrategy === "test-after";
70641
- if (isSimple && currentBatch.length < maxBatchSize) {
70835
+ const isNoTest = story.routing?.testStrategy === "no-test";
70836
+ const isBatchable = isSimple || isNoTest;
70837
+ if (isBatchable && currentBatch.length < maxBatchSize) {
70838
+ const batchIsNoTest = currentBatch.length > 0 && currentBatch[0]?.routing?.testStrategy === "no-test";
70839
+ if (currentBatch.length > 0 && batchIsNoTest !== isNoTest) {
70840
+ batches.push({ stories: [...currentBatch], isBatch: currentBatch.length > 1 });
70841
+ currentBatch = [];
70842
+ }
70642
70843
  currentBatch.push(story);
70643
70844
  } else {
70644
70845
  if (currentBatch.length > 0) {
@@ -70648,11 +70849,8 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
70648
70849
  });
70649
70850
  currentBatch = [];
70650
70851
  }
70651
- if (!isSimple) {
70652
- batches.push({
70653
- stories: [story],
70654
- isBatch: false
70655
- });
70852
+ if (!isBatchable) {
70853
+ batches.push({ stories: [story], isBatch: false });
70656
70854
  } else {
70657
70855
  currentBatch.push(story);
70658
70856
  }
@@ -78331,15 +78529,15 @@ Next: nax generate --package ${options.package}`));
78331
78529
  }
78332
78530
  return;
78333
78531
  }
78334
- const naxDir = join52(workdir, "nax");
78532
+ const naxDir = join53(workdir, "nax");
78335
78533
  if (existsSync34(naxDir) && !options.force) {
78336
78534
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
78337
78535
  return;
78338
78536
  }
78339
- mkdirSync6(join52(naxDir, "features"), { recursive: true });
78340
- mkdirSync6(join52(naxDir, "hooks"), { recursive: true });
78341
- await Bun.write(join52(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
78342
- await Bun.write(join52(naxDir, "hooks.json"), JSON.stringify({
78537
+ mkdirSync6(join53(naxDir, "features"), { recursive: true });
78538
+ mkdirSync6(join53(naxDir, "hooks"), { recursive: true });
78539
+ await Bun.write(join53(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
78540
+ await Bun.write(join53(naxDir, "hooks.json"), JSON.stringify({
78343
78541
  hooks: {
78344
78542
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
78345
78543
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -78347,12 +78545,12 @@ Next: nax generate --package ${options.package}`));
78347
78545
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
78348
78546
  }
78349
78547
  }, null, 2));
78350
- await Bun.write(join52(naxDir, ".gitignore"), `# nax temp files
78548
+ await Bun.write(join53(naxDir, ".gitignore"), `# nax temp files
78351
78549
  *.tmp
78352
78550
  .paused.json
78353
78551
  .nax-verifier-verdict.json
78354
78552
  `);
78355
- await Bun.write(join52(naxDir, "context.md"), `# Project Context
78553
+ await Bun.write(join53(naxDir, "context.md"), `# Project Context
78356
78554
 
78357
78555
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
78358
78556
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -78478,8 +78676,8 @@ program2.command("run").description("Run the orchestration loop for a feature").
78478
78676
  console.error(source_default.red("nax not initialized. Run: nax init"));
78479
78677
  process.exit(1);
78480
78678
  }
78481
- const featureDir = join52(naxDir, "features", options.feature);
78482
- const prdPath = join52(featureDir, "prd.json");
78679
+ const featureDir = join53(naxDir, "features", options.feature);
78680
+ const prdPath = join53(featureDir, "prd.json");
78483
78681
  if (options.plan && options.from) {
78484
78682
  if (existsSync34(prdPath) && !options.force) {
78485
78683
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
@@ -78501,10 +78699,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78501
78699
  }
78502
78700
  }
78503
78701
  try {
78504
- const planLogDir = join52(featureDir, "plan");
78702
+ const planLogDir = join53(featureDir, "plan");
78505
78703
  mkdirSync6(planLogDir, { recursive: true });
78506
78704
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78507
- const planLogPath = join52(planLogDir, `${planLogId}.jsonl`);
78705
+ const planLogPath = join53(planLogDir, `${planLogId}.jsonl`);
78508
78706
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78509
78707
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78510
78708
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -78542,10 +78740,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78542
78740
  process.exit(1);
78543
78741
  }
78544
78742
  resetLogger();
78545
- const runsDir = join52(featureDir, "runs");
78743
+ const runsDir = join53(featureDir, "runs");
78546
78744
  mkdirSync6(runsDir, { recursive: true });
78547
78745
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78548
- const logFilePath = join52(runsDir, `${runId}.jsonl`);
78746
+ const logFilePath = join53(runsDir, `${runId}.jsonl`);
78549
78747
  const isTTY = process.stdout.isTTY ?? false;
78550
78748
  const headlessFlag = options.headless ?? false;
78551
78749
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -78561,7 +78759,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78561
78759
  config2.autoMode.defaultAgent = options.agent;
78562
78760
  }
78563
78761
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
78564
- const globalNaxDir = join52(homedir10(), ".nax");
78762
+ const globalNaxDir = join53(homedir10(), ".nax");
78565
78763
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
78566
78764
  const eventEmitter = new PipelineEventEmitter;
78567
78765
  let tuiInstance;
@@ -78584,7 +78782,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78584
78782
  } else {
78585
78783
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
78586
78784
  }
78587
- const statusFilePath = join52(workdir, "nax", "status.json");
78785
+ const statusFilePath = join53(workdir, "nax", "status.json");
78588
78786
  let parallel;
78589
78787
  if (options.parallel !== undefined) {
78590
78788
  parallel = Number.parseInt(options.parallel, 10);
@@ -78610,7 +78808,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78610
78808
  headless: useHeadless,
78611
78809
  skipPrecheck: options.skipPrecheck ?? false
78612
78810
  });
78613
- const latestSymlink = join52(runsDir, "latest.jsonl");
78811
+ const latestSymlink = join53(runsDir, "latest.jsonl");
78614
78812
  try {
78615
78813
  if (existsSync34(latestSymlink)) {
78616
78814
  Bun.spawnSync(["rm", latestSymlink]);
@@ -78648,34 +78846,42 @@ features.command("create <name>").description("Create a new feature").option("-d
78648
78846
  console.error(source_default.red("nax not initialized. Run: nax init"));
78649
78847
  process.exit(1);
78650
78848
  }
78651
- const featureDir = join52(naxDir, "features", name);
78849
+ const featureDir = join53(naxDir, "features", name);
78652
78850
  mkdirSync6(featureDir, { recursive: true });
78653
- await Bun.write(join52(featureDir, "spec.md"), `# Feature: ${name}
78851
+ await Bun.write(join53(featureDir, "spec.md"), `# Feature: ${name}
78654
78852
 
78655
78853
  ## Overview
78656
78854
 
78657
- ## Requirements
78855
+ <!-- One paragraph describing what this feature does and why it's needed. -->
78658
78856
 
78659
- ## Acceptance Criteria
78660
- `);
78661
- await Bun.write(join52(featureDir, "plan.md"), `# Plan: ${name}
78857
+ ## Background / Context
78662
78858
 
78663
- ## Architecture
78859
+ <!-- Optional: relevant background, existing behaviour, or constraints. -->
78664
78860
 
78665
- ## Phases
78861
+ ## User Stories
78666
78862
 
78667
- ## Dependencies
78668
- `);
78669
- await Bun.write(join52(featureDir, "tasks.md"), `# Tasks: ${name}
78863
+ <!-- Describe what users need. Each story becomes a unit of work for nax.
78864
+ Be specific \u2014 the more detail here, the better the generated plan. -->
78670
78865
 
78671
- ## US-001: [Title]
78866
+ - As a [user], I want to [goal] so that [benefit].
78672
78867
 
78673
- ### Description
78868
+ ## Technical Requirements
78674
78869
 
78675
- ### Acceptance Criteria
78676
- - [ ] Criterion 1
78870
+ <!-- Optional: specific technical constraints, patterns to follow, APIs to use, etc. -->
78871
+
78872
+ ## Acceptance Criteria
78873
+
78874
+ <!-- These are parsed by nax to generate acceptance tests.
78875
+ Use clear, testable statements. Each criterion = one AC test. -->
78876
+
78877
+ - [ ] [Describe observable outcome 1]
78878
+ - [ ] [Describe observable outcome 2]
78879
+
78880
+ ## Out of Scope
78881
+
78882
+ <!-- What this feature explicitly does NOT cover. -->
78677
78883
  `);
78678
- await Bun.write(join52(featureDir, "progress.txt"), `# Progress: ${name}
78884
+ await Bun.write(join53(featureDir, "progress.txt"), `# Progress: ${name}
78679
78885
 
78680
78886
  Created: ${new Date().toISOString()}
78681
78887
 
@@ -78684,11 +78890,9 @@ Created: ${new Date().toISOString()}
78684
78890
  console.log(source_default.green(`\u2705 Created feature: ${name}`));
78685
78891
  console.log(source_default.dim(` ${featureDir}/`));
78686
78892
  console.log(source_default.dim(" \u251C\u2500\u2500 spec.md"));
78687
- console.log(source_default.dim(" \u251C\u2500\u2500 plan.md"));
78688
- console.log(source_default.dim(" \u251C\u2500\u2500 tasks.md"));
78689
78893
  console.log(source_default.dim(" \u2514\u2500\u2500 progress.txt"));
78690
78894
  console.log(source_default.dim(`
78691
- Next: Edit spec.md and tasks.md, then: nax plan -f ${name} --from spec.md --auto`));
78895
+ Next: Edit spec.md, then: nax plan -f ${name} --from spec.md --auto`));
78692
78896
  });
78693
78897
  features.command("list").description("List all features").option("-d, --dir <path>", "Project directory", process.cwd()).action(async (options) => {
78694
78898
  let workdir;
@@ -78703,7 +78907,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78703
78907
  console.error(source_default.red("nax not initialized."));
78704
78908
  process.exit(1);
78705
78909
  }
78706
- const featuresDir = join52(naxDir, "features");
78910
+ const featuresDir = join53(naxDir, "features");
78707
78911
  if (!existsSync34(featuresDir)) {
78708
78912
  console.log(source_default.dim("No features yet."));
78709
78913
  return;
@@ -78718,7 +78922,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78718
78922
  Features:
78719
78923
  `));
78720
78924
  for (const name of entries) {
78721
- const prdPath = join52(featuresDir, name, "prd.json");
78925
+ const prdPath = join53(featuresDir, name, "prd.json");
78722
78926
  if (existsSync34(prdPath)) {
78723
78927
  const prd = await loadPRD(prdPath);
78724
78928
  const c = countStories(prd);
@@ -78749,10 +78953,10 @@ Use: nax plan -f <feature> --from <spec>`));
78749
78953
  process.exit(1);
78750
78954
  }
78751
78955
  const config2 = await loadConfig(workdir);
78752
- const featureLogDir = join52(naxDir, "features", options.feature, "plan");
78956
+ const featureLogDir = join53(naxDir, "features", options.feature, "plan");
78753
78957
  mkdirSync6(featureLogDir, { recursive: true });
78754
78958
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78755
- const planLogPath = join52(featureLogDir, `${planLogId}.jsonl`);
78959
+ const planLogPath = join53(featureLogDir, `${planLogId}.jsonl`);
78756
78960
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78757
78961
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78758
78962
  try {
@@ -78789,7 +78993,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78789
78993
  console.error(source_default.red("nax not initialized. Run: nax init"));
78790
78994
  process.exit(1);
78791
78995
  }
78792
- const featureDir = join52(naxDir, "features", options.feature);
78996
+ const featureDir = join53(naxDir, "features", options.feature);
78793
78997
  if (!existsSync34(featureDir)) {
78794
78998
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
78795
78999
  process.exit(1);
@@ -78805,7 +79009,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78805
79009
  specPath: options.from,
78806
79010
  reclassify: options.reclassify
78807
79011
  });
78808
- const prdPath = join52(featureDir, "prd.json");
79012
+ const prdPath = join53(featureDir, "prd.json");
78809
79013
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
78810
79014
  const c = countStories(prd);
78811
79015
  console.log(source_default.green(`