@nathapp/nax 0.56.5 → 0.57.0

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 (2) hide show
  1. package/dist/nax.js +463 -15
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17991,7 +17991,13 @@ var init_defaults = __esm(() => {
17991
17991
  model: "fast",
17992
17992
  refinement: true,
17993
17993
  redGate: true,
17994
- timeoutMs: 1800000
17994
+ timeoutMs: 1800000,
17995
+ fix: {
17996
+ diagnoseModel: "fast",
17997
+ fixModel: "balanced",
17998
+ strategy: "diagnose-first",
17999
+ maxRetries: 2
18000
+ }
17995
18001
  },
17996
18002
  context: {
17997
18003
  fileInjection: "disabled",
@@ -18086,7 +18092,7 @@ function isLegacyFlatModels(val) {
18086
18092
  }
18087
18093
  return false;
18088
18094
  }
18089
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
18095
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
18090
18096
  type: exports_external.enum(RESOLVER_TYPES).default(defaultType),
18091
18097
  agent: exports_external.string().min(1).optional(),
18092
18098
  tieBreaker: exports_external.string().min(1).optional(),
@@ -18299,6 +18305,12 @@ var init_schemas3 = __esm(() => {
18299
18305
  model: ModelTierSchema,
18300
18306
  outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty")
18301
18307
  });
18308
+ AcceptanceFixConfigSchema = exports_external.object({
18309
+ diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty"),
18310
+ fixModel: exports_external.string().min(1, "acceptance.fix.fixModel must be non-empty"),
18311
+ strategy: exports_external.enum(["diagnose-first", "implement-only"]),
18312
+ maxRetries: exports_external.number().int().nonnegative()
18313
+ });
18302
18314
  AcceptanceConfigSchema = exports_external.object({
18303
18315
  enabled: exports_external.boolean(),
18304
18316
  maxRetries: exports_external.number().int().nonnegative(),
@@ -18310,7 +18322,13 @@ var init_schemas3 = __esm(() => {
18310
18322
  redGate: exports_external.boolean().default(true),
18311
18323
  testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
18312
18324
  testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional(),
18313
- timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000)
18325
+ timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000),
18326
+ fix: AcceptanceFixConfigSchema.optional().default({
18327
+ diagnoseModel: "fast",
18328
+ fixModel: "balanced",
18329
+ strategy: "diagnose-first",
18330
+ maxRetries: 2
18331
+ })
18314
18332
  });
18315
18333
  TestCoverageConfigSchema = exports_external.object({
18316
18334
  enabled: exports_external.boolean().default(true),
@@ -18982,7 +19000,8 @@ class AcpAgentAdapter {
18982
19000
  const tryOneAgent = async (agentName) => {
18983
19001
  const model = await resolveModel2(agentName);
18984
19002
  const cmdStr = `acpx --model ${model} ${agentName}`;
18985
- const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19003
+ const timeoutSeconds = Math.ceil(timeoutMs / 1000);
19004
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir, timeoutSeconds);
18986
19005
  await client.start();
18987
19006
  let session = null;
18988
19007
  let hadError = false;
@@ -20909,7 +20928,8 @@ async function refineAcceptanceCriteria(criteria, context) {
20909
20928
  featureName,
20910
20929
  storyId,
20911
20930
  workdir,
20912
- sessionRole: "refine"
20931
+ sessionRole: "refine",
20932
+ timeoutMs: config2.acceptance?.timeoutMs ?? 120000
20913
20933
  });
20914
20934
  response = typeof completeResult === "string" ? completeResult : completeResult.output;
20915
20935
  } catch (error48) {
@@ -20958,6 +20978,7 @@ var init_refinement = __esm(() => {
20958
20978
  // src/acceptance/generator.ts
20959
20979
  var exports_generator = {};
20960
20980
  __export(exports_generator, {
20981
+ resolveAcceptanceTestFile: () => resolveAcceptanceTestFile,
20961
20982
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
20962
20983
  generateSkeletonTests: () => generateSkeletonTests,
20963
20984
  generateFromPRD: () => generateFromPRD,
@@ -20993,6 +21014,9 @@ function acceptanceTestFilename(language) {
20993
21014
  return ".nax-acceptance.test.ts";
20994
21015
  }
20995
21016
  }
21017
+ function resolveAcceptanceTestFile(language, testPathConfig) {
21018
+ return testPathConfig ?? acceptanceTestFilename(language);
21019
+ }
20996
21020
  function buildAcceptanceRunCommand(testPath, testFramework, commandOverride) {
20997
21021
  if (commandOverride) {
20998
21022
  const resolved = commandOverride.replace(/\{\{files\}\}/g, testPath).replace(/\{\{file\}\}/g, testPath).replace(/\{\{FILE\}\}/g, testPath);
@@ -21061,7 +21085,7 @@ Rules:
21061
21085
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
21062
21086
  - **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
21063
21087
  - **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
21064
- - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join5(options.workdir, ".nax", "features", options.featureName, acceptanceTestFilename(options.language))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
21088
+ - **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
21065
21089
  const prompt = basePrompt;
21066
21090
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
21067
21091
  const completeResult = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
@@ -21080,7 +21104,7 @@ Rules:
21080
21104
  outputPreview: rawOutput.slice(0, 300)
21081
21105
  });
21082
21106
  if (!testCode) {
21083
- const targetPath = join5(options.workdir, ".nax", "features", options.featureName, acceptanceTestFilename(options.language));
21107
+ const targetPath = join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
21084
21108
  let recoveryFailed = false;
21085
21109
  logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
21086
21110
  try {
@@ -22132,7 +22156,7 @@ var package_default;
22132
22156
  var init_package = __esm(() => {
22133
22157
  package_default = {
22134
22158
  name: "@nathapp/nax",
22135
- version: "0.56.5",
22159
+ version: "0.57.0",
22136
22160
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22137
22161
  type: "module",
22138
22162
  bin: {
@@ -22211,8 +22235,8 @@ var init_version = __esm(() => {
22211
22235
  NAX_VERSION = package_default.version;
22212
22236
  NAX_COMMIT = (() => {
22213
22237
  try {
22214
- if (/^[0-9a-f]{6,10}$/.test("01c24d40"))
22215
- return "01c24d40";
22238
+ if (/^[0-9a-f]{6,10}$/.test("478df448"))
22239
+ return "478df448";
22216
22240
  } catch {}
22217
22241
  try {
22218
22242
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -25977,6 +26001,7 @@ ${stderr}` };
25977
26001
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
25978
26002
  }
25979
26003
  const language = (ctx.effectiveConfig ?? ctx.config).project?.language;
26004
+ const testPathConfig = (ctx.effectiveConfig ?? ctx.config).acceptance.testPath;
25980
26005
  const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
25981
26006
  const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria);
25982
26007
  const nonFixStories = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-"));
@@ -25999,7 +26024,7 @@ ${stderr}` };
25999
26024
  const testPaths = [];
26000
26025
  for (const [workdir] of workdirGroups) {
26001
26026
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
26002
- const testPath = path5.join(packageDir, ".nax", "features", featureName, acceptanceTestFilename(language));
26027
+ const testPath = path5.join(packageDir, ".nax", "features", featureName, resolveAcceptanceTestFile(language, testPathConfig));
26003
26028
  testPaths.push({ testPath, packageDir });
26004
26029
  }
26005
26030
  let totalCriteria = 0;
@@ -26062,7 +26087,7 @@ ${stderr}` };
26062
26087
  testableCount = allRefinedCriteria.filter((r) => r.testable).length;
26063
26088
  for (const [workdir, group] of workdirGroups) {
26064
26089
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
26065
- const testPath = path5.join(packageDir, acceptanceTestFilename(language));
26090
+ const testPath = path5.join(packageDir, resolveAcceptanceTestFile(language, testPathConfig));
26066
26091
  const groupStoryIds = new Set(group.stories.map((s) => s.id));
26067
26092
  const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
26068
26093
  const result = await _acceptanceSetupDeps.generate(group.stories, groupRefined, {
@@ -34087,6 +34112,214 @@ var init_crash_recovery = __esm(() => {
34087
34112
  init_crash_heartbeat();
34088
34113
  });
34089
34114
 
34115
+ // src/acceptance/fix-diagnosis.ts
34116
+ function parseImportStatements(content) {
34117
+ const importRegex = /import\s+(?:{[^}]+}|[^;]+)\s+from\s+["']([^"']+)["']/g;
34118
+ const imports = [];
34119
+ const regexMatch = content.matchAll(importRegex);
34120
+ for (const match of regexMatch) {
34121
+ imports.push(match[1]);
34122
+ }
34123
+ return imports;
34124
+ }
34125
+ function resolveImportPaths(imports, workdir) {
34126
+ const resolved = [];
34127
+ for (const imp of imports) {
34128
+ if (imp.startsWith(".")) {
34129
+ resolved.push(imp);
34130
+ }
34131
+ }
34132
+ return resolved.slice(0, MAX_SOURCE_FILES);
34133
+ }
34134
+ async function readSourceFileContent(filePath, workdir) {
34135
+ try {
34136
+ const fullPath = `${workdir}/${filePath}`;
34137
+ const file3 = await Bun.file(fullPath).text();
34138
+ const lines = file3.split(`
34139
+ `).slice(0, MAX_FILE_LINES);
34140
+ return { path: filePath, content: lines.join(`
34141
+ `) };
34142
+ } catch {
34143
+ return null;
34144
+ }
34145
+ }
34146
+ function buildDiagnosisPrompt(options) {
34147
+ const truncatedOutput = options.testOutput.slice(0, MAX_TEST_OUTPUT_CHARS);
34148
+ const sourceFilesSection = options.sourceFiles.length > 0 ? options.sourceFiles.map((f) => `FILE: ${f.path}
34149
+ \`\`\`
34150
+ ${f.content}
34151
+ \`\`\``).join(`
34152
+
34153
+ `) : "(No source files could be resolved from imports)";
34154
+ return `You are a debugging expert. An acceptance test has failed.
34155
+
34156
+ TASK: Diagnose whether the failure is due to a bug in the SOURCE CODE or a bug in the TEST CODE.
34157
+
34158
+ FAILING TEST OUTPUT:
34159
+ ${truncatedOutput}
34160
+
34161
+ ACCEPTANCE TEST FILE CONTENT:
34162
+ \`\`\`typescript
34163
+ ${options.testFileContent}
34164
+ \`\`\`
34165
+
34166
+ SOURCE FILES (auto-detected from imports, up to ${MAX_FILE_LINES} lines each):
34167
+ ${sourceFilesSection}
34168
+
34169
+ Respond with ONLY a JSON object in this exact format (no markdown, no extra text):
34170
+ {
34171
+ "verdict": "source_bug" | "test_bug" | "both",
34172
+ "reasoning": "Your analysis explaining why this is a source_bug, test_bug, or both",
34173
+ "confidence": 0.0-1.0,
34174
+ "testIssues": ["Issue in test code if any"],
34175
+ "sourceIssues": ["Issue in source code if any"]
34176
+ }`;
34177
+ }
34178
+ async function diagnoseAcceptanceFailure(agent, options) {
34179
+ if (!agent) {
34180
+ throw new Error("[diagnosis] Agent adapter is required");
34181
+ }
34182
+ const { testOutput, testFileContent, config: config2, workdir, featureName, storyId } = options;
34183
+ const sessionName = buildSessionName(workdir, featureName, storyId, "diagnose");
34184
+ const diagnoseModelTier = config2.acceptance.fix.diagnoseModel;
34185
+ const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, diagnoseModelTier, config2.autoMode.defaultAgent);
34186
+ const imports = parseImportStatements(testFileContent);
34187
+ const relativeImports = resolveImportPaths(imports, workdir);
34188
+ const sourceFiles = await Promise.all(relativeImports.map((imp) => readSourceFileContent(imp, workdir)));
34189
+ const validSourceFiles = sourceFiles.filter((f) => f !== null);
34190
+ const prompt = buildDiagnosisPrompt({
34191
+ testOutput,
34192
+ testFileContent,
34193
+ sourceFiles: validSourceFiles
34194
+ });
34195
+ try {
34196
+ const result = await agent.run({
34197
+ prompt,
34198
+ workdir,
34199
+ modelTier: undefined,
34200
+ modelDef,
34201
+ timeoutSeconds: 300,
34202
+ sessionRole: "diagnose",
34203
+ acpSessionName: sessionName,
34204
+ featureName,
34205
+ storyId,
34206
+ config: config2
34207
+ });
34208
+ const diagnosis = parseDiagnosisResult(result.output);
34209
+ if (diagnosis) {
34210
+ return diagnosis;
34211
+ }
34212
+ return {
34213
+ verdict: "source_bug",
34214
+ reasoning: "diagnosis failed \u2014 falling back to source fix",
34215
+ confidence: 0
34216
+ };
34217
+ } catch (err) {
34218
+ return {
34219
+ verdict: "source_bug",
34220
+ reasoning: "diagnosis failed \u2014 falling back to source fix",
34221
+ confidence: 0
34222
+ };
34223
+ }
34224
+ }
34225
+ function parseDiagnosisResult(output) {
34226
+ if (!output || output.trim() === "") {
34227
+ return null;
34228
+ }
34229
+ try {
34230
+ const cleaned = output.trim();
34231
+ let jsonStr = cleaned;
34232
+ const firstBrace = cleaned.indexOf("{");
34233
+ const lastBrace = cleaned.lastIndexOf("}");
34234
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
34235
+ jsonStr = cleaned.slice(firstBrace, lastBrace + 1);
34236
+ }
34237
+ const parsed = JSON.parse(jsonStr);
34238
+ if (typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
34239
+ return {
34240
+ verdict: parsed.verdict,
34241
+ reasoning: parsed.reasoning,
34242
+ confidence: parsed.confidence,
34243
+ testIssues: parsed.testIssues,
34244
+ sourceIssues: parsed.sourceIssues
34245
+ };
34246
+ }
34247
+ return null;
34248
+ } catch {
34249
+ return null;
34250
+ }
34251
+ }
34252
+ var MAX_SOURCE_FILES = 5, MAX_FILE_LINES = 500, MAX_TEST_OUTPUT_CHARS = 2000;
34253
+ var init_fix_diagnosis = __esm(() => {
34254
+ init_adapter();
34255
+ });
34256
+
34257
+ // src/acceptance/fix-executor.ts
34258
+ function buildSourceFixPrompt(options) {
34259
+ const { testOutput, diagnosis, acceptanceTestPath } = options;
34260
+ let prompt = `ACCEPTANCE TEST FAILURE:
34261
+ ${testOutput}
34262
+
34263
+ `;
34264
+ if (diagnosis.reasoning) {
34265
+ prompt += `DIAGNOSIS:
34266
+ ${diagnosis.reasoning}
34267
+
34268
+ `;
34269
+ }
34270
+ prompt += `ACCEPTANCE TEST FILE: ${acceptanceTestPath}
34271
+
34272
+ `;
34273
+ prompt += "Fix the source implementation. Do NOT modify the test file.";
34274
+ return prompt;
34275
+ }
34276
+ async function executeSourceFix(agent, options) {
34277
+ if (!agent) {
34278
+ throw new Error("[fix-executor] agent is required");
34279
+ }
34280
+ const { testOutput, testFileContent, diagnosis, config: config2, workdir, featureName, storyId, acceptanceTestPath } = options;
34281
+ const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, config2.acceptance.fix.fixModel, config2.autoMode.defaultAgent);
34282
+ const sessionName = buildSessionName(workdir, featureName, storyId, "source-fix");
34283
+ const prompt = buildSourceFixPrompt(options);
34284
+ const timeoutSeconds = config2.execution?.sessionTimeoutSeconds ?? 3600;
34285
+ const runOptions = {
34286
+ prompt,
34287
+ workdir,
34288
+ modelTier: undefined,
34289
+ modelDef,
34290
+ timeoutSeconds,
34291
+ sessionRole: "source-fix",
34292
+ acpSessionName: sessionName,
34293
+ featureName,
34294
+ storyId,
34295
+ config: config2,
34296
+ pipelineStage: "acceptance"
34297
+ };
34298
+ const result = await agent.run(runOptions);
34299
+ let success2 = result.success;
34300
+ try {
34301
+ const verifyProc = _fixExecutorDeps.spawn(["bun", "test", acceptanceTestPath], {
34302
+ cwd: workdir,
34303
+ stdout: "pipe",
34304
+ stderr: "pipe"
34305
+ });
34306
+ const exitCode = await verifyProc.exited;
34307
+ success2 = exitCode === 0;
34308
+ } catch {
34309
+ success2 = result.success;
34310
+ }
34311
+ return {
34312
+ success: success2,
34313
+ cost: result.estimatedCost
34314
+ };
34315
+ }
34316
+ var _fixExecutorDeps;
34317
+ var init_fix_executor = __esm(() => {
34318
+ init_adapter();
34319
+ init_bun_deps();
34320
+ _fixExecutorDeps = { spawn };
34321
+ });
34322
+
34090
34323
  // src/execution/lifecycle/acceptance-loop.ts
34091
34324
  var exports_acceptance_loop = {};
34092
34325
  __export(exports_acceptance_loop, {
@@ -34113,6 +34346,14 @@ async function loadSpecContent(featureDir) {
34113
34346
  const specFile = Bun.file(specPath);
34114
34347
  return await specFile.exists() ? await specFile.text() : "";
34115
34348
  }
34349
+ async function loadAcceptanceTestContent(featureDir) {
34350
+ if (!featureDir)
34351
+ return { content: "", path: "" };
34352
+ const testPath = path14.join(featureDir, "acceptance.test.ts");
34353
+ const testFile = Bun.file(testPath);
34354
+ const content = await testFile.exists() ? await testFile.text() : "";
34355
+ return { content, path: testPath };
34356
+ }
34116
34357
  function buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty) {
34117
34358
  return { success: success2, prd, totalCost, iterations, storiesCompleted, prdDirty };
34118
34359
  }
@@ -34198,6 +34439,191 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
34198
34439
  logger?.info("acceptance", "Acceptance test regenerated successfully");
34199
34440
  return true;
34200
34441
  }
34442
+ async function runFixRouting(options) {
34443
+ const logger = getSafeLogger();
34444
+ const { ctx, failures, prd, acceptanceContext } = options;
34445
+ const agentName = ctx.config.autoMode.defaultAgent;
34446
+ const agent = (ctx.agentGetFn ?? _acceptanceLoopDeps.getAgent)(agentName);
34447
+ if (!agent) {
34448
+ logger?.error("acceptance", "Agent not found for fix routing");
34449
+ return { fixed: false, cost: 0, prdDirty: false };
34450
+ }
34451
+ const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
34452
+ const fixMaxRetries = ctx.config.acceptance.fix?.maxRetries ?? 2;
34453
+ const { content: testFileContent, path: acceptanceTestPath } = await loadAcceptanceTestContent(ctx.featureDir);
34454
+ const firstStory = prd.userStories[0];
34455
+ const storyId = firstStory?.id ?? "unknown";
34456
+ if (strategy === "implement-only") {
34457
+ logger?.info("acceptance", "Strategy is implement-only \u2014 executing source fix directly");
34458
+ let fixAttempts = 0;
34459
+ while (fixAttempts < fixMaxRetries) {
34460
+ fixAttempts++;
34461
+ logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
34462
+ const defaultDiagnosis = {
34463
+ verdict: "source_bug",
34464
+ reasoning: "implement-only strategy \u2014 skipping diagnosis",
34465
+ confidence: 1
34466
+ };
34467
+ const fixResult = await executeSourceFix(agent, {
34468
+ testOutput: failures.testOutput,
34469
+ testFileContent,
34470
+ diagnosis: defaultDiagnosis,
34471
+ config: ctx.config,
34472
+ workdir: ctx.workdir,
34473
+ featureName: ctx.feature,
34474
+ storyId,
34475
+ acceptanceTestPath
34476
+ });
34477
+ logger?.info("acceptance.source-fix", "Source fix completed", {
34478
+ success: fixResult.success,
34479
+ cost: fixResult.cost,
34480
+ attempt: fixAttempts
34481
+ });
34482
+ if (fixResult.success) {
34483
+ return { fixed: true, cost: fixResult.cost, prdDirty: false };
34484
+ }
34485
+ if (fixAttempts >= fixMaxRetries) {
34486
+ logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
34487
+ break;
34488
+ }
34489
+ }
34490
+ return { fixed: false, cost: 0, prdDirty: false };
34491
+ }
34492
+ logger?.info("acceptance", "Strategy is diagnose-first \u2014 running diagnosis");
34493
+ const diagnosis = await diagnoseAcceptanceFailure(agent, {
34494
+ testOutput: failures.testOutput,
34495
+ testFileContent,
34496
+ config: ctx.config,
34497
+ workdir: ctx.workdir,
34498
+ featureName: ctx.feature,
34499
+ storyId
34500
+ });
34501
+ logger?.info("acceptance.diagnosis", "Diagnosis complete", {
34502
+ verdict: diagnosis.verdict,
34503
+ confidence: diagnosis.confidence,
34504
+ reasoning: diagnosis.reasoning
34505
+ });
34506
+ if (diagnosis.verdict === "source_bug") {
34507
+ logger?.info("acceptance", "Diagnosis: source_bug \u2014 executing source fix");
34508
+ let fixAttempts = 0;
34509
+ while (fixAttempts < fixMaxRetries) {
34510
+ fixAttempts++;
34511
+ logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
34512
+ const fixResult = await executeSourceFix(agent, {
34513
+ testOutput: failures.testOutput,
34514
+ testFileContent,
34515
+ diagnosis,
34516
+ config: ctx.config,
34517
+ workdir: ctx.workdir,
34518
+ featureName: ctx.feature,
34519
+ storyId,
34520
+ acceptanceTestPath
34521
+ });
34522
+ logger?.info("acceptance.source-fix", "Source fix completed", {
34523
+ success: fixResult.success,
34524
+ cost: fixResult.cost,
34525
+ attempt: fixAttempts
34526
+ });
34527
+ if (fixResult.success) {
34528
+ return { fixed: true, cost: fixResult.cost, prdDirty: false };
34529
+ }
34530
+ if (fixAttempts >= fixMaxRetries) {
34531
+ logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
34532
+ break;
34533
+ }
34534
+ }
34535
+ return { fixed: false, cost: 0, prdDirty: false };
34536
+ }
34537
+ if (diagnosis.verdict === "test_bug") {
34538
+ logger?.info("acceptance", "Diagnosis: test_bug \u2014 regenerating acceptance test");
34539
+ if (!ctx.featureDir) {
34540
+ logger?.error("acceptance", "Cannot regenerate test without featureDir");
34541
+ return { fixed: false, cost: 0, prdDirty: false };
34542
+ }
34543
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
34544
+ const testFile = Bun.file(testPath);
34545
+ if (!await testFile.exists()) {
34546
+ logger?.error("acceptance", "Acceptance test file not found for regeneration");
34547
+ return { fixed: false, cost: 0, prdDirty: false };
34548
+ }
34549
+ const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
34550
+ logger?.info("acceptance.test-regen", "Test regeneration completed", {
34551
+ outcome: regenerated ? "success" : "failure"
34552
+ });
34553
+ if (!regenerated) {
34554
+ return { fixed: false, cost: 0, prdDirty: false };
34555
+ }
34556
+ const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
34557
+ const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
34558
+ if (acceptanceResult.action === "continue") {
34559
+ logger?.info("acceptance", "Acceptance passed after test regeneration");
34560
+ return { fixed: true, cost: 0, prdDirty: true };
34561
+ }
34562
+ logger?.warn("acceptance", "Acceptance still failing after test regeneration");
34563
+ return { fixed: false, cost: 0, prdDirty: true };
34564
+ }
34565
+ if (diagnosis.verdict === "both") {
34566
+ logger?.info("acceptance", "Diagnosis: both \u2014 executing source fix then regenerating test if needed");
34567
+ let sourceFixSuccess = false;
34568
+ let sourceFixCost = 0;
34569
+ let fixAttempts = 0;
34570
+ while (fixAttempts < fixMaxRetries && !sourceFixSuccess) {
34571
+ fixAttempts++;
34572
+ logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
34573
+ const fixResult = await executeSourceFix(agent, {
34574
+ testOutput: failures.testOutput,
34575
+ testFileContent,
34576
+ diagnosis,
34577
+ config: ctx.config,
34578
+ workdir: ctx.workdir,
34579
+ featureName: ctx.feature,
34580
+ storyId,
34581
+ acceptanceTestPath
34582
+ });
34583
+ logger?.info("acceptance.source-fix", "Source fix completed", {
34584
+ success: fixResult.success,
34585
+ cost: fixResult.cost,
34586
+ attempt: fixAttempts
34587
+ });
34588
+ sourceFixSuccess = fixResult.success;
34589
+ sourceFixCost += fixResult.cost;
34590
+ if (fixResult.success) {
34591
+ break;
34592
+ }
34593
+ if (fixAttempts >= fixMaxRetries) {
34594
+ logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
34595
+ break;
34596
+ }
34597
+ }
34598
+ if (!sourceFixSuccess) {
34599
+ return { fixed: false, cost: sourceFixCost, prdDirty: false };
34600
+ }
34601
+ logger?.info("acceptance", "Source fix succeeded \u2014 re-running acceptance to verify");
34602
+ const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
34603
+ const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
34604
+ if (acceptanceResult.action === "continue") {
34605
+ logger?.info("acceptance", "Acceptance passed after source fix");
34606
+ return { fixed: true, cost: sourceFixCost, prdDirty: false };
34607
+ }
34608
+ logger?.info("acceptance", "Acceptance still failing after source fix \u2014 regenerating test");
34609
+ if (!ctx.featureDir) {
34610
+ logger?.error("acceptance", "Cannot regenerate test without featureDir");
34611
+ return { fixed: false, cost: sourceFixCost, prdDirty: false };
34612
+ }
34613
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
34614
+ const testFile = Bun.file(testPath);
34615
+ if (!await testFile.exists()) {
34616
+ logger?.error("acceptance", "Acceptance test file not found for regeneration");
34617
+ return { fixed: false, cost: sourceFixCost, prdDirty: false };
34618
+ }
34619
+ const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
34620
+ logger?.info("acceptance.test-regen", "Test regeneration completed", {
34621
+ outcome: regenerated ? "success" : "failure"
34622
+ });
34623
+ return { fixed: regenerated, cost: sourceFixCost, prdDirty: regenerated };
34624
+ }
34625
+ return { fixed: false, cost: 0, prdDirty: false };
34626
+ }
34201
34627
  async function runAcceptanceLoop(ctx) {
34202
34628
  const logger = getSafeLogger();
34203
34629
  const maxRetries = ctx.config.acceptance.maxRetries;
@@ -34292,6 +34718,23 @@ async function runAcceptanceLoop(ctx) {
34292
34718
  continue;
34293
34719
  }
34294
34720
  }
34721
+ const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
34722
+ if (strategy === "diagnose-first" || strategy === "implement-only") {
34723
+ logger?.info("acceptance", `Running fix routing with strategy: ${strategy}`);
34724
+ const fixResult = await runFixRouting({
34725
+ ctx,
34726
+ failures,
34727
+ prd,
34728
+ acceptanceContext
34729
+ });
34730
+ totalCost += fixResult.cost;
34731
+ if (fixResult.fixed) {
34732
+ logger?.info("acceptance", "Fix succeeded \u2014 re-running acceptance tests...");
34733
+ continue;
34734
+ }
34735
+ logger?.error("acceptance", "Fix routing failed to resolve acceptance failures");
34736
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
34737
+ }
34295
34738
  logger?.info("acceptance", "Generating fix stories...");
34296
34739
  const fixStories = await generateAndAddFixStories(ctx, failures, prd);
34297
34740
  if (!fixStories) {
@@ -34323,6 +34766,8 @@ async function runAcceptanceLoop(ctx) {
34323
34766
  var _acceptanceLoopDeps;
34324
34767
  var init_acceptance_loop = __esm(() => {
34325
34768
  init_acceptance();
34769
+ init_fix_diagnosis();
34770
+ init_fix_executor();
34326
34771
  init_registry();
34327
34772
  init_config();
34328
34773
  init_loader();
@@ -69944,7 +70389,8 @@ async function planCommand(workdir, config2, options) {
69944
70389
  workdir,
69945
70390
  config: config2,
69946
70391
  featureName: options.feature,
69947
- sessionRole: "plan"
70392
+ sessionRole: "plan",
70393
+ timeoutMs: (config2?.execution?.sessionTimeoutSeconds ?? 3600) * 1000
69948
70394
  });
69949
70395
  let result = typeof completeResult === "string" ? completeResult : completeResult.output;
69950
70396
  try {
@@ -70337,7 +70783,8 @@ async function planDecomposeCommand(workdir, config2, options) {
70337
70783
  workdir,
70338
70784
  sessionRole: "decompose",
70339
70785
  featureName: options.feature,
70340
- storyId: options.storyId
70786
+ storyId: options.storyId,
70787
+ timeoutMs: (config2?.execution?.sessionTimeoutSeconds ?? 3600) * 1000
70341
70788
  });
70342
70789
  rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
70343
70790
  }
@@ -70347,7 +70794,8 @@ async function planDecomposeCommand(workdir, config2, options) {
70347
70794
  workdir,
70348
70795
  sessionRole: "decompose",
70349
70796
  featureName: options.feature,
70350
- storyId: options.storyId
70797
+ storyId: options.storyId,
70798
+ timeoutMs: (config2?.execution?.sessionTimeoutSeconds ?? 3600) * 1000
70351
70799
  });
70352
70800
  rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
70353
70801
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.56.5",
3
+ "version": "0.57.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {