@nathapp/nax 0.56.5 → 0.57.1-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +558 -42
  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",
@@ -18042,31 +18048,36 @@ var init_defaults = __esm(() => {
18042
18048
  enabled: true,
18043
18049
  resolver: { type: "synthesis" },
18044
18050
  sessionMode: "stateful",
18045
- rounds: 3
18051
+ rounds: 3,
18052
+ timeoutSeconds: 600
18046
18053
  },
18047
18054
  review: {
18048
18055
  enabled: true,
18049
18056
  resolver: { type: "majority-fail-closed" },
18050
18057
  sessionMode: "one-shot",
18051
- rounds: 2
18058
+ rounds: 2,
18059
+ timeoutSeconds: 600
18052
18060
  },
18053
18061
  acceptance: {
18054
18062
  enabled: false,
18055
18063
  resolver: { type: "majority-fail-closed" },
18056
18064
  sessionMode: "one-shot",
18057
- rounds: 1
18065
+ rounds: 1,
18066
+ timeoutSeconds: 600
18058
18067
  },
18059
18068
  rectification: {
18060
18069
  enabled: false,
18061
18070
  resolver: { type: "synthesis" },
18062
18071
  sessionMode: "one-shot",
18063
- rounds: 1
18072
+ rounds: 1,
18073
+ timeoutSeconds: 600
18064
18074
  },
18065
18075
  escalation: {
18066
18076
  enabled: false,
18067
18077
  resolver: { type: "majority-fail-closed" },
18068
18078
  sessionMode: "one-shot",
18069
- rounds: 1
18079
+ rounds: 1,
18080
+ timeoutSeconds: 600
18070
18081
  }
18071
18082
  }
18072
18083
  }
@@ -18086,7 +18097,7 @@ function isLegacyFlatModels(val) {
18086
18097
  }
18087
18098
  return false;
18088
18099
  }
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({
18100
+ 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
18101
  type: exports_external.enum(RESOLVER_TYPES).default(defaultType),
18091
18102
  agent: exports_external.string().min(1).optional(),
18092
18103
  tieBreaker: exports_external.string().min(1).optional(),
@@ -18096,7 +18107,8 @@ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema
18096
18107
  resolver: makeResolverSchema(defaults.resolverType),
18097
18108
  sessionMode: exports_external.enum(["one-shot", "stateful"]).default(defaults.sessionMode),
18098
18109
  rounds: exports_external.number().int().min(1).default(defaults.rounds),
18099
- debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional()
18110
+ debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional(),
18111
+ timeoutSeconds: exports_external.number().int().positive().default(600)
18100
18112
  })), DebateConfigSchema, NaxConfigSchema;
18101
18113
  var init_schemas3 = __esm(() => {
18102
18114
  init_zod();
@@ -18297,7 +18309,14 @@ var init_schemas3 = __esm(() => {
18297
18309
  });
18298
18310
  PlanConfigSchema = exports_external.object({
18299
18311
  model: ModelTierSchema,
18300
- outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty")
18312
+ outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty"),
18313
+ timeoutSeconds: exports_external.number().int().positive().default(600)
18314
+ });
18315
+ AcceptanceFixConfigSchema = exports_external.object({
18316
+ diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty"),
18317
+ fixModel: exports_external.string().min(1, "acceptance.fix.fixModel must be non-empty"),
18318
+ strategy: exports_external.enum(["diagnose-first", "implement-only"]),
18319
+ maxRetries: exports_external.number().int().nonnegative()
18301
18320
  });
18302
18321
  AcceptanceConfigSchema = exports_external.object({
18303
18322
  enabled: exports_external.boolean(),
@@ -18310,7 +18329,13 @@ var init_schemas3 = __esm(() => {
18310
18329
  redGate: exports_external.boolean().default(true),
18311
18330
  testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
18312
18331
  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)
18332
+ timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000),
18333
+ fix: AcceptanceFixConfigSchema.optional().default({
18334
+ diagnoseModel: "fast",
18335
+ fixModel: "balanced",
18336
+ strategy: "diagnose-first",
18337
+ maxRetries: 2
18338
+ })
18314
18339
  });
18315
18340
  TestCoverageConfigSchema = exports_external.object({
18316
18341
  enabled: exports_external.boolean().default(true),
@@ -18982,7 +19007,8 @@ class AcpAgentAdapter {
18982
19007
  const tryOneAgent = async (agentName) => {
18983
19008
  const model = await resolveModel2(agentName);
18984
19009
  const cmdStr = `acpx --model ${model} ${agentName}`;
18985
- const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19010
+ const timeoutSeconds = Math.ceil(timeoutMs / 1000);
19011
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir, timeoutSeconds);
18986
19012
  await client.start();
18987
19013
  let session = null;
18988
19014
  let hadError = false;
@@ -19827,9 +19853,9 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
19827
19853
  modelDef,
19828
19854
  prompt: "",
19829
19855
  modelTier: options.modelTier || "balanced",
19830
- timeoutSeconds: 600
19856
+ timeoutSeconds: options.timeoutSeconds ?? 600
19831
19857
  };
19832
- const PLAN_TIMEOUT_MS = 600000;
19858
+ const planTimeoutMs = (options.timeoutSeconds ?? 600) * 1000;
19833
19859
  if (options.interactive) {
19834
19860
  const proc = Bun.spawn(cmd, {
19835
19861
  cwd: options.workdir,
@@ -19841,7 +19867,7 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
19841
19867
  await pidRegistry.register(proc.pid);
19842
19868
  let exitCode;
19843
19869
  try {
19844
- const timeoutResult = await withProcessTimeout(proc, PLAN_TIMEOUT_MS, {
19870
+ const timeoutResult = await withProcessTimeout(proc, planTimeoutMs, {
19845
19871
  graceMs: 5000
19846
19872
  });
19847
19873
  exitCode = timeoutResult.exitCode;
@@ -19867,7 +19893,7 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
19867
19893
  await pidRegistry.register(proc.pid);
19868
19894
  let exitCode;
19869
19895
  try {
19870
- const timeoutResult = await withProcessTimeout(proc, PLAN_TIMEOUT_MS, {
19896
+ const timeoutResult = await withProcessTimeout(proc, planTimeoutMs, {
19871
19897
  graceMs: 5000
19872
19898
  });
19873
19899
  exitCode = timeoutResult.exitCode;
@@ -20909,7 +20935,8 @@ async function refineAcceptanceCriteria(criteria, context) {
20909
20935
  featureName,
20910
20936
  storyId,
20911
20937
  workdir,
20912
- sessionRole: "refine"
20938
+ sessionRole: "refine",
20939
+ timeoutMs: config2.acceptance?.timeoutMs ?? 120000
20913
20940
  });
20914
20941
  response = typeof completeResult === "string" ? completeResult : completeResult.output;
20915
20942
  } catch (error48) {
@@ -20958,6 +20985,7 @@ var init_refinement = __esm(() => {
20958
20985
  // src/acceptance/generator.ts
20959
20986
  var exports_generator = {};
20960
20987
  __export(exports_generator, {
20988
+ resolveAcceptanceTestFile: () => resolveAcceptanceTestFile,
20961
20989
  parseAcceptanceCriteria: () => parseAcceptanceCriteria,
20962
20990
  generateSkeletonTests: () => generateSkeletonTests,
20963
20991
  generateFromPRD: () => generateFromPRD,
@@ -20993,6 +21021,9 @@ function acceptanceTestFilename(language) {
20993
21021
  return ".nax-acceptance.test.ts";
20994
21022
  }
20995
21023
  }
21024
+ function resolveAcceptanceTestFile(language, testPathConfig) {
21025
+ return testPathConfig ?? acceptanceTestFilename(language);
21026
+ }
20996
21027
  function buildAcceptanceRunCommand(testPath, testFramework, commandOverride) {
20997
21028
  if (commandOverride) {
20998
21029
  const resolved = commandOverride.replace(/\{\{files\}\}/g, testPath).replace(/\{\{file\}\}/g, testPath).replace(/\{\{FILE\}\}/g, testPath);
@@ -21061,7 +21092,7 @@ Rules:
21061
21092
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
21062
21093
  - **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
21094
  - **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).`;
21095
+ - **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
21096
  const prompt = basePrompt;
21066
21097
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
21067
21098
  const completeResult = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
@@ -21080,7 +21111,7 @@ Rules:
21080
21111
  outputPreview: rawOutput.slice(0, 300)
21081
21112
  });
21082
21113
  if (!testCode) {
21083
- const targetPath = join5(options.workdir, ".nax", "features", options.featureName, acceptanceTestFilename(options.language));
21114
+ const targetPath = join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
21084
21115
  let recoveryFailed = false;
21085
21116
  logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
21086
21117
  try {
@@ -22132,7 +22163,7 @@ var package_default;
22132
22163
  var init_package = __esm(() => {
22133
22164
  package_default = {
22134
22165
  name: "@nathapp/nax",
22135
- version: "0.56.5",
22166
+ version: "0.57.1-canary.1",
22136
22167
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22137
22168
  type: "module",
22138
22169
  bin: {
@@ -22211,8 +22242,8 @@ var init_version = __esm(() => {
22211
22242
  NAX_VERSION = package_default.version;
22212
22243
  NAX_COMMIT = (() => {
22213
22244
  try {
22214
- if (/^[0-9a-f]{6,10}$/.test("01c24d40"))
22215
- return "01c24d40";
22245
+ if (/^[0-9a-f]{6,10}$/.test("814ed29f"))
22246
+ return "814ed29f";
22216
22247
  } catch {}
22217
22248
  try {
22218
22249
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22499,10 +22530,11 @@ function modelTierFromDebater(debater) {
22499
22530
  function isTierLabel(value) {
22500
22531
  return value === "fast" || value === "balanced" || value === "powerful";
22501
22532
  }
22502
- async function runComplete(adapter, prompt, options, modelTier) {
22533
+ async function runComplete(adapter, prompt, options, modelTier, timeoutMs) {
22503
22534
  return adapter.complete(prompt, {
22504
22535
  ...options,
22505
- modelTier
22536
+ modelTier,
22537
+ ...timeoutMs !== undefined && { timeoutMs }
22506
22538
  });
22507
22539
  }
22508
22540
 
@@ -22514,6 +22546,9 @@ class DebateSession {
22514
22546
  workdir;
22515
22547
  featureName;
22516
22548
  timeoutSeconds;
22549
+ get timeoutMs() {
22550
+ return this.timeoutSeconds * 1000;
22551
+ }
22517
22552
  constructor(opts) {
22518
22553
  this.storyId = opts.storyId;
22519
22554
  this.stage = opts.stage;
@@ -22521,7 +22556,7 @@ class DebateSession {
22521
22556
  this.config = opts.config;
22522
22557
  this.workdir = opts.workdir ?? process.cwd();
22523
22558
  this.featureName = opts.featureName ?? opts.stage;
22524
- this.timeoutSeconds = opts.timeoutSeconds ?? opts.config?.execution?.sessionTimeoutSeconds ?? 600;
22559
+ this.timeoutSeconds = opts.timeoutSeconds ?? opts.stageConfig.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
22525
22560
  }
22526
22561
  pipelineStageForDebate() {
22527
22562
  switch (this.stage) {
@@ -22761,7 +22796,8 @@ class DebateSession {
22761
22796
  featureName: this.stage,
22762
22797
  config: this.config,
22763
22798
  storyId: this.storyId,
22764
- sessionRole: "debate-proposal"
22799
+ sessionRole: "debate-proposal",
22800
+ timeoutMs: this.timeoutMs
22765
22801
  }, modelTierFromDebater(debater)).then((result) => ({ debater, adapter, output: result.output, cost: result.costUsd }))));
22766
22802
  const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
22767
22803
  for (const r of proposalSettled) {
@@ -22815,7 +22851,8 @@ class DebateSession {
22815
22851
  featureName: this.stage,
22816
22852
  config: this.config,
22817
22853
  storyId: this.storyId,
22818
- sessionRole: "debate-fallback"
22854
+ sessionRole: "debate-fallback",
22855
+ timeoutMs: this.timeoutMs
22819
22856
  }, modelTierFromDebater(fallbackDebater));
22820
22857
  totalCostUsd += fallbackResult.costUsd;
22821
22858
  logger?.info("debate", "debate:result", {
@@ -22845,7 +22882,8 @@ class DebateSession {
22845
22882
  featureName: this.stage,
22846
22883
  config: this.config,
22847
22884
  storyId: this.storyId,
22848
- sessionRole: "debate-critique"
22885
+ sessionRole: "debate-critique",
22886
+ timeoutMs: this.timeoutMs
22849
22887
  }, modelTierFromDebater(debater))));
22850
22888
  for (const r of critiqueSettled) {
22851
22889
  if (r.status === "fulfilled") {
@@ -22988,7 +23026,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
22988
23026
  model: resolveDebaterModel({ agent: agentName }, this.config),
22989
23027
  config: this.config,
22990
23028
  storyId: this.storyId,
22991
- sessionRole: "synthesis"
23029
+ sessionRole: "synthesis",
23030
+ timeoutMs: this.timeoutMs
22992
23031
  }
22993
23032
  });
22994
23033
  return {
@@ -23010,7 +23049,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
23010
23049
  model: resolveDebaterModel({ agent: agentName }, this.config),
23011
23050
  config: this.config,
23012
23051
  storyId: this.storyId,
23013
- sessionRole: "judge"
23052
+ sessionRole: "judge",
23053
+ timeoutMs: this.timeoutMs
23014
23054
  }
23015
23055
  });
23016
23056
  return {
@@ -23024,7 +23064,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
23024
23064
  };
23025
23065
  }
23026
23066
  }
23027
- var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps;
23067
+ var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps, DEFAULT_TIMEOUT_SECONDS = 600;
23028
23068
  var init_session = __esm(() => {
23029
23069
  init_registry();
23030
23070
  init_config();
@@ -23228,13 +23268,15 @@ class AutoInteractionPlugin {
23228
23268
  const modelDef = resolveModelForAgent(naxConfig.models, naxConfig.autoMode.defaultAgent, modelTier, naxConfig.autoMode.defaultAgent);
23229
23269
  modelArg = modelDef.model;
23230
23270
  }
23271
+ const timeoutMs = this.config.naxConfig ? (this.config.naxConfig.execution?.sessionTimeoutSeconds ?? 600) * 1000 : undefined;
23231
23272
  const result = await adapter.complete(prompt, {
23232
23273
  ...modelArg && { model: modelArg },
23233
23274
  jsonMode: true,
23234
23275
  ...this.config.naxConfig && { config: this.config.naxConfig },
23235
23276
  featureName: request.featureName,
23236
23277
  storyId: request.storyId,
23237
- sessionRole: "auto"
23278
+ sessionRole: "auto",
23279
+ ...timeoutMs !== undefined && { timeoutMs }
23238
23280
  });
23239
23281
  const output = typeof result === "string" ? result : result.output;
23240
23282
  return this.parseResponse(output);
@@ -25977,6 +26019,7 @@ ${stderr}` };
25977
26019
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
25978
26020
  }
25979
26021
  const language = (ctx.effectiveConfig ?? ctx.config).project?.language;
26022
+ const testPathConfig = (ctx.effectiveConfig ?? ctx.config).acceptance.testPath;
25980
26023
  const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
25981
26024
  const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria);
25982
26025
  const nonFixStories = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-"));
@@ -25999,7 +26042,7 @@ ${stderr}` };
25999
26042
  const testPaths = [];
26000
26043
  for (const [workdir] of workdirGroups) {
26001
26044
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
26002
- const testPath = path5.join(packageDir, ".nax", "features", featureName, acceptanceTestFilename(language));
26045
+ const testPath = path5.join(packageDir, ".nax", "features", featureName, resolveAcceptanceTestFile(language, testPathConfig));
26003
26046
  testPaths.push({ testPath, packageDir });
26004
26047
  }
26005
26048
  let totalCriteria = 0;
@@ -26062,7 +26105,7 @@ ${stderr}` };
26062
26105
  testableCount = allRefinedCriteria.filter((r) => r.testable).length;
26063
26106
  for (const [workdir, group] of workdirGroups) {
26064
26107
  const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
26065
- const testPath = path5.join(packageDir, acceptanceTestFilename(language));
26108
+ const testPath = path5.join(packageDir, resolveAcceptanceTestFile(language, testPathConfig));
26066
26109
  const groupStoryIds = new Set(group.stories.map((s) => s.id));
26067
26110
  const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
26068
26111
  const result = await _acceptanceSetupDeps.generate(group.stories, groupRefined, {
@@ -31531,8 +31574,15 @@ async function _defaultRunDebate(storyId, stageConfig, prompt, config2) {
31531
31574
  if (resolved.length === 0) {
31532
31575
  return { output: null, totalCostUsd: 0 };
31533
31576
  }
31577
+ const timeoutMs = (config2?.execution?.sessionTimeoutSeconds ?? 600) * 1000;
31534
31578
  const startMs = Date.now();
31535
- const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, { model: debater.model }).then((out) => typeof out === "string" ? out : out.output)));
31579
+ const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, {
31580
+ model: debater.model,
31581
+ config: config2,
31582
+ storyId,
31583
+ sessionRole: "debate-proposal",
31584
+ timeoutMs
31585
+ }).then((out) => typeof out === "string" ? out : out.output)));
31536
31586
  const durationMs = Date.now() - startMs;
31537
31587
  const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
31538
31588
  if (successful.length === 0) {
@@ -34087,6 +34137,214 @@ var init_crash_recovery = __esm(() => {
34087
34137
  init_crash_heartbeat();
34088
34138
  });
34089
34139
 
34140
+ // src/acceptance/fix-diagnosis.ts
34141
+ function parseImportStatements(content) {
34142
+ const importRegex = /import\s+(?:{[^}]+}|[^;]+)\s+from\s+["']([^"']+)["']/g;
34143
+ const imports = [];
34144
+ const regexMatch = content.matchAll(importRegex);
34145
+ for (const match of regexMatch) {
34146
+ imports.push(match[1]);
34147
+ }
34148
+ return imports;
34149
+ }
34150
+ function resolveImportPaths(imports, workdir) {
34151
+ const resolved = [];
34152
+ for (const imp of imports) {
34153
+ if (imp.startsWith(".")) {
34154
+ resolved.push(imp);
34155
+ }
34156
+ }
34157
+ return resolved.slice(0, MAX_SOURCE_FILES);
34158
+ }
34159
+ async function readSourceFileContent(filePath, workdir) {
34160
+ try {
34161
+ const fullPath = `${workdir}/${filePath}`;
34162
+ const file3 = await Bun.file(fullPath).text();
34163
+ const lines = file3.split(`
34164
+ `).slice(0, MAX_FILE_LINES);
34165
+ return { path: filePath, content: lines.join(`
34166
+ `) };
34167
+ } catch {
34168
+ return null;
34169
+ }
34170
+ }
34171
+ function buildDiagnosisPrompt(options) {
34172
+ const truncatedOutput = options.testOutput.slice(0, MAX_TEST_OUTPUT_CHARS);
34173
+ const sourceFilesSection = options.sourceFiles.length > 0 ? options.sourceFiles.map((f) => `FILE: ${f.path}
34174
+ \`\`\`
34175
+ ${f.content}
34176
+ \`\`\``).join(`
34177
+
34178
+ `) : "(No source files could be resolved from imports)";
34179
+ return `You are a debugging expert. An acceptance test has failed.
34180
+
34181
+ TASK: Diagnose whether the failure is due to a bug in the SOURCE CODE or a bug in the TEST CODE.
34182
+
34183
+ FAILING TEST OUTPUT:
34184
+ ${truncatedOutput}
34185
+
34186
+ ACCEPTANCE TEST FILE CONTENT:
34187
+ \`\`\`typescript
34188
+ ${options.testFileContent}
34189
+ \`\`\`
34190
+
34191
+ SOURCE FILES (auto-detected from imports, up to ${MAX_FILE_LINES} lines each):
34192
+ ${sourceFilesSection}
34193
+
34194
+ Respond with ONLY a JSON object in this exact format (no markdown, no extra text):
34195
+ {
34196
+ "verdict": "source_bug" | "test_bug" | "both",
34197
+ "reasoning": "Your analysis explaining why this is a source_bug, test_bug, or both",
34198
+ "confidence": 0.0-1.0,
34199
+ "testIssues": ["Issue in test code if any"],
34200
+ "sourceIssues": ["Issue in source code if any"]
34201
+ }`;
34202
+ }
34203
+ async function diagnoseAcceptanceFailure(agent, options) {
34204
+ if (!agent) {
34205
+ throw new Error("[diagnosis] Agent adapter is required");
34206
+ }
34207
+ const { testOutput, testFileContent, config: config2, workdir, featureName, storyId } = options;
34208
+ const sessionName = buildSessionName(workdir, featureName, storyId, "diagnose");
34209
+ const diagnoseModelTier = config2.acceptance.fix.diagnoseModel;
34210
+ const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, diagnoseModelTier, config2.autoMode.defaultAgent);
34211
+ const imports = parseImportStatements(testFileContent);
34212
+ const relativeImports = resolveImportPaths(imports, workdir);
34213
+ const sourceFiles = await Promise.all(relativeImports.map((imp) => readSourceFileContent(imp, workdir)));
34214
+ const validSourceFiles = sourceFiles.filter((f) => f !== null);
34215
+ const prompt = buildDiagnosisPrompt({
34216
+ testOutput,
34217
+ testFileContent,
34218
+ sourceFiles: validSourceFiles
34219
+ });
34220
+ try {
34221
+ const result = await agent.run({
34222
+ prompt,
34223
+ workdir,
34224
+ modelTier: undefined,
34225
+ modelDef,
34226
+ timeoutSeconds: 300,
34227
+ sessionRole: "diagnose",
34228
+ acpSessionName: sessionName,
34229
+ featureName,
34230
+ storyId,
34231
+ config: config2
34232
+ });
34233
+ const diagnosis = parseDiagnosisResult(result.output);
34234
+ if (diagnosis) {
34235
+ return diagnosis;
34236
+ }
34237
+ return {
34238
+ verdict: "source_bug",
34239
+ reasoning: "diagnosis failed \u2014 falling back to source fix",
34240
+ confidence: 0
34241
+ };
34242
+ } catch (err) {
34243
+ return {
34244
+ verdict: "source_bug",
34245
+ reasoning: "diagnosis failed \u2014 falling back to source fix",
34246
+ confidence: 0
34247
+ };
34248
+ }
34249
+ }
34250
+ function parseDiagnosisResult(output) {
34251
+ if (!output || output.trim() === "") {
34252
+ return null;
34253
+ }
34254
+ try {
34255
+ const cleaned = output.trim();
34256
+ let jsonStr = cleaned;
34257
+ const firstBrace = cleaned.indexOf("{");
34258
+ const lastBrace = cleaned.lastIndexOf("}");
34259
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
34260
+ jsonStr = cleaned.slice(firstBrace, lastBrace + 1);
34261
+ }
34262
+ const parsed = JSON.parse(jsonStr);
34263
+ if (typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
34264
+ return {
34265
+ verdict: parsed.verdict,
34266
+ reasoning: parsed.reasoning,
34267
+ confidence: parsed.confidence,
34268
+ testIssues: parsed.testIssues,
34269
+ sourceIssues: parsed.sourceIssues
34270
+ };
34271
+ }
34272
+ return null;
34273
+ } catch {
34274
+ return null;
34275
+ }
34276
+ }
34277
+ var MAX_SOURCE_FILES = 5, MAX_FILE_LINES = 500, MAX_TEST_OUTPUT_CHARS = 2000;
34278
+ var init_fix_diagnosis = __esm(() => {
34279
+ init_adapter();
34280
+ });
34281
+
34282
+ // src/acceptance/fix-executor.ts
34283
+ function buildSourceFixPrompt(options) {
34284
+ const { testOutput, diagnosis, acceptanceTestPath } = options;
34285
+ let prompt = `ACCEPTANCE TEST FAILURE:
34286
+ ${testOutput}
34287
+
34288
+ `;
34289
+ if (diagnosis.reasoning) {
34290
+ prompt += `DIAGNOSIS:
34291
+ ${diagnosis.reasoning}
34292
+
34293
+ `;
34294
+ }
34295
+ prompt += `ACCEPTANCE TEST FILE: ${acceptanceTestPath}
34296
+
34297
+ `;
34298
+ prompt += "Fix the source implementation. Do NOT modify the test file.";
34299
+ return prompt;
34300
+ }
34301
+ async function executeSourceFix(agent, options) {
34302
+ if (!agent) {
34303
+ throw new Error("[fix-executor] agent is required");
34304
+ }
34305
+ const { testOutput, testFileContent, diagnosis, config: config2, workdir, featureName, storyId, acceptanceTestPath } = options;
34306
+ const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, config2.acceptance.fix.fixModel, config2.autoMode.defaultAgent);
34307
+ const sessionName = buildSessionName(workdir, featureName, storyId, "source-fix");
34308
+ const prompt = buildSourceFixPrompt(options);
34309
+ const timeoutSeconds = config2.execution?.sessionTimeoutSeconds ?? 3600;
34310
+ const runOptions = {
34311
+ prompt,
34312
+ workdir,
34313
+ modelTier: undefined,
34314
+ modelDef,
34315
+ timeoutSeconds,
34316
+ sessionRole: "source-fix",
34317
+ acpSessionName: sessionName,
34318
+ featureName,
34319
+ storyId,
34320
+ config: config2,
34321
+ pipelineStage: "acceptance"
34322
+ };
34323
+ const result = await agent.run(runOptions);
34324
+ let success2 = result.success;
34325
+ try {
34326
+ const verifyProc = _fixExecutorDeps.spawn(["bun", "test", acceptanceTestPath], {
34327
+ cwd: workdir,
34328
+ stdout: "pipe",
34329
+ stderr: "pipe"
34330
+ });
34331
+ const exitCode = await verifyProc.exited;
34332
+ success2 = exitCode === 0;
34333
+ } catch {
34334
+ success2 = result.success;
34335
+ }
34336
+ return {
34337
+ success: success2,
34338
+ cost: result.estimatedCost
34339
+ };
34340
+ }
34341
+ var _fixExecutorDeps;
34342
+ var init_fix_executor = __esm(() => {
34343
+ init_adapter();
34344
+ init_bun_deps();
34345
+ _fixExecutorDeps = { spawn };
34346
+ });
34347
+
34090
34348
  // src/execution/lifecycle/acceptance-loop.ts
34091
34349
  var exports_acceptance_loop = {};
34092
34350
  __export(exports_acceptance_loop, {
@@ -34113,6 +34371,14 @@ async function loadSpecContent(featureDir) {
34113
34371
  const specFile = Bun.file(specPath);
34114
34372
  return await specFile.exists() ? await specFile.text() : "";
34115
34373
  }
34374
+ async function loadAcceptanceTestContent(featureDir) {
34375
+ if (!featureDir)
34376
+ return { content: "", path: "" };
34377
+ const testPath = path14.join(featureDir, "acceptance.test.ts");
34378
+ const testFile = Bun.file(testPath);
34379
+ const content = await testFile.exists() ? await testFile.text() : "";
34380
+ return { content, path: testPath };
34381
+ }
34116
34382
  function buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty) {
34117
34383
  return { success: success2, prd, totalCost, iterations, storiesCompleted, prdDirty };
34118
34384
  }
@@ -34198,6 +34464,191 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
34198
34464
  logger?.info("acceptance", "Acceptance test regenerated successfully");
34199
34465
  return true;
34200
34466
  }
34467
+ async function runFixRouting(options) {
34468
+ const logger = getSafeLogger();
34469
+ const { ctx, failures, prd, acceptanceContext } = options;
34470
+ const agentName = ctx.config.autoMode.defaultAgent;
34471
+ const agent = (ctx.agentGetFn ?? _acceptanceLoopDeps.getAgent)(agentName);
34472
+ if (!agent) {
34473
+ logger?.error("acceptance", "Agent not found for fix routing");
34474
+ return { fixed: false, cost: 0, prdDirty: false };
34475
+ }
34476
+ const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
34477
+ const fixMaxRetries = ctx.config.acceptance.fix?.maxRetries ?? 2;
34478
+ const { content: testFileContent, path: acceptanceTestPath } = await loadAcceptanceTestContent(ctx.featureDir);
34479
+ const firstStory = prd.userStories[0];
34480
+ const storyId = firstStory?.id ?? "unknown";
34481
+ if (strategy === "implement-only") {
34482
+ logger?.info("acceptance", "Strategy is implement-only \u2014 executing source fix directly");
34483
+ let fixAttempts = 0;
34484
+ while (fixAttempts < fixMaxRetries) {
34485
+ fixAttempts++;
34486
+ logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
34487
+ const defaultDiagnosis = {
34488
+ verdict: "source_bug",
34489
+ reasoning: "implement-only strategy \u2014 skipping diagnosis",
34490
+ confidence: 1
34491
+ };
34492
+ const fixResult = await executeSourceFix(agent, {
34493
+ testOutput: failures.testOutput,
34494
+ testFileContent,
34495
+ diagnosis: defaultDiagnosis,
34496
+ config: ctx.config,
34497
+ workdir: ctx.workdir,
34498
+ featureName: ctx.feature,
34499
+ storyId,
34500
+ acceptanceTestPath
34501
+ });
34502
+ logger?.info("acceptance.source-fix", "Source fix completed", {
34503
+ success: fixResult.success,
34504
+ cost: fixResult.cost,
34505
+ attempt: fixAttempts
34506
+ });
34507
+ if (fixResult.success) {
34508
+ return { fixed: true, cost: fixResult.cost, prdDirty: false };
34509
+ }
34510
+ if (fixAttempts >= fixMaxRetries) {
34511
+ logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
34512
+ break;
34513
+ }
34514
+ }
34515
+ return { fixed: false, cost: 0, prdDirty: false };
34516
+ }
34517
+ logger?.info("acceptance", "Strategy is diagnose-first \u2014 running diagnosis");
34518
+ const diagnosis = await diagnoseAcceptanceFailure(agent, {
34519
+ testOutput: failures.testOutput,
34520
+ testFileContent,
34521
+ config: ctx.config,
34522
+ workdir: ctx.workdir,
34523
+ featureName: ctx.feature,
34524
+ storyId
34525
+ });
34526
+ logger?.info("acceptance.diagnosis", "Diagnosis complete", {
34527
+ verdict: diagnosis.verdict,
34528
+ confidence: diagnosis.confidence,
34529
+ reasoning: diagnosis.reasoning
34530
+ });
34531
+ if (diagnosis.verdict === "source_bug") {
34532
+ logger?.info("acceptance", "Diagnosis: source_bug \u2014 executing source fix");
34533
+ let fixAttempts = 0;
34534
+ while (fixAttempts < fixMaxRetries) {
34535
+ fixAttempts++;
34536
+ logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
34537
+ const fixResult = await executeSourceFix(agent, {
34538
+ testOutput: failures.testOutput,
34539
+ testFileContent,
34540
+ diagnosis,
34541
+ config: ctx.config,
34542
+ workdir: ctx.workdir,
34543
+ featureName: ctx.feature,
34544
+ storyId,
34545
+ acceptanceTestPath
34546
+ });
34547
+ logger?.info("acceptance.source-fix", "Source fix completed", {
34548
+ success: fixResult.success,
34549
+ cost: fixResult.cost,
34550
+ attempt: fixAttempts
34551
+ });
34552
+ if (fixResult.success) {
34553
+ return { fixed: true, cost: fixResult.cost, prdDirty: false };
34554
+ }
34555
+ if (fixAttempts >= fixMaxRetries) {
34556
+ logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
34557
+ break;
34558
+ }
34559
+ }
34560
+ return { fixed: false, cost: 0, prdDirty: false };
34561
+ }
34562
+ if (diagnosis.verdict === "test_bug") {
34563
+ logger?.info("acceptance", "Diagnosis: test_bug \u2014 regenerating acceptance test");
34564
+ if (!ctx.featureDir) {
34565
+ logger?.error("acceptance", "Cannot regenerate test without featureDir");
34566
+ return { fixed: false, cost: 0, prdDirty: false };
34567
+ }
34568
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
34569
+ const testFile = Bun.file(testPath);
34570
+ if (!await testFile.exists()) {
34571
+ logger?.error("acceptance", "Acceptance test file not found for regeneration");
34572
+ return { fixed: false, cost: 0, prdDirty: false };
34573
+ }
34574
+ const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
34575
+ logger?.info("acceptance.test-regen", "Test regeneration completed", {
34576
+ outcome: regenerated ? "success" : "failure"
34577
+ });
34578
+ if (!regenerated) {
34579
+ return { fixed: false, cost: 0, prdDirty: false };
34580
+ }
34581
+ const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
34582
+ const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
34583
+ if (acceptanceResult.action === "continue") {
34584
+ logger?.info("acceptance", "Acceptance passed after test regeneration");
34585
+ return { fixed: true, cost: 0, prdDirty: true };
34586
+ }
34587
+ logger?.warn("acceptance", "Acceptance still failing after test regeneration");
34588
+ return { fixed: false, cost: 0, prdDirty: true };
34589
+ }
34590
+ if (diagnosis.verdict === "both") {
34591
+ logger?.info("acceptance", "Diagnosis: both \u2014 executing source fix then regenerating test if needed");
34592
+ let sourceFixSuccess = false;
34593
+ let sourceFixCost = 0;
34594
+ let fixAttempts = 0;
34595
+ while (fixAttempts < fixMaxRetries && !sourceFixSuccess) {
34596
+ fixAttempts++;
34597
+ logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
34598
+ const fixResult = await executeSourceFix(agent, {
34599
+ testOutput: failures.testOutput,
34600
+ testFileContent,
34601
+ diagnosis,
34602
+ config: ctx.config,
34603
+ workdir: ctx.workdir,
34604
+ featureName: ctx.feature,
34605
+ storyId,
34606
+ acceptanceTestPath
34607
+ });
34608
+ logger?.info("acceptance.source-fix", "Source fix completed", {
34609
+ success: fixResult.success,
34610
+ cost: fixResult.cost,
34611
+ attempt: fixAttempts
34612
+ });
34613
+ sourceFixSuccess = fixResult.success;
34614
+ sourceFixCost += fixResult.cost;
34615
+ if (fixResult.success) {
34616
+ break;
34617
+ }
34618
+ if (fixAttempts >= fixMaxRetries) {
34619
+ logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
34620
+ break;
34621
+ }
34622
+ }
34623
+ if (!sourceFixSuccess) {
34624
+ return { fixed: false, cost: sourceFixCost, prdDirty: false };
34625
+ }
34626
+ logger?.info("acceptance", "Source fix succeeded \u2014 re-running acceptance to verify");
34627
+ const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
34628
+ const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
34629
+ if (acceptanceResult.action === "continue") {
34630
+ logger?.info("acceptance", "Acceptance passed after source fix");
34631
+ return { fixed: true, cost: sourceFixCost, prdDirty: false };
34632
+ }
34633
+ logger?.info("acceptance", "Acceptance still failing after source fix \u2014 regenerating test");
34634
+ if (!ctx.featureDir) {
34635
+ logger?.error("acceptance", "Cannot regenerate test without featureDir");
34636
+ return { fixed: false, cost: sourceFixCost, prdDirty: false };
34637
+ }
34638
+ const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
34639
+ const testFile = Bun.file(testPath);
34640
+ if (!await testFile.exists()) {
34641
+ logger?.error("acceptance", "Acceptance test file not found for regeneration");
34642
+ return { fixed: false, cost: sourceFixCost, prdDirty: false };
34643
+ }
34644
+ const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
34645
+ logger?.info("acceptance.test-regen", "Test regeneration completed", {
34646
+ outcome: regenerated ? "success" : "failure"
34647
+ });
34648
+ return { fixed: regenerated, cost: sourceFixCost, prdDirty: regenerated };
34649
+ }
34650
+ return { fixed: false, cost: 0, prdDirty: false };
34651
+ }
34201
34652
  async function runAcceptanceLoop(ctx) {
34202
34653
  const logger = getSafeLogger();
34203
34654
  const maxRetries = ctx.config.acceptance.maxRetries;
@@ -34292,6 +34743,23 @@ async function runAcceptanceLoop(ctx) {
34292
34743
  continue;
34293
34744
  }
34294
34745
  }
34746
+ const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
34747
+ if (strategy === "diagnose-first" || strategy === "implement-only") {
34748
+ logger?.info("acceptance", `Running fix routing with strategy: ${strategy}`);
34749
+ const fixResult = await runFixRouting({
34750
+ ctx,
34751
+ failures,
34752
+ prd,
34753
+ acceptanceContext
34754
+ });
34755
+ totalCost += fixResult.cost;
34756
+ if (fixResult.fixed) {
34757
+ logger?.info("acceptance", "Fix succeeded \u2014 re-running acceptance tests...");
34758
+ continue;
34759
+ }
34760
+ logger?.error("acceptance", "Fix routing failed to resolve acceptance failures");
34761
+ return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
34762
+ }
34295
34763
  logger?.info("acceptance", "Generating fix stories...");
34296
34764
  const fixStories = await generateAndAddFixStories(ctx, failures, prd);
34297
34765
  if (!fixStories) {
@@ -34323,6 +34791,8 @@ async function runAcceptanceLoop(ctx) {
34323
34791
  var _acceptanceLoopDeps;
34324
34792
  var init_acceptance_loop = __esm(() => {
34325
34793
  init_acceptance();
34794
+ init_fix_diagnosis();
34795
+ init_fix_executor();
34326
34796
  init_registry();
34327
34797
  init_config();
34328
34798
  init_loader();
@@ -34437,6 +34907,16 @@ async function runDeferredRegression(options) {
34437
34907
  };
34438
34908
  }
34439
34909
  const testSummary = _regressionDeps.parseBunTestOutput(fullSuiteResult.output);
34910
+ if (testSummary.failed === 0 && testSummary.passed === 0) {
34911
+ logger?.warn("regression", "No test results parsed from output \u2014 test runner likely crashed or errored (not a regression, accepting as pass)", { output: fullSuiteResult.output.slice(0, 500) });
34912
+ return {
34913
+ success: true,
34914
+ failedTests: 0,
34915
+ passedTests: 0,
34916
+ rectificationAttempts: 0,
34917
+ affectedStories: []
34918
+ };
34919
+ }
34440
34920
  const affectedStories = new Set;
34441
34921
  const affectedStoriesObjs = new Map;
34442
34922
  logger?.warn("regression", "Regression detected", {
@@ -36618,13 +37098,25 @@ async function executeUnified(ctx, initialPrd) {
36618
37098
  prd = await loadPRD(ctx.prdPath);
36619
37099
  prdDirty = false;
36620
37100
  }
37101
+ const storyCounts = countStories(prd);
37102
+ logger?.debug("execution", "Loop iteration", {
37103
+ iteration: iterations,
37104
+ isComplete: isComplete(prd),
37105
+ passed: storyCounts.passed,
37106
+ pending: storyCounts.pending,
37107
+ failed: storyCounts.failed,
37108
+ total: storyCounts.total
37109
+ });
36621
37110
  if (isComplete(prd)) {
37111
+ logger?.debug("execution", "All stories complete \u2014 entering completion path");
36622
37112
  if (ctx.interactionChain && isTriggerEnabled("pre-merge", ctx.config)) {
36623
37113
  const shouldProceed = await checkPreMerge({ featureName: ctx.feature, totalStories: prd.userStories.length, cost: totalCost }, ctx.config, ctx.interactionChain);
36624
37114
  if (!shouldProceed)
36625
37115
  return buildResult2("pre-merge-aborted");
36626
37116
  }
37117
+ logger?.debug("execution", "Running deferred review");
36627
37118
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
37119
+ logger?.debug("execution", "Deferred review done \u2014 returning completed");
36628
37120
  return buildResult2("completed");
36629
37121
  }
36630
37122
  const costLimit = ctx.config.execution.costLimit;
@@ -36877,9 +37369,7 @@ async function executeUnified(ctx, initialPrd) {
36877
37369
  }, ctx.eventEmitter);
36878
37370
  }
36879
37371
  return buildResult2("max-iterations");
36880
- } finally {
36881
- stopHeartbeat();
36882
- }
37372
+ } finally {}
36883
37373
  }
36884
37374
  var _unifiedExecutorDeps;
36885
37375
  var init_unified_executor = __esm(() => {
@@ -69804,6 +70294,7 @@ function validatePlanOutput(raw, feature, branch) {
69804
70294
  }
69805
70295
 
69806
70296
  // src/cli/plan.ts
70297
+ var DEFAULT_TIMEOUT_SECONDS2 = 600;
69807
70298
  var _planDeps = {
69808
70299
  readFile: (path) => Bun.file(path).text(),
69809
70300
  writeFile: (path, content) => Bun.write(path, content).then(() => {}),
@@ -69854,7 +70345,7 @@ async function planCommand(workdir, config2, options) {
69854
70345
  const outputPath = join12(outputDir, "prd.json");
69855
70346
  await _planDeps.mkdirp(outputDir);
69856
70347
  const agentName = config2?.autoMode?.defaultAgent ?? "claude";
69857
- const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
70348
+ const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
69858
70349
  let rawResponse;
69859
70350
  const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
69860
70351
  if (debateEnabled) {
@@ -69938,13 +70429,15 @@ async function planCommand(workdir, config2, options) {
69938
70429
  }
69939
70430
  rawResponse = await _planDeps.readFile(outputPath);
69940
70431
  } else {
70432
+ const timeoutMs = (config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2) * 1000;
69941
70433
  const completeResult = await adapter.complete(prompt, {
69942
70434
  model: autoModel,
69943
70435
  jsonMode: true,
69944
70436
  workdir,
69945
70437
  config: config2,
69946
70438
  featureName: options.feature,
69947
- sessionRole: "plan"
70439
+ sessionRole: "plan",
70440
+ timeoutMs
69948
70441
  });
69949
70442
  let result = typeof completeResult === "string" ? completeResult : completeResult.output;
69950
70443
  try {
@@ -70317,6 +70810,8 @@ async function planDecomposeCommand(workdir, config2, options) {
70317
70810
  const stages = config2?.debate?.stages;
70318
70811
  const debateEnabled = config2?.debate?.enabled && stages?.decompose?.enabled;
70319
70812
  let rawResponse;
70813
+ const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
70814
+ const timeoutMs = timeoutSeconds * 1000;
70320
70815
  if (debateEnabled) {
70321
70816
  const stageConfig = stages?.decompose;
70322
70817
  const debateSession = _planDeps.createDebateSession({
@@ -70326,7 +70821,7 @@ async function planDecomposeCommand(workdir, config2, options) {
70326
70821
  config: config2,
70327
70822
  workdir,
70328
70823
  featureName: options.feature,
70329
- timeoutSeconds: config2?.execution?.sessionTimeoutSeconds
70824
+ timeoutSeconds
70330
70825
  });
70331
70826
  const debateResult = await debateSession.run(prompt);
70332
70827
  if (debateResult.outcome !== "failed" && debateResult.output) {
@@ -70337,7 +70832,8 @@ async function planDecomposeCommand(workdir, config2, options) {
70337
70832
  workdir,
70338
70833
  sessionRole: "decompose",
70339
70834
  featureName: options.feature,
70340
- storyId: options.storyId
70835
+ storyId: options.storyId,
70836
+ timeoutMs
70341
70837
  });
70342
70838
  rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
70343
70839
  }
@@ -70347,7 +70843,8 @@ async function planDecomposeCommand(workdir, config2, options) {
70347
70843
  workdir,
70348
70844
  sessionRole: "decompose",
70349
70845
  featureName: options.feature,
70350
- storyId: options.storyId
70846
+ storyId: options.storyId,
70847
+ timeoutMs
70351
70848
  });
70352
70849
  rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
70353
70850
  }
@@ -72990,6 +73487,10 @@ init_crash_recovery();
72990
73487
  init_story_context();
72991
73488
  async function runCompletionPhase(options) {
72992
73489
  const logger = getSafeLogger();
73490
+ logger?.debug("execution", "Completion phase started", {
73491
+ acceptanceEnabled: options.config.acceptance?.enabled,
73492
+ isComplete: isComplete(options.prd)
73493
+ });
72993
73494
  if (options.config.acceptance.enabled && isComplete(options.prd)) {
72994
73495
  const { runAcceptanceLoop: runAcceptanceLoop2 } = await Promise.resolve().then(() => (init_acceptance_loop(), exports_acceptance_loop));
72995
73496
  const acceptanceResult = await runAcceptanceLoop2({
@@ -73057,9 +73558,12 @@ async function runCompletionPhase(options) {
73057
73558
  formatterMode: options.formatterMode
73058
73559
  });
73059
73560
  }
73561
+ logger?.debug("execution", "Completion phase \u2014 stopping heartbeat and writing exit summary");
73060
73562
  stopHeartbeat();
73061
73563
  await writeExitSummary(options.logFilePath, options.totalCost, options.iterations, options.storiesCompleted, durationMs);
73564
+ logger?.debug("execution", "Completion phase \u2014 auto-committing dirty files");
73062
73565
  await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
73566
+ logger?.debug("execution", "Completion phase done \u2014 returning to runner");
73063
73567
  return {
73064
73568
  durationMs,
73065
73569
  runCompletedAt
@@ -73197,6 +73701,12 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
73197
73701
  storiesCompleted = unifiedResult.storiesCompleted;
73198
73702
  totalCost = unifiedResult.totalCost;
73199
73703
  allStoryMetrics.push(...unifiedResult.allStoryMetrics);
73704
+ logger?.debug("execution", "Execution phase complete \u2014 handing off to completion phase", {
73705
+ exitReason: unifiedResult.exitReason,
73706
+ iterations,
73707
+ storiesCompleted,
73708
+ totalCost
73709
+ });
73200
73710
  return { prd, iterations, storiesCompleted, totalCost, allStoryMetrics };
73201
73711
  }
73202
73712
 
@@ -73355,15 +73865,20 @@ async function run(options) {
73355
73865
  durationMs
73356
73866
  };
73357
73867
  } finally {
73868
+ const logger2 = getSafeLogger();
73869
+ logger2?.debug("execution", "Runner finally block \u2014 starting cleanup");
73358
73870
  stopHeartbeat();
73359
73871
  cleanupCrashHandlers();
73872
+ logger2?.debug("execution", "Runner finally \u2014 sweeping ACP sessions");
73360
73873
  await sweepFeatureSessions(workdir, feature).catch(() => {});
73874
+ logger2?.debug("execution", "Runner finally \u2014 ACP sweep done");
73361
73875
  let branch = "";
73362
73876
  try {
73363
73877
  const { stdout, exitCode } = await gitWithTimeout(["branch", "--show-current"], workdir);
73364
73878
  if (exitCode === 0)
73365
73879
  branch = stdout.trim();
73366
73880
  } catch {}
73881
+ logger2?.debug("execution", "Runner finally \u2014 running cleanupRun");
73367
73882
  const { cleanupRun: cleanupRun2 } = await Promise.resolve().then(() => (init_run_cleanup(), exports_run_cleanup));
73368
73883
  await cleanupRun2({
73369
73884
  runId,
@@ -73379,6 +73894,7 @@ async function run(options) {
73379
73894
  branch,
73380
73895
  version: NAX_VERSION
73381
73896
  });
73897
+ logger2?.debug("execution", "Runner finally \u2014 cleanupRun done, run() returning");
73382
73898
  }
73383
73899
  }
73384
73900
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.56.5",
3
+ "version": "0.57.1-canary.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {