@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.
Files changed (49) hide show
  1. package/dist/nax.js +543 -154
  2. package/package.json +1 -1
  3. package/src/agents/claude-decompose.ts +3 -3
  4. package/src/cli/constitution.ts +0 -92
  5. package/src/constitution/generator.ts +0 -33
  6. package/src/constitution/index.ts +2 -1
  7. package/src/constitution/loader.ts +1 -13
  8. package/src/context/builder.ts +1 -2
  9. package/src/context/elements.ts +1 -12
  10. package/src/context/index.ts +2 -1
  11. package/src/context/test-scanner.ts +1 -1
  12. package/src/execution/dry-run.ts +1 -1
  13. package/src/execution/escalation/escalation.ts +5 -3
  14. package/src/execution/escalation/tier-escalation.ts +41 -4
  15. package/src/execution/iteration-runner.ts +5 -0
  16. package/src/execution/parallel-executor.ts +293 -9
  17. package/src/execution/parallel.ts +40 -21
  18. package/src/execution/pipeline-result-handler.ts +3 -2
  19. package/src/execution/runner.ts +13 -3
  20. package/src/interaction/chain.ts +17 -1
  21. package/src/metrics/tracker.ts +8 -4
  22. package/src/metrics/types.ts +2 -0
  23. package/src/pipeline/event-bus.ts +1 -1
  24. package/src/pipeline/stages/completion.ts +1 -1
  25. package/src/pipeline/stages/execution.ts +23 -1
  26. package/src/pipeline/stages/verify.ts +8 -1
  27. package/src/pipeline/subscribers/reporters.ts +3 -3
  28. package/src/pipeline/types.ts +4 -0
  29. package/src/plugins/types.ts +1 -1
  30. package/src/prd/types.ts +2 -0
  31. package/src/prompts/builder.ts +13 -6
  32. package/src/prompts/sections/conventions.ts +5 -7
  33. package/src/prompts/sections/isolation.ts +7 -7
  34. package/src/prompts/sections/role-task.ts +64 -64
  35. package/src/review/orchestrator.ts +11 -1
  36. package/src/routing/strategies/llm-prompts.ts +1 -1
  37. package/src/routing/strategies/llm.ts +3 -3
  38. package/src/tdd/index.ts +2 -3
  39. package/src/tdd/isolation.ts +0 -13
  40. package/src/tdd/orchestrator.ts +5 -0
  41. package/src/tdd/prompts.ts +1 -231
  42. package/src/tdd/session-runner.ts +2 -0
  43. package/src/tdd/types.ts +2 -1
  44. package/src/tdd/verdict.ts +20 -2
  45. package/src/verification/crash-detector.ts +34 -0
  46. package/src/verification/orchestrator-types.ts +8 -1
  47. package/src/verification/parser.ts +0 -10
  48. package/src/verification/rectification-loop.ts +2 -51
  49. package/src/worktree/dispatcher.ts +0 -59
package/dist/nax.js CHANGED
@@ -3914,7 +3914,7 @@ ${output.slice(0, 500)}`);
3914
3914
  acceptanceCriteria: Array.isArray(record.acceptanceCriteria) ? record.acceptanceCriteria : ["Implementation complete"],
3915
3915
  tags: Array.isArray(record.tags) ? record.tags : [],
3916
3916
  dependencies: Array.isArray(record.dependencies) ? record.dependencies : [],
3917
- complexity: validateComplexity(record.complexity),
3917
+ complexity: coerceComplexity(record.complexity),
3918
3918
  contextFiles: Array.isArray(record.contextFiles) ? record.contextFiles : Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
3919
3919
  relevantFiles: Array.isArray(record.relevantFiles) ? record.relevantFiles : [],
3920
3920
  reasoning: String(record.reasoning || "No reasoning provided"),
@@ -3928,7 +3928,7 @@ ${output.slice(0, 500)}`);
3928
3928
  }
3929
3929
  return stories;
3930
3930
  }
3931
- function validateComplexity(value) {
3931
+ function coerceComplexity(value) {
3932
3932
  if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
3933
3933
  return value;
3934
3934
  }
@@ -19471,7 +19471,7 @@ Your complexity classification will determine the execution strategy:
19471
19471
  Respond with ONLY this JSON (no markdown, no explanation):
19472
19472
  {"complexity":"simple|medium|complex|expert","modelTier":"fast|balanced|powerful","reasoning":"<one line>"}`;
19473
19473
  }
19474
- function buildBatchPrompt(stories, config2) {
19474
+ function buildBatchRoutingPrompt(stories, config2) {
19475
19475
  const storyBlocks = stories.map((story, idx) => {
19476
19476
  const criteria = story.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join(`
19477
19477
  `);
@@ -19651,7 +19651,7 @@ async function routeBatch(stories, context) {
19651
19651
  throw new Error("No agent adapter available for batch routing (AA-003)");
19652
19652
  }
19653
19653
  const modelTier = llmConfig.model ?? "fast";
19654
- const prompt = buildBatchPrompt(stories, config2);
19654
+ const prompt = buildBatchRoutingPrompt(stories, config2);
19655
19655
  try {
19656
19656
  const output = await callLlm(adapter, modelTier, prompt, config2);
19657
19657
  const decisions = parseBatchResponse(output, stories, config2);
@@ -20063,6 +20063,22 @@ function computeStoryContentHash(story) {
20063
20063
  }
20064
20064
 
20065
20065
  // src/routing/index.ts
20066
+ var exports_routing = {};
20067
+ __export(exports_routing, {
20068
+ tryLlmBatchRoute: () => tryLlmBatchRoute,
20069
+ routeTask: () => routeTask,
20070
+ routeStory: () => routeStory,
20071
+ manualStrategy: () => manualStrategy,
20072
+ loadCustomStrategy: () => loadCustomStrategy,
20073
+ llmStrategy: () => llmStrategy,
20074
+ keywordStrategy: () => keywordStrategy,
20075
+ determineTestStrategy: () => determineTestStrategy2,
20076
+ computeStoryContentHash: () => computeStoryContentHash,
20077
+ complexityToModelTier: () => complexityToModelTier2,
20078
+ classifyComplexity: () => classifyComplexity2,
20079
+ buildStrategyChain: () => buildStrategyChain,
20080
+ StrategyChain: () => StrategyChain
20081
+ });
20066
20082
  var init_routing = __esm(() => {
20067
20083
  init_router();
20068
20084
  init_chain();
@@ -20073,7 +20089,7 @@ var init_routing = __esm(() => {
20073
20089
  });
20074
20090
 
20075
20091
  // src/decompose/validators/complexity.ts
20076
- function validateComplexity2(substories, maxComplexity) {
20092
+ function validateComplexity(substories, maxComplexity) {
20077
20093
  const errors3 = [];
20078
20094
  const warnings = [];
20079
20095
  const maxOrder = COMPLEXITY_ORDER[maxComplexity];
@@ -20385,7 +20401,7 @@ function runAllValidators(originalStory, substories, existingStories, config2) {
20385
20401
  const results = [
20386
20402
  validateOverlap(substories, existingStories),
20387
20403
  validateCoverage(originalStory, substories),
20388
- validateComplexity2(substories, maxComplexity),
20404
+ validateComplexity(substories, maxComplexity),
20389
20405
  validateDependencies(substories, existingIds)
20390
20406
  ];
20391
20407
  const errors3 = results.flatMap((r) => r.errors);
@@ -20657,7 +20673,7 @@ var package_default;
20657
20673
  var init_package = __esm(() => {
20658
20674
  package_default = {
20659
20675
  name: "@nathapp/nax",
20660
- version: "0.36.0",
20676
+ version: "0.36.2",
20661
20677
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20662
20678
  type: "module",
20663
20679
  bin: {
@@ -20718,8 +20734,8 @@ var init_version = __esm(() => {
20718
20734
  NAX_VERSION = package_default.version;
20719
20735
  NAX_COMMIT = (() => {
20720
20736
  try {
20721
- if (/^[0-9a-f]{6,10}$/.test("78b52b0"))
20722
- return "78b52b0";
20737
+ if (/^[0-9a-f]{6,10}$/.test("eb77e7d"))
20738
+ return "eb77e7d";
20723
20739
  } catch {}
20724
20740
  try {
20725
20741
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -20791,9 +20807,10 @@ function collectStoryMetrics(ctx, storyStartTime) {
20791
20807
  const routing = ctx.routing;
20792
20808
  const agentResult = ctx.agentResult;
20793
20809
  const escalationCount = story.escalations?.length || 0;
20794
- const attempts = Math.max(1, story.attempts || 1);
20810
+ const priorFailureCount = story.priorFailures?.length || 0;
20811
+ const attempts = priorFailureCount + Math.max(1, story.attempts || 1);
20795
20812
  const finalTier = escalationCount > 0 ? story.escalations[escalationCount - 1].toTier : routing.modelTier;
20796
- const firstPassSuccess = agentResult?.success === true && escalationCount === 0;
20813
+ const firstPassSuccess = agentResult?.success === true && escalationCount === 0 && priorFailureCount === 0;
20797
20814
  const modelEntry = ctx.config.models[routing.modelTier];
20798
20815
  const modelDef = modelEntry ? resolveModel(modelEntry) : null;
20799
20816
  const modelUsed = modelDef?.model || routing.modelTier;
@@ -20809,12 +20826,13 @@ function collectStoryMetrics(ctx, storyStartTime) {
20809
20826
  attempts,
20810
20827
  finalTier,
20811
20828
  success: agentResult?.success || false,
20812
- cost: agentResult?.estimatedCost || 0,
20829
+ cost: (ctx.accumulatedAttemptCost ?? 0) + (agentResult?.estimatedCost || 0),
20813
20830
  durationMs: agentResult?.durationMs || 0,
20814
20831
  firstPassSuccess,
20815
20832
  startedAt: storyStartTime,
20816
20833
  completedAt: new Date().toISOString(),
20817
- fullSuiteGatePassed
20834
+ fullSuiteGatePassed,
20835
+ runtimeCrashes: ctx.storyRuntimeCrashes ?? 0
20818
20836
  };
20819
20837
  }
20820
20838
  function collectBatchMetrics(ctx, storyStartTime) {
@@ -20844,7 +20862,8 @@ function collectBatchMetrics(ctx, storyStartTime) {
20844
20862
  firstPassSuccess: true,
20845
20863
  startedAt: storyStartTime,
20846
20864
  completedAt: new Date().toISOString(),
20847
- fullSuiteGatePassed: false
20865
+ fullSuiteGatePassed: false,
20866
+ runtimeCrashes: 0
20848
20867
  };
20849
20868
  });
20850
20869
  }
@@ -21092,6 +21111,12 @@ class InteractionChain {
21092
21111
  async prompt(request) {
21093
21112
  await this.send(request);
21094
21113
  const response = await this.receive(request.id, request.timeout);
21114
+ if (response.action === "choose" && response.value && request.options) {
21115
+ const matched = request.options.find((o) => o.key === response.value);
21116
+ if (matched) {
21117
+ return { ...response, action: matched.key };
21118
+ }
21119
+ }
21095
21120
  return response;
21096
21121
  }
21097
21122
  async cancel(requestId) {
@@ -22221,6 +22246,11 @@ var init_interaction = __esm(() => {
22221
22246
  });
22222
22247
 
22223
22248
  // src/pipeline/runner.ts
22249
+ var exports_runner = {};
22250
+ __export(exports_runner, {
22251
+ runPipeline: () => runPipeline,
22252
+ MAX_STAGE_RETRIES: () => MAX_STAGE_RETRIES
22253
+ });
22224
22254
  async function runPipeline(stages, context, eventEmitter) {
22225
22255
  const logger = getLogger();
22226
22256
  const retryCountMap = new Map;
@@ -22681,9 +22711,17 @@ class ReviewOrchestrator {
22681
22711
  const changedFiles = await getChangedFiles(workdir);
22682
22712
  const pluginResults = [];
22683
22713
  for (const reviewer of reviewers) {
22684
- logger?.info("review", `Running plugin reviewer: ${reviewer.name}`);
22714
+ logger?.info("review", `Running plugin reviewer: ${reviewer.name}`, {
22715
+ changedFiles: changedFiles.length
22716
+ });
22685
22717
  try {
22686
22718
  const result = await reviewer.check(workdir, changedFiles);
22719
+ logger?.info("review", `Plugin reviewer result: ${reviewer.name}`, {
22720
+ passed: result.passed,
22721
+ exitCode: result.exitCode,
22722
+ output: result.output?.slice(0, 500),
22723
+ findings: result.findings?.length ?? 0
22724
+ });
22687
22725
  pluginResults.push({
22688
22726
  name: reviewer.name,
22689
22727
  passed: result.passed,
@@ -22702,6 +22740,7 @@ class ReviewOrchestrator {
22702
22740
  }
22703
22741
  } catch (error48) {
22704
22742
  const errorMsg = error48 instanceof Error ? error48.message : String(error48);
22743
+ logger?.warn("review", `Plugin reviewer threw error: ${reviewer.name}`, { error: errorMsg });
22705
22744
  pluginResults.push({ name: reviewer.name, passed: false, output: "", error: errorMsg });
22706
22745
  builtIn.pluginReviewers = pluginResults;
22707
22746
  return {
@@ -22931,7 +22970,7 @@ var init_completion = __esm(() => {
22931
22970
  storyId: completedStory.id,
22932
22971
  story: completedStory,
22933
22972
  passed: true,
22934
- durationMs: storyMetric?.durationMs ?? 0,
22973
+ runElapsedMs: storyMetric?.durationMs ?? 0,
22935
22974
  cost: costPerStory,
22936
22975
  modelTier: ctx.routing?.modelTier,
22937
22976
  testStrategy: ctx.routing?.testStrategy
@@ -22959,12 +22998,14 @@ var init_completion = __esm(() => {
22959
22998
  };
22960
22999
  });
22961
23000
 
23001
+ // src/optimizer/types.ts
23002
+ function estimateTokens(text) {
23003
+ return Math.ceil(text.length / 4);
23004
+ }
23005
+
22962
23006
  // src/constitution/loader.ts
22963
23007
  import { existsSync as existsSync13 } from "fs";
22964
23008
  import { join as join14 } from "path";
22965
- function estimateTokens(text) {
22966
- return Math.ceil(text.length / 3);
22967
- }
22968
23009
  function truncateToTokens(text, maxTokens) {
22969
23010
  const maxChars = maxTokens * 3;
22970
23011
  if (text.length <= maxChars) {
@@ -23214,32 +23255,29 @@ var init_auto_detect = __esm(() => {
23214
23255
  });
23215
23256
 
23216
23257
  // src/context/elements.ts
23217
- function estimateTokens2(text) {
23218
- return Math.ceil(text.length / CHARS_PER_TOKEN);
23219
- }
23220
23258
  function createStoryContext(story, priority) {
23221
23259
  const content = formatStoryAsText(story);
23222
- return { type: "story", storyId: story.id, content, priority, tokens: estimateTokens2(content) };
23260
+ return { type: "story", storyId: story.id, content, priority, tokens: estimateTokens(content) };
23223
23261
  }
23224
23262
  function createDependencyContext(story, priority) {
23225
23263
  const content = formatStoryAsText(story);
23226
- return { type: "dependency", storyId: story.id, content, priority, tokens: estimateTokens2(content) };
23264
+ return { type: "dependency", storyId: story.id, content, priority, tokens: estimateTokens(content) };
23227
23265
  }
23228
23266
  function createErrorContext(errorMessage, priority) {
23229
- return { type: "error", content: errorMessage, priority, tokens: estimateTokens2(errorMessage) };
23267
+ return { type: "error", content: errorMessage, priority, tokens: estimateTokens(errorMessage) };
23230
23268
  }
23231
23269
  function createProgressContext(progressText, priority) {
23232
- return { type: "progress", content: progressText, priority, tokens: estimateTokens2(progressText) };
23270
+ return { type: "progress", content: progressText, priority, tokens: estimateTokens(progressText) };
23233
23271
  }
23234
23272
  function createFileContext(filePath, content, priority) {
23235
- return { type: "file", filePath, content, priority, tokens: estimateTokens2(content) };
23273
+ return { type: "file", filePath, content, priority, tokens: estimateTokens(content) };
23236
23274
  }
23237
23275
  function createTestCoverageContext(content, tokens, priority) {
23238
23276
  return { type: "test-coverage", content, priority, tokens };
23239
23277
  }
23240
23278
  function createPriorFailuresContext(failures, priority) {
23241
23279
  const content = formatPriorFailures(failures);
23242
- return { type: "prior-failures", content, priority, tokens: estimateTokens2(content) };
23280
+ return { type: "prior-failures", content, priority, tokens: estimateTokens(content) };
23243
23281
  }
23244
23282
  function formatPriorFailures(failures) {
23245
23283
  if (!failures || failures.length === 0) {
@@ -23310,7 +23348,6 @@ function formatStoryAsText(story) {
23310
23348
  return parts.join(`
23311
23349
  `);
23312
23350
  }
23313
- var CHARS_PER_TOKEN = 3;
23314
23351
  var init_elements = __esm(() => {
23315
23352
  init_logger2();
23316
23353
  });
@@ -23467,7 +23504,7 @@ function truncateToTokenBudget(files, maxTokens, preferredDetail) {
23467
23504
  for (let i = startIndex;i < detailLevels.length; i++) {
23468
23505
  const detail = detailLevels[i];
23469
23506
  const summary = formatTestSummary(files, detail);
23470
- const tokens = estimateTokens2(summary);
23507
+ const tokens = estimateTokens(summary);
23471
23508
  if (tokens <= maxTokens) {
23472
23509
  return { summary, detail, truncated: i !== startIndex };
23473
23510
  }
@@ -23477,7 +23514,7 @@ function truncateToTokenBudget(files, maxTokens, preferredDetail) {
23477
23514
  truncatedFiles = truncatedFiles.slice(0, truncatedFiles.length - 1);
23478
23515
  const summary = `${formatTestSummary(truncatedFiles, "names-only")}
23479
23516
  ... and ${files.length - truncatedFiles.length} more test files`;
23480
- if (estimateTokens2(summary) <= maxTokens) {
23517
+ if (estimateTokens(summary) <= maxTokens) {
23481
23518
  return { summary, detail: "names-only", truncated: true };
23482
23519
  }
23483
23520
  }
@@ -23500,13 +23537,12 @@ async function generateTestCoverageSummary(options) {
23500
23537
  }
23501
23538
  const totalTests = files.reduce((sum, f) => sum + f.testCount, 0);
23502
23539
  const { summary } = truncateToTokenBudget(files, maxTokens, detail);
23503
- const tokens = estimateTokens2(summary);
23540
+ const tokens = estimateTokens(summary);
23504
23541
  return { files, totalTests, summary, tokens };
23505
23542
  }
23506
23543
  var COMMON_TEST_DIRS;
23507
23544
  var init_test_scanner = __esm(() => {
23508
23545
  init_logger2();
23509
- init_builder3();
23510
23546
  COMMON_TEST_DIRS = ["test", "tests", "__tests__", "src/__tests__", "spec"];
23511
23547
  });
23512
23548
 
@@ -24758,7 +24794,7 @@ var init_cleanup = __esm(() => {
24758
24794
  });
24759
24795
 
24760
24796
  // src/tdd/prompts.ts
24761
- function buildImplementerRectificationPrompt(failures, story, contextMarkdown, config2) {
24797
+ function buildImplementerRectificationPrompt(failures, story, _contextMarkdown, config2) {
24762
24798
  return createRectificationPrompt(failures, story, config2);
24763
24799
  }
24764
24800
  var init_prompts = __esm(() => {
@@ -24908,9 +24944,9 @@ var init_rectification_gate = __esm(() => {
24908
24944
  function buildConventionsSection() {
24909
24945
  return `# Conventions
24910
24946
 
24911
- ` + `Follow existing code patterns and conventions. Write idiomatic, maintainable code.
24947
+ Follow existing code patterns and conventions. Write idiomatic, maintainable code.
24912
24948
 
24913
- ` + "When running tests, run ONLY test files related to your changes (e.g. `bun test ./test/specific.test.ts`). " + "NEVER run `bun test` without a file filter \u2014 full suite output will flood your context window and cause failures.\n\n" + "Commit your changes when done using conventional commit format (e.g. `feat:`, `fix:`, `test:`).";
24949
+ Commit your changes when done using conventional commit format (e.g. \`feat:\`, \`fix:\`, \`test:\`).`;
24914
24950
  }
24915
24951
 
24916
24952
  // src/prompts/sections/isolation.ts
@@ -24919,29 +24955,39 @@ function buildIsolationSection(roleOrMode, mode) {
24919
24955
  return buildIsolationSection("test-writer", roleOrMode);
24920
24956
  }
24921
24957
  const role = roleOrMode;
24922
- const header = `# Isolation Rules
24923
-
24924
- `;
24958
+ const header = "# Isolation Rules";
24925
24959
  const footer = `
24926
24960
 
24927
24961
  ${TEST_FILTER_RULE}`;
24928
24962
  if (role === "test-writer") {
24929
24963
  const m = mode ?? "strict";
24930
24964
  if (m === "strict") {
24931
- return `${header}isolation scope: Only create or modify files in the test/ directory. Tests must fail because the feature is not yet implemented. Do NOT modify any source files in src/.${footer}`;
24965
+ return `${header}
24966
+
24967
+ isolation scope: Only create or modify files in the test/ directory. Tests must fail because the feature is not yet implemented. Do NOT modify any source files in src/.${footer}`;
24932
24968
  }
24933
- return `${header}isolation scope: Create test files in test/. MAY read src/ files and MAY import from src/ to ensure correct types/interfaces. May create minimal stubs in src/ if needed to make imports work, but do NOT implement real logic.${footer}`;
24969
+ return `${header}
24970
+
24971
+ isolation scope: Create test files in test/. MAY read src/ files and MAY import from src/ to ensure correct types/interfaces. May create minimal stubs in src/ if needed to make imports work, but do NOT implement real logic.${footer}`;
24934
24972
  }
24935
24973
  if (role === "implementer") {
24936
- return `${header}isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
24974
+ return `${header}
24975
+
24976
+ isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
24937
24977
  }
24938
24978
  if (role === "verifier") {
24939
- return `${header}isolation scope: Read-only inspection. Review all test results, implementation code, and acceptance criteria compliance. You MAY write a verdict file (.nax-verifier-verdict.json) and apply legitimate fixes if needed.${footer}`;
24979
+ return `${header}
24980
+
24981
+ isolation scope: Read-only inspection. Review all test results, implementation code, and acceptance criteria compliance. You MAY write a verdict file (.nax-verifier-verdict.json) and apply legitimate fixes if needed.${footer}`;
24940
24982
  }
24941
24983
  if (role === "single-session") {
24942
- return `${header}isolation scope: Create test files in test/ directory, then implement source code in src/ to make tests pass. Both directories are in scope for this session.${footer}`;
24984
+ return `${header}
24985
+
24986
+ isolation scope: Create test files in test/ directory, then implement source code in src/ to make tests pass. Both directories are in scope for this session.${footer}`;
24943
24987
  }
24944
- return `${header}isolation scope: You may modify both src/ and test/ files. Write failing tests FIRST, then implement to make them pass.`;
24988
+ return `${header}
24989
+
24990
+ isolation scope: You may modify both src/ and test/ files. Write failing tests FIRST, then implement to make them pass.`;
24945
24991
  }
24946
24992
  var TEST_FILTER_RULE;
24947
24993
  var init_isolation2 = __esm(() => {
@@ -24959,76 +25005,76 @@ function buildRoleTaskSection(roleOrVariant, variant) {
24959
25005
  if (v === "standard") {
24960
25006
  return `# Role: Implementer
24961
25007
 
24962
- ` + `Your task: make failing tests pass.
25008
+ Your task: make failing tests pass.
24963
25009
 
24964
- ` + `Instructions:
24965
- ` + `- Implement source code in src/ to make tests pass
24966
- ` + `- Do NOT modify test files
24967
- ` + `- Run tests frequently to track progress
24968
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24969
- ` + "- Goal: all tests green, all changes committed";
25010
+ Instructions:
25011
+ - Implement source code in src/ to make tests pass
25012
+ - Do NOT modify test files
25013
+ - Run tests frequently to track progress
25014
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25015
+ - Goal: all tests green, all changes committed`;
24970
25016
  }
24971
25017
  return `# Role: Implementer (Lite)
24972
25018
 
24973
- ` + `Your task: Write tests AND implement the feature in a single session.
25019
+ Your task: Write tests AND implement the feature in a single session.
24974
25020
 
24975
- ` + `Instructions:
24976
- ` + `- Write tests first (test/ directory), then implement (src/ directory)
24977
- ` + `- All tests must pass by the end
24978
- ` + `- Use Bun test (describe/test/expect)
24979
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
24980
- ` + "- Goal: all tests green, all criteria met, all changes committed";
25021
+ Instructions:
25022
+ - Write tests first (test/ directory), then implement (src/ directory)
25023
+ - All tests must pass by the end
25024
+ - Use Bun test (describe/test/expect)
25025
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25026
+ - Goal: all tests green, all criteria met, all changes committed`;
24981
25027
  }
24982
25028
  if (role === "test-writer") {
24983
25029
  return `# Role: Test-Writer
24984
25030
 
24985
- ` + `Your task: Write comprehensive failing tests for the feature.
25031
+ Your task: Write comprehensive failing tests for the feature.
24986
25032
 
24987
- ` + `Instructions:
24988
- ` + `- Create test files in test/ directory that cover acceptance criteria
24989
- ` + `- Tests must fail initially (RED phase) \u2014 the feature is not yet implemented
24990
- ` + `- Use Bun test (describe/test/expect)
24991
- ` + `- Write clear test names that document expected behavior
24992
- ` + `- Focus on behavior, not implementation details
24993
- ` + "- Goal: comprehensive test suite ready for implementation";
25033
+ Instructions:
25034
+ - Create test files in test/ directory that cover acceptance criteria
25035
+ - Tests must fail initially (RED phase) \u2014 the feature is not yet implemented
25036
+ - Use Bun test (describe/test/expect)
25037
+ - Write clear test names that document expected behavior
25038
+ - Focus on behavior, not implementation details
25039
+ - Goal: comprehensive test suite ready for implementation`;
24994
25040
  }
24995
25041
  if (role === "verifier") {
24996
25042
  return `# Role: Verifier
24997
25043
 
24998
- ` + `Your task: Review and verify the implementation against acceptance criteria.
25044
+ Your task: Review and verify the implementation against acceptance criteria.
24999
25045
 
25000
- ` + `Instructions:
25001
- ` + `- Review all test results \u2014 verify tests pass
25002
- ` + `- Check that implementation meets all acceptance criteria
25003
- ` + `- Inspect code quality, error handling, and edge cases
25004
- ` + `- Verify test modifications (if any) are legitimate fixes
25005
- ` + `- Write a detailed verdict with reasoning
25006
- ` + "- Goal: provide comprehensive verification and quality assurance";
25046
+ Instructions:
25047
+ - Review all test results \u2014 verify tests pass
25048
+ - Check that implementation meets all acceptance criteria
25049
+ - Inspect code quality, error handling, and edge cases
25050
+ - Verify test modifications (if any) are legitimate fixes
25051
+ - Write a detailed verdict with reasoning
25052
+ - Goal: provide comprehensive verification and quality assurance`;
25007
25053
  }
25008
25054
  if (role === "single-session") {
25009
25055
  return `# Role: Single-Session
25010
25056
 
25011
- ` + `Your task: Write tests AND implement the feature in a single focused session.
25057
+ Your task: Write tests AND implement the feature in a single focused session.
25012
25058
 
25013
- ` + `Instructions:
25014
- ` + `- Phase 1: Write comprehensive tests (test/ directory)
25015
- ` + `- Phase 2: Implement to make all tests pass (src/ directory)
25016
- ` + `- Use Bun test (describe/test/expect)
25017
- ` + `- Run tests frequently throughout implementation
25018
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25019
- ` + "- Goal: all tests passing, all changes committed, full story complete";
25059
+ Instructions:
25060
+ - Phase 1: Write comprehensive tests (test/ directory)
25061
+ - Phase 2: Implement to make all tests pass (src/ directory)
25062
+ - Use Bun test (describe/test/expect)
25063
+ - Run tests frequently throughout implementation
25064
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25065
+ - Goal: all tests passing, all changes committed, full story complete`;
25020
25066
  }
25021
25067
  return `# Role: TDD-Simple
25022
25068
 
25023
- ` + `Your task: Write failing tests FIRST, then implement to make them pass.
25069
+ Your task: Write failing tests FIRST, then implement to make them pass.
25024
25070
 
25025
- ` + `Instructions:
25026
- ` + `- RED phase: Write failing tests FIRST for the acceptance criteria
25027
- ` + `- RED phase: Run the tests to confirm they fail
25028
- ` + `- GREEN phase: Implement the minimum code to make tests pass
25029
- ` + `- REFACTOR phase: Refactor while keeping tests green
25030
- ` + `- When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25031
- ` + "- Goal: all tests passing, feature complete, all changes committed";
25071
+ Instructions:
25072
+ - RED phase: Write failing tests FIRST for the acceptance criteria
25073
+ - RED phase: Run the tests to confirm they fail
25074
+ - GREEN phase: Implement the minimum code to make tests pass
25075
+ - REFACTOR phase: Refactor while keeping tests green
25076
+ - When all tests are green, stage and commit ALL changed files with: git commit -m 'feat: <description>'
25077
+ - Goal: all tests passing, feature complete, all changes committed`;
25032
25078
  }
25033
25079
 
25034
25080
  // src/prompts/sections/story.ts
@@ -25046,6 +25092,69 @@ ${story.description}
25046
25092
  ${criteria}`;
25047
25093
  }
25048
25094
 
25095
+ // src/prompts/sections/verdict.ts
25096
+ function buildVerdictSection(story) {
25097
+ return `# Verdict Instructions
25098
+
25099
+ ## Write Verdict File
25100
+
25101
+ After completing your verification, you **MUST** write a verdict file at the **project root**:
25102
+
25103
+ **File:** \`.nax-verifier-verdict.json\`
25104
+
25105
+ Set \`approved: true\` when ALL of these conditions are met:
25106
+ - All tests pass
25107
+ - Implementation is clean and follows conventions
25108
+ - All acceptance criteria met
25109
+ - Any test modifications by implementer are legitimate fixes
25110
+
25111
+ Set \`approved: false\` when ANY of these conditions are true:
25112
+ - Tests are failing and you cannot fix them
25113
+ - The implementer loosened test assertions to mask bugs
25114
+ - Critical acceptance criteria are not met
25115
+ - Code quality is poor (security issues, severe bugs, etc.)
25116
+
25117
+ **Full JSON schema example** (fill in all fields with real values):
25118
+
25119
+ \`\`\`json
25120
+ {
25121
+ "version": 1,
25122
+ "approved": true,
25123
+ "tests": {
25124
+ "allPassing": true,
25125
+ "passCount": 42,
25126
+ "failCount": 0
25127
+ },
25128
+ "testModifications": {
25129
+ "detected": false,
25130
+ "files": [],
25131
+ "legitimate": true,
25132
+ "reasoning": "No test files were modified by the implementer"
25133
+ },
25134
+ "acceptanceCriteria": {
25135
+ "allMet": true,
25136
+ "criteria": [
25137
+ { "criterion": "Example criterion", "met": true }
25138
+ ]
25139
+ },
25140
+ "quality": {
25141
+ "rating": "good",
25142
+ "issues": []
25143
+ },
25144
+ "fixes": [],
25145
+ "reasoning": "All tests pass, implementation is clean, all acceptance criteria are met."
25146
+ }
25147
+ \`\`\`
25148
+
25149
+ **Field notes:**
25150
+ - \`quality.rating\` must be one of: \`"good"\`, \`"acceptable"\`, \`"poor"\`
25151
+ - \`testModifications.files\` \u2014 list any test files the implementer changed
25152
+ - \`fixes\` \u2014 list any fixes you applied yourself during this verification session
25153
+ - \`reasoning\` \u2014 brief summary of your overall assessment
25154
+
25155
+ When done, commit any fixes with message: "fix: verify and adjust ${story.title}"`;
25156
+ }
25157
+
25049
25158
  // src/prompts/loader.ts
25050
25159
  var exports_loader = {};
25051
25160
  __export(exports_loader, {
@@ -25121,6 +25230,9 @@ ${this._constitution}`);
25121
25230
  if (this._story) {
25122
25231
  sections.push(buildStorySection(this._story));
25123
25232
  }
25233
+ if (this._role === "verifier" && this._story) {
25234
+ sections.push(buildVerdictSection(this._story));
25235
+ }
25124
25236
  const isolation = this._options.isolation;
25125
25237
  sections.push(buildIsolationSection(this._role, isolation));
25126
25238
  if (this._contextMd) {
@@ -25209,7 +25321,7 @@ async function rollbackToRef(workdir, ref) {
25209
25321
  }
25210
25322
  logger.info("tdd", "Successfully rolled back git changes", { ref });
25211
25323
  }
25212
- async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false) {
25324
+ async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution) {
25213
25325
  const startTime = Date.now();
25214
25326
  let prompt;
25215
25327
  switch (role) {
@@ -25217,7 +25329,7 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
25217
25329
  prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).build();
25218
25330
  break;
25219
25331
  case "implementer":
25220
- prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).build();
25332
+ prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).build();
25221
25333
  break;
25222
25334
  case "verifier":
25223
25335
  prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).build();
@@ -25361,7 +25473,7 @@ function isValidVerdict(obj) {
25361
25473
  function coerceVerdict(obj) {
25362
25474
  try {
25363
25475
  const verdictStr = String(obj.verdict ?? "").toUpperCase();
25364
- const approved = verdictStr === "PASS" || verdictStr === "APPROVED" || obj.approved === true;
25476
+ const approved = verdictStr === "PASS" || verdictStr === "APPROVED" || verdictStr.startsWith("VERIFIED") || verdictStr.includes("ALL ACCEPTANCE CRITERIA MET") || obj.approved === true;
25365
25477
  let passCount = 0;
25366
25478
  let failCount = 0;
25367
25479
  let allPassing = approved;
@@ -25456,13 +25568,24 @@ async function readVerdict(workdir) {
25456
25568
  if (!exists) {
25457
25569
  return null;
25458
25570
  }
25571
+ let rawText;
25572
+ try {
25573
+ rawText = await file2.text();
25574
+ } catch (readErr) {
25575
+ logger.warn("tdd", "Failed to read verifier verdict file", {
25576
+ path: verdictPath,
25577
+ error: String(readErr)
25578
+ });
25579
+ return null;
25580
+ }
25459
25581
  let parsed;
25460
25582
  try {
25461
- parsed = await file2.json();
25583
+ parsed = JSON.parse(rawText);
25462
25584
  } catch (parseErr) {
25463
25585
  logger.warn("tdd", "Verifier verdict file is not valid JSON \u2014 ignoring", {
25464
25586
  path: verdictPath,
25465
- error: String(parseErr)
25587
+ error: String(parseErr),
25588
+ rawContent: rawText.slice(0, 1000)
25466
25589
  });
25467
25590
  return null;
25468
25591
  }
@@ -25564,6 +25687,7 @@ async function runThreeSessionTdd(options) {
25564
25687
  workdir,
25565
25688
  modelTier,
25566
25689
  contextMarkdown,
25690
+ constitution,
25567
25691
  dryRun = false,
25568
25692
  lite = false,
25569
25693
  _recursionDepth = 0
@@ -25625,7 +25749,7 @@ async function runThreeSessionTdd(options) {
25625
25749
  let session1;
25626
25750
  if (!isRetry) {
25627
25751
  const testWriterTier = config2.tdd.sessionTiers?.testWriter ?? "balanced";
25628
- session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite);
25752
+ session1 = await runTddSession("test-writer", agent, story, config2, workdir, testWriterTier, session1Ref, contextMarkdown, lite, lite, constitution);
25629
25753
  sessions.push(session1);
25630
25754
  }
25631
25755
  if (session1 && !session1.success) {
@@ -25687,7 +25811,7 @@ async function runThreeSessionTdd(options) {
25687
25811
  });
25688
25812
  const session2Ref = await captureGitRef(workdir) ?? "HEAD";
25689
25813
  const implementerTier = config2.tdd.sessionTiers?.implementer ?? modelTier;
25690
- const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite);
25814
+ const session2 = await runTddSession("implementer", agent, story, config2, workdir, implementerTier, session2Ref, contextMarkdown, lite, lite, constitution);
25691
25815
  sessions.push(session2);
25692
25816
  if (!session2.success) {
25693
25817
  needsHumanReview = true;
@@ -25706,7 +25830,7 @@ async function runThreeSessionTdd(options) {
25706
25830
  const fullSuiteGatePassed = await runFullSuiteGate(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger);
25707
25831
  const session3Ref = await captureGitRef(workdir) ?? "HEAD";
25708
25832
  const verifierTier = config2.tdd.sessionTiers?.verifier ?? "fast";
25709
- const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false);
25833
+ const session3 = await runTddSession("verifier", agent, story, config2, workdir, verifierTier, session3Ref, undefined, false, false, constitution);
25710
25834
  sessions.push(session3);
25711
25835
  const verdict = await readVerdict(workdir);
25712
25836
  await cleanupVerdict(workdir);
@@ -25886,6 +26010,7 @@ var init_execution = __esm(() => {
25886
26010
  workdir: ctx.workdir,
25887
26011
  modelTier: ctx.routing.modelTier,
25888
26012
  contextMarkdown: ctx.contextMarkdown,
26013
+ constitution: ctx.constitution?.content,
25889
26014
  dryRun: false,
25890
26015
  lite: isLiteMode
25891
26016
  });
@@ -25909,6 +26034,28 @@ var init_execution = __esm(() => {
25909
26034
  lite: tddResult.lite,
25910
26035
  failureCategory: tddResult.failureCategory
25911
26036
  });
26037
+ if (ctx.interaction) {
26038
+ try {
26039
+ await ctx.interaction.send({
26040
+ id: `human-review-${ctx.story.id}-${Date.now()}`,
26041
+ type: "notify",
26042
+ featureName: ctx.featureDir ? ctx.featureDir.split("/").pop() ?? "unknown" : "unknown",
26043
+ storyId: ctx.story.id,
26044
+ stage: "execution",
26045
+ summary: `\u26A0\uFE0F Human review needed: ${ctx.story.id}`,
26046
+ detail: `Story: ${ctx.story.title}
26047
+ Reason: ${tddResult.reviewReason ?? "No reason provided"}
26048
+ Category: ${tddResult.failureCategory ?? "unknown"}`,
26049
+ fallback: "continue",
26050
+ createdAt: Date.now()
26051
+ });
26052
+ } catch (notifyErr) {
26053
+ logger.warn("execution", "Failed to send human review notification", {
26054
+ storyId: ctx.story.id,
26055
+ error: String(notifyErr)
26056
+ });
26057
+ }
26058
+ }
25912
26059
  }
25913
26060
  return routeTddFailure(tddResult.failureCategory, isLiteMode, ctx, tddResult.reviewReason);
25914
26061
  }
@@ -25979,16 +26126,11 @@ var init_execution = __esm(() => {
25979
26126
  };
25980
26127
  });
25981
26128
 
25982
- // src/optimizer/types.ts
25983
- function estimateTokens4(text) {
25984
- return Math.ceil(text.length / 4);
25985
- }
25986
-
25987
26129
  // src/optimizer/noop.optimizer.ts
25988
26130
  class NoopOptimizer {
25989
26131
  name = "noop";
25990
26132
  async optimize(input) {
25991
- const tokens = estimateTokens4(input.prompt);
26133
+ const tokens = estimateTokens(input.prompt);
25992
26134
  return {
25993
26135
  prompt: input.prompt,
25994
26136
  originalTokens: tokens,
@@ -26004,7 +26146,7 @@ var init_noop_optimizer = () => {};
26004
26146
  class RuleBasedOptimizer {
26005
26147
  name = "rule-based";
26006
26148
  async optimize(input) {
26007
- const originalTokens = estimateTokens4(input.prompt);
26149
+ const originalTokens = estimateTokens(input.prompt);
26008
26150
  const appliedRules = [];
26009
26151
  let optimized = input.prompt;
26010
26152
  const config2 = {
@@ -26033,13 +26175,13 @@ class RuleBasedOptimizer {
26033
26175
  }
26034
26176
  }
26035
26177
  if (config2.maxPromptTokens) {
26036
- const currentTokens = estimateTokens4(optimized);
26178
+ const currentTokens = estimateTokens(optimized);
26037
26179
  if (currentTokens > config2.maxPromptTokens) {
26038
26180
  optimized = this.trimToMaxTokens(optimized, config2.maxPromptTokens);
26039
26181
  appliedRules.push("maxPromptTokens");
26040
26182
  }
26041
26183
  }
26042
- const optimizedTokens = estimateTokens4(optimized);
26184
+ const optimizedTokens = estimateTokens(optimized);
26043
26185
  const savings = originalTokens > 0 ? (originalTokens - optimizedTokens) / originalTokens : 0;
26044
26186
  return {
26045
26187
  prompt: optimized,
@@ -26082,7 +26224,7 @@ ${newContextSection}`);
26082
26224
  return prompt;
26083
26225
  }
26084
26226
  trimToMaxTokens(prompt, maxTokens) {
26085
- const currentTokens = estimateTokens4(prompt);
26227
+ const currentTokens = estimateTokens(prompt);
26086
26228
  if (currentTokens <= maxTokens) {
26087
26229
  return prompt;
26088
26230
  }
@@ -26221,7 +26363,7 @@ var init_optimizer2 = __esm(() => {
26221
26363
  });
26222
26364
 
26223
26365
  // src/execution/prompts.ts
26224
- function buildBatchPrompt2(stories, contextMarkdown, constitution) {
26366
+ function buildBatchPrompt(stories, contextMarkdown, constitution) {
26225
26367
  const storyPrompts = stories.map((story, idx) => {
26226
26368
  return `## Story ${idx + 1}: ${story.id} \u2014 ${story.title}
26227
26369
 
@@ -26279,7 +26421,7 @@ var init_prompt = __esm(() => {
26279
26421
  const isBatch = ctx.stories.length > 1;
26280
26422
  let prompt;
26281
26423
  if (isBatch) {
26282
- prompt = buildBatchPrompt2(ctx.stories, ctx.contextMarkdown, ctx.constitution);
26424
+ prompt = buildBatchPrompt(ctx.stories, ctx.contextMarkdown, ctx.constitution);
26283
26425
  } else {
26284
26426
  const role = ctx.routing.testStrategy === "tdd-simple" ? "tdd-simple" : "single-session";
26285
26427
  const builder = PromptBuilder.for(role).withLoader(ctx.workdir, ctx.config).story(ctx.story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content);
@@ -26567,7 +26709,6 @@ ${rectificationPrompt}`;
26567
26709
  var init_rectification_loop = __esm(() => {
26568
26710
  init_agents();
26569
26711
  init_config();
26570
- init_progress();
26571
26712
  init_test_output_parser();
26572
26713
  init_logger2();
26573
26714
  init_prd();
@@ -27339,6 +27480,22 @@ var init_routing2 = __esm(() => {
27339
27480
  };
27340
27481
  });
27341
27482
 
27483
+ // src/verification/crash-detector.ts
27484
+ function detectRuntimeCrash(output) {
27485
+ if (!output)
27486
+ return false;
27487
+ return CRASH_PATTERNS.some((pattern) => output.includes(pattern));
27488
+ }
27489
+ var CRASH_PATTERNS;
27490
+ var init_crash_detector = __esm(() => {
27491
+ CRASH_PATTERNS = [
27492
+ "panic(main thread)",
27493
+ "Segmentation fault",
27494
+ "Bun has crashed",
27495
+ "oh no: Bun has crashed"
27496
+ ];
27497
+ });
27498
+
27342
27499
  // src/pipeline/stages/verify.ts
27343
27500
  function coerceSmartTestRunner(val) {
27344
27501
  if (val === undefined || val === true)
@@ -27356,6 +27513,7 @@ function buildScopedCommand2(testFiles, baseCommand, testScopedTemplate) {
27356
27513
  var DEFAULT_SMART_RUNNER_CONFIG2, verifyStage, _verifyDeps;
27357
27514
  var init_verify = __esm(() => {
27358
27515
  init_logger2();
27516
+ init_crash_detector();
27359
27517
  init_runners();
27360
27518
  init_smart_runner();
27361
27519
  DEFAULT_SMART_RUNNER_CONFIG2 = {
@@ -27427,7 +27585,7 @@ var init_verify = __esm(() => {
27427
27585
  });
27428
27586
  ctx.verifyResult = {
27429
27587
  success: result.success,
27430
- status: result.status === "TIMEOUT" ? "TIMEOUT" : result.success ? "PASS" : "TEST_FAILURE",
27588
+ status: result.status === "TIMEOUT" ? "TIMEOUT" : result.success ? "PASS" : detectRuntimeCrash(result.output) ? "RUNTIME_CRASH" : "TEST_FAILURE",
27431
27589
  storyId: ctx.story.id,
27432
27590
  strategy: "scoped",
27433
27591
  passCount: result.passCount ?? 0,
@@ -27478,6 +27636,25 @@ var init_verify = __esm(() => {
27478
27636
  });
27479
27637
 
27480
27638
  // src/pipeline/stages/index.ts
27639
+ var exports_stages = {};
27640
+ __export(exports_stages, {
27641
+ verifyStage: () => verifyStage,
27642
+ routingStage: () => routingStage,
27643
+ reviewStage: () => reviewStage,
27644
+ regressionStage: () => regressionStage,
27645
+ rectifyStage: () => rectifyStage,
27646
+ queueCheckStage: () => queueCheckStage,
27647
+ promptStage: () => promptStage,
27648
+ postRunPipeline: () => postRunPipeline,
27649
+ optimizerStage: () => optimizerStage,
27650
+ executionStage: () => executionStage,
27651
+ defaultPipeline: () => defaultPipeline,
27652
+ contextStage: () => contextStage,
27653
+ constitutionStage: () => constitutionStage,
27654
+ completionStage: () => completionStage,
27655
+ autofixStage: () => autofixStage,
27656
+ acceptanceStage: () => acceptanceStage
27657
+ });
27481
27658
  var defaultPipeline, postRunPipeline;
27482
27659
  var init_stages = __esm(() => {
27483
27660
  init_acceptance2();
@@ -29127,11 +29304,12 @@ var init_crash_recovery = __esm(() => {
29127
29304
 
29128
29305
  // src/execution/escalation/escalation.ts
29129
29306
  function escalateTier(currentTier, tierOrder) {
29130
- const currentIndex = tierOrder.findIndex((t) => t.tier === currentTier);
29307
+ const getName = (t) => t.tier ?? t.name ?? null;
29308
+ const currentIndex = tierOrder.findIndex((t) => getName(t) === currentTier);
29131
29309
  if (currentIndex === -1 || currentIndex === tierOrder.length - 1) {
29132
29310
  return null;
29133
29311
  }
29134
- return tierOrder[currentIndex + 1].tier;
29312
+ return getName(tierOrder[currentIndex + 1]);
29135
29313
  }
29136
29314
  function calculateMaxIterations(tierOrder) {
29137
29315
  return tierOrder.reduce((sum, t) => sum + t.attempts, 0);
@@ -29228,13 +29406,14 @@ var init_tier_outcome = __esm(() => {
29228
29406
  });
29229
29407
 
29230
29408
  // src/execution/escalation/tier-escalation.ts
29231
- function buildEscalationFailure(story, currentTier, reviewFindings) {
29409
+ function buildEscalationFailure(story, currentTier, reviewFindings, cost) {
29232
29410
  return {
29233
29411
  attempt: (story.attempts ?? 0) + 1,
29234
29412
  modelTier: currentTier,
29235
29413
  stage: "escalation",
29236
29414
  summary: `Failed with tier ${currentTier}, escalating to next tier`,
29237
29415
  reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
29416
+ cost: cost ?? 0,
29238
29417
  timestamp: new Date().toISOString()
29239
29418
  };
29240
29419
  }
@@ -29247,6 +29426,8 @@ function resolveMaxAttemptsOutcome(failureCategory) {
29247
29426
  case "verifier-rejected":
29248
29427
  case "greenfield-no-tests":
29249
29428
  return "pause";
29429
+ case "runtime-crash":
29430
+ return "pause";
29250
29431
  case "session-failure":
29251
29432
  case "tests-failing":
29252
29433
  return "fail";
@@ -29270,8 +29451,17 @@ async function tryLlmBatchRoute2(config2, stories, label = "routing") {
29270
29451
  });
29271
29452
  }
29272
29453
  }
29454
+ function shouldRetrySameTier(verifyResult) {
29455
+ return verifyResult?.status === "RUNTIME_CRASH";
29456
+ }
29273
29457
  async function handleTierEscalation(ctx) {
29274
29458
  const logger = getSafeLogger();
29459
+ if (shouldRetrySameTier(ctx.verifyResult)) {
29460
+ logger?.warn("escalation", "Runtime crash detected \u2014 retrying same tier (transient, not a code issue)", {
29461
+ storyId: ctx.story.id
29462
+ });
29463
+ return { outcome: "retry-same", prdDirty: false, prd: ctx.prd };
29464
+ }
29275
29465
  const nextTier = escalateTier(ctx.routing.modelTier, ctx.config.autoMode.escalation.tierOrder);
29276
29466
  const escalateWholeBatch = ctx.config.autoMode.escalation.escalateEntireBatch ?? true;
29277
29467
  const storiesToEscalate = ctx.isBatchExecution && escalateWholeBatch ? ctx.storiesToExecute : [ctx.story];
@@ -29325,7 +29515,7 @@ async function handleTierEscalation(ctx) {
29325
29515
  const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
29326
29516
  const isChangingTier = currentStoryTier !== nextTier;
29327
29517
  const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
29328
- const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings);
29518
+ const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost);
29329
29519
  return {
29330
29520
  ...s,
29331
29521
  attempts: shouldResetAttempts ? 0 : (s.attempts ?? 0) + 1,
@@ -29335,7 +29525,7 @@ async function handleTierEscalation(ctx) {
29335
29525
  };
29336
29526
  })
29337
29527
  };
29338
- await savePRD(updatedPrd, ctx.prdPath);
29528
+ await _tierEscalationDeps.savePRD(updatedPrd, ctx.prdPath);
29339
29529
  for (const story of storiesToEscalate) {
29340
29530
  clearCacheForStory(story.id);
29341
29531
  }
@@ -29348,6 +29538,7 @@ async function handleTierEscalation(ctx) {
29348
29538
  prd: updatedPrd
29349
29539
  };
29350
29540
  }
29541
+ var _tierEscalationDeps;
29351
29542
  var init_tier_escalation = __esm(() => {
29352
29543
  init_hooks();
29353
29544
  init_logger2();
@@ -29357,6 +29548,9 @@ var init_tier_escalation = __esm(() => {
29357
29548
  init_helpers();
29358
29549
  init_progress();
29359
29550
  init_tier_outcome();
29551
+ _tierEscalationDeps = {
29552
+ savePRD
29553
+ };
29360
29554
  });
29361
29555
 
29362
29556
  // src/execution/escalation/index.ts
@@ -29958,6 +30152,10 @@ var init_headless_formatter = __esm(() => {
29958
30152
  });
29959
30153
 
29960
30154
  // src/worktree/manager.ts
30155
+ var exports_manager = {};
30156
+ __export(exports_manager, {
30157
+ WorktreeManager: () => WorktreeManager
30158
+ });
29961
30159
  import { existsSync as existsSync26, symlinkSync } from "fs";
29962
30160
  import { join as join32 } from "path";
29963
30161
 
@@ -30102,6 +30300,11 @@ var init_manager = __esm(() => {
30102
30300
  });
30103
30301
 
30104
30302
  // src/worktree/merge.ts
30303
+ var exports_merge = {};
30304
+ __export(exports_merge, {
30305
+ MergeEngine: () => MergeEngine
30306
+ });
30307
+
30105
30308
  class MergeEngine {
30106
30309
  worktreeManager;
30107
30310
  constructor(worktreeManager) {
@@ -30380,10 +30583,12 @@ async function executeParallelBatch(stories, projectRoot, config2, prd, context,
30380
30583
  const logger = getSafeLogger();
30381
30584
  const worktreeManager = new WorktreeManager;
30382
30585
  const results = {
30383
- successfulStories: [],
30384
- failedStories: [],
30586
+ pipelinePassed: [],
30587
+ merged: [],
30588
+ failed: [],
30385
30589
  totalCost: 0,
30386
- conflictedStories: []
30590
+ mergeConflicts: [],
30591
+ storyCosts: new Map
30387
30592
  };
30388
30593
  const worktreeSetup = [];
30389
30594
  for (const story of stories) {
@@ -30396,7 +30601,7 @@ async function executeParallelBatch(stories, projectRoot, config2, prd, context,
30396
30601
  worktreePath
30397
30602
  });
30398
30603
  } catch (error48) {
30399
- results.failedStories.push({
30604
+ results.failed.push({
30400
30605
  story,
30401
30606
  error: `Failed to create worktree: ${error48 instanceof Error ? error48.message : String(error48)}`
30402
30607
  });
@@ -30411,14 +30616,15 @@ async function executeParallelBatch(stories, projectRoot, config2, prd, context,
30411
30616
  const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
30412
30617
  const executePromise = executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter).then((result) => {
30413
30618
  results.totalCost += result.cost;
30619
+ results.storyCosts.set(story.id, result.cost);
30414
30620
  if (result.success) {
30415
- results.successfulStories.push(story);
30621
+ results.pipelinePassed.push(story);
30416
30622
  logger?.info("parallel", "Story execution succeeded", {
30417
30623
  storyId: story.id,
30418
30624
  cost: result.cost
30419
30625
  });
30420
30626
  } else {
30421
- results.failedStories.push({ story, error: result.error || "Unknown error" });
30627
+ results.failed.push({ story, error: result.error || "Unknown error" });
30422
30628
  logger?.error("parallel", "Story execution failed", {
30423
30629
  storyId: story.id,
30424
30630
  error: result.error
@@ -30458,6 +30664,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
30458
30664
  let storiesCompleted = 0;
30459
30665
  let totalCost = 0;
30460
30666
  const currentPrd = prd;
30667
+ const allMergeConflicts = [];
30461
30668
  for (let batchIndex = 0;batchIndex < batches.length; batchIndex++) {
30462
30669
  const batch = batches[batchIndex];
30463
30670
  logger?.info("parallel", `Executing batch ${batchIndex + 1}/${batches.length}`, {
@@ -30474,8 +30681,8 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
30474
30681
  };
30475
30682
  const batchResult = await executeParallelBatch(batch, projectRoot, config2, currentPrd, baseContext, maxConcurrency, eventEmitter);
30476
30683
  totalCost += batchResult.totalCost;
30477
- if (batchResult.successfulStories.length > 0) {
30478
- const successfulIds = batchResult.successfulStories.map((s) => s.id);
30684
+ if (batchResult.pipelinePassed.length > 0) {
30685
+ const successfulIds = batchResult.pipelinePassed.map((s) => s.id);
30479
30686
  const deps = buildDependencyMap(batch);
30480
30687
  logger?.info("parallel", "Merging successful stories", {
30481
30688
  storyIds: successfulIds
@@ -30485,15 +30692,19 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
30485
30692
  if (mergeResult.success) {
30486
30693
  markStoryPassed(currentPrd, mergeResult.storyId);
30487
30694
  storiesCompleted++;
30695
+ const mergedStory = batchResult.pipelinePassed.find((s) => s.id === mergeResult.storyId);
30696
+ if (mergedStory)
30697
+ batchResult.merged.push(mergedStory);
30488
30698
  logger?.info("parallel", "Story merged successfully", {
30489
30699
  storyId: mergeResult.storyId,
30490
30700
  retryCount: mergeResult.retryCount
30491
30701
  });
30492
30702
  } else {
30493
30703
  markStoryFailed(currentPrd, mergeResult.storyId);
30494
- batchResult.conflictedStories.push({
30704
+ batchResult.mergeConflicts.push({
30495
30705
  storyId: mergeResult.storyId,
30496
- conflictFiles: mergeResult.conflictFiles || []
30706
+ conflictFiles: mergeResult.conflictFiles || [],
30707
+ originalCost: batchResult.storyCosts.get(mergeResult.storyId) ?? 0
30497
30708
  });
30498
30709
  logger?.error("parallel", "Merge conflict", {
30499
30710
  storyId: mergeResult.storyId,
@@ -30506,7 +30717,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
30506
30717
  }
30507
30718
  }
30508
30719
  }
30509
- for (const { story, error: error48 } of batchResult.failedStories) {
30720
+ for (const { story, error: error48 } of batchResult.failed) {
30510
30721
  markStoryFailed(currentPrd, story.id);
30511
30722
  logger?.error("parallel", "Cleaning up failed story worktree", {
30512
30723
  storyId: story.id,
@@ -30522,10 +30733,12 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
30522
30733
  }
30523
30734
  }
30524
30735
  await savePRD(currentPrd, prdPath);
30736
+ allMergeConflicts.push(...batchResult.mergeConflicts);
30525
30737
  logger?.info("parallel", `Batch ${batchIndex + 1} complete`, {
30526
- successful: batchResult.successfulStories.length,
30527
- failed: batchResult.failedStories.length,
30528
- conflicts: batchResult.conflictedStories.length,
30738
+ pipelinePassed: batchResult.pipelinePassed.length,
30739
+ merged: batchResult.merged.length,
30740
+ failed: batchResult.failed.length,
30741
+ mergeConflicts: batchResult.mergeConflicts.length,
30529
30742
  batchCost: batchResult.totalCost
30530
30743
  });
30531
30744
  }
@@ -30533,7 +30746,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
30533
30746
  storiesCompleted,
30534
30747
  totalCost
30535
30748
  });
30536
- return { storiesCompleted, totalCost, updatedPrd: currentPrd };
30749
+ return { storiesCompleted, totalCost, updatedPrd: currentPrd, mergeConflicts: allMergeConflicts };
30537
30750
  }
30538
30751
  var init_parallel = __esm(() => {
30539
30752
  init_logger2();
@@ -30625,6 +30838,123 @@ __export(exports_parallel_executor, {
30625
30838
  });
30626
30839
  import * as os5 from "os";
30627
30840
  import path15 from "path";
30841
+ async function rectifyConflictedStory(options) {
30842
+ const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter } = options;
30843
+ const logger = getSafeLogger();
30844
+ logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
30845
+ try {
30846
+ const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
30847
+ const { MergeEngine: MergeEngine2 } = await Promise.resolve().then(() => (init_merge(), exports_merge));
30848
+ const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
30849
+ const { defaultPipeline: defaultPipeline2 } = await Promise.resolve().then(() => (init_stages(), exports_stages));
30850
+ const { routeTask: routeTask2 } = await Promise.resolve().then(() => (init_routing(), exports_routing));
30851
+ const worktreeManager = new WorktreeManager2;
30852
+ const mergeEngine = new MergeEngine2(worktreeManager);
30853
+ try {
30854
+ await worktreeManager.remove(workdir, storyId);
30855
+ } catch {}
30856
+ await worktreeManager.create(workdir, storyId);
30857
+ const worktreePath = path15.join(workdir, ".nax-wt", storyId);
30858
+ const story = prd.userStories.find((s) => s.id === storyId);
30859
+ if (!story) {
30860
+ return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
30861
+ }
30862
+ const routing = routeTask2(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
30863
+ const pipelineContext = {
30864
+ config: config2,
30865
+ prd,
30866
+ story,
30867
+ stories: [story],
30868
+ workdir: worktreePath,
30869
+ featureDir: undefined,
30870
+ hooks,
30871
+ plugins: pluginRegistry,
30872
+ storyStartTime: new Date().toISOString(),
30873
+ routing
30874
+ };
30875
+ const pipelineResult = await runPipeline2(defaultPipeline2, pipelineContext, eventEmitter);
30876
+ const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
30877
+ if (!pipelineResult.success) {
30878
+ logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
30879
+ return { success: false, storyId, cost, finalConflict: false, pipelineFailure: true };
30880
+ }
30881
+ const mergeResults = await mergeEngine.mergeAll(workdir, [storyId], { [storyId]: [] });
30882
+ const mergeResult = mergeResults[0];
30883
+ if (!mergeResult || !mergeResult.success) {
30884
+ const conflictFiles = mergeResult?.conflictFiles ?? [];
30885
+ logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
30886
+ return { success: false, storyId, cost, finalConflict: true, conflictFiles };
30887
+ }
30888
+ logger?.info("parallel", "Rectification succeeded - story merged", {
30889
+ storyId,
30890
+ originalCost: options.originalCost,
30891
+ rectificationCost: cost
30892
+ });
30893
+ return { success: true, storyId, cost };
30894
+ } catch (error48) {
30895
+ logger?.error("parallel", "Rectification failed - preserving worktree", {
30896
+ storyId,
30897
+ error: error48 instanceof Error ? error48.message : String(error48)
30898
+ });
30899
+ return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
30900
+ }
30901
+ }
30902
+ async function runRectificationPass(conflictedStories, options, prd) {
30903
+ const logger = getSafeLogger();
30904
+ const { workdir, config: config2, hooks, pluginRegistry, eventEmitter } = options;
30905
+ const rectificationMetrics = [];
30906
+ let rectifiedCount = 0;
30907
+ let stillConflictingCount = 0;
30908
+ let additionalCost = 0;
30909
+ logger?.info("parallel", "Starting merge conflict rectification", {
30910
+ stories: conflictedStories.map((s) => s.storyId),
30911
+ totalConflicts: conflictedStories.length
30912
+ });
30913
+ for (const conflictInfo of conflictedStories) {
30914
+ const result = await _parallelExecutorDeps.rectifyConflictedStory({
30915
+ ...conflictInfo,
30916
+ workdir,
30917
+ config: config2,
30918
+ hooks,
30919
+ pluginRegistry,
30920
+ prd,
30921
+ eventEmitter
30922
+ });
30923
+ additionalCost += result.cost;
30924
+ if (result.success) {
30925
+ markStoryPassed(prd, result.storyId);
30926
+ rectifiedCount++;
30927
+ rectificationMetrics.push({
30928
+ storyId: result.storyId,
30929
+ complexity: "unknown",
30930
+ modelTier: "parallel",
30931
+ modelUsed: "parallel",
30932
+ attempts: 1,
30933
+ finalTier: "parallel",
30934
+ success: true,
30935
+ cost: result.cost,
30936
+ durationMs: 0,
30937
+ firstPassSuccess: false,
30938
+ startedAt: new Date().toISOString(),
30939
+ completedAt: new Date().toISOString(),
30940
+ source: "rectification",
30941
+ rectifiedFromConflict: true,
30942
+ originalCost: conflictInfo.originalCost,
30943
+ rectificationCost: result.cost
30944
+ });
30945
+ } else {
30946
+ const isFinalConflict = result.finalConflict === true;
30947
+ if (isFinalConflict) {
30948
+ stillConflictingCount++;
30949
+ }
30950
+ }
30951
+ }
30952
+ logger?.info("parallel", "Rectification complete", {
30953
+ rectified: rectifiedCount,
30954
+ stillConflicting: stillConflictingCount
30955
+ });
30956
+ return { rectifiedCount, stillConflictingCount, additionalCost, updatedPrd: prd, rectificationMetrics };
30957
+ }
30628
30958
  async function runParallelExecution(options, initialPrd) {
30629
30959
  const logger = getSafeLogger();
30630
30960
  const {
@@ -30649,7 +30979,14 @@ async function runParallelExecution(options, initialPrd) {
30649
30979
  const readyStories = getAllReadyStories(prd);
30650
30980
  if (readyStories.length === 0) {
30651
30981
  logger?.info("parallel", "No stories ready for parallel execution");
30652
- return { prd, totalCost, storiesCompleted, completed: false };
30982
+ return {
30983
+ prd,
30984
+ totalCost,
30985
+ storiesCompleted,
30986
+ completed: false,
30987
+ storyMetrics: [],
30988
+ rectificationStats: { rectified: 0, stillConflicting: 0 }
30989
+ };
30653
30990
  }
30654
30991
  const maxConcurrency = parallelCount === 0 ? os5.cpus().length : Math.max(1, parallelCount);
30655
30992
  logger?.info("parallel", "Starting parallel execution mode", {
@@ -30667,15 +31004,45 @@ async function runParallelExecution(options, initialPrd) {
30667
31004
  }))
30668
31005
  }
30669
31006
  });
31007
+ const initialPassedIds = new Set(initialPrd.userStories.filter((s) => s.status === "passed").map((s) => s.id));
31008
+ const batchStartedAt = new Date().toISOString();
31009
+ const batchStartMs = Date.now();
31010
+ const batchStoryMetrics = [];
31011
+ let conflictedStories = [];
30670
31012
  try {
30671
- const parallelResult = await executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter);
31013
+ const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter);
31014
+ const batchDurationMs = Date.now() - batchStartMs;
31015
+ const batchCompletedAt = new Date().toISOString();
30672
31016
  prd = parallelResult.updatedPrd;
30673
31017
  storiesCompleted += parallelResult.storiesCompleted;
30674
31018
  totalCost += parallelResult.totalCost;
30675
- logger?.info("parallel", "Parallel execution complete", {
30676
- storiesCompleted: parallelResult.storiesCompleted,
30677
- totalCost: parallelResult.totalCost
30678
- });
31019
+ conflictedStories = parallelResult.mergeConflicts ?? [];
31020
+ const newlyPassedStories = prd.userStories.filter((s) => s.status === "passed" && !initialPassedIds.has(s.id));
31021
+ const costPerStory = newlyPassedStories.length > 0 ? parallelResult.totalCost / newlyPassedStories.length : 0;
31022
+ for (const story of newlyPassedStories) {
31023
+ batchStoryMetrics.push({
31024
+ storyId: story.id,
31025
+ complexity: "unknown",
31026
+ modelTier: "parallel",
31027
+ modelUsed: "parallel",
31028
+ attempts: 1,
31029
+ finalTier: "parallel",
31030
+ success: true,
31031
+ cost: costPerStory,
31032
+ durationMs: batchDurationMs,
31033
+ firstPassSuccess: true,
31034
+ startedAt: batchStartedAt,
31035
+ completedAt: batchCompletedAt,
31036
+ source: "parallel"
31037
+ });
31038
+ }
31039
+ allStoryMetrics.push(...batchStoryMetrics);
31040
+ for (const conflict of conflictedStories) {
31041
+ logger?.info("parallel", "Merge conflict detected - scheduling for rectification", {
31042
+ storyId: conflict.storyId,
31043
+ conflictFiles: conflict.conflictFiles
31044
+ });
31045
+ }
30679
31046
  statusWriter.setPrd(prd);
30680
31047
  await statusWriter.update(totalCost, iterations, {
30681
31048
  parallel: {
@@ -30693,6 +31060,19 @@ async function runParallelExecution(options, initialPrd) {
30693
31060
  });
30694
31061
  throw error48;
30695
31062
  }
31063
+ let rectificationStats = { rectified: 0, stillConflicting: 0 };
31064
+ if (conflictedStories.length > 0) {
31065
+ const rectResult = await runRectificationPass(conflictedStories, options, prd);
31066
+ prd = rectResult.updatedPrd;
31067
+ storiesCompleted += rectResult.rectifiedCount;
31068
+ totalCost += rectResult.additionalCost;
31069
+ rectificationStats = {
31070
+ rectified: rectResult.rectifiedCount,
31071
+ stillConflicting: rectResult.stillConflictingCount
31072
+ };
31073
+ batchStoryMetrics.push(...rectResult.rectificationMetrics);
31074
+ allStoryMetrics.push(...rectResult.rectificationMetrics);
31075
+ }
30696
31076
  if (isComplete(prd)) {
30697
31077
  logger?.info("execution", "All stories complete!", {
30698
31078
  feature,
@@ -30742,10 +31122,12 @@ async function runParallelExecution(options, initialPrd) {
30742
31122
  totalCost,
30743
31123
  storiesCompleted,
30744
31124
  completed: true,
30745
- durationMs
31125
+ durationMs,
31126
+ storyMetrics: batchStoryMetrics,
31127
+ rectificationStats
30746
31128
  };
30747
31129
  }
30748
- return { prd, totalCost, storiesCompleted, completed: false };
31130
+ return { prd, totalCost, storiesCompleted, completed: false, storyMetrics: batchStoryMetrics, rectificationStats };
30749
31131
  }
30750
31132
  var _parallelExecutorDeps;
30751
31133
  var init_parallel_executor = __esm(() => {
@@ -30755,7 +31137,9 @@ var init_parallel_executor = __esm(() => {
30755
31137
  init_helpers();
30756
31138
  init_parallel();
30757
31139
  _parallelExecutorDeps = {
30758
- fireHook
31140
+ fireHook,
31141
+ executeParallel,
31142
+ rectifyConflictedStory
30759
31143
  };
30760
31144
  });
30761
31145
 
@@ -30997,7 +31381,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
30997
31381
  runId,
30998
31382
  storyId: ev.storyId,
30999
31383
  status: "completed",
31000
- durationMs: ev.durationMs,
31384
+ runElapsedMs: ev.runElapsedMs,
31001
31385
  cost: ev.cost ?? 0,
31002
31386
  tier: ev.modelTier ?? "balanced",
31003
31387
  testStrategy: ev.testStrategy ?? "test-after"
@@ -31019,7 +31403,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
31019
31403
  runId,
31020
31404
  storyId: ev.storyId,
31021
31405
  status: "failed",
31022
- durationMs: Date.now() - startTime,
31406
+ runElapsedMs: Date.now() - startTime,
31023
31407
  cost: 0,
31024
31408
  tier: "balanced",
31025
31409
  testStrategy: "test-after"
@@ -31041,7 +31425,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
31041
31425
  runId,
31042
31426
  storyId: ev.storyId,
31043
31427
  status: "paused",
31044
- durationMs: Date.now() - startTime,
31428
+ runElapsedMs: Date.now() - startTime,
31045
31429
  cost: 0,
31046
31430
  tier: "balanced",
31047
31431
  testStrategy: "test-after"
@@ -31119,7 +31503,7 @@ async function handleDryRun(ctx) {
31119
31503
  storyId: s.id,
31120
31504
  story: s,
31121
31505
  passed: true,
31122
- durationMs: 0,
31506
+ runElapsedMs: 0,
31123
31507
  cost: 0,
31124
31508
  modelTier: ctx.routing.modelTier,
31125
31509
  testStrategy: ctx.routing.testStrategy
@@ -31151,7 +31535,7 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
31151
31535
  storyId: completedStory.id,
31152
31536
  storyTitle: completedStory.title,
31153
31537
  totalCost: ctx.totalCost + costDelta,
31154
- durationMs: now - ctx.startTime,
31538
+ runElapsedMs: now - ctx.startTime,
31155
31539
  storyDurationMs: ctx.storyStartTime ? now - ctx.storyStartTime : undefined
31156
31540
  });
31157
31541
  pipelineEventBus.emit({
@@ -31159,7 +31543,7 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
31159
31543
  storyId: completedStory.id,
31160
31544
  story: completedStory,
31161
31545
  passed: true,
31162
- durationMs: Date.now() - ctx.startTime,
31546
+ runElapsedMs: Date.now() - ctx.startTime,
31163
31547
  cost: costDelta,
31164
31548
  modelTier: ctx.routing.modelTier,
31165
31549
  testStrategy: ctx.routing.testStrategy
@@ -31240,7 +31624,8 @@ async function handlePipelineFailure(ctx, pipelineResult) {
31240
31624
  hooks: ctx.hooks,
31241
31625
  feature: ctx.feature,
31242
31626
  totalCost: ctx.totalCost,
31243
- workdir: ctx.workdir
31627
+ workdir: ctx.workdir,
31628
+ attemptCost: pipelineResult.context.agentResult?.estimatedCost || 0
31244
31629
  });
31245
31630
  prd = escalationResult.prd;
31246
31631
  prdDirty = escalationResult.prdDirty;
@@ -31282,6 +31667,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
31282
31667
  }
31283
31668
  const storyStartTime = Date.now();
31284
31669
  const storyGitRef = await captureGitRef(ctx.workdir);
31670
+ const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
31285
31671
  const pipelineContext = {
31286
31672
  config: ctx.config,
31287
31673
  prd,
@@ -31295,7 +31681,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
31295
31681
  plugins: ctx.pluginRegistry,
31296
31682
  storyStartTime: new Date().toISOString(),
31297
31683
  storyGitRef: storyGitRef ?? undefined,
31298
- interaction: ctx.interactionChain ?? undefined
31684
+ interaction: ctx.interactionChain ?? undefined,
31685
+ accumulatedAttemptCost: accumulatedAttemptCost > 0 ? accumulatedAttemptCost : undefined
31299
31686
  };
31300
31687
  ctx.statusWriter.setPrd(prd);
31301
31688
  ctx.statusWriter.setCurrentStory({
@@ -66382,7 +66769,8 @@ init_story_context();
66382
66769
  init_escalation();
66383
66770
  init_escalation();
66384
66771
  var _runnerDeps = {
66385
- fireHook
66772
+ fireHook,
66773
+ runParallelExecution: null
66386
66774
  };
66387
66775
  async function run(options) {
66388
66776
  const {
@@ -66484,7 +66872,7 @@ async function run(options) {
66484
66872
  await tryLlmBatchRoute(config2, getAllReadyStories(prd), "routing");
66485
66873
  }
66486
66874
  if (options.parallel !== undefined) {
66487
- const { runParallelExecution: runParallelExecution2 } = await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor));
66875
+ const runParallelExecution2 = _runnerDeps.runParallelExecution ?? (await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor))).runParallelExecution;
66488
66876
  const parallelResult = await runParallelExecution2({
66489
66877
  prdPath,
66490
66878
  workdir,
@@ -66509,6 +66897,7 @@ async function run(options) {
66509
66897
  prd = parallelResult.prd;
66510
66898
  totalCost = parallelResult.totalCost;
66511
66899
  storiesCompleted = parallelResult.storiesCompleted;
66900
+ allStoryMetrics.push(...parallelResult.storyMetrics);
66512
66901
  if (parallelResult.completed && parallelResult.durationMs !== undefined) {
66513
66902
  return {
66514
66903
  success: true,
@@ -66539,8 +66928,8 @@ async function run(options) {
66539
66928
  }, prd);
66540
66929
  prd = sequentialResult.prd;
66541
66930
  iterations = sequentialResult.iterations;
66542
- storiesCompleted = sequentialResult.storiesCompleted;
66543
- totalCost = sequentialResult.totalCost;
66931
+ totalCost += sequentialResult.totalCost;
66932
+ storiesCompleted += sequentialResult.storiesCompleted;
66544
66933
  allStoryMetrics.push(...sequentialResult.allStoryMetrics);
66545
66934
  if (config2.acceptance.enabled && isComplete(prd)) {
66546
66935
  const { runAcceptanceLoop: runAcceptanceLoop2 } = await Promise.resolve().then(() => (init_acceptance_loop(), exports_acceptance_loop));