@nathapp/nax 0.50.3 → 0.51.0

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