@nathapp/nax 0.37.0 → 0.38.1

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 (72) hide show
  1. package/dist/nax.js +3258 -2894
  2. package/package.json +4 -1
  3. package/src/agents/claude-complete.ts +72 -0
  4. package/src/agents/claude-execution.ts +189 -0
  5. package/src/agents/claude-interactive.ts +77 -0
  6. package/src/agents/claude-plan.ts +23 -8
  7. package/src/agents/claude.ts +64 -349
  8. package/src/analyze/classifier.ts +2 -1
  9. package/src/cli/config-descriptions.ts +206 -0
  10. package/src/cli/config-diff.ts +103 -0
  11. package/src/cli/config-display.ts +285 -0
  12. package/src/cli/config-get.ts +55 -0
  13. package/src/cli/config.ts +7 -618
  14. package/src/cli/prompts-export.ts +58 -0
  15. package/src/cli/prompts-init.ts +200 -0
  16. package/src/cli/prompts-main.ts +237 -0
  17. package/src/cli/prompts-tdd.ts +78 -0
  18. package/src/cli/prompts.ts +10 -541
  19. package/src/commands/logs-formatter.ts +201 -0
  20. package/src/commands/logs-reader.ts +171 -0
  21. package/src/commands/logs.ts +11 -362
  22. package/src/config/loader.ts +4 -15
  23. package/src/config/runtime-types.ts +448 -0
  24. package/src/config/schema-types.ts +53 -0
  25. package/src/config/types.ts +49 -486
  26. package/src/context/auto-detect.ts +2 -1
  27. package/src/context/builder.ts +3 -2
  28. package/src/execution/crash-heartbeat.ts +77 -0
  29. package/src/execution/crash-recovery.ts +23 -365
  30. package/src/execution/crash-signals.ts +149 -0
  31. package/src/execution/crash-writer.ts +154 -0
  32. package/src/execution/parallel-coordinator.ts +278 -0
  33. package/src/execution/parallel-executor-rectification-pass.ts +117 -0
  34. package/src/execution/parallel-executor-rectify.ts +135 -0
  35. package/src/execution/parallel-executor.ts +19 -211
  36. package/src/execution/parallel-worker.ts +148 -0
  37. package/src/execution/parallel.ts +5 -404
  38. package/src/execution/pid-registry.ts +3 -8
  39. package/src/execution/runner-completion.ts +160 -0
  40. package/src/execution/runner-execution.ts +221 -0
  41. package/src/execution/runner-setup.ts +82 -0
  42. package/src/execution/runner.ts +53 -202
  43. package/src/execution/timeout-handler.ts +100 -0
  44. package/src/hooks/runner.ts +11 -21
  45. package/src/metrics/tracker.ts +7 -30
  46. package/src/pipeline/runner.ts +2 -1
  47. package/src/pipeline/stages/completion.ts +0 -1
  48. package/src/pipeline/stages/context.ts +2 -1
  49. package/src/plugins/extensions.ts +225 -0
  50. package/src/plugins/loader.ts +2 -1
  51. package/src/plugins/types.ts +16 -221
  52. package/src/prd/index.ts +2 -1
  53. package/src/prd/validate.ts +41 -0
  54. package/src/precheck/checks-blockers.ts +15 -419
  55. package/src/precheck/checks-cli.ts +68 -0
  56. package/src/precheck/checks-config.ts +102 -0
  57. package/src/precheck/checks-git.ts +87 -0
  58. package/src/precheck/checks-system.ts +163 -0
  59. package/src/review/orchestrator.ts +19 -6
  60. package/src/review/runner.ts +17 -5
  61. package/src/routing/chain.ts +2 -1
  62. package/src/routing/loader.ts +2 -5
  63. package/src/tdd/orchestrator.ts +2 -1
  64. package/src/tdd/verdict-reader.ts +266 -0
  65. package/src/tdd/verdict.ts +6 -271
  66. package/src/utils/errors.ts +12 -0
  67. package/src/utils/git.ts +12 -5
  68. package/src/utils/json-file.ts +72 -0
  69. package/src/verification/executor.ts +2 -1
  70. package/src/verification/smart-runner.ts +23 -3
  71. package/src/worktree/manager.ts +9 -3
  72. package/src/worktree/merge.ts +3 -2
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Runner Execution Phase
3
+ *
4
+ * Handles parallel and sequential story execution paths.
5
+ * Extracted from runner.ts for better code organization.
6
+ */
7
+
8
+ import type { NaxConfig } from "../config";
9
+ import type { LoadedHooksConfig } from "../hooks";
10
+ import { getSafeLogger } from "../logger";
11
+ import type { StoryMetrics } from "../metrics";
12
+ import type { PipelineEventEmitter } from "../pipeline/events";
13
+ import type { PluginRegistry } from "../plugins/registry";
14
+ import type { PRD } from "../prd";
15
+ import { tryLlmBatchRoute } from "../routing/batch-route";
16
+ import { clearCache as clearLlmCache, routeBatch as llmRouteBatch } from "../routing/strategies/llm";
17
+ import { precomputeBatchPlan } from "./batching";
18
+ import { getAllReadyStories } from "./helpers";
19
+ import type { ParallelExecutorOptions, ParallelExecutorResult } from "./parallel-executor";
20
+
21
+ /**
22
+ * Options for the execution phase.
23
+ */
24
+ export interface RunnerExecutionOptions {
25
+ prdPath: string;
26
+ workdir: string;
27
+ config: NaxConfig;
28
+ hooks: LoadedHooksConfig;
29
+ feature: string;
30
+ featureDir?: string;
31
+ dryRun: boolean;
32
+ useBatch: boolean;
33
+ eventEmitter?: PipelineEventEmitter;
34
+ // biome-ignore lint/suspicious/noExplicitAny: StatusWriter interface varies by platform
35
+ statusWriter: any;
36
+ statusFile: string;
37
+ logFilePath?: string;
38
+ runId: string;
39
+ startedAt: string;
40
+ startTime: number;
41
+ formatterMode: "quiet" | "normal" | "verbose" | "json";
42
+ headless: boolean;
43
+ parallel?: number;
44
+ runParallelExecution?: (options: ParallelExecutorOptions, prd: PRD) => Promise<ParallelExecutorResult>;
45
+ }
46
+
47
+ /**
48
+ * Result from the execution phase.
49
+ */
50
+ export interface RunnerExecutionResult {
51
+ prd: PRD;
52
+ iterations: number;
53
+ storiesCompleted: number;
54
+ totalCost: number;
55
+ allStoryMetrics: StoryMetrics[];
56
+ completedEarly?: boolean;
57
+ durationMs?: number;
58
+ }
59
+
60
+ /**
61
+ * Execute the main execution phase (parallel and/or sequential paths).
62
+ *
63
+ * @param options - Execution options
64
+ * @param prd - Product requirements document
65
+ * @param pluginRegistry - Plugin registry
66
+ * @returns Execution result
67
+ */
68
+ export async function runExecutionPhase(
69
+ options: RunnerExecutionOptions,
70
+ prd: PRD,
71
+ pluginRegistry: PluginRegistry,
72
+ ): Promise<RunnerExecutionResult> {
73
+ const logger = getSafeLogger();
74
+
75
+ let iterations = 0;
76
+ let storiesCompleted = 0;
77
+ let totalCost = 0;
78
+ const allStoryMetrics: StoryMetrics[] = [];
79
+
80
+ // Output run header in headless mode
81
+ if (options.headless && options.formatterMode !== "json") {
82
+ const { outputRunHeader } = await import("./lifecycle/headless-formatter");
83
+ await outputRunHeader({
84
+ feature: options.feature,
85
+ totalStories: prd.userStories.length,
86
+ pendingStories: prd.userStories.filter((s) => s.status === "pending").length,
87
+ workdir: options.workdir,
88
+ formatterMode: options.formatterMode,
89
+ });
90
+ }
91
+
92
+ // Status write point 1: run started
93
+ options.statusWriter.setPrd(prd);
94
+ options.statusWriter.setRunStatus("running");
95
+ options.statusWriter.setCurrentStory(null);
96
+ await options.statusWriter.update(totalCost, iterations);
97
+
98
+ // Update reporters with correct totalStories count
99
+ const reporters = pluginRegistry.getReporters();
100
+ for (const reporter of reporters) {
101
+ if (reporter.onRunStart) {
102
+ try {
103
+ await reporter.onRunStart({
104
+ runId: options.runId,
105
+ feature: options.feature,
106
+ totalStories: prd.userStories.length,
107
+ startTime: options.startedAt,
108
+ });
109
+ } catch (error) {
110
+ logger?.warn("plugins", `Reporter '${reporter.name}' onRunStart failed`, { error });
111
+ }
112
+ }
113
+ }
114
+
115
+ logger?.info("execution", `Starting ${options.feature}`, {
116
+ totalStories: prd.userStories.length,
117
+ doneStories: prd.userStories.filter((s) => s.status === "passed").length,
118
+ pendingStories: prd.userStories.filter((s) => s.status === "pending").length,
119
+ batchingEnabled: options.useBatch,
120
+ });
121
+
122
+ // Clear LLM routing cache at start of new run
123
+ clearLlmCache();
124
+
125
+ // PERF-1: Precompute batch plan once from ready stories
126
+ const batchPlan = options.useBatch ? precomputeBatchPlan(getAllReadyStories(prd), 4) : [];
127
+
128
+ if (options.useBatch) {
129
+ await tryLlmBatchRoute(options.config, getAllReadyStories(prd), "routing");
130
+ }
131
+
132
+ // Parallel Execution Path (when --parallel is set)
133
+ if (options.parallel !== undefined) {
134
+ const runParallelExecution =
135
+ options.runParallelExecution ?? (await import("./parallel-executor")).runParallelExecution;
136
+ const parallelResult = await runParallelExecution(
137
+ {
138
+ prdPath: options.prdPath,
139
+ workdir: options.workdir,
140
+ config: options.config,
141
+ hooks: options.hooks,
142
+ feature: options.feature,
143
+ featureDir: options.featureDir,
144
+ parallelCount: options.parallel,
145
+ eventEmitter: options.eventEmitter,
146
+ statusWriter: options.statusWriter,
147
+ runId: options.runId,
148
+ startedAt: options.startedAt,
149
+ startTime: options.startTime,
150
+ totalCost,
151
+ iterations,
152
+ storiesCompleted,
153
+ allStoryMetrics,
154
+ pluginRegistry,
155
+ formatterMode: options.formatterMode,
156
+ headless: options.headless,
157
+ },
158
+ prd,
159
+ );
160
+
161
+ // biome-ignore lint/style/noParameterAssign: Update prd state through pipeline
162
+ prd = parallelResult.prd;
163
+ totalCost = parallelResult.totalCost;
164
+ storiesCompleted = parallelResult.storiesCompleted;
165
+ // BUG-066: merge parallel story metrics into the running accumulator
166
+ allStoryMetrics.push(...parallelResult.storyMetrics);
167
+
168
+ // If parallel execution completed everything, return early
169
+ if (parallelResult.completed && parallelResult.durationMs !== undefined) {
170
+ return {
171
+ prd,
172
+ iterations,
173
+ storiesCompleted,
174
+ totalCost,
175
+ allStoryMetrics,
176
+ completedEarly: true,
177
+ durationMs: parallelResult.durationMs,
178
+ };
179
+ }
180
+ }
181
+
182
+ // Sequential Execution Path (default)
183
+ const { executeSequential } = await import("./sequential-executor");
184
+ const sequentialResult = await executeSequential(
185
+ {
186
+ prdPath: options.prdPath,
187
+ workdir: options.workdir,
188
+ config: options.config,
189
+ hooks: options.hooks,
190
+ feature: options.feature,
191
+ featureDir: options.featureDir,
192
+ dryRun: options.dryRun,
193
+ useBatch: options.useBatch,
194
+ pluginRegistry,
195
+ eventEmitter: options.eventEmitter,
196
+ statusWriter: options.statusWriter,
197
+ logFilePath: options.logFilePath,
198
+ runId: options.runId,
199
+ startTime: options.startTime,
200
+ batchPlan,
201
+ },
202
+ prd,
203
+ );
204
+
205
+ // biome-ignore lint/style/noParameterAssign: Update prd state through pipeline
206
+ prd = sequentialResult.prd;
207
+ iterations = sequentialResult.iterations;
208
+ // BUG-064: accumulate (not overwrite) totalCost from sequential path
209
+ totalCost += sequentialResult.totalCost;
210
+ // BUG-065: accumulate (not overwrite) storiesCompleted from sequential path
211
+ storiesCompleted += sequentialResult.storiesCompleted;
212
+ allStoryMetrics.push(...sequentialResult.allStoryMetrics);
213
+
214
+ return {
215
+ prd,
216
+ iterations,
217
+ storiesCompleted,
218
+ totalCost,
219
+ allStoryMetrics,
220
+ };
221
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Runner Setup Phase
3
+ *
4
+ * Handles initial setup: loading PRD, initializing status, loggers, and crash handlers.
5
+ * Extracted from runner.ts for better code organization.
6
+ */
7
+
8
+ import type { NaxConfig } from "../config";
9
+ import type { LoadedHooksConfig } from "../hooks";
10
+
11
+ /**
12
+ * Options for the setup phase.
13
+ */
14
+ export interface RunnerSetupOptions {
15
+ prdPath: string;
16
+ workdir: string;
17
+ config: NaxConfig;
18
+ hooks: LoadedHooksConfig;
19
+ feature: string;
20
+ featureDir?: string;
21
+ dryRun: boolean;
22
+ statusFile: string;
23
+ logFilePath?: string;
24
+ runId: string;
25
+ startedAt: string;
26
+ startTime: number;
27
+ skipPrecheck?: boolean;
28
+ headless?: boolean;
29
+ formatterMode?: "quiet" | "normal" | "verbose" | "json";
30
+ getTotalCost: () => number;
31
+ getIterations: () => number;
32
+ getStoriesCompleted: () => number;
33
+ getTotalStories: () => number;
34
+ }
35
+
36
+ /**
37
+ * Result from the setup phase.
38
+ */
39
+ export interface RunnerSetupResult {
40
+ statusWriter: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["statusWriter"];
41
+ pidRegistry: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["pidRegistry"];
42
+ cleanupCrashHandlers: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["cleanupCrashHandlers"];
43
+ pluginRegistry: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["pluginRegistry"];
44
+ storyCounts: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["storyCounts"];
45
+ interactionChain: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["interactionChain"];
46
+ prd: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["prd"];
47
+ }
48
+
49
+ /**
50
+ * Execute the setup phase of the run.
51
+ *
52
+ * @param options - Setup options
53
+ * @returns Setup result with initialized components
54
+ */
55
+ export async function runSetupPhase(options: RunnerSetupOptions): Promise<RunnerSetupResult> {
56
+ // ── Execute initial setup phase ──────────────────────────────────────────────
57
+ const { setupRun } = await import("./lifecycle/run-setup");
58
+ const setupResult = await setupRun({
59
+ prdPath: options.prdPath,
60
+ workdir: options.workdir,
61
+ config: options.config,
62
+ hooks: options.hooks,
63
+ feature: options.feature,
64
+ featureDir: options.featureDir,
65
+ dryRun: options.dryRun,
66
+ statusFile: options.statusFile,
67
+ logFilePath: options.logFilePath,
68
+ runId: options.runId,
69
+ startedAt: options.startedAt,
70
+ startTime: options.startTime,
71
+ skipPrecheck: options.skipPrecheck ?? false,
72
+ headless: options.headless ?? false,
73
+ formatterMode: options.formatterMode ?? "normal",
74
+ getTotalCost: options.getTotalCost,
75
+ getIterations: options.getIterations,
76
+ // BUG-017: Pass getters for run.complete event on SIGTERM
77
+ getStoriesCompleted: options.getStoriesCompleted,
78
+ getTotalStories: options.getTotalStories,
79
+ });
80
+
81
+ return setupResult;
82
+ }
@@ -6,23 +6,24 @@
6
6
  * 2. Run pipeline for each story/batch
7
7
  * 3. Handle pipeline results (escalate, mark complete, etc.)
8
8
  * 4. Loop until complete or blocked
9
+ *
10
+ * Delegates to extracted modules for each phase:
11
+ * - runner-setup.ts: Initial setup (PRD, status, loggers)
12
+ * - runner-execution.ts: Parallel/sequential execution
13
+ * - runner-completion.ts: Acceptance loop, hooks, metrics
9
14
  */
10
15
 
11
16
  import type { NaxConfig } from "../config";
12
17
  import type { LoadedHooksConfig } from "../hooks";
13
18
  import { fireHook } from "../hooks";
14
19
  import { getSafeLogger } from "../logger";
15
- import type { StoryMetrics } from "../metrics";
16
20
  import type { PipelineEventEmitter } from "../pipeline/events";
17
21
  import { countStories, isComplete } from "../prd";
18
- import type { UserStory } from "../prd";
19
- import { tryLlmBatchRoute } from "../routing/batch-route";
20
- import { clearCache as clearLlmCache, routeBatch as llmRouteBatch } from "../routing/strategies/llm";
21
- import { precomputeBatchPlan } from "./batching";
22
- import { stopHeartbeat, writeExitSummary } from "./crash-recovery";
23
- import { getAllReadyStories } from "./helpers";
22
+ import { stopHeartbeat } from "./crash-recovery";
24
23
  import type { ParallelExecutorOptions, ParallelExecutorResult } from "./parallel-executor";
25
- import { hookCtx } from "./story-context";
24
+ import { type RunnerCompletionOptions, runCompletionPhase } from "./runner-completion";
25
+ import { type RunnerExecutionOptions, runExecutionPhase } from "./runner-execution";
26
+ import { type RunnerSetupOptions, runSetupPhase } from "./runner-setup";
26
27
 
27
28
  /**
28
29
  * Injectable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
@@ -110,16 +111,17 @@ export async function run(options: RunOptions): Promise<RunResult> {
110
111
  let iterations = 0;
111
112
  let storiesCompleted = 0;
112
113
  let totalCost = 0;
113
- const allStoryMetrics: StoryMetrics[] = [];
114
+ // biome-ignore lint/suspicious/noExplicitAny: Metrics array type varies
115
+ const allStoryMetrics: any[] = [];
114
116
 
115
117
  const logger = getSafeLogger();
116
118
 
117
- // Declare prd before crash handler setup to avoid TDZ if SIGTERM arrives during setupRun
118
- let prd: Awaited<ReturnType<typeof import("./lifecycle/run-setup").setupRun>>["prd"] | undefined;
119
+ // Declare prd before crash handler setup to avoid TDZ if SIGTERM arrives during setup
120
+ // biome-ignore lint/suspicious/noExplicitAny: PRD type initialized during setup
121
+ let prd: any | undefined;
119
122
 
120
- // ── Execute initial setup phase ──────────────────────────────────────────────
121
- const { setupRun } = await import("./lifecycle/run-setup");
122
- const setupResult = await setupRun({
123
+ // ── Phase 1: Setup ──────────────────────────────────────────────────────────
124
+ const setupResult = await runSetupPhase({
123
125
  prdPath,
124
126
  workdir,
125
127
  config,
@@ -142,119 +144,12 @@ export async function run(options: RunOptions): Promise<RunResult> {
142
144
  getTotalStories: () => (prd ? countStories(prd).total : 0),
143
145
  });
144
146
 
145
- const {
146
- statusWriter,
147
- pidRegistry,
148
- cleanupCrashHandlers,
149
- pluginRegistry,
150
- storyCounts: counts,
151
- interactionChain,
152
- } = setupResult;
147
+ const { statusWriter, pidRegistry, cleanupCrashHandlers, pluginRegistry, interactionChain } = setupResult;
153
148
  prd = setupResult.prd;
154
149
 
155
150
  try {
156
- // ── Output run header in headless mode ─────────────────────────────────
157
- if (headless && formatterMode !== "json") {
158
- const { outputRunHeader } = await import("./lifecycle/headless-formatter");
159
- await outputRunHeader({
160
- feature,
161
- totalStories: counts.total,
162
- pendingStories: counts.pending,
163
- workdir,
164
- formatterMode,
165
- });
166
- }
167
-
168
- // ── Status write point 1: run started ───────────────────────────────────
169
- statusWriter.setPrd(prd);
170
- statusWriter.setRunStatus("running");
171
- statusWriter.setCurrentStory(null);
172
- await statusWriter.update(totalCost, iterations);
173
-
174
- // Update reporters with correct totalStories count
175
- const reporters = pluginRegistry.getReporters();
176
- for (const reporter of reporters) {
177
- if (reporter.onRunStart) {
178
- try {
179
- await reporter.onRunStart({
180
- runId,
181
- feature,
182
- totalStories: counts.total,
183
- startTime: runStartedAt,
184
- });
185
- } catch (error) {
186
- logger?.warn("plugins", `Reporter '${reporter.name}' onRunStart failed`, { error });
187
- }
188
- }
189
- }
190
-
191
- logger?.info("execution", `Starting ${feature}`, {
192
- totalStories: counts.total,
193
- doneStories: counts.passed,
194
- pendingStories: counts.pending,
195
- batchingEnabled: useBatch,
196
- });
197
-
198
- // Clear LLM routing cache at start of new run
199
- clearLlmCache();
200
-
201
- // PERF-1: Precompute batch plan once from ready stories
202
- const batchPlan = useBatch ? precomputeBatchPlan(getAllReadyStories(prd), 4) : [];
203
-
204
- if (useBatch) {
205
- await tryLlmBatchRoute(config, getAllReadyStories(prd), "routing");
206
- }
207
-
208
- // ── Parallel Execution Path (when --parallel is set) ──────────────────────
209
- if (options.parallel !== undefined) {
210
- const runParallelExecution =
211
- _runnerDeps.runParallelExecution ?? (await import("./parallel-executor")).runParallelExecution;
212
- const parallelResult = await runParallelExecution(
213
- {
214
- prdPath,
215
- workdir,
216
- config,
217
- hooks,
218
- feature,
219
- featureDir,
220
- parallelCount: options.parallel,
221
- eventEmitter,
222
- statusWriter,
223
- runId,
224
- startedAt: runStartedAt,
225
- startTime,
226
- totalCost,
227
- iterations,
228
- storiesCompleted,
229
- allStoryMetrics,
230
- pluginRegistry,
231
- formatterMode,
232
- headless,
233
- },
234
- prd,
235
- );
236
-
237
- prd = parallelResult.prd;
238
- totalCost = parallelResult.totalCost;
239
- storiesCompleted = parallelResult.storiesCompleted;
240
- // BUG-066: merge parallel story metrics into the running accumulator
241
- allStoryMetrics.push(...parallelResult.storyMetrics);
242
-
243
- // If parallel execution completed everything, return early
244
- if (parallelResult.completed && parallelResult.durationMs !== undefined) {
245
- return {
246
- success: true,
247
- iterations,
248
- storiesCompleted,
249
- totalCost,
250
- durationMs: parallelResult.durationMs,
251
- };
252
- }
253
- }
254
-
255
- // ── Sequential Execution Path (default) ────────────────────────────────────
256
- const { executeSequential } = await import("./sequential-executor");
257
- const sequentialResult = await executeSequential(
151
+ // ── Phase 2: Execution ──────────────────────────────────────────────────────
152
+ const executionResult = await runExecutionPhase(
258
153
  {
259
154
  prdPath,
260
155
  workdir,
@@ -264,108 +159,64 @@ export async function run(options: RunOptions): Promise<RunResult> {
264
159
  featureDir,
265
160
  dryRun,
266
161
  useBatch,
267
- pluginRegistry,
268
162
  eventEmitter,
269
163
  statusWriter,
164
+ statusFile,
270
165
  logFilePath,
271
166
  runId,
167
+ startedAt: runStartedAt,
272
168
  startTime,
273
- batchPlan,
169
+ formatterMode,
170
+ headless,
171
+ parallel,
172
+ runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
274
173
  },
275
174
  prd,
175
+ pluginRegistry,
276
176
  );
277
177
 
278
- prd = sequentialResult.prd;
279
- iterations = sequentialResult.iterations;
280
- // BUG-064: accumulate (not overwrite) totalCost from sequential path
281
- totalCost += sequentialResult.totalCost;
282
- // BUG-065: accumulate (not overwrite) storiesCompleted from sequential path
283
- storiesCompleted += sequentialResult.storiesCompleted;
284
- allStoryMetrics.push(...sequentialResult.allStoryMetrics);
178
+ prd = executionResult.prd;
179
+ iterations = executionResult.iterations;
180
+ storiesCompleted = executionResult.storiesCompleted;
181
+ totalCost = executionResult.totalCost;
182
+ allStoryMetrics.push(...executionResult.allStoryMetrics);
285
183
 
286
- // After main loop: Check if we need acceptance retry loop
287
- if (config.acceptance.enabled && isComplete(prd)) {
288
- const { runAcceptanceLoop } = await import("./lifecycle/acceptance-loop");
289
- const acceptanceResult = await runAcceptanceLoop({
290
- config,
291
- prd,
292
- prdPath,
293
- workdir,
294
- featureDir,
295
- hooks,
296
- feature,
297
- totalCost,
184
+ // Return early if parallel execution completed everything
185
+ if (executionResult.completedEarly && executionResult.durationMs !== undefined) {
186
+ return {
187
+ success: isComplete(prd),
298
188
  iterations,
299
189
  storiesCompleted,
300
- allStoryMetrics,
301
- pluginRegistry,
302
- eventEmitter,
303
- statusWriter,
304
- });
305
-
306
- prd = acceptanceResult.prd;
307
- totalCost = acceptanceResult.totalCost;
308
- iterations = acceptanceResult.iterations;
309
- storiesCompleted = acceptanceResult.storiesCompleted;
310
- }
311
-
312
- // Fire on-all-stories-complete before regression gate (RL-001)
313
- if (isComplete(prd)) {
314
- await _runnerDeps.fireHook(
315
- hooks,
316
- "on-all-stories-complete",
317
- hookCtx(feature, { status: "passed", cost: totalCost }),
318
- workdir,
319
- );
190
+ totalCost,
191
+ durationMs: executionResult.durationMs,
192
+ };
320
193
  }
321
194
 
322
- // Handle run completion: save metrics, log summary, update status
323
- const { handleRunCompletion } = await import("./lifecycle/run-completion");
324
- const completionResult = await handleRunCompletion({
325
- runId,
195
+ // ── Phase 3: Completion ────────────────────────────────────────────────────
196
+ const completionResult = await runCompletionPhase({
197
+ config,
198
+ hooks,
326
199
  feature,
200
+ workdir,
201
+ statusFile,
202
+ logFilePath,
203
+ runId,
327
204
  startedAt: runStartedAt,
205
+ startTime,
206
+ formatterMode,
207
+ headless,
208
+ featureDir,
328
209
  prd,
329
210
  allStoryMetrics,
330
211
  totalCost,
331
212
  storiesCompleted,
332
213
  iterations,
333
- startTime,
334
- workdir,
335
214
  statusWriter,
336
- config,
215
+ pluginRegistry,
216
+ eventEmitter,
337
217
  });
338
218
 
339
- const { durationMs, runCompletedAt, finalCounts } = completionResult;
340
-
341
- // ── Write feature-level status (SFC-002) ────────────────────────────────
342
- if (featureDir) {
343
- const finalStatus = isComplete(prd) ? "completed" : "failed";
344
- statusWriter.setRunStatus(finalStatus);
345
- await statusWriter.writeFeatureStatus(featureDir, totalCost, iterations);
346
- }
347
-
348
- // ── Output run footer in headless mode ─────────────────────────────────
349
- if (headless && formatterMode !== "json") {
350
- const { outputRunFooter } = await import("./lifecycle/headless-formatter");
351
- outputRunFooter({
352
- finalCounts: {
353
- total: finalCounts.total,
354
- passed: finalCounts.passed,
355
- failed: finalCounts.failed,
356
- skipped: finalCounts.skipped,
357
- },
358
- durationMs,
359
- totalCost,
360
- startedAt: runStartedAt,
361
- completedAt: runCompletedAt,
362
- formatterMode,
363
- });
364
- }
365
-
366
- // Stop heartbeat and write exit summary (US-007)
367
- stopHeartbeat();
368
- await writeExitSummary(logFilePath, totalCost, iterations, storiesCompleted, durationMs);
219
+ const { durationMs } = completionResult;
369
220
 
370
221
  return {
371
222
  success: isComplete(prd),