@nathapp/nax 0.21.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 (210) hide show
  1. package/.mcp.json +8 -0
  2. package/docs/ROADMAP.md +20 -5
  3. package/docs/adr/ADR-005-implementation-plan.md +655 -0
  4. package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
  5. package/package.json +1 -1
  6. package/src/agents/claude.ts +44 -9
  7. package/src/config/types.ts +11 -0
  8. package/src/execution/dry-run.ts +81 -0
  9. package/src/execution/escalation/tier-outcome.ts +29 -44
  10. package/src/execution/executor-types.ts +65 -0
  11. package/src/execution/index.ts +0 -17
  12. package/src/execution/iteration-runner.ts +132 -0
  13. package/src/execution/lifecycle/index.ts +0 -1
  14. package/src/execution/lifecycle/run-regression.ts +5 -5
  15. package/src/execution/pipeline-result-handler.ts +51 -254
  16. package/src/execution/sequential-executor.ts +72 -316
  17. package/src/execution/story-selector.ts +75 -0
  18. package/src/pipeline/event-bus.ts +276 -0
  19. package/src/pipeline/runner.ts +51 -77
  20. package/src/pipeline/stages/autofix.ts +133 -0
  21. package/src/pipeline/stages/completion.ts +22 -30
  22. package/src/pipeline/stages/index.ts +30 -13
  23. package/src/pipeline/stages/rectify.ts +93 -0
  24. package/src/pipeline/stages/regression.ts +88 -0
  25. package/src/pipeline/stages/review.ts +19 -153
  26. package/src/pipeline/stages/verify.ts +18 -2
  27. package/src/pipeline/subscribers/hooks.ts +133 -0
  28. package/src/pipeline/subscribers/interaction.ts +68 -0
  29. package/src/pipeline/subscribers/reporters.ts +174 -0
  30. package/src/pipeline/types.ts +10 -1
  31. package/src/review/orchestrator.ts +105 -0
  32. package/src/tdd/prompts.ts +1 -1
  33. package/src/verification/index.ts +1 -1
  34. package/src/verification/orchestrator-types.ts +145 -0
  35. package/src/verification/orchestrator.ts +76 -0
  36. package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
  37. package/src/verification/{gate.ts → runners.ts} +17 -105
  38. package/src/verification/strategies/acceptance.ts +133 -0
  39. package/src/verification/strategies/regression.ts +90 -0
  40. package/src/verification/strategies/scoped.ts +123 -0
  41. package/test/COVERAGE-GAPS.md +333 -0
  42. package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
  43. package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
  44. package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
  45. package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
  46. package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
  47. package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
  48. package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
  49. package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
  50. package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
  51. package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
  52. package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
  53. package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
  54. package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
  55. package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
  56. package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
  57. package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
  58. package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
  59. package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
  60. package/test/integration/config/merger.test.ts +1 -0
  61. package/test/integration/config/paths.test.ts +1 -0
  62. package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
  63. package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
  64. package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
  65. package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
  66. package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
  67. package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
  68. package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
  69. package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
  70. package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
  71. package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
  72. package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
  73. package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
  74. package/test/integration/execution/runner-batching.test.ts +682 -0
  75. package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
  76. package/test/integration/execution/runner-escalation.test.ts +561 -0
  77. package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
  78. package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
  79. package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
  80. package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
  81. package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
  82. package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
  83. package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
  84. package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
  85. package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
  86. package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
  87. package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
  88. package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
  89. package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
  90. package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
  91. package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
  92. package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
  93. package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
  94. package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
  95. package/test/integration/plugins/config-integration.test.ts +1 -0
  96. package/test/integration/plugins/config-resolution.test.ts +1 -0
  97. package/test/integration/plugins/loader.test.ts +1 -0
  98. package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
  99. package/test/integration/plugins/validator.test.ts +1 -0
  100. package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
  101. package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
  102. package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
  103. package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
  104. package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
  105. package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +9 -403
  106. package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
  107. package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
  108. package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
  109. package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
  110. package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
  111. package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
  112. package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
  113. package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
  114. package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
  115. package/test/integration/tmp/headless-test/test.jsonl +30 -0
  116. package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
  117. package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
  118. package/test/unit/acceptance.test.ts +1 -0
  119. package/test/unit/agent-stderr-capture.test.ts +1 -0
  120. package/test/unit/agents/claude.test.ts +1 -0
  121. package/test/unit/analyze-classifier.test.ts +1 -0
  122. package/test/unit/auto-detect.test.ts +1 -0
  123. package/test/unit/cli-status.test.ts +1 -0
  124. package/test/unit/commands/common.test.ts +1 -0
  125. package/test/unit/commands/logs.test.ts +1 -0
  126. package/test/unit/commands/unlock.test.ts +1 -0
  127. package/test/unit/config/defaults.test.ts +1 -0
  128. package/test/unit/config/regression-gate-schema.test.ts +1 -0
  129. package/test/unit/config/smart-runner-flag.test.ts +1 -0
  130. package/test/unit/constitution-generators.test.ts +1 -0
  131. package/test/unit/constitution.test.ts +1 -0
  132. package/test/unit/context/context-autodetect.test.ts +297 -0
  133. package/test/unit/context/context-build.test.ts +575 -0
  134. package/test/unit/context/context-coverage.test.ts +236 -0
  135. package/test/unit/context/context-error.test.ts +93 -0
  136. package/test/unit/context/context-estimate-tokens.test.ts +201 -0
  137. package/test/unit/context/context-format.test.ts +302 -0
  138. package/test/unit/context/context-isolation.test.ts +267 -0
  139. package/test/unit/context/context-sort.test.ts +93 -0
  140. package/test/unit/context/context-story.test.ts +108 -0
  141. package/test/{context → unit/context}/prior-failures.test.ts +5 -4
  142. package/test/unit/context.test.ts +1 -0
  143. package/test/unit/crash-recovery.test.ts +1 -0
  144. package/test/unit/escalation.test.ts +1 -0
  145. package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
  146. package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
  147. package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
  148. package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
  149. package/test/unit/execution-logging-stderr.test.ts +1 -0
  150. package/test/unit/execution-stage.test.ts +1 -0
  151. package/test/unit/fix-generator.test.ts +1 -0
  152. package/test/unit/greenfield.test.ts +1 -0
  153. package/test/unit/interaction/human-review-trigger.test.ts +1 -0
  154. package/test/unit/interaction-network-failures.test.ts +1 -0
  155. package/test/unit/interaction-plugins.test.ts +1 -0
  156. package/test/unit/logging/formatter.test.ts +1 -0
  157. package/test/unit/merge.test.ts +1 -0
  158. package/test/unit/pipeline/event-bus.test.ts +105 -0
  159. package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
  160. package/test/unit/pipeline/runner-retry.test.ts +89 -0
  161. package/test/unit/pipeline/stages/autofix.test.ts +97 -0
  162. package/test/unit/pipeline/stages/rectify.test.ts +101 -0
  163. package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
  164. package/test/unit/pipeline/stages/verify.test.ts +1 -0
  165. package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
  166. package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
  167. package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
  168. package/test/unit/pipeline/verify-smart-runner.test.ts +1 -0
  169. package/test/unit/prd-auto-default.test.ts +1 -0
  170. package/test/unit/prd-failure-category.test.ts +1 -0
  171. package/test/unit/prd-get-next-story.test.ts +1 -0
  172. package/test/unit/precheck-checks.test.ts +1 -0
  173. package/test/unit/precheck-story-size-gate.test.ts +1 -0
  174. package/test/unit/precheck-types.test.ts +1 -0
  175. package/test/unit/prompts.test.ts +1 -0
  176. package/test/unit/rectification.test.ts +2 -1
  177. package/test/unit/registry.test.ts +1 -0
  178. package/test/unit/routing/routing-stability.test.ts +1 -0
  179. package/test/unit/routing/strategies/llm.test.ts +1 -0
  180. package/test/unit/routing-advanced.test.ts +313 -0
  181. package/test/unit/routing-core.test.ts +341 -0
  182. package/test/unit/routing-strategies.test.ts +442 -0
  183. package/test/unit/storyid-events.test.ts +1 -0
  184. package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
  185. package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
  186. package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
  187. package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
  188. package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
  189. package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
  190. package/test/unit/verdict.test.ts +1 -0
  191. package/test/unit/verification/orchestrator-types.test.ts +54 -0
  192. package/test/unit/verification/orchestrator.test.ts +66 -0
  193. package/test/unit/verification/smart-runner-config.test.ts +1 -0
  194. package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
  195. package/test/unit/verification/strategies/acceptance.test.ts +33 -0
  196. package/test/unit/verification/strategies/regression.test.ts +87 -0
  197. package/test/unit/verification/strategies/scoped.test.ts +100 -0
  198. package/test/unit/worktree-manager.test.ts +1 -0
  199. package/src/execution/lifecycle/story-hooks.ts +0 -38
  200. package/src/execution/post-verify.ts +0 -193
  201. package/src/execution/rectification.ts +0 -13
  202. package/src/execution/verification.ts +0 -72
  203. package/test/integration/rectification-flow.test.ts +0 -512
  204. package/test/integration/runner.test.ts +0 -1679
  205. package/test/integration/tdd-orchestrator.test.ts +0 -1762
  206. package/test/unit/execution/post-verify-regression.test.ts +0 -362
  207. package/test/unit/execution/post-verify.test.ts +0 -236
  208. package/test/unit/routing.test.ts +0 -1039
  209. /package/test/{integration → helpers}/helpers.test.ts +0 -0
  210. /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,263 +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
- storyGitRef: storyGitRef ?? undefined, // FEAT-010: per-attempt baseRef for precise smart-runner diff
275
- interaction: ctx.interactionChain ?? undefined,
276
- };
277
-
278
- // Log agent start
279
- logger?.info("agent.start", "Starting agent execution", {
280
- storyId: story.id,
104
+ modelTier: selection.routing.modelTier,
281
105
  agent: ctx.config.autoMode.defaultAgent,
282
- modelTier: routing.modelTier,
283
- testStrategy: routing.testStrategy,
284
- isBatch: isBatchExecution,
285
- });
286
-
287
- // Update status before execution
288
- ctx.statusWriter.setPrd(prd);
289
- ctx.statusWriter.setCurrentStory({
290
- storyId: story.id,
291
- title: story.title,
292
- complexity: routing.complexity,
293
- tddStrategy: routing.testStrategy,
294
- model: routing.modelTier,
295
- attempt: (story.attempts ?? 0) + 1,
296
- phase: "routing",
297
- });
298
- await ctx.statusWriter.update(totalCost, iterations);
299
-
300
- // Run pipeline
301
- const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, ctx.eventEmitter);
302
-
303
- // Log agent complete
304
- logger?.info("agent.complete", "Agent execution completed", {
305
- storyId: story.id,
306
- success: pipelineResult.success,
307
- finalAction: pipelineResult.finalAction,
308
- estimatedCost: pipelineResult.context.agentResult?.estimatedCost,
106
+ iteration: iterations,
309
107
  });
310
108
 
311
- // Update PRD reference (pipeline may have modified it)
312
- prd = pipelineResult.context.prd;
313
-
314
- // Handle pipeline result
315
- const handlerCtx = {
316
- config: ctx.config,
317
- prd,
318
- prdPath: ctx.prdPath,
319
- workdir: ctx.workdir,
320
- featureDir: ctx.featureDir,
321
- hooks: ctx.hooks,
322
- feature: ctx.feature,
323
- totalCost,
324
- startTime: ctx.startTime,
325
- runId: ctx.runId,
326
- pluginRegistry: ctx.pluginRegistry,
327
- story,
328
- storiesToExecute,
329
- routing,
330
- isBatchExecution,
331
- allStoryMetrics,
332
- timeoutRetryCountMap,
333
- storyGitRef,
334
- interactionChain: ctx.interactionChain,
335
- };
336
-
337
- if (pipelineResult.success) {
338
- const successResult = await handlePipelineSuccess(handlerCtx, pipelineResult);
339
- totalCost += successResult.costDelta;
340
- storiesCompleted += successResult.storiesCompletedDelta;
341
- prd = successResult.prd;
342
- prdDirty = successResult.prdDirty;
343
- } else {
344
- const failResult = await handlePipelineFailure(handlerCtx, pipelineResult);
345
- prd = failResult.prd;
346
- prdDirty = failResult.prdDirty;
347
- }
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
+ ];
348
116
 
349
- // Update status after story complete
350
- if (prdDirty) {
117
+ if (iter.prdDirty) {
351
118
  prd = await loadPRD(ctx.prdPath);
352
119
  prdDirty = false;
353
120
  }
@@ -355,35 +122,24 @@ export async function executeSequential(
355
122
  ctx.statusWriter.setCurrentStory(null);
356
123
  await ctx.statusWriter.update(totalCost, iterations);
357
124
 
358
- // Stall detection
359
125
  if (isStalled(prd)) {
360
- const summary = generateHumanHaltSummary(prd);
361
- logger?.error("execution", "Execution stalled", {
362
- reason: "All remaining stories blocked or dependent on blocked stories",
363
- summary,
364
- });
365
- await fireHook(
366
- ctx.hooks,
367
- "on-pause",
368
- hookCtx(ctx.feature, {
369
- reason: "All remaining stories blocked or dependent on blocked stories",
370
- cost: totalCost,
371
- }),
372
- ctx.workdir,
373
- );
126
+ pipelineEventBus.emit({ type: "run:paused", reason: "All remaining stories blocked", cost: totalCost });
374
127
  return buildResult("stalled");
375
128
  }
376
-
377
- // Delay between iterations
378
- if (ctx.config.execution.iterationDelayMs > 0) {
379
- await Bun.sleep(ctx.config.execution.iterationDelayMs);
380
- }
129
+ if (ctx.config.execution.iterationDelayMs > 0) await Bun.sleep(ctx.config.execution.iterationDelayMs);
381
130
  }
382
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
+
383
140
  return buildResult("max-iterations");
384
141
  } finally {
385
- // Stop heartbeat and write exit summary
386
142
  stopHeartbeat();
387
- await writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
143
+ writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
388
144
  }
389
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
+ }