@nathapp/nax 0.20.0 → 0.22.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 (233) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.mcp.json +8 -0
  3. package/docs/20260304-review-nax.md +492 -0
  4. package/docs/ROADMAP.md +65 -18
  5. package/docs/adr/ADR-005-implementation-plan.md +655 -0
  6. package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
  7. package/docs/specs/bug-039-orphan-processes.md +131 -0
  8. package/docs/specs/bug-040-review-rectification.md +82 -0
  9. package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
  10. package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
  11. package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
  12. package/docs/specs/feat-011-file-context-strategy.md +73 -0
  13. package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
  14. package/docs/specs/feat-013-test-after-review.md +89 -0
  15. package/docs/specs/feat-014-heartbeat-observability.md +127 -0
  16. package/memory/topic/feat-010-baseref.md +28 -0
  17. package/memory/topic/feat-013-test-after-deprecation.md +22 -0
  18. package/nax/config.json +7 -4
  19. package/nax/features/bug-039-medium/prd.json +45 -0
  20. package/package.json +2 -2
  21. package/src/agents/claude.ts +109 -15
  22. package/src/config/types.ts +11 -0
  23. package/src/context/builder.ts +9 -1
  24. package/src/execution/dry-run.ts +81 -0
  25. package/src/execution/escalation/tier-outcome.ts +29 -44
  26. package/src/execution/executor-types.ts +65 -0
  27. package/src/execution/index.ts +0 -17
  28. package/src/execution/iteration-runner.ts +132 -0
  29. package/src/execution/lifecycle/index.ts +0 -1
  30. package/src/execution/lifecycle/run-regression.ts +5 -5
  31. package/src/execution/pipeline-result-handler.ts +51 -254
  32. package/src/execution/sequential-executor.ts +72 -315
  33. package/src/execution/story-selector.ts +75 -0
  34. package/src/pipeline/event-bus.ts +276 -0
  35. package/src/pipeline/runner.ts +51 -77
  36. package/src/pipeline/stages/autofix.ts +133 -0
  37. package/src/pipeline/stages/completion.ts +22 -30
  38. package/src/pipeline/stages/index.ts +30 -13
  39. package/src/pipeline/stages/rectify.ts +93 -0
  40. package/src/pipeline/stages/regression.ts +88 -0
  41. package/src/pipeline/stages/review.ts +19 -153
  42. package/src/pipeline/stages/verify.ts +19 -3
  43. package/src/pipeline/subscribers/hooks.ts +133 -0
  44. package/src/pipeline/subscribers/interaction.ts +68 -0
  45. package/src/pipeline/subscribers/reporters.ts +174 -0
  46. package/src/pipeline/types.ts +12 -1
  47. package/src/review/orchestrator.ts +105 -0
  48. package/src/review/runner.ts +39 -4
  49. package/src/routing/router.ts +3 -3
  50. package/src/routing/strategies/keyword.ts +5 -2
  51. package/src/routing/strategies/llm.ts +27 -1
  52. package/src/tdd/prompts.ts +1 -1
  53. package/src/utils/git.ts +49 -25
  54. package/src/verification/executor.ts +8 -2
  55. package/src/verification/index.ts +1 -1
  56. package/src/verification/orchestrator-types.ts +145 -0
  57. package/src/verification/orchestrator.ts +76 -0
  58. package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
  59. package/src/verification/{gate.ts → runners.ts} +17 -105
  60. package/src/verification/smart-runner.ts +6 -10
  61. package/src/verification/strategies/acceptance.ts +133 -0
  62. package/src/verification/strategies/regression.ts +90 -0
  63. package/src/verification/strategies/scoped.ts +123 -0
  64. package/test/COVERAGE-GAPS.md +333 -0
  65. package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
  66. package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
  67. package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
  68. package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
  69. package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
  70. package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
  71. package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
  72. package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
  73. package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
  74. package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
  75. package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
  76. package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
  77. package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
  78. package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
  79. package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
  80. package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
  81. package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
  82. package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
  83. package/test/integration/config/merger.test.ts +1 -0
  84. package/test/integration/config/paths.test.ts +1 -0
  85. package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
  86. package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
  87. package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
  88. package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
  89. package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
  90. package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
  91. package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
  92. package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
  93. package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
  94. package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
  95. package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
  96. package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
  97. package/test/integration/execution/runner-batching.test.ts +682 -0
  98. package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
  99. package/test/integration/execution/runner-escalation.test.ts +561 -0
  100. package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
  101. package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
  102. package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
  103. package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
  104. package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
  105. package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
  106. package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
  107. package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
  108. package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
  109. package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
  110. package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
  111. package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
  112. package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
  113. package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
  114. package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
  115. package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
  116. package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
  117. package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
  118. package/test/integration/plugins/config-integration.test.ts +1 -0
  119. package/test/integration/plugins/config-resolution.test.ts +1 -0
  120. package/test/integration/plugins/loader.test.ts +1 -0
  121. package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
  122. package/test/integration/plugins/validator.test.ts +1 -0
  123. package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
  124. package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
  125. package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
  126. package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
  127. package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
  128. package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +10 -404
  129. package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
  130. package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
  131. package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
  132. package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
  133. package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
  134. package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
  135. package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
  136. package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
  137. package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
  138. package/test/integration/tmp/headless-test/test.jsonl +30 -0
  139. package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
  140. package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
  141. package/test/unit/acceptance.test.ts +1 -0
  142. package/test/unit/agent-stderr-capture.test.ts +1 -0
  143. package/test/unit/agents/claude.test.ts +107 -0
  144. package/test/unit/analyze-classifier.test.ts +1 -0
  145. package/test/unit/auto-detect.test.ts +1 -0
  146. package/test/unit/cli-status.test.ts +1 -0
  147. package/test/unit/commands/common.test.ts +1 -0
  148. package/test/unit/commands/logs.test.ts +1 -0
  149. package/test/unit/commands/unlock.test.ts +1 -0
  150. package/test/unit/config/defaults.test.ts +1 -0
  151. package/test/unit/config/regression-gate-schema.test.ts +1 -0
  152. package/test/unit/config/smart-runner-flag.test.ts +1 -0
  153. package/test/unit/constitution-generators.test.ts +1 -0
  154. package/test/unit/constitution.test.ts +1 -0
  155. package/test/unit/context/context-autodetect.test.ts +297 -0
  156. package/test/unit/context/context-build.test.ts +575 -0
  157. package/test/unit/context/context-coverage.test.ts +236 -0
  158. package/test/unit/context/context-error.test.ts +93 -0
  159. package/test/unit/context/context-estimate-tokens.test.ts +201 -0
  160. package/test/unit/context/context-format.test.ts +302 -0
  161. package/test/unit/context/context-isolation.test.ts +267 -0
  162. package/test/unit/context/context-sort.test.ts +93 -0
  163. package/test/unit/context/context-story.test.ts +108 -0
  164. package/test/{context → unit/context}/prior-failures.test.ts +5 -4
  165. package/test/unit/context.test.ts +7 -3
  166. package/test/unit/crash-recovery.test.ts +1 -0
  167. package/test/unit/escalation.test.ts +1 -0
  168. package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
  169. package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
  170. package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
  171. package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
  172. package/test/unit/execution-logging-stderr.test.ts +1 -0
  173. package/test/unit/execution-stage.test.ts +1 -0
  174. package/test/unit/fix-generator.test.ts +1 -0
  175. package/test/unit/greenfield.test.ts +1 -0
  176. package/test/unit/interaction/human-review-trigger.test.ts +1 -0
  177. package/test/unit/interaction-network-failures.test.ts +1 -0
  178. package/test/unit/interaction-plugins.test.ts +1 -0
  179. package/test/unit/logging/formatter.test.ts +1 -0
  180. package/test/unit/merge.test.ts +1 -0
  181. package/test/unit/pipeline/event-bus.test.ts +105 -0
  182. package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
  183. package/test/unit/pipeline/runner-retry.test.ts +89 -0
  184. package/test/unit/pipeline/stages/autofix.test.ts +97 -0
  185. package/test/unit/pipeline/stages/rectify.test.ts +101 -0
  186. package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
  187. package/test/unit/pipeline/stages/verify.test.ts +1 -0
  188. package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
  189. package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
  190. package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
  191. package/test/unit/pipeline/verify-smart-runner.test.ts +2 -1
  192. package/test/unit/prd-auto-default.test.ts +3 -2
  193. package/test/unit/prd-failure-category.test.ts +1 -0
  194. package/test/unit/prd-get-next-story.test.ts +1 -0
  195. package/test/unit/precheck-checks.test.ts +1 -0
  196. package/test/unit/precheck-story-size-gate.test.ts +1 -0
  197. package/test/unit/precheck-types.test.ts +1 -0
  198. package/test/unit/prompts.test.ts +1 -0
  199. package/test/unit/rectification.test.ts +2 -1
  200. package/test/unit/registry.test.ts +1 -0
  201. package/test/unit/routing/routing-stability.test.ts +2 -1
  202. package/test/unit/routing/strategies/llm.test.ts +251 -0
  203. package/test/unit/routing-advanced.test.ts +313 -0
  204. package/test/unit/routing-core.test.ts +341 -0
  205. package/test/unit/routing-strategies.test.ts +442 -0
  206. package/test/unit/storyid-events.test.ts +1 -0
  207. package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
  208. package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
  209. package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
  210. package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
  211. package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
  212. package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
  213. package/test/unit/verdict.test.ts +1 -0
  214. package/test/unit/verification/orchestrator-types.test.ts +54 -0
  215. package/test/unit/verification/orchestrator.test.ts +66 -0
  216. package/test/unit/verification/smart-runner-config.test.ts +1 -0
  217. package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
  218. package/test/unit/verification/strategies/acceptance.test.ts +33 -0
  219. package/test/unit/verification/strategies/regression.test.ts +87 -0
  220. package/test/unit/verification/strategies/scoped.test.ts +100 -0
  221. package/test/unit/worktree-manager.test.ts +1 -0
  222. package/src/execution/lifecycle/story-hooks.ts +0 -38
  223. package/src/execution/post-verify.ts +0 -193
  224. package/src/execution/rectification.ts +0 -13
  225. package/src/execution/verification.ts +0 -72
  226. package/test/integration/rectification-flow.test.ts +0 -512
  227. package/test/integration/runner.test.ts +0 -1679
  228. package/test/integration/tdd-orchestrator.test.ts +0 -1762
  229. package/test/unit/execution/post-verify-regression.test.ts +0 -362
  230. package/test/unit/execution/post-verify.test.ts +0 -236
  231. package/test/unit/routing.test.ts +0 -1039
  232. /package/test/{integration → helpers}/helpers.test.ts +0 -0
  233. /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
@@ -1,77 +1,43 @@
1
- /** Sequential Story Executor — main execution loop for story pipeline. */
1
+ /** Sequential Story Executor (ADR-005, Phase 4) — main execution loop. */
2
2
 
3
- import type { NaxConfig } from "../config";
4
- import { type LoadedHooksConfig, fireHook } from "../hooks";
5
- import type { InteractionChain } from "../interaction/chain";
6
3
  import { getSafeLogger } from "../logger";
7
4
  import type { StoryMetrics } from "../metrics";
8
- import type { PipelineEventEmitter } from "../pipeline/events";
5
+ import { pipelineEventBus } from "../pipeline/event-bus";
9
6
  import { runPipeline } from "../pipeline/runner";
10
- import { defaultPipeline } from "../pipeline/stages";
11
- import type { PipelineContext, RoutingResult } from "../pipeline/types";
12
- import type { PluginRegistry } from "../plugins";
13
- import { generateHumanHaltSummary, getNextStory, isComplete, isStalled, loadPRD } from "../prd";
14
- import type { PRD, UserStory } from "../prd/types";
15
- import { routeTask, tryLlmBatchRoute } from "../routing";
16
- import { captureGitRef } from "../utils/git";
17
- import type { StoryBatch } from "./batching";
7
+ import { postRunPipeline } from "../pipeline/stages";
8
+ import { wireHooks } from "../pipeline/subscribers/hooks";
9
+ import { wireInteraction } from "../pipeline/subscribers/interaction";
10
+ import { wireReporters } from "../pipeline/subscribers/reporters";
11
+ import type { PipelineContext } from "../pipeline/types";
12
+ import { generateHumanHaltSummary, isComplete, isStalled, loadPRD } from "../prd";
13
+ import type { PRD } from "../prd/types";
18
14
  import { startHeartbeat, stopHeartbeat, writeExitSummary } from "./crash-recovery";
19
- import { preIterationTierCheck } from "./escalation";
20
- import { hookCtx } from "./helpers";
21
- import {
22
- applyCachedRouting,
23
- handleDryRun,
24
- handlePipelineFailure,
25
- handlePipelineSuccess,
26
- } from "./pipeline-result-handler";
27
- import type { StatusWriter } from "./status-writer";
15
+ import type { SequentialExecutionContext, SequentialExecutionResult } from "./executor-types";
16
+ import { runIteration } from "./iteration-runner";
17
+ import { selectNextStories } from "./story-selector";
28
18
 
29
- export interface SequentialExecutionContext {
30
- prdPath: string;
31
- workdir: string;
32
- config: NaxConfig;
33
- hooks: LoadedHooksConfig;
34
- feature: string;
35
- featureDir?: string;
36
- dryRun: boolean;
37
- useBatch: boolean;
38
- pluginRegistry: PluginRegistry;
39
- eventEmitter?: PipelineEventEmitter;
40
- statusWriter: StatusWriter;
41
- logFilePath?: string;
42
- runId: string;
43
- startTime: number;
44
- batchPlan: StoryBatch[];
45
- interactionChain?: InteractionChain | null;
46
- }
47
-
48
- export interface SequentialExecutionResult {
49
- prd: PRD;
50
- iterations: number;
51
- storiesCompleted: number;
52
- totalCost: number;
53
- allStoryMetrics: StoryMetrics[];
54
- timeoutRetryCountMap: Map<string, number>;
55
- exitReason: "completed" | "cost-limit" | "max-iterations" | "stalled" | "no-stories";
56
- }
19
+ export type { SequentialExecutionContext, SequentialExecutionResult } from "./executor-types";
57
20
 
58
- /**
59
- * Execute stories sequentially through the pipeline
60
- */
61
21
  export async function executeSequential(
62
22
  ctx: SequentialExecutionContext,
63
23
  initialPrd: PRD,
64
24
  ): Promise<SequentialExecutionResult> {
65
25
  const logger = getSafeLogger();
66
- let prd = initialPrd;
67
- let prdDirty = false;
68
- let iterations = 0;
69
- let storiesCompleted = 0;
70
- let totalCost = 0;
26
+ let [prd, prdDirty, iterations, storiesCompleted, totalCost, lastStoryId, currentBatchIndex] = [
27
+ initialPrd,
28
+ false,
29
+ 0,
30
+ 0,
31
+ 0,
32
+ null as string | null,
33
+ 0,
34
+ ];
71
35
  const allStoryMetrics: StoryMetrics[] = [];
72
- const timeoutRetryCountMap = new Map<string, number>();
73
- let currentBatchIndex = 0;
74
- let lastStoryId: string | null = null;
36
+
37
+ pipelineEventBus.clear();
38
+ wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature);
39
+ wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime);
40
+ wireInteraction(pipelineEventBus, ctx.interactionChain, ctx.config);
75
41
 
76
42
  const buildResult = (exitReason: SequentialExecutionResult["exitReason"]): SequentialExecutionResult => ({
77
43
  prd,
@@ -79,7 +45,6 @@ export async function executeSequential(
79
45
  storiesCompleted,
80
46
  totalCost,
81
47
  allStoryMetrics,
82
- timeoutRetryCountMap,
83
48
  exitReason,
84
49
  });
85
50
 
@@ -91,262 +56,65 @@ export async function executeSequential(
91
56
  );
92
57
 
93
58
  try {
94
- // Main execution loop
95
59
  while (iterations < ctx.config.execution.maxIterations) {
96
60
  iterations++;
97
-
98
- // MEM-1: Check memory usage (warn if > 1GB heap)
99
- const memUsage = process.memoryUsage();
100
- const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
101
- if (heapUsedMB > 1024) {
102
- logger?.warn("execution", "High memory usage detected", {
103
- heapUsedMB,
104
- suggestion: "Consider pausing (echo PAUSE > .queue.txt) if this continues to grow",
105
- });
106
- }
107
-
108
- // Reload PRD only if dirty (modified since last load)
61
+ if (Math.round(process.memoryUsage().heapUsed / 1024 / 1024) > 1024)
62
+ logger?.warn("execution", "High memory usage detected");
109
63
  if (prdDirty) {
110
64
  prd = await loadPRD(ctx.prdPath);
111
65
  prdDirty = false;
112
66
  }
113
-
114
- // Check completion
115
67
  if (isComplete(prd)) {
116
- logger?.info("execution", "All stories complete!", {
117
- feature: ctx.feature,
68
+ pipelineEventBus.emit({
69
+ type: "run:completed",
70
+ totalStories: 0,
71
+ passedStories: 0,
72
+ failedStories: 0,
73
+ durationMs: Date.now() - ctx.startTime,
118
74
  totalCost,
119
75
  });
120
- await fireHook(
121
- ctx.hooks,
122
- "on-complete",
123
- hookCtx(ctx.feature, { status: "complete", cost: totalCost }),
124
- ctx.workdir,
125
- );
126
76
  return buildResult("completed");
127
77
  }
128
78
 
129
- // Get next story/batch
130
- let storiesToExecute: UserStory[];
131
- let isBatchExecution: boolean;
132
- let story: UserStory;
133
- let routing: ReturnType<typeof routeTask>;
134
-
135
- if (ctx.useBatch && currentBatchIndex < ctx.batchPlan.length) {
136
- // Get next batch from precomputed plan
137
- const batch = ctx.batchPlan[currentBatchIndex];
138
- currentBatchIndex++;
139
-
140
- // Filter out already-completed stories
141
- storiesToExecute = batch.stories.filter(
142
- (s) =>
143
- !s.passes &&
144
- s.status !== "passed" &&
145
- s.status !== "skipped" &&
146
- s.status !== "blocked" &&
147
- s.status !== "failed" &&
148
- s.status !== "paused",
149
- );
150
- isBatchExecution = batch.isBatch && storiesToExecute.length > 1;
151
-
152
- if (storiesToExecute.length === 0) {
153
- // All stories in this batch already completed, move to next batch
154
- continue;
155
- }
156
-
157
- // Use first story as the primary story for routing/context
158
- story = storiesToExecute[0];
159
- routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, ctx.config);
160
- routing = applyCachedRouting(routing, story, ctx.config);
161
- } else {
162
- // Fallback to single-story mode (when batching disabled or batch plan exhausted)
163
- const nextStory = getNextStory(prd, lastStoryId, ctx.config.execution.rectification?.maxRetries ?? 2);
164
- if (!nextStory) {
165
- logger?.warn("execution", "No actionable stories (check dependencies)");
166
- return buildResult("no-stories");
167
- }
168
-
169
- story = nextStory;
170
- lastStoryId = story.id;
171
- storiesToExecute = [story];
172
- isBatchExecution = false;
173
-
174
- routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, ctx.config);
175
- routing = applyCachedRouting(routing, story, ctx.config);
176
- }
177
-
178
- // Pre-iteration tier escalation check
179
- const tierCheckResult = await preIterationTierCheck(
180
- story,
181
- routing,
182
- ctx.config,
183
- prd,
184
- ctx.prdPath,
185
- ctx.featureDir,
186
- ctx.hooks,
187
- ctx.feature,
188
- totalCost,
189
- ctx.workdir,
190
- );
191
-
192
- if (tierCheckResult.shouldSkipIteration) {
193
- prd = tierCheckResult.prd;
194
- prdDirty = tierCheckResult.prdDirty;
79
+ const selected = selectNextStories(prd, ctx.config, ctx.batchPlan, currentBatchIndex, lastStoryId, ctx.useBatch);
80
+ if (!selected) return buildResult("no-stories");
81
+ if (!selected.selection) {
82
+ currentBatchIndex = selected.nextBatchIndex;
195
83
  continue;
196
84
  }
85
+ currentBatchIndex = selected.nextBatchIndex;
86
+ const { selection } = selected;
87
+ if (!ctx.useBatch) lastStoryId = selection.story.id;
197
88
 
198
- // Check cost limit
199
89
  if (totalCost >= ctx.config.execution.costLimit) {
200
- logger?.warn("execution", "Cost limit reached, pausing", {
201
- totalCost,
202
- costLimit: ctx.config.execution.costLimit,
90
+ pipelineEventBus.emit({
91
+ type: "run:paused",
92
+ reason: `Cost limit reached: $${totalCost.toFixed(2)}`,
93
+ storyId: selection.story.id,
94
+ cost: totalCost,
203
95
  });
204
- await fireHook(
205
- ctx.hooks,
206
- "on-pause",
207
- hookCtx(ctx.feature, {
208
- storyId: story.id,
209
- reason: `Cost limit reached: $${totalCost.toFixed(2)}`,
210
- cost: totalCost,
211
- }),
212
- ctx.workdir,
213
- );
214
96
  return buildResult("cost-limit");
215
97
  }
216
98
 
217
- logger?.info("iteration.start", `Starting iteration ${iterations}`, {
218
- iteration: iterations,
219
- storyId: story.id,
220
- storyTitle: story.title,
221
- isBatch: isBatchExecution,
222
- batchSize: isBatchExecution ? storiesToExecute.length : 1,
223
- modelTier: routing.modelTier,
224
- complexity: routing.complexity,
225
- ...(isBatchExecution && { batchStoryIds: storiesToExecute.map((s) => s.id) }),
226
- });
227
-
228
- // Fire story-start hook
229
- await fireHook(
230
- ctx.hooks,
231
- "on-story-start",
232
- hookCtx(ctx.feature, {
233
- storyId: story.id,
234
- model: routing.modelTier,
235
- agent: ctx.config.autoMode.defaultAgent,
236
- iteration: iterations,
237
- }),
238
- ctx.workdir,
239
- );
240
-
241
- if (ctx.dryRun) {
242
- const dryRunResult = await handleDryRun({
243
- prd,
244
- prdPath: ctx.prdPath,
245
- storiesToExecute,
246
- routing,
247
- statusWriter: ctx.statusWriter,
248
- pluginRegistry: ctx.pluginRegistry,
249
- runId: ctx.runId,
250
- totalCost,
251
- iterations,
252
- });
253
- storiesCompleted += dryRunResult.storiesCompletedDelta;
254
- prdDirty = dryRunResult.prdDirty;
255
- continue;
256
- }
257
-
258
- // Capture git ref for scoped verification
259
- const storyGitRef = await captureGitRef(ctx.workdir);
260
-
261
- // Build pipeline context
262
- const storyStartTime = new Date().toISOString();
263
- const pipelineContext: PipelineContext = {
264
- config: ctx.config,
265
- prd,
266
- story,
267
- stories: storiesToExecute,
268
- routing: routing as RoutingResult,
99
+ pipelineEventBus.emit({
100
+ type: "story:started",
101
+ storyId: selection.story.id,
102
+ story: selection.story,
269
103
  workdir: ctx.workdir,
270
- featureDir: ctx.featureDir,
271
- hooks: ctx.hooks,
272
- plugins: ctx.pluginRegistry,
273
- storyStartTime,
274
- interaction: ctx.interactionChain ?? undefined,
275
- };
276
-
277
- // Log agent start
278
- logger?.info("agent.start", "Starting agent execution", {
279
- storyId: story.id,
104
+ modelTier: selection.routing.modelTier,
280
105
  agent: ctx.config.autoMode.defaultAgent,
281
- modelTier: routing.modelTier,
282
- testStrategy: routing.testStrategy,
283
- isBatch: isBatchExecution,
284
- });
285
-
286
- // Update status before execution
287
- ctx.statusWriter.setPrd(prd);
288
- ctx.statusWriter.setCurrentStory({
289
- storyId: story.id,
290
- title: story.title,
291
- complexity: routing.complexity,
292
- tddStrategy: routing.testStrategy,
293
- model: routing.modelTier,
294
- attempt: (story.attempts ?? 0) + 1,
295
- phase: "routing",
296
- });
297
- await ctx.statusWriter.update(totalCost, iterations);
298
-
299
- // Run pipeline
300
- const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, ctx.eventEmitter);
301
-
302
- // Log agent complete
303
- logger?.info("agent.complete", "Agent execution completed", {
304
- storyId: story.id,
305
- success: pipelineResult.success,
306
- finalAction: pipelineResult.finalAction,
307
- estimatedCost: pipelineResult.context.agentResult?.estimatedCost,
106
+ iteration: iterations,
308
107
  });
309
108
 
310
- // Update PRD reference (pipeline may have modified it)
311
- prd = pipelineResult.context.prd;
312
-
313
- // Handle pipeline result
314
- const handlerCtx = {
315
- config: ctx.config,
316
- prd,
317
- prdPath: ctx.prdPath,
318
- workdir: ctx.workdir,
319
- featureDir: ctx.featureDir,
320
- hooks: ctx.hooks,
321
- feature: ctx.feature,
322
- totalCost,
323
- startTime: ctx.startTime,
324
- runId: ctx.runId,
325
- pluginRegistry: ctx.pluginRegistry,
326
- story,
327
- storiesToExecute,
328
- routing,
329
- isBatchExecution,
330
- allStoryMetrics,
331
- timeoutRetryCountMap,
332
- storyGitRef,
333
- interactionChain: ctx.interactionChain,
334
- };
335
-
336
- if (pipelineResult.success) {
337
- const successResult = await handlePipelineSuccess(handlerCtx, pipelineResult);
338
- totalCost += successResult.costDelta;
339
- storiesCompleted += successResult.storiesCompletedDelta;
340
- prd = successResult.prd;
341
- prdDirty = successResult.prdDirty;
342
- } else {
343
- const failResult = await handlePipelineFailure(handlerCtx, pipelineResult);
344
- prd = failResult.prd;
345
- prdDirty = failResult.prdDirty;
346
- }
109
+ const iter = await runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
110
+ [prd, storiesCompleted, totalCost, prdDirty] = [
111
+ iter.prd,
112
+ storiesCompleted + iter.storiesCompletedDelta,
113
+ totalCost + iter.costDelta,
114
+ iter.prdDirty,
115
+ ];
347
116
 
348
- // Update status after story complete
349
- if (prdDirty) {
117
+ if (iter.prdDirty) {
350
118
  prd = await loadPRD(ctx.prdPath);
351
119
  prdDirty = false;
352
120
  }
@@ -354,35 +122,24 @@ export async function executeSequential(
354
122
  ctx.statusWriter.setCurrentStory(null);
355
123
  await ctx.statusWriter.update(totalCost, iterations);
356
124
 
357
- // Stall detection
358
125
  if (isStalled(prd)) {
359
- const summary = generateHumanHaltSummary(prd);
360
- logger?.error("execution", "Execution stalled", {
361
- reason: "All remaining stories blocked or dependent on blocked stories",
362
- summary,
363
- });
364
- await fireHook(
365
- ctx.hooks,
366
- "on-pause",
367
- hookCtx(ctx.feature, {
368
- reason: "All remaining stories blocked or dependent on blocked stories",
369
- cost: totalCost,
370
- }),
371
- ctx.workdir,
372
- );
126
+ pipelineEventBus.emit({ type: "run:paused", reason: "All remaining stories blocked", cost: totalCost });
373
127
  return buildResult("stalled");
374
128
  }
375
-
376
- // Delay between iterations
377
- if (ctx.config.execution.iterationDelayMs > 0) {
378
- await Bun.sleep(ctx.config.execution.iterationDelayMs);
379
- }
129
+ if (ctx.config.execution.iterationDelayMs > 0) await Bun.sleep(ctx.config.execution.iterationDelayMs);
380
130
  }
381
131
 
132
+ // Post-run pipeline (acceptance tests)
133
+ logger?.info("execution", "Running post-run pipeline (acceptance tests)");
134
+ await runPipeline(
135
+ postRunPipeline,
136
+ { config: ctx.config, prd, workdir: ctx.workdir, story: prd.userStories[0] } as unknown as PipelineContext,
137
+ ctx.eventEmitter,
138
+ );
139
+
382
140
  return buildResult("max-iterations");
383
141
  } finally {
384
- // Stop heartbeat and write exit summary
385
142
  stopHeartbeat();
386
- await writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
143
+ writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
387
144
  }
388
145
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Story Selector (ADR-005, Phase 4)
3
+ *
4
+ * Extracted from sequential-executor.ts: batch/single-story selection logic.
5
+ */
6
+
7
+ import type { NaxConfig } from "../config";
8
+ import type { RoutingResult } from "../pipeline/types";
9
+ import { getNextStory } from "../prd";
10
+ import type { PRD, UserStory } from "../prd/types";
11
+ import type { StoryBatch } from "./batching";
12
+ import { buildPreviewRouting } from "./executor-types";
13
+
14
+ export interface StorySelection {
15
+ story: UserStory;
16
+ storiesToExecute: UserStory[];
17
+ routing: RoutingResult;
18
+ isBatchExecution: boolean;
19
+ }
20
+
21
+ /**
22
+ * Select the next story (or batch) to execute.
23
+ * Returns null when there are no more stories to run.
24
+ */
25
+ export function selectNextStories(
26
+ prd: PRD,
27
+ config: NaxConfig,
28
+ batchPlan: StoryBatch[],
29
+ currentBatchIndex: number,
30
+ lastStoryId: string | null,
31
+ useBatch: boolean,
32
+ ): { selection: StorySelection; nextBatchIndex: number } | null {
33
+ if (useBatch && currentBatchIndex < batchPlan.length) {
34
+ const batch = batchPlan[currentBatchIndex];
35
+ const storiesToExecute = batch.stories.filter(
36
+ (s) =>
37
+ !s.passes &&
38
+ s.status !== "passed" &&
39
+ s.status !== "skipped" &&
40
+ s.status !== "blocked" &&
41
+ s.status !== "failed" &&
42
+ s.status !== "paused",
43
+ );
44
+
45
+ if (storiesToExecute.length === 0) {
46
+ // Batch exhausted (all already done) — advance index, caller retries
47
+ return { selection: null as unknown as StorySelection, nextBatchIndex: currentBatchIndex + 1 };
48
+ }
49
+
50
+ const story = storiesToExecute[0];
51
+ return {
52
+ selection: {
53
+ story,
54
+ storiesToExecute,
55
+ routing: buildPreviewRouting(story, config),
56
+ isBatchExecution: batch.isBatch && storiesToExecute.length > 1,
57
+ },
58
+ nextBatchIndex: currentBatchIndex + 1,
59
+ };
60
+ }
61
+
62
+ // Single-story fallback
63
+ const story = getNextStory(prd, lastStoryId, config.execution.rectification?.maxRetries ?? 2);
64
+ if (!story) return null;
65
+
66
+ return {
67
+ selection: {
68
+ story,
69
+ storiesToExecute: [story],
70
+ routing: buildPreviewRouting(story, config),
71
+ isBatchExecution: false,
72
+ },
73
+ nextBatchIndex: currentBatchIndex,
74
+ };
75
+ }