@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.
- package/dist/nax.js +3258 -2894
- package/package.json +4 -1
- package/src/agents/claude-complete.ts +72 -0
- package/src/agents/claude-execution.ts +189 -0
- package/src/agents/claude-interactive.ts +77 -0
- package/src/agents/claude-plan.ts +23 -8
- package/src/agents/claude.ts +64 -349
- package/src/analyze/classifier.ts +2 -1
- package/src/cli/config-descriptions.ts +206 -0
- package/src/cli/config-diff.ts +103 -0
- package/src/cli/config-display.ts +285 -0
- package/src/cli/config-get.ts +55 -0
- package/src/cli/config.ts +7 -618
- package/src/cli/prompts-export.ts +58 -0
- package/src/cli/prompts-init.ts +200 -0
- package/src/cli/prompts-main.ts +237 -0
- package/src/cli/prompts-tdd.ts +78 -0
- package/src/cli/prompts.ts +10 -541
- package/src/commands/logs-formatter.ts +201 -0
- package/src/commands/logs-reader.ts +171 -0
- package/src/commands/logs.ts +11 -362
- package/src/config/loader.ts +4 -15
- package/src/config/runtime-types.ts +448 -0
- package/src/config/schema-types.ts +53 -0
- package/src/config/types.ts +49 -486
- package/src/context/auto-detect.ts +2 -1
- package/src/context/builder.ts +3 -2
- package/src/execution/crash-heartbeat.ts +77 -0
- package/src/execution/crash-recovery.ts +23 -365
- package/src/execution/crash-signals.ts +149 -0
- package/src/execution/crash-writer.ts +154 -0
- package/src/execution/parallel-coordinator.ts +278 -0
- package/src/execution/parallel-executor-rectification-pass.ts +117 -0
- package/src/execution/parallel-executor-rectify.ts +135 -0
- package/src/execution/parallel-executor.ts +19 -211
- package/src/execution/parallel-worker.ts +148 -0
- package/src/execution/parallel.ts +5 -404
- package/src/execution/pid-registry.ts +3 -8
- package/src/execution/runner-completion.ts +160 -0
- package/src/execution/runner-execution.ts +221 -0
- package/src/execution/runner-setup.ts +82 -0
- package/src/execution/runner.ts +53 -202
- package/src/execution/timeout-handler.ts +100 -0
- package/src/hooks/runner.ts +11 -21
- package/src/metrics/tracker.ts +7 -30
- package/src/pipeline/runner.ts +2 -1
- package/src/pipeline/stages/completion.ts +0 -1
- package/src/pipeline/stages/context.ts +2 -1
- package/src/plugins/extensions.ts +225 -0
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/types.ts +16 -221
- package/src/prd/index.ts +2 -1
- package/src/prd/validate.ts +41 -0
- package/src/precheck/checks-blockers.ts +15 -419
- package/src/precheck/checks-cli.ts +68 -0
- package/src/precheck/checks-config.ts +102 -0
- package/src/precheck/checks-git.ts +87 -0
- package/src/precheck/checks-system.ts +163 -0
- package/src/review/orchestrator.ts +19 -6
- package/src/review/runner.ts +17 -5
- package/src/routing/chain.ts +2 -1
- package/src/routing/loader.ts +2 -5
- package/src/tdd/orchestrator.ts +2 -1
- package/src/tdd/verdict-reader.ts +266 -0
- package/src/tdd/verdict.ts +6 -271
- package/src/utils/errors.ts +12 -0
- package/src/utils/git.ts +12 -5
- package/src/utils/json-file.ts +72 -0
- package/src/verification/executor.ts +2 -1
- package/src/verification/smart-runner.ts +23 -3
- package/src/worktree/manager.ts +9 -3
- 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
|
+
}
|
package/src/execution/runner.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
118
|
-
|
|
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
|
-
// ──
|
|
121
|
-
const
|
|
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
|
-
// ──
|
|
157
|
-
|
|
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
|
-
|
|
169
|
+
formatterMode,
|
|
170
|
+
headless,
|
|
171
|
+
parallel,
|
|
172
|
+
runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
|
|
274
173
|
},
|
|
275
174
|
prd,
|
|
175
|
+
pluginRegistry,
|
|
276
176
|
);
|
|
277
177
|
|
|
278
|
-
prd =
|
|
279
|
-
iterations =
|
|
280
|
-
|
|
281
|
-
totalCost
|
|
282
|
-
|
|
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
|
-
//
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
//
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
215
|
+
pluginRegistry,
|
|
216
|
+
eventEmitter,
|
|
337
217
|
});
|
|
338
218
|
|
|
339
|
-
const { durationMs
|
|
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),
|