@nathapp/nax 0.36.0 → 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 +543 -154
- package/package.json +1 -1
- package/src/agents/claude-decompose.ts +3 -3
- package/src/cli/constitution.ts +0 -92
- package/src/constitution/generator.ts +0 -33
- package/src/constitution/index.ts +2 -1
- package/src/constitution/loader.ts +1 -13
- package/src/context/builder.ts +1 -2
- package/src/context/elements.ts +1 -12
- package/src/context/index.ts +2 -1
- package/src/context/test-scanner.ts +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/interaction/chain.ts +17 -1
- 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/execution.ts +23 -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/prompts/builder.ts +13 -6
- package/src/prompts/sections/conventions.ts +5 -7
- package/src/prompts/sections/isolation.ts +7 -7
- package/src/prompts/sections/role-task.ts +64 -64
- package/src/review/orchestrator.ts +11 -1
- package/src/routing/strategies/llm-prompts.ts +1 -1
- package/src/routing/strategies/llm.ts +3 -3
- package/src/tdd/index.ts +2 -3
- package/src/tdd/isolation.ts +0 -13
- package/src/tdd/orchestrator.ts +5 -0
- package/src/tdd/prompts.ts +1 -231
- package/src/tdd/session-runner.ts +2 -0
- package/src/tdd/types.ts +2 -1
- package/src/tdd/verdict.ts +20 -2
- package/src/verification/crash-detector.ts +34 -0
- package/src/verification/orchestrator-types.ts +8 -1
- package/src/verification/parser.ts +0 -10
- package/src/verification/rectification-loop.ts +2 -51
- package/src/worktree/dispatcher.ts +0 -59
package/package.json
CHANGED
|
@@ -130,7 +130,7 @@ export function parseDecomposeOutput(output: string): DecomposedStory[] {
|
|
|
130
130
|
: ["Implementation complete"],
|
|
131
131
|
tags: Array.isArray(record.tags) ? record.tags : [],
|
|
132
132
|
dependencies: Array.isArray(record.dependencies) ? record.dependencies : [],
|
|
133
|
-
complexity:
|
|
133
|
+
complexity: coerceComplexity(record.complexity),
|
|
134
134
|
// contextFiles: prefer the new field; fall back to legacy relevantFiles from older LLM responses
|
|
135
135
|
contextFiles: Array.isArray(record.contextFiles)
|
|
136
136
|
? record.contextFiles
|
|
@@ -158,9 +158,9 @@ export function parseDecomposeOutput(output: string): DecomposedStory[] {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* Coerce complexity value from decompose output.
|
|
162
162
|
*/
|
|
163
|
-
export function
|
|
163
|
+
export function coerceComplexity(value: unknown): "simple" | "medium" | "complex" | "expert" {
|
|
164
164
|
if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
|
|
165
165
|
return value;
|
|
166
166
|
}
|
package/src/cli/constitution.ts
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
* Generates agent-specific config files from nax/constitution.md.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import { generateAll, generateFor } from "../constitution/generator";
|
|
11
|
-
import type { AgentType } from "../constitution/generators/types";
|
|
12
|
-
|
|
13
7
|
/** Constitution generate options */
|
|
14
8
|
export interface ConstitutionGenerateOptions {
|
|
15
9
|
/** Path to constitution file (default: nax/constitution.md) */
|
|
@@ -21,89 +15,3 @@ export interface ConstitutionGenerateOptions {
|
|
|
21
15
|
/** Dry run mode (don't write files) */
|
|
22
16
|
dryRun?: boolean;
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Constitution generate command
|
|
27
|
-
*/
|
|
28
|
-
export async function constitutionGenerateCommand(options: ConstitutionGenerateOptions): Promise<void> {
|
|
29
|
-
const workdir = process.cwd();
|
|
30
|
-
const constitutionPath = options.constitution
|
|
31
|
-
? join(workdir, options.constitution)
|
|
32
|
-
: join(workdir, "nax/constitution.md");
|
|
33
|
-
const outputDir = options.output ? join(workdir, options.output) : workdir;
|
|
34
|
-
|
|
35
|
-
// Validate constitution file exists
|
|
36
|
-
if (!existsSync(constitutionPath)) {
|
|
37
|
-
console.error(chalk.red(`[FAIL] Constitution file not found: ${constitutionPath}`));
|
|
38
|
-
console.error(chalk.yellow(`Create ${constitutionPath} first or use --constitution to specify a different path.`));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(chalk.blue(`[OK] Loading constitution from ${constitutionPath}`));
|
|
43
|
-
|
|
44
|
-
// Validate agent type if specified
|
|
45
|
-
const validAgents: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
46
|
-
if (options.agent && !validAgents.includes(options.agent as AgentType)) {
|
|
47
|
-
console.error(chalk.red(`[FAIL] Unknown agent type: ${options.agent}`));
|
|
48
|
-
console.error(chalk.yellow(`Valid agents: ${validAgents.join(", ")}`));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const dryRun = options.dryRun ?? false;
|
|
53
|
-
|
|
54
|
-
if (dryRun) {
|
|
55
|
-
console.log(chalk.yellow("[DRY RUN] No files will be written"));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Generate for specific agent or all agents
|
|
60
|
-
if (options.agent) {
|
|
61
|
-
const agent = options.agent as AgentType;
|
|
62
|
-
console.log(chalk.blue(`-> Generating config for ${agent}...`));
|
|
63
|
-
|
|
64
|
-
const result = await generateFor(agent, constitutionPath, outputDir, dryRun);
|
|
65
|
-
|
|
66
|
-
if (result.error) {
|
|
67
|
-
console.error(chalk.red(`[FAIL] ${agent}: ${result.error}`));
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (dryRun) {
|
|
72
|
-
console.log(chalk.green(`[OK] ${agent} -> ${result.outputFile} (${result.content.length} bytes, dry run)`));
|
|
73
|
-
} else {
|
|
74
|
-
console.log(chalk.green(`[OK] ${agent} -> ${result.outputFile} (${result.content.length} bytes)`));
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
console.log(chalk.blue("-> Generating configs for all agents..."));
|
|
78
|
-
|
|
79
|
-
const results = await generateAll(constitutionPath, outputDir, dryRun);
|
|
80
|
-
|
|
81
|
-
let errorCount = 0;
|
|
82
|
-
for (const result of results) {
|
|
83
|
-
if (result.error) {
|
|
84
|
-
console.error(chalk.red(`[FAIL] ${result.agent}: ${result.error}`));
|
|
85
|
-
errorCount++;
|
|
86
|
-
} else if (dryRun) {
|
|
87
|
-
console.log(
|
|
88
|
-
chalk.green(`[OK] ${result.agent} -> ${result.outputFile} (${result.content.length} bytes, dry run)`),
|
|
89
|
-
);
|
|
90
|
-
} else {
|
|
91
|
-
console.log(chalk.green(`[OK] ${result.agent} -> ${result.outputFile} (${result.content.length} bytes)`));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (errorCount > 0) {
|
|
96
|
-
console.error(chalk.red(`[FAIL] ${errorCount} generation(s) failed`));
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!dryRun) {
|
|
102
|
-
console.log(chalk.green(`\n[OK] Constitution config(s) generated in ${outputDir}`));
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
106
|
-
console.error(chalk.red(`[FAIL] Generation failed: ${error}`));
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -156,36 +156,3 @@ export async function generateAll(
|
|
|
156
156
|
|
|
157
157
|
return results;
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Check if constitution file is newer than generated configs
|
|
162
|
-
*/
|
|
163
|
-
export function isConstitutionNewer(constitutionPath: string, outputDir: string): boolean {
|
|
164
|
-
if (!existsSync(constitutionPath)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const constitutionStat = Bun.file(constitutionPath);
|
|
169
|
-
const constitutionMtime = constitutionStat.lastModified;
|
|
170
|
-
|
|
171
|
-
// Check all generated files
|
|
172
|
-
const agents: AgentType[] = ["claude", "opencode", "cursor", "windsurf", "aider"];
|
|
173
|
-
for (const agent of agents) {
|
|
174
|
-
const outputFile = GENERATORS[agent].outputFile;
|
|
175
|
-
const outputPath = join(outputDir, outputFile);
|
|
176
|
-
|
|
177
|
-
if (!existsSync(outputPath)) {
|
|
178
|
-
// If any config file is missing, consider constitution newer
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const outputStat = Bun.file(outputPath);
|
|
183
|
-
const outputMtime = outputStat.lastModified;
|
|
184
|
-
|
|
185
|
-
if (constitutionMtime > outputMtime) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export type { ConstitutionConfig, ConstitutionResult } from "./types";
|
|
10
|
-
export { loadConstitution,
|
|
10
|
+
export { loadConstitution, truncateToTokens } from "./loader";
|
|
11
|
+
export { estimateTokens } from "../optimizer/types";
|
|
@@ -8,21 +8,9 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { validateFilePath } from "../config/path-security";
|
|
10
10
|
import { globalConfigDir } from "../config/paths";
|
|
11
|
+
import { estimateTokens } from "../optimizer/types";
|
|
11
12
|
import type { ConstitutionConfig, ConstitutionResult } from "./types";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Estimate token count for text
|
|
15
|
-
*
|
|
16
|
-
* Uses simple heuristic: 1 token ≈ 3 characters (conservative estimate)
|
|
17
|
-
* This is a rough approximation sufficient for quota management.
|
|
18
|
-
*
|
|
19
|
-
* @param text - Text to estimate tokens for
|
|
20
|
-
* @returns Estimated token count
|
|
21
|
-
*/
|
|
22
|
-
export function estimateTokens(text: string): number {
|
|
23
|
-
return Math.ceil(text.length / 3);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
14
|
/**
|
|
27
15
|
* Truncate text to fit within token limit
|
|
28
16
|
*
|
package/src/context/builder.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { NaxConfig } from "../config";
|
|
9
9
|
import { getLogger } from "../logger";
|
|
10
|
+
import { estimateTokens } from "../optimizer/types";
|
|
10
11
|
import type { UserStory } from "../prd";
|
|
11
12
|
import { countStories, getContextFiles } from "../prd";
|
|
12
13
|
import { autoDetectContextFiles } from "./auto-detect";
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
createProgressContext,
|
|
19
20
|
createStoryContext,
|
|
20
21
|
createTestCoverageContext,
|
|
21
|
-
estimateTokens,
|
|
22
22
|
} from "./elements";
|
|
23
23
|
import { generateTestCoverageSummary } from "./test-scanner";
|
|
24
24
|
import type { BuiltContext, ContextBudget, ContextElement, StoryContext } from "./types";
|
|
@@ -30,7 +30,6 @@ export const _deps = {
|
|
|
30
30
|
|
|
31
31
|
// Re-export for backward compatibility
|
|
32
32
|
export {
|
|
33
|
-
estimateTokens,
|
|
34
33
|
createStoryContext,
|
|
35
34
|
createDependencyContext,
|
|
36
35
|
createErrorContext,
|
package/src/context/elements.ts
CHANGED
|
@@ -5,21 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getLogger } from "../logger";
|
|
8
|
+
import { estimateTokens } from "../optimizer/types";
|
|
8
9
|
import type { StructuredFailure, UserStory } from "../prd";
|
|
9
10
|
import type { ContextElement } from "./types";
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* Approximate character-to-token ratio for token estimation.
|
|
13
|
-
* Value of 3 is a middle ground optimized for mixed content (prose + code + markdown).
|
|
14
|
-
* Slightly overestimates tokens, preventing budget overflow.
|
|
15
|
-
*/
|
|
16
|
-
const CHARS_PER_TOKEN = 3;
|
|
17
|
-
|
|
18
|
-
/** Estimate token count for text using character-to-token ratio. */
|
|
19
|
-
export function estimateTokens(text: string): number {
|
|
20
|
-
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
/** Create context element from current story */
|
|
24
13
|
export function createStoryContext(story: UserStory, priority: number): ContextElement {
|
|
25
14
|
const content = formatStoryAsText(story);
|
package/src/context/index.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
export type { ContextElement, ContextBudget, StoryContext, BuiltContext } from "./types";
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
|
-
estimateTokens,
|
|
9
8
|
createStoryContext,
|
|
10
9
|
createDependencyContext,
|
|
11
10
|
createErrorContext,
|
|
@@ -17,6 +16,8 @@ export {
|
|
|
17
16
|
formatContextAsMarkdown,
|
|
18
17
|
} from "./builder";
|
|
19
18
|
|
|
19
|
+
export { estimateTokens } from "../optimizer/types";
|
|
20
|
+
|
|
20
21
|
export {
|
|
21
22
|
generateTestCoverageSummary,
|
|
22
23
|
scanTestFiles,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { Glob } from "bun";
|
|
11
11
|
import { getLogger } from "../logger";
|
|
12
|
-
import { estimateTokens } from "
|
|
12
|
+
import { estimateTokens } from "../optimizer/types";
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Types
|
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);
|