@nathapp/nax 0.36.1 → 0.36.2
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 +323 -51
- package/package.json +1 -1
- package/src/execution/dry-run.ts +1 -1
- package/src/execution/escalation/escalation.ts +5 -3
- package/src/execution/escalation/tier-escalation.ts +41 -4
- package/src/execution/iteration-runner.ts +5 -0
- package/src/execution/parallel-executor.ts +293 -9
- package/src/execution/parallel.ts +40 -21
- package/src/execution/pipeline-result-handler.ts +3 -2
- package/src/execution/runner.ts +13 -3
- package/src/metrics/tracker.ts +8 -4
- package/src/metrics/types.ts +2 -0
- package/src/pipeline/event-bus.ts +1 -1
- package/src/pipeline/stages/completion.ts +1 -1
- package/src/pipeline/stages/verify.ts +8 -1
- package/src/pipeline/subscribers/reporters.ts +3 -3
- package/src/pipeline/types.ts +4 -0
- package/src/plugins/types.ts +1 -1
- package/src/prd/types.ts +2 -0
- package/src/tdd/types.ts +2 -1
- package/src/verification/crash-detector.ts +34 -0
- package/src/verification/orchestrator-types.ts +8 -1
package/package.json
CHANGED
package/src/execution/dry-run.ts
CHANGED
|
@@ -22,18 +22,20 @@ import type { TierConfig } from "../../config";
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export function escalateTier(currentTier: string, tierOrder: TierConfig[]): string | null {
|
|
25
|
-
const
|
|
25
|
+
const getName = (t: TierConfig) => t.tier ?? (t as unknown as { name?: string }).name ?? null;
|
|
26
|
+
const currentIndex = tierOrder.findIndex((t) => getName(t) === currentTier);
|
|
26
27
|
if (currentIndex === -1 || currentIndex === tierOrder.length - 1) {
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
29
|
-
return tierOrder[currentIndex + 1]
|
|
30
|
+
return getName(tierOrder[currentIndex + 1]);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Get the tier config for a given tier name.
|
|
34
35
|
*/
|
|
35
36
|
export function getTierConfig(tierName: string, tierOrder: TierConfig[]): TierConfig | undefined {
|
|
36
|
-
|
|
37
|
+
const getName = (t: TierConfig) => t.tier ?? (t as unknown as { name?: string }).name ?? null;
|
|
38
|
+
return tierOrder.find((t) => getName(t) === tierName);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
@@ -24,6 +24,7 @@ function buildEscalationFailure(
|
|
|
24
24
|
story: UserStory,
|
|
25
25
|
currentTier: string,
|
|
26
26
|
reviewFindings?: import("../../plugins/types").ReviewFinding[],
|
|
27
|
+
cost?: number,
|
|
27
28
|
): StructuredFailure {
|
|
28
29
|
return {
|
|
29
30
|
attempt: (story.attempts ?? 0) + 1,
|
|
@@ -31,6 +32,7 @@ function buildEscalationFailure(
|
|
|
31
32
|
stage: "escalation" as const,
|
|
32
33
|
summary: `Failed with tier ${currentTier}, escalating to next tier`,
|
|
33
34
|
reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
|
|
35
|
+
cost: cost ?? 0,
|
|
34
36
|
timestamp: new Date().toISOString(),
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -54,6 +56,8 @@ export function resolveMaxAttemptsOutcome(failureCategory?: FailureCategory): "p
|
|
|
54
56
|
case "verifier-rejected":
|
|
55
57
|
case "greenfield-no-tests":
|
|
56
58
|
return "pause";
|
|
59
|
+
case "runtime-crash":
|
|
60
|
+
return "pause";
|
|
57
61
|
case "session-failure":
|
|
58
62
|
case "tests-failing":
|
|
59
63
|
return "fail";
|
|
@@ -208,14 +212,38 @@ export interface EscalationHandlerContext {
|
|
|
208
212
|
feature: string;
|
|
209
213
|
totalCost: number;
|
|
210
214
|
workdir: string;
|
|
215
|
+
/** Verify result from the pipeline verify stage — used to detect RUNTIME_CRASH (BUG-070) */
|
|
216
|
+
verifyResult?: { status: string; success: boolean };
|
|
217
|
+
/** Cost of the failed attempt being escalated (BUG-067: accumulated across escalations) */
|
|
218
|
+
attemptCost?: number;
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
export interface EscalationHandlerResult {
|
|
214
|
-
outcome: "escalated" | "paused" | "failed";
|
|
222
|
+
outcome: "escalated" | "paused" | "failed" | "retry-same";
|
|
215
223
|
prdDirty: boolean;
|
|
216
224
|
prd: PRD;
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Determine if the pipeline should retry the same tier due to a transient runtime crash.
|
|
229
|
+
*
|
|
230
|
+
* Returns true when the verify result status is RUNTIME_CRASH — these are Bun
|
|
231
|
+
* runtime-level failures, not code quality issues, so escalating the model tier
|
|
232
|
+
* would not help. Instead the same tier should be retried.
|
|
233
|
+
*
|
|
234
|
+
* @param verifyResult - Verify result from the pipeline verify stage
|
|
235
|
+
*/
|
|
236
|
+
export function shouldRetrySameTier(verifyResult: { status: string; success: boolean } | undefined): boolean {
|
|
237
|
+
return verifyResult?.status === "RUNTIME_CRASH";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Swappable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
242
|
+
*/
|
|
243
|
+
export const _tierEscalationDeps = {
|
|
244
|
+
savePRD,
|
|
245
|
+
};
|
|
246
|
+
|
|
219
247
|
/**
|
|
220
248
|
* Handle tier escalation after pipeline escalation action
|
|
221
249
|
*
|
|
@@ -223,6 +251,15 @@ export interface EscalationHandlerResult {
|
|
|
223
251
|
*/
|
|
224
252
|
export async function handleTierEscalation(ctx: EscalationHandlerContext): Promise<EscalationHandlerResult> {
|
|
225
253
|
const logger = getSafeLogger();
|
|
254
|
+
|
|
255
|
+
// BUG-070: Runtime crashes are transient — retry same tier, do NOT escalate
|
|
256
|
+
if (shouldRetrySameTier(ctx.verifyResult)) {
|
|
257
|
+
logger?.warn("escalation", "Runtime crash detected — retrying same tier (transient, not a code issue)", {
|
|
258
|
+
storyId: ctx.story.id,
|
|
259
|
+
});
|
|
260
|
+
return { outcome: "retry-same", prdDirty: false, prd: ctx.prd };
|
|
261
|
+
}
|
|
262
|
+
|
|
226
263
|
const nextTier = escalateTier(ctx.routing.modelTier, ctx.config.autoMode.escalation.tierOrder);
|
|
227
264
|
const escalateWholeBatch = ctx.config.autoMode.escalation.escalateEntireBatch ?? true;
|
|
228
265
|
const storiesToEscalate = ctx.isBatchExecution && escalateWholeBatch ? ctx.storiesToExecute : [ctx.story];
|
|
@@ -294,8 +331,8 @@ export async function handleTierEscalation(ctx: EscalationHandlerContext): Promi
|
|
|
294
331
|
const isChangingTier = currentStoryTier !== nextTier;
|
|
295
332
|
const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
|
|
296
333
|
|
|
297
|
-
// Build escalation failure
|
|
298
|
-
const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings);
|
|
334
|
+
// Build escalation failure (BUG-067: include cost for accumulatedAttemptCost in metrics)
|
|
335
|
+
const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost);
|
|
299
336
|
|
|
300
337
|
return {
|
|
301
338
|
...s,
|
|
@@ -307,7 +344,7 @@ export async function handleTierEscalation(ctx: EscalationHandlerContext): Promi
|
|
|
307
344
|
}) as PRD["userStories"],
|
|
308
345
|
} as PRD;
|
|
309
346
|
|
|
310
|
-
await savePRD(updatedPrd, ctx.prdPath);
|
|
347
|
+
await _tierEscalationDeps.savePRD(updatedPrd, ctx.prdPath);
|
|
311
348
|
|
|
312
349
|
// Clear routing cache for all escalated stories to avoid returning old cached decisions
|
|
313
350
|
for (const story of storiesToEscalate) {
|
|
@@ -60,6 +60,10 @@ export async function runIteration(
|
|
|
60
60
|
|
|
61
61
|
const storyStartTime = Date.now();
|
|
62
62
|
const storyGitRef = await captureGitRef(ctx.workdir);
|
|
63
|
+
|
|
64
|
+
// BUG-067: Accumulate cost from all prior failed attempts (stored in priorFailures by handleTierEscalation)
|
|
65
|
+
const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
|
|
66
|
+
|
|
63
67
|
const pipelineContext: PipelineContext = {
|
|
64
68
|
config: ctx.config,
|
|
65
69
|
prd,
|
|
@@ -74,6 +78,7 @@ export async function runIteration(
|
|
|
74
78
|
storyStartTime: new Date().toISOString(),
|
|
75
79
|
storyGitRef: storyGitRef ?? undefined,
|
|
76
80
|
interaction: ctx.interactionChain ?? undefined,
|
|
81
|
+
accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined,
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
ctx.statusWriter.setPrd(prd);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Handles the full parallel execution flow:
|
|
5
5
|
* - Status updates with parallel info
|
|
6
6
|
* - Execute parallel stories
|
|
7
|
+
* - Rectify merge conflicts (MFX-005): re-run conflicted stories sequentially
|
|
8
|
+
* on the updated base branch so each sees all previously merged stories
|
|
7
9
|
* - Handle completion or continue to sequential
|
|
8
10
|
*/
|
|
9
11
|
|
|
@@ -17,17 +19,147 @@ import type { StoryMetrics } from "../metrics";
|
|
|
17
19
|
import type { PipelineEventEmitter } from "../pipeline/events";
|
|
18
20
|
import type { PluginRegistry } from "../plugins/registry";
|
|
19
21
|
import type { PRD } from "../prd";
|
|
20
|
-
import { countStories, isComplete } from "../prd";
|
|
22
|
+
import { countStories, isComplete, markStoryPassed } from "../prd";
|
|
21
23
|
import { getAllReadyStories, hookCtx } from "./helpers";
|
|
22
24
|
import { executeParallel } from "./parallel";
|
|
23
25
|
import type { StatusWriter } from "./status-writer";
|
|
24
26
|
|
|
27
|
+
/** StoryMetrics extended with execution-path source */
|
|
28
|
+
export type ParallelStoryMetrics = StoryMetrics & {
|
|
29
|
+
source: "parallel" | "sequential" | "rectification";
|
|
30
|
+
rectifiedFromConflict?: boolean;
|
|
31
|
+
originalCost?: number;
|
|
32
|
+
rectificationCost?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** A story that conflicted during the initial parallel merge pass */
|
|
36
|
+
export interface ConflictedStoryInfo {
|
|
37
|
+
storyId: string;
|
|
38
|
+
conflictFiles: string[];
|
|
39
|
+
originalCost: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Result from attempting to rectify a single conflicted story */
|
|
43
|
+
export type RectificationResult =
|
|
44
|
+
| { success: true; storyId: string; cost: number }
|
|
45
|
+
| {
|
|
46
|
+
success: false;
|
|
47
|
+
storyId: string;
|
|
48
|
+
cost: number;
|
|
49
|
+
finalConflict: boolean;
|
|
50
|
+
pipelineFailure?: boolean;
|
|
51
|
+
conflictFiles?: string[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Options passed to rectifyConflictedStory */
|
|
55
|
+
export interface RectifyConflictedStoryOptions extends ConflictedStoryInfo {
|
|
56
|
+
workdir: string;
|
|
57
|
+
config: NaxConfig;
|
|
58
|
+
hooks: LoadedHooksConfig;
|
|
59
|
+
pluginRegistry: PluginRegistry;
|
|
60
|
+
prd: PRD;
|
|
61
|
+
eventEmitter?: PipelineEventEmitter;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Actual implementation of rectifyConflictedStory.
|
|
66
|
+
*
|
|
67
|
+
* Steps:
|
|
68
|
+
* 1. Remove the old worktree
|
|
69
|
+
* 2. Create a fresh worktree from current HEAD (post-merge state)
|
|
70
|
+
* 3. Re-run the full story pipeline
|
|
71
|
+
* 4. Attempt merge on the updated base
|
|
72
|
+
* 5. Return success/finalConflict
|
|
73
|
+
*/
|
|
74
|
+
async function rectifyConflictedStory(options: RectifyConflictedStoryOptions): Promise<RectificationResult> {
|
|
75
|
+
const { storyId, workdir, config, hooks, pluginRegistry, prd, eventEmitter } = options;
|
|
76
|
+
const logger = getSafeLogger();
|
|
77
|
+
|
|
78
|
+
logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const { WorktreeManager } = await import("../worktree/manager");
|
|
82
|
+
const { MergeEngine } = await import("../worktree/merge");
|
|
83
|
+
const { runPipeline } = await import("../pipeline/runner");
|
|
84
|
+
const { defaultPipeline } = await import("../pipeline/stages");
|
|
85
|
+
const { routeTask } = await import("../routing");
|
|
86
|
+
|
|
87
|
+
const worktreeManager = new WorktreeManager();
|
|
88
|
+
const mergeEngine = new MergeEngine(worktreeManager);
|
|
89
|
+
|
|
90
|
+
// Step 1: Remove old worktree
|
|
91
|
+
try {
|
|
92
|
+
await worktreeManager.remove(workdir, storyId);
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore — worktree may have already been removed
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 2: Create fresh worktree from current HEAD
|
|
98
|
+
await worktreeManager.create(workdir, storyId);
|
|
99
|
+
const worktreePath = path.join(workdir, ".nax-wt", storyId);
|
|
100
|
+
|
|
101
|
+
// Step 3: Re-run the story pipeline
|
|
102
|
+
const story = prd.userStories.find((s) => s.id === storyId);
|
|
103
|
+
if (!story) {
|
|
104
|
+
return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config);
|
|
108
|
+
|
|
109
|
+
const pipelineContext = {
|
|
110
|
+
config,
|
|
111
|
+
prd,
|
|
112
|
+
story,
|
|
113
|
+
stories: [story],
|
|
114
|
+
workdir: worktreePath,
|
|
115
|
+
featureDir: undefined,
|
|
116
|
+
hooks,
|
|
117
|
+
plugins: pluginRegistry,
|
|
118
|
+
storyStartTime: new Date().toISOString(),
|
|
119
|
+
routing: routing as import("../pipeline/types").RoutingResult,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
|
|
123
|
+
const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
|
|
124
|
+
|
|
125
|
+
if (!pipelineResult.success) {
|
|
126
|
+
logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
|
|
127
|
+
return { success: false, storyId, cost, finalConflict: false, pipelineFailure: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Step 4: Attempt merge on updated base
|
|
131
|
+
const mergeResults = await mergeEngine.mergeAll(workdir, [storyId], { [storyId]: [] });
|
|
132
|
+
const mergeResult = mergeResults[0];
|
|
133
|
+
|
|
134
|
+
if (!mergeResult || !mergeResult.success) {
|
|
135
|
+
const conflictFiles = mergeResult?.conflictFiles ?? [];
|
|
136
|
+
logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
|
|
137
|
+
return { success: false, storyId, cost, finalConflict: true, conflictFiles };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logger?.info("parallel", "Rectification succeeded - story merged", {
|
|
141
|
+
storyId,
|
|
142
|
+
originalCost: options.originalCost,
|
|
143
|
+
rectificationCost: cost,
|
|
144
|
+
});
|
|
145
|
+
return { success: true, storyId, cost };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger?.error("parallel", "Rectification failed - preserving worktree", {
|
|
148
|
+
storyId,
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
25
155
|
/**
|
|
26
156
|
* Injectable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
|
|
27
157
|
* @internal - test use only.
|
|
28
158
|
*/
|
|
29
159
|
export const _parallelExecutorDeps = {
|
|
30
160
|
fireHook,
|
|
161
|
+
executeParallel,
|
|
162
|
+
rectifyConflictedStory,
|
|
31
163
|
};
|
|
32
164
|
|
|
33
165
|
export interface ParallelExecutorOptions {
|
|
@@ -52,12 +184,101 @@ export interface ParallelExecutorOptions {
|
|
|
52
184
|
headless: boolean;
|
|
53
185
|
}
|
|
54
186
|
|
|
187
|
+
export interface RectificationStats {
|
|
188
|
+
rectified: number;
|
|
189
|
+
stillConflicting: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
55
192
|
export interface ParallelExecutorResult {
|
|
56
193
|
prd: PRD;
|
|
57
194
|
totalCost: number;
|
|
58
195
|
storiesCompleted: number;
|
|
59
196
|
completed: boolean;
|
|
60
197
|
durationMs?: number;
|
|
198
|
+
/** Per-story metrics for stories completed via the parallel path */
|
|
199
|
+
storyMetrics: ParallelStoryMetrics[];
|
|
200
|
+
/** Stats from the merge-conflict rectification pass (MFX-005) */
|
|
201
|
+
rectificationStats: RectificationStats;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run the rectification pass: sequentially re-run each conflicted story on
|
|
206
|
+
* the updated base (which already includes all clean merges from the first pass).
|
|
207
|
+
*/
|
|
208
|
+
async function runRectificationPass(
|
|
209
|
+
conflictedStories: ConflictedStoryInfo[],
|
|
210
|
+
options: ParallelExecutorOptions,
|
|
211
|
+
prd: PRD,
|
|
212
|
+
): Promise<{
|
|
213
|
+
rectifiedCount: number;
|
|
214
|
+
stillConflictingCount: number;
|
|
215
|
+
additionalCost: number;
|
|
216
|
+
updatedPrd: PRD;
|
|
217
|
+
rectificationMetrics: ParallelStoryMetrics[];
|
|
218
|
+
}> {
|
|
219
|
+
const logger = getSafeLogger();
|
|
220
|
+
const { workdir, config, hooks, pluginRegistry, eventEmitter } = options;
|
|
221
|
+
const rectificationMetrics: ParallelStoryMetrics[] = [];
|
|
222
|
+
let rectifiedCount = 0;
|
|
223
|
+
let stillConflictingCount = 0;
|
|
224
|
+
let additionalCost = 0;
|
|
225
|
+
|
|
226
|
+
logger?.info("parallel", "Starting merge conflict rectification", {
|
|
227
|
+
stories: conflictedStories.map((s) => s.storyId),
|
|
228
|
+
totalConflicts: conflictedStories.length,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Sequential — each story sees all previously rectified stories in the base
|
|
232
|
+
for (const conflictInfo of conflictedStories) {
|
|
233
|
+
const result = await _parallelExecutorDeps.rectifyConflictedStory({
|
|
234
|
+
...conflictInfo,
|
|
235
|
+
workdir,
|
|
236
|
+
config,
|
|
237
|
+
hooks,
|
|
238
|
+
pluginRegistry,
|
|
239
|
+
prd,
|
|
240
|
+
eventEmitter,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
additionalCost += result.cost;
|
|
244
|
+
|
|
245
|
+
if (result.success) {
|
|
246
|
+
markStoryPassed(prd, result.storyId);
|
|
247
|
+
rectifiedCount++;
|
|
248
|
+
|
|
249
|
+
rectificationMetrics.push({
|
|
250
|
+
storyId: result.storyId,
|
|
251
|
+
complexity: "unknown",
|
|
252
|
+
modelTier: "parallel",
|
|
253
|
+
modelUsed: "parallel",
|
|
254
|
+
attempts: 1,
|
|
255
|
+
finalTier: "parallel",
|
|
256
|
+
success: true,
|
|
257
|
+
cost: result.cost,
|
|
258
|
+
durationMs: 0,
|
|
259
|
+
firstPassSuccess: false,
|
|
260
|
+
startedAt: new Date().toISOString(),
|
|
261
|
+
completedAt: new Date().toISOString(),
|
|
262
|
+
source: "rectification" as const,
|
|
263
|
+
rectifiedFromConflict: true,
|
|
264
|
+
originalCost: conflictInfo.originalCost,
|
|
265
|
+
rectificationCost: result.cost,
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
const isFinalConflict = result.finalConflict === true;
|
|
269
|
+
if (isFinalConflict) {
|
|
270
|
+
stillConflictingCount++;
|
|
271
|
+
}
|
|
272
|
+
// pipelineFailure — not counted as structural conflict, story remains failed
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
logger?.info("parallel", "Rectification complete", {
|
|
277
|
+
rectified: rectifiedCount,
|
|
278
|
+
stillConflicting: stillConflictingCount,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return { rectifiedCount, stillConflictingCount, additionalCost, updatedPrd: prd, rectificationMetrics };
|
|
61
282
|
}
|
|
62
283
|
|
|
63
284
|
/**
|
|
@@ -92,7 +313,14 @@ export async function runParallelExecution(
|
|
|
92
313
|
const readyStories = getAllReadyStories(prd);
|
|
93
314
|
if (readyStories.length === 0) {
|
|
94
315
|
logger?.info("parallel", "No stories ready for parallel execution");
|
|
95
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
prd,
|
|
318
|
+
totalCost,
|
|
319
|
+
storiesCompleted,
|
|
320
|
+
completed: false,
|
|
321
|
+
storyMetrics: [],
|
|
322
|
+
rectificationStats: { rectified: 0, stillConflicting: 0 },
|
|
323
|
+
};
|
|
96
324
|
}
|
|
97
325
|
|
|
98
326
|
const maxConcurrency = parallelCount === 0 ? os.cpus().length : Math.max(1, parallelCount);
|
|
@@ -115,8 +343,16 @@ export async function runParallelExecution(
|
|
|
115
343
|
},
|
|
116
344
|
});
|
|
117
345
|
|
|
346
|
+
// Track which stories were already passed before this batch
|
|
347
|
+
const initialPassedIds = new Set(initialPrd.userStories.filter((s) => s.status === "passed").map((s) => s.id));
|
|
348
|
+
const batchStartedAt = new Date().toISOString();
|
|
349
|
+
const batchStartMs = Date.now();
|
|
350
|
+
const batchStoryMetrics: ParallelStoryMetrics[] = [];
|
|
351
|
+
|
|
352
|
+
let conflictedStories: ConflictedStoryInfo[] = [];
|
|
353
|
+
|
|
118
354
|
try {
|
|
119
|
-
const parallelResult = await executeParallel(
|
|
355
|
+
const parallelResult = await _parallelExecutorDeps.executeParallel(
|
|
120
356
|
readyStories,
|
|
121
357
|
prdPath,
|
|
122
358
|
workdir,
|
|
@@ -129,14 +365,44 @@ export async function runParallelExecution(
|
|
|
129
365
|
eventEmitter,
|
|
130
366
|
);
|
|
131
367
|
|
|
368
|
+
const batchDurationMs = Date.now() - batchStartMs;
|
|
369
|
+
const batchCompletedAt = new Date().toISOString();
|
|
370
|
+
|
|
132
371
|
prd = parallelResult.updatedPrd;
|
|
133
372
|
storiesCompleted += parallelResult.storiesCompleted;
|
|
134
373
|
totalCost += parallelResult.totalCost;
|
|
374
|
+
conflictedStories = parallelResult.mergeConflicts ?? [];
|
|
135
375
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
376
|
+
// BUG-066: Build per-story metrics for stories newly completed by this parallel batch
|
|
377
|
+
const newlyPassedStories = prd.userStories.filter((s) => s.status === "passed" && !initialPassedIds.has(s.id));
|
|
378
|
+
const costPerStory = newlyPassedStories.length > 0 ? parallelResult.totalCost / newlyPassedStories.length : 0;
|
|
379
|
+
for (const story of newlyPassedStories) {
|
|
380
|
+
batchStoryMetrics.push({
|
|
381
|
+
storyId: story.id,
|
|
382
|
+
complexity: "unknown",
|
|
383
|
+
modelTier: "parallel",
|
|
384
|
+
modelUsed: "parallel",
|
|
385
|
+
attempts: 1,
|
|
386
|
+
finalTier: "parallel",
|
|
387
|
+
success: true,
|
|
388
|
+
cost: costPerStory,
|
|
389
|
+
durationMs: batchDurationMs,
|
|
390
|
+
firstPassSuccess: true,
|
|
391
|
+
startedAt: batchStartedAt,
|
|
392
|
+
completedAt: batchCompletedAt,
|
|
393
|
+
source: "parallel" as const,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
allStoryMetrics.push(...batchStoryMetrics);
|
|
398
|
+
|
|
399
|
+
// Log each conflict before scheduling rectification
|
|
400
|
+
for (const conflict of conflictedStories) {
|
|
401
|
+
logger?.info("parallel", "Merge conflict detected - scheduling for rectification", {
|
|
402
|
+
storyId: conflict.storyId,
|
|
403
|
+
conflictFiles: conflict.conflictFiles,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
140
406
|
|
|
141
407
|
// Clear parallel status
|
|
142
408
|
statusWriter.setPrd(prd);
|
|
@@ -160,7 +426,23 @@ export async function runParallelExecution(
|
|
|
160
426
|
throw error;
|
|
161
427
|
}
|
|
162
428
|
|
|
163
|
-
//
|
|
429
|
+
// ── MFX-005: Rectification pass ────────────────────────────────────────────
|
|
430
|
+
let rectificationStats: RectificationStats = { rectified: 0, stillConflicting: 0 };
|
|
431
|
+
|
|
432
|
+
if (conflictedStories.length > 0) {
|
|
433
|
+
const rectResult = await runRectificationPass(conflictedStories, options, prd);
|
|
434
|
+
prd = rectResult.updatedPrd;
|
|
435
|
+
storiesCompleted += rectResult.rectifiedCount;
|
|
436
|
+
totalCost += rectResult.additionalCost;
|
|
437
|
+
rectificationStats = {
|
|
438
|
+
rectified: rectResult.rectifiedCount,
|
|
439
|
+
stillConflicting: rectResult.stillConflictingCount,
|
|
440
|
+
};
|
|
441
|
+
batchStoryMetrics.push(...rectResult.rectificationMetrics);
|
|
442
|
+
allStoryMetrics.push(...rectResult.rectificationMetrics);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check if all stories are complete after parallel execution + rectification
|
|
164
446
|
if (isComplete(prd)) {
|
|
165
447
|
logger?.info("execution", "All stories complete!", {
|
|
166
448
|
feature,
|
|
@@ -228,8 +510,10 @@ export async function runParallelExecution(
|
|
|
228
510
|
storiesCompleted,
|
|
229
511
|
completed: true,
|
|
230
512
|
durationMs,
|
|
513
|
+
storyMetrics: batchStoryMetrics,
|
|
514
|
+
rectificationStats,
|
|
231
515
|
};
|
|
232
516
|
}
|
|
233
517
|
|
|
234
|
-
return { prd, totalCost, storiesCompleted, completed: false };
|
|
518
|
+
return { prd, totalCost, storiesCompleted, completed: false, storyMetrics: batchStoryMetrics, rectificationStats };
|
|
235
519
|
}
|