@nathapp/nax 0.57.2 → 0.57.3

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 +517 -281
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -3044,16 +3044,6 @@ var init_logger = __esm(() => {
3044
3044
  });
3045
3045
 
3046
3046
  // src/logger/index.ts
3047
- var exports_logger = {};
3048
- __export(exports_logger, {
3049
- resetLogger: () => resetLogger,
3050
- initLogger: () => initLogger,
3051
- getSafeLogger: () => getSafeLogger,
3052
- getLogger: () => getLogger,
3053
- formatJsonl: () => formatJsonl,
3054
- formatConsole: () => formatConsole,
3055
- Logger: () => Logger
3056
- });
3057
3047
  var init_logger2 = __esm(() => {
3058
3048
  init_logger();
3059
3049
  init_formatters();
@@ -3304,6 +3294,55 @@ var init_test_strategy = __esm(() => {
3304
3294
 
3305
3295
  // src/agents/shared/decompose.ts
3306
3296
  function buildDecomposePrompt(options) {
3297
+ if (options.targetStory) {
3298
+ return buildPlanModeDecomposePrompt(options);
3299
+ }
3300
+ return buildSpecDecomposePrompt(options);
3301
+ }
3302
+ function buildPlanModeDecomposePrompt(options) {
3303
+ const targetStory = options.targetStory;
3304
+ const siblings = options.siblings ?? [];
3305
+ const siblingsSummary = siblings.length > 0 ? `
3306
+ ## Sibling Stories
3307
+
3308
+ ${siblings.map((s) => `- ${s.id}: ${s.title}`).join(`
3309
+ `)}
3310
+ ` : "";
3311
+ return `You are a senior software architect decomposing a complex user story into smaller, implementable sub-stories.
3312
+
3313
+ ## Target Story
3314
+
3315
+ ${JSON.stringify(targetStory, null, 2)}${siblingsSummary}
3316
+ ## Codebase Context
3317
+
3318
+ ${options.codebaseContext}
3319
+
3320
+ ${COMPLEXITY_GUIDE}
3321
+
3322
+ ${TEST_STRATEGY_GUIDE}
3323
+
3324
+ ${GROUPING_RULES}
3325
+
3326
+ ## Output
3327
+
3328
+ Return a JSON array of sub-stories (no markdown code fences, no explanation \u2014 JSON array only):
3329
+
3330
+ [{
3331
+ "id": "string \u2014 e.g. ${targetStory.id}-A",
3332
+ "title": "string",
3333
+ "description": "string",
3334
+ "acceptanceCriteria": ["string \u2014 behavioral, testable criteria"],
3335
+ "contextFiles": ["string \u2014 required, non-empty list of key source files"],
3336
+ "tags": ["string"],
3337
+ "dependencies": ["string"],
3338
+ "complexity": "simple | medium | complex | expert",
3339
+ "reasoning": "string",
3340
+ "estimatedLOC": 0,
3341
+ "risks": ["string"],
3342
+ "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after"
3343
+ }]`;
3344
+ }
3345
+ function buildSpecDecomposePrompt(options) {
3307
3346
  return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
3308
3347
 
3309
3348
  CODEBASE CONTEXT:
@@ -3614,9 +3653,11 @@ class SpawnAcpSession {
3614
3653
  try {
3615
3654
  proc.stdin?.write(text);
3616
3655
  proc.stdin?.end();
3617
- const exitCode = await proc.exited;
3618
- const stdout = await new Response(proc.stdout).text();
3619
- const stderr = await new Response(proc.stderr).text();
3656
+ const [exitCode, stdout, stderr] = await Promise.all([
3657
+ proc.exited,
3658
+ new Response(proc.stdout).text(),
3659
+ new Response(proc.stderr).text()
3660
+ ]);
3620
3661
  if (exitCode !== 0) {
3621
3662
  getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
3622
3663
  exitCode,
@@ -3646,6 +3687,21 @@ class SpawnAcpSession {
3646
3687
  await this.pidRegistry?.unregister(processPid);
3647
3688
  }
3648
3689
  }
3690
+ async trackedSpawn(cmd, opts) {
3691
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe", ...opts });
3692
+ const pid = proc.pid;
3693
+ await this.pidRegistry?.register(pid);
3694
+ try {
3695
+ const [exitCode, stdout, stderr] = await Promise.all([
3696
+ proc.exited,
3697
+ new Response(proc.stdout).text(),
3698
+ new Response(proc.stderr).text()
3699
+ ]);
3700
+ return { exitCode, stdout, stderr };
3701
+ } finally {
3702
+ await this.pidRegistry?.unregister(pid);
3703
+ }
3704
+ }
3649
3705
  async close(options) {
3650
3706
  if (this.activeProc) {
3651
3707
  try {
@@ -3656,10 +3712,8 @@ class SpawnAcpSession {
3656
3712
  }
3657
3713
  const cmd = ["acpx", "--cwd", this.cwd, this.agentName, "sessions", "close", this.sessionName];
3658
3714
  getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
3659
- const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
3660
- const exitCode = await proc.exited;
3715
+ const { exitCode, stderr } = await this.trackedSpawn(cmd);
3661
3716
  if (exitCode !== 0) {
3662
- const stderr = await new Response(proc.stderr).text();
3663
3717
  getSafeLogger()?.warn("acp-adapter", "Failed to close session", {
3664
3718
  sessionName: this.sessionName,
3665
3719
  stderr: stderr.slice(0, 200)
@@ -3667,8 +3721,7 @@ class SpawnAcpSession {
3667
3721
  }
3668
3722
  if (options?.forceTerminate) {
3669
3723
  try {
3670
- const stopProc = _spawnClientDeps.spawn(["acpx", this.agentName, "stop"], { stdout: "pipe", stderr: "pipe" });
3671
- await stopProc.exited;
3724
+ await this.trackedSpawn(["acpx", this.agentName, "stop"]);
3672
3725
  } catch (err) {
3673
3726
  getSafeLogger()?.debug("acp-adapter", "acpx stop failed (swallowed)", { cause: String(err) });
3674
3727
  }
@@ -3683,8 +3736,7 @@ class SpawnAcpSession {
3683
3736
  }
3684
3737
  const cmd = ["acpx", this.agentName, "cancel"];
3685
3738
  getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
3686
- const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
3687
- await proc.exited;
3739
+ await this.trackedSpawn(cmd);
3688
3740
  }
3689
3741
  }
3690
3742
 
@@ -3710,14 +3762,27 @@ class SpawnAcpClient {
3710
3762
  this.pidRegistry = pidRegistry;
3711
3763
  }
3712
3764
  async start() {}
3765
+ async trackedSpawn(cmd) {
3766
+ const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
3767
+ const pid = proc.pid;
3768
+ await this.pidRegistry?.register(pid);
3769
+ try {
3770
+ const [exitCode, stdout, stderr] = await Promise.all([
3771
+ proc.exited,
3772
+ new Response(proc.stdout).text(),
3773
+ new Response(proc.stderr).text()
3774
+ ]);
3775
+ return { exitCode, stdout, stderr };
3776
+ } finally {
3777
+ await this.pidRegistry?.unregister(pid);
3778
+ }
3779
+ }
3713
3780
  async createSession(opts) {
3714
3781
  const sessionName = opts.sessionName || `nax-${Date.now()}`;
3715
3782
  const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "ensure", "--name", sessionName];
3716
3783
  getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
3717
- const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
3718
- const exitCode = await proc.exited;
3784
+ const { exitCode, stderr } = await this.trackedSpawn(cmd);
3719
3785
  if (exitCode !== 0) {
3720
- const stderr = await new Response(proc.stderr).text();
3721
3786
  throw new Error(`[acp-adapter] Failed to create session: ${stderr || `exit code ${exitCode}`}`);
3722
3787
  }
3723
3788
  return new SpawnAcpSession({
@@ -3733,8 +3798,7 @@ class SpawnAcpClient {
3733
3798
  }
3734
3799
  async loadSession(sessionName, agentName, permissionMode) {
3735
3800
  const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
3736
- const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
3737
- const exitCode = await proc.exited;
3801
+ const { exitCode } = await this.trackedSpawn(cmd);
3738
3802
  if (exitCode !== 0) {
3739
3803
  return null;
3740
3804
  }
@@ -18082,6 +18146,7 @@ var init_schemas3 = __esm(() => {
18082
18146
  command: exports_external.string().optional(),
18083
18147
  model: exports_external.enum(["fast", "balanced", "powerful"]).default("fast"),
18084
18148
  refinement: exports_external.boolean().default(true),
18149
+ refinementConcurrency: exports_external.number().int().min(1).max(10).default(3),
18085
18150
  redGate: exports_external.boolean().default(true),
18086
18151
  testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
18087
18152
  testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional(),
@@ -18285,8 +18350,6 @@ var init_schemas3 = __esm(() => {
18285
18350
  maxRectificationAttempts: 3
18286
18351
  },
18287
18352
  contextProviderTokenBudget: 2000,
18288
- lintCommand: null,
18289
- typecheckCommand: null,
18290
18353
  dangerouslySkipPermissions: true,
18291
18354
  permissionProfile: "unrestricted",
18292
18355
  smartTestRunner: true
@@ -18392,6 +18455,7 @@ var init_schemas3 = __esm(() => {
18392
18455
  testPath: ".nax-acceptance.test.ts",
18393
18456
  model: "fast",
18394
18457
  refinement: true,
18458
+ refinementConcurrency: 3,
18395
18459
  redGate: true,
18396
18460
  timeoutMs: 1800000,
18397
18461
  fix: {
@@ -18641,7 +18705,7 @@ async function readAcpSession(workdir, featureName, storyId) {
18641
18705
  return null;
18642
18706
  }
18643
18707
  }
18644
- async function sweepFeatureSessions(workdir, featureName) {
18708
+ async function sweepFeatureSessions(workdir, featureName, pidRegistry) {
18645
18709
  const path = acpSessionsPath(workdir, featureName);
18646
18710
  let sessions;
18647
18711
  try {
@@ -18665,7 +18729,7 @@ async function sweepFeatureSessions(workdir, featureName) {
18665
18729
  }
18666
18730
  for (const [agentName, sessionNames] of byAgent) {
18667
18731
  const cmdStr = `acpx ${agentName}`;
18668
- const client = _acpAdapterDeps.createClient(cmdStr, workdir);
18732
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir, undefined, pidRegistry);
18669
18733
  try {
18670
18734
  await client.start();
18671
18735
  for (const sessionName of sessionNames) {
@@ -18690,7 +18754,7 @@ async function sweepFeatureSessions(workdir, featureName) {
18690
18754
  logger?.warn("acp-adapter", "[sweep] Failed to clear sidecar after sweep", { error: String(err) });
18691
18755
  }
18692
18756
  }
18693
- async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS) {
18757
+ async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS, pidRegistry) {
18694
18758
  const path = acpSessionsPath(workdir, featureName);
18695
18759
  const file3 = Bun.file(path);
18696
18760
  if (!await file3.exists())
@@ -18702,7 +18766,7 @@ async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SE
18702
18766
  featureName,
18703
18767
  ageMs
18704
18768
  });
18705
- await sweepFeatureSessions(workdir, featureName);
18769
+ await sweepFeatureSessions(workdir, featureName, pidRegistry);
18706
18770
  }
18707
18771
  function extractOutput(response) {
18708
18772
  if (!response)
@@ -19202,7 +19266,9 @@ class AcpAgentAdapter {
19202
19266
  jsonMode: true,
19203
19267
  config: options.config,
19204
19268
  workdir: options.workdir,
19205
- sessionRole: "decompose"
19269
+ featureName: options.featureName,
19270
+ storyId: options.storyId,
19271
+ sessionRole: options.sessionRole ?? "decompose"
19206
19272
  });
19207
19273
  output = completeResult.output;
19208
19274
  } catch (err) {
@@ -20040,17 +20106,27 @@ class ClaudeCodeAdapter {
20040
20106
  cmd.splice(cmd.length - 2, 0, "--dangerously-skip-permissions");
20041
20107
  }
20042
20108
  const pidRegistry = this.getPidRegistry(options.workdir);
20109
+ const env2 = this.buildAllowedEnv({
20110
+ workdir: options.workdir,
20111
+ modelDef,
20112
+ prompt: "",
20113
+ modelTier: options.modelTier || "balanced",
20114
+ timeoutSeconds: 600
20115
+ });
20116
+ if (options.featureName) {
20117
+ env2.NAX_FEATURE_NAME = options.featureName;
20118
+ }
20119
+ if (options.storyId) {
20120
+ env2.NAX_STORY_ID = options.storyId;
20121
+ }
20122
+ if (options.sessionRole) {
20123
+ env2.NAX_SESSION_ROLE = options.sessionRole;
20124
+ }
20043
20125
  const proc = _decomposeDeps.spawn(cmd, {
20044
20126
  cwd: options.workdir,
20045
20127
  stdout: "pipe",
20046
20128
  stderr: "inherit",
20047
- env: this.buildAllowedEnv({
20048
- workdir: options.workdir,
20049
- modelDef,
20050
- prompt: "",
20051
- modelTier: options.modelTier || "balanced",
20052
- timeoutSeconds: 600
20053
- })
20129
+ env: env2
20054
20130
  });
20055
20131
  await pidRegistry.register(proc.pid);
20056
20132
  const DECOMPOSE_TIMEOUT_MS = 300000;
@@ -21188,6 +21264,9 @@ function skeletonImportLine(testFramework) {
21188
21264
  }
21189
21265
  return `import { describe, test, expect } from "bun:test";`;
21190
21266
  }
21267
+ function hasLikelyTestContent(content) {
21268
+ return /\b(?:describe|test|it|expect)\s*\(/.test(content) || /func\s+Test\w+\s*\(/.test(content) || /def\s+test_\w+/.test(content) || /#\[test\]/.test(content);
21269
+ }
21191
21270
  function acceptanceTestFilename(language) {
21192
21271
  switch (language?.toLowerCase()) {
21193
21272
  case "go":
@@ -21291,36 +21370,80 @@ Rules:
21291
21370
  });
21292
21371
  if (!testCode) {
21293
21372
  const targetPath = join6(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
21373
+ const backupPath = `${targetPath}.llm-recovery.bak`;
21294
21374
  let recoveryFailed = false;
21295
- logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
21375
+ logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", {
21376
+ targetPath,
21377
+ backupPath,
21378
+ featureName: options.featureName,
21379
+ workdir: options.workdir
21380
+ });
21296
21381
  try {
21297
21382
  const existing = await Bun.file(targetPath).text();
21298
21383
  const recovered = extractTestCode(existing);
21384
+ const likelyTestContent = hasLikelyTestContent(existing);
21299
21385
  logger.debug("acceptance", "BUG-076 recovery: file check result", {
21300
21386
  fileSize: existing.length,
21301
21387
  extractedCode: recovered !== null,
21388
+ likelyTestContent,
21302
21389
  filePreview: existing.slice(0, 300)
21303
21390
  });
21304
21391
  if (recovered) {
21305
21392
  logger.info("acceptance", "Acceptance test written directly by agent \u2014 using existing file", { targetPath });
21306
21393
  testCode = recovered;
21394
+ } else if (existing.trim().length > 0 && likelyTestContent) {
21395
+ let backupCreated = false;
21396
+ try {
21397
+ await _generatorPRDDeps.backupFile(backupPath, existing);
21398
+ backupCreated = true;
21399
+ } catch (backupError) {
21400
+ logger.warn("acceptance", "BUG-076: failed to create recovery backup; preserving file anyway", {
21401
+ targetPath,
21402
+ backupPath,
21403
+ backupError: backupError instanceof Error ? backupError.message : String(backupError)
21404
+ });
21405
+ }
21406
+ logger.warn("acceptance", "BUG-076: preserving agent-written file with backup (heuristic recovery)", {
21407
+ targetPath,
21408
+ backupPath,
21409
+ backupCreated,
21410
+ reason: "extractTestCode returned null"
21411
+ });
21412
+ testCode = existing;
21307
21413
  } else {
21414
+ if (existing.trim().length > 0) {
21415
+ try {
21416
+ await _generatorPRDDeps.backupFile(backupPath, existing);
21417
+ } catch (backupError) {
21418
+ logger.warn("acceptance", "BUG-076: failed to create fallback backup for unrecognized file", {
21419
+ targetPath,
21420
+ backupPath,
21421
+ backupError: backupError instanceof Error ? backupError.message : String(backupError)
21422
+ });
21423
+ }
21424
+ }
21308
21425
  recoveryFailed = true;
21309
- logger.error("acceptance", "BUG-076: ACP adapter wrote file but no code extractable \u2014 falling back to skeleton", {
21426
+ logger.error("acceptance", "BUG-076: agent-written file not recognized as test code \u2014 falling back to skeleton", {
21310
21427
  targetPath,
21428
+ backupPath,
21429
+ fileSize: existing.length,
21311
21430
  filePreview: existing.slice(0, 300)
21312
21431
  });
21313
21432
  }
21314
- } catch {
21433
+ } catch (error48) {
21315
21434
  recoveryFailed = true;
21316
- logger.debug("acceptance", "BUG-076 recovery: no file written by agent, falling back to skeleton", {
21435
+ logger.debug("acceptance", "BUG-076 recovery: failed to read agent-written file, falling back to skeleton", {
21317
21436
  targetPath,
21437
+ backupPath,
21438
+ error: error48 instanceof Error ? error48.message : String(error48),
21318
21439
  rawOutputPreview: rawOutput.slice(0, 500)
21319
21440
  });
21320
21441
  }
21321
21442
  if (recoveryFailed) {
21322
- logger.error("acceptance", "BUG-076: LLM returned non-code output and no file was written by agent \u2014 falling back to skeleton", {
21323
- rawOutputPreview: rawOutput.slice(0, 500)
21443
+ logger.error("acceptance", "BUG-076: LLM returned non-code output and recovery could not produce runnable tests \u2014 falling back to skeleton", {
21444
+ rawOutputPreview: rawOutput.slice(0, 500),
21445
+ targetPath,
21446
+ backupPath
21324
21447
  });
21325
21448
  }
21326
21449
  }
@@ -21586,6 +21709,9 @@ var init_generator = __esm(() => {
21586
21709
  },
21587
21710
  writeFile: async (path, content) => {
21588
21711
  await Bun.write(path, content);
21712
+ },
21713
+ backupFile: async (path, content) => {
21714
+ await Bun.write(path, content);
21589
21715
  }
21590
21716
  };
21591
21717
  });
@@ -22342,7 +22468,7 @@ var package_default;
22342
22468
  var init_package = __esm(() => {
22343
22469
  package_default = {
22344
22470
  name: "@nathapp/nax",
22345
- version: "0.57.2",
22471
+ version: "0.57.3",
22346
22472
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22347
22473
  type: "module",
22348
22474
  bin: {
@@ -22421,8 +22547,8 @@ var init_version = __esm(() => {
22421
22547
  NAX_VERSION = package_default.version;
22422
22548
  NAX_COMMIT = (() => {
22423
22549
  try {
22424
- if (/^[0-9a-f]{6,10}$/.test("2ffb62ec"))
22425
- return "2ffb62ec";
22550
+ if (/^[0-9a-f]{6,10}$/.test("166deae0"))
22551
+ return "166deae0";
22426
22552
  } catch {}
22427
22553
  try {
22428
22554
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -25043,7 +25169,7 @@ var init_version_detection = __esm(() => {
25043
25169
  // src/precheck/checks-agents.ts
25044
25170
  async function checkMultiAgentHealth() {
25045
25171
  try {
25046
- const versions2 = await getAgentVersions();
25172
+ const versions2 = await _checkAgentsDeps.getAgentVersions();
25047
25173
  const installed = versions2.filter((v) => v.installed);
25048
25174
  const notInstalled = versions2.filter((v) => !v.installed);
25049
25175
  const lines = [];
@@ -25080,8 +25206,12 @@ Available but not installed (${notInstalled.length}):`);
25080
25206
  };
25081
25207
  }
25082
25208
  }
25209
+ var _checkAgentsDeps;
25083
25210
  var init_checks_agents = __esm(() => {
25084
25211
  init_version_detection();
25212
+ _checkAgentsDeps = {
25213
+ getAgentVersions
25214
+ };
25085
25215
  });
25086
25216
 
25087
25217
  // src/precheck/checks.ts
@@ -26252,9 +26382,12 @@ ${stderr}` };
26252
26382
  const agent = (ctx.agentGetFn ?? _acceptanceSetupDeps.getAgent)(ctx.config.autoMode.defaultAgent);
26253
26383
  let allRefinedCriteria;
26254
26384
  if (ctx.config.acceptance.refinement) {
26255
- allRefinedCriteria = [];
26256
- for (const story of nonFixStories) {
26257
- const storyRefined = await _acceptanceSetupDeps.refine(story.acceptanceCriteria, {
26385
+ const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
26386
+ const results = new Array(nonFixStories.length);
26387
+ const executing = new Set;
26388
+ for (let i = 0;i < nonFixStories.length; i++) {
26389
+ const story = nonFixStories[i];
26390
+ const task = _acceptanceSetupDeps.refine(story.acceptanceCriteria, {
26258
26391
  storyId: story.id,
26259
26392
  featureName: ctx.prd.feature,
26260
26393
  workdir: ctx.workdir,
@@ -26262,9 +26395,18 @@ ${stderr}` };
26262
26395
  config: ctx.config,
26263
26396
  testStrategy: ctx.config.acceptance.testStrategy,
26264
26397
  testFramework: ctx.config.acceptance.testFramework
26398
+ }).then((refined) => {
26399
+ results[i] = refined;
26400
+ }).finally(() => {
26401
+ executing.delete(task);
26265
26402
  });
26266
- allRefinedCriteria = allRefinedCriteria.concat(storyRefined);
26403
+ executing.add(task);
26404
+ if (executing.size >= maxConcurrency) {
26405
+ await Promise.race(executing);
26406
+ }
26267
26407
  }
26408
+ await Promise.all(executing);
26409
+ allRefinedCriteria = results.flat();
26268
26410
  } else {
26269
26411
  allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
26270
26412
  original: c,
@@ -26593,6 +26735,93 @@ var init_event_bus = __esm(() => {
26593
26735
  pipelineEventBus = new PipelineEventBus;
26594
26736
  });
26595
26737
 
26738
+ // src/pipeline/stages/autofix-prompts.ts
26739
+ function formatCheckErrors(checks3) {
26740
+ return checks3.map((c) => `## ${c.check} errors (exit code ${c.exitCode})
26741
+ \`\`\`
26742
+ ${c.output}
26743
+ \`\`\``).join(`
26744
+
26745
+ `);
26746
+ }
26747
+ function buildSemanticRectificationPrompt(semanticChecks, story, scopeConstraint) {
26748
+ const errors3 = formatCheckErrors(semanticChecks);
26749
+ const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
26750
+ `);
26751
+ return `You are fixing acceptance criteria compliance issues found during semantic review.
26752
+
26753
+ Story: ${story.title} (${story.id})
26754
+
26755
+ ### Acceptance Criteria
26756
+ ${acList}
26757
+
26758
+ ### Semantic Review Findings
26759
+ ${errors3}
26760
+
26761
+ **Important:** The semantic reviewer only analyzed the git diff and may have flagged false positives (e.g., claiming a key or function is "missing" when it already exists in the codebase). Before making any changes:
26762
+ 1. Read the relevant files to verify each finding is a real issue
26763
+ 2. Only fix findings that are actually valid problems
26764
+ 3. Do NOT add keys, functions, or imports that already exist \u2014 check first
26765
+
26766
+ Do NOT change test files or test behavior.
26767
+ Do NOT add new features \u2014 only fix valid issues.
26768
+ Commit your fixes when done.${scopeConstraint}`;
26769
+ }
26770
+ function buildMechanicalRectificationPrompt(mechanicalChecks, story, scopeConstraint) {
26771
+ const errors3 = formatCheckErrors(mechanicalChecks);
26772
+ return `You are fixing lint/typecheck errors from a code review.
26773
+
26774
+ Story: ${story.title} (${story.id})
26775
+
26776
+ The following quality checks failed after implementation:
26777
+
26778
+ ${errors3}
26779
+
26780
+ Fix ALL errors listed above. Do NOT change test files or test behavior.
26781
+ Do NOT add new features \u2014 only fix the quality check errors.
26782
+ Commit your fixes when done.${scopeConstraint}`;
26783
+ }
26784
+ function buildReviewRectificationPrompt(failedChecks, story) {
26785
+ const scopeConstraint = story.workdir ? `
26786
+
26787
+ IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
26788
+ const semanticChecks = failedChecks.filter((c) => c.check === "semantic");
26789
+ const mechanicalChecks = failedChecks.filter((c) => c.check !== "semantic");
26790
+ if (semanticChecks.length > 0 && mechanicalChecks.length === 0) {
26791
+ return buildSemanticRectificationPrompt(semanticChecks, story, scopeConstraint);
26792
+ }
26793
+ if (mechanicalChecks.length > 0 && semanticChecks.length === 0) {
26794
+ return buildMechanicalRectificationPrompt(mechanicalChecks, story, scopeConstraint);
26795
+ }
26796
+ const mechanicalSection = formatCheckErrors(mechanicalChecks);
26797
+ const semanticSection = formatCheckErrors(semanticChecks);
26798
+ const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
26799
+ `);
26800
+ return `You are fixing issues from a code review.
26801
+
26802
+ Story: ${story.title} (${story.id})
26803
+
26804
+ ## Lint/Typecheck Errors
26805
+
26806
+ ${mechanicalSection}
26807
+
26808
+ Fix ALL lint/typecheck errors listed above.
26809
+
26810
+ ## Semantic Review Findings (AC Compliance)
26811
+
26812
+ ### Acceptance Criteria
26813
+ ${acList}
26814
+
26815
+ ### Findings
26816
+ ${semanticSection}
26817
+
26818
+ **Important:** The semantic reviewer may have flagged false positives. Before making changes for semantic findings, read the relevant files to verify each finding is a real issue. Do NOT add keys, functions, or imports that already exist.
26819
+
26820
+ Do NOT change test files or test behavior.
26821
+ Do NOT add new features \u2014 only fix the identified issues.
26822
+ Commit your fixes when done.${scopeConstraint}`;
26823
+ }
26824
+
26596
26825
  // src/utils/git.ts
26597
26826
  async function gitWithTimeout(args, workdir) {
26598
26827
  const proc = _gitDeps.spawn(["git", ...args], {
@@ -26853,7 +27082,7 @@ ${stat}
26853
27082
  return `${statPreamble}${truncated}
26854
27083
  ... (truncated at ${DIFF_CAP_BYTES} bytes, showing ${visibleFiles}/${totalFiles} files)`;
26855
27084
  }
26856
- function buildPrompt(story, semanticConfig, diff) {
27085
+ function buildPrompt(story, semanticConfig, diff, stat) {
26857
27086
  const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
26858
27087
  `);
26859
27088
  const customRulesSection = semanticConfig.rules.length > 0 ? `
@@ -26861,7 +27090,7 @@ function buildPrompt(story, semanticConfig, diff) {
26861
27090
  ${semanticConfig.rules.map((r, i) => `${i + 1}. ${r}`).join(`
26862
27091
  `)}
26863
27092
  ` : "";
26864
- return `You are a semantic code reviewer. Your job is to verify that the implementation satisfies the story's acceptance criteria (ACs). You are NOT a linter or style checker \u2014 lint, typecheck, and convention checks are handled separately.
27093
+ return `You are a semantic code reviewer with access to the repository files. Your job is to verify that the implementation satisfies the story's acceptance criteria (ACs). You are NOT a linter or style checker \u2014 lint, typecheck, and convention checks are handled separately.
26865
27094
 
26866
27095
  ## Story: ${story.title}
26867
27096
 
@@ -26878,20 +27107,28 @@ ${diff}\`\`\`
26878
27107
 
26879
27108
  ## Instructions
26880
27109
 
26881
- For each acceptance criterion, verify the diff implements it correctly. Flag issues only when:
26882
- 1. An AC is not implemented or partially implemented
27110
+ For each acceptance criterion, verify the diff implements it correctly.
27111
+
27112
+ **Before reporting any finding as "error", you MUST verify it using your tools:**
27113
+ - If you suspect a key, function, import, or variable is missing, READ the relevant file to confirm before flagging.
27114
+ - If you suspect a code path is not wired in, GREP for its usage to confirm.
27115
+ - Do NOT flag something as missing based solely on its absence from the diff \u2014 it may already exist in the codebase. Check the actual file first.
27116
+ - If you cannot verify a claim even after checking, use "unverifiable" severity instead of "error".
27117
+
27118
+ Flag issues only when you have confirmed:
27119
+ 1. An AC is not implemented or partially implemented (verified by reading the actual files)
26883
27120
  2. The implementation contradicts what the AC specifies
26884
27121
  3. New code has dead paths that will never execute (stubs, noops, unreachable branches)
26885
- 4. New code is not wired into callers/exports (written but never used)
27122
+ 4. New code is not wired into callers/exports (verified by grepping for usage)
26886
27123
 
26887
27124
  Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.
26888
27125
 
26889
- Respond in JSON format:
27126
+ Respond with JSON only \u2014 no explanation text before or after:
26890
27127
  {
26891
27128
  "passed": boolean,
26892
27129
  "findings": [
26893
27130
  {
26894
- "severity": "error" | "warn" | "info",
27131
+ "severity": "error" | "warn" | "info" | "unverifiable",
26895
27132
  "file": "path/to/file",
26896
27133
  "line": 42,
26897
27134
  "issue": "description of the issue",
@@ -26929,10 +27166,15 @@ function formatFindings(findings) {
26929
27166
  function normalizeSeverity(sev) {
26930
27167
  if (sev === "warn")
26931
27168
  return "warning";
27169
+ if (sev === "unverifiable")
27170
+ return "info";
26932
27171
  if (sev === "critical" || sev === "error" || sev === "warning" || sev === "info" || sev === "low")
26933
27172
  return sev;
26934
27173
  return "info";
26935
27174
  }
27175
+ function isBlockingSeverity(sev) {
27176
+ return sev !== "unverifiable";
27177
+ }
26936
27178
  function toReviewFindings(findings) {
26937
27179
  return findings.map((f) => ({
26938
27180
  ruleId: "semantic",
@@ -26975,10 +27217,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
26975
27217
  modelTier: semanticConfig.modelTier,
26976
27218
  configProvided: !!naxConfig
26977
27219
  });
26978
- const rawDiff = await collectDiff(workdir, effectiveRef, semanticConfig.excludePatterns);
26979
- const needsTruncation = rawDiff.length > DIFF_CAP_BYTES;
26980
- const stat = needsTruncation ? await collectDiffStat(workdir, effectiveRef) : undefined;
26981
- const diff = truncateDiff(rawDiff, stat);
27220
+ const [rawDiff, stat] = await Promise.all([
27221
+ collectDiff(workdir, effectiveRef, semanticConfig.excludePatterns),
27222
+ collectDiffStat(workdir, effectiveRef)
27223
+ ]);
27224
+ const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat : undefined);
26982
27225
  if (!diff) {
26983
27226
  return {
26984
27227
  check: "semantic",
@@ -27003,7 +27246,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27003
27246
  durationMs: Date.now() - startTime
27004
27247
  };
27005
27248
  }
27006
- const prompt = buildPrompt(story, semanticConfig, diff);
27249
+ const prompt = buildPrompt(story, semanticConfig, diff, stat || undefined);
27007
27250
  const reviewDebateEnabled = naxConfig?.debate?.enabled && naxConfig?.debate?.stages?.review?.enabled;
27008
27251
  if (reviewDebateEnabled) {
27009
27252
  const reviewStageConfig = naxConfig?.debate?.stages.review;
@@ -27042,10 +27285,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27042
27285
  deduped.push(f);
27043
27286
  }
27044
27287
  }
27288
+ const debateBlocking = deduped.filter((f) => isBlockingSeverity(f.severity));
27045
27289
  const durationMs2 = Date.now() - startTime;
27046
27290
  if (!majorityPassed) {
27047
- if (deduped.length > 0) {
27048
- logger?.warn("review", `Semantic review failed (debate): ${deduped.length} findings`, {
27291
+ if (debateBlocking.length > 0) {
27292
+ logger?.warn("review", `Semantic review failed (debate): ${debateBlocking.length} findings`, {
27049
27293
  storyId: story.id,
27050
27294
  durationMs: durationMs2
27051
27295
  });
@@ -27056,17 +27300,21 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
27056
27300
  exitCode: 1,
27057
27301
  output: `Semantic review failed:
27058
27302
 
27059
- ${formatFindings(deduped)}`,
27303
+ ${formatFindings(debateBlocking)}`,
27060
27304
  durationMs: durationMs2,
27061
- findings: toReviewFindings(deduped)
27305
+ findings: toReviewFindings(debateBlocking)
27062
27306
  };
27063
27307
  }
27308
+ logger?.info("review", "Semantic review passed (debate, all findings non-blocking)", {
27309
+ storyId: story.id,
27310
+ durationMs: durationMs2
27311
+ });
27064
27312
  return {
27065
27313
  check: "semantic",
27066
- success: false,
27314
+ success: true,
27067
27315
  command: "",
27068
- exitCode: 1,
27069
- output: "Semantic review failed (debate, no findings)",
27316
+ exitCode: 0,
27317
+ output: "Semantic review passed (debate, all findings were unverifiable or informational)",
27070
27318
  durationMs: durationMs2
27071
27319
  };
27072
27320
  }
@@ -27127,15 +27375,23 @@ ${formatFindings(deduped)}`,
27127
27375
  durationMs: Date.now() - startTime
27128
27376
  };
27129
27377
  }
27130
- if (!parsed.passed && parsed.findings.length > 0) {
27378
+ const blockingFindings = parsed.findings.filter((f) => isBlockingSeverity(f.severity));
27379
+ const nonBlockingFindings = parsed.findings.filter((f) => !isBlockingSeverity(f.severity));
27380
+ if (nonBlockingFindings.length > 0) {
27381
+ logger?.debug("review", `Semantic review: ${nonBlockingFindings.length} non-blocking findings (unverifiable/info)`, {
27382
+ storyId: story.id,
27383
+ findings: nonBlockingFindings.map((f) => ({ severity: f.severity, file: f.file, issue: f.issue }))
27384
+ });
27385
+ }
27386
+ if (!parsed.passed && blockingFindings.length > 0) {
27131
27387
  const durationMs2 = Date.now() - startTime;
27132
- logger?.warn("review", `Semantic review failed: ${parsed.findings.length} findings`, {
27388
+ logger?.warn("review", `Semantic review failed: ${blockingFindings.length} findings`, {
27133
27389
  storyId: story.id,
27134
27390
  durationMs: durationMs2
27135
27391
  });
27136
27392
  logger?.debug("review", "Semantic review findings", {
27137
27393
  storyId: story.id,
27138
- findings: parsed.findings.map((f) => ({
27394
+ findings: blockingFindings.map((f) => ({
27139
27395
  severity: f.severity,
27140
27396
  file: f.file,
27141
27397
  line: f.line,
@@ -27145,7 +27401,7 @@ ${formatFindings(deduped)}`,
27145
27401
  });
27146
27402
  const output = `Semantic review failed:
27147
27403
 
27148
- ${formatFindings(parsed.findings)}`;
27404
+ ${formatFindings(blockingFindings)}`;
27149
27405
  return {
27150
27406
  check: "semantic",
27151
27407
  success: false,
@@ -27153,7 +27409,19 @@ ${formatFindings(parsed.findings)}`;
27153
27409
  exitCode: 1,
27154
27410
  output,
27155
27411
  durationMs: durationMs2,
27156
- findings: toReviewFindings(parsed.findings)
27412
+ findings: toReviewFindings(blockingFindings)
27413
+ };
27414
+ }
27415
+ if (!parsed.passed && blockingFindings.length === 0) {
27416
+ const durationMs2 = Date.now() - startTime;
27417
+ logger?.info("review", "Semantic review passed (all findings non-blocking)", { storyId: story.id, durationMs: durationMs2 });
27418
+ return {
27419
+ check: "semantic",
27420
+ success: true,
27421
+ command: "",
27422
+ exitCode: 0,
27423
+ output: "Semantic review passed (all findings were unverifiable or informational)",
27424
+ durationMs: durationMs2
27157
27425
  };
27158
27426
  }
27159
27427
  const durationMs = Date.now() - startTime;
@@ -27560,28 +27828,6 @@ async function recheckReview(ctx) {
27560
27828
  function collectFailedChecks(ctx) {
27561
27829
  return (ctx.reviewResult?.checks ?? []).filter((c) => !c.success);
27562
27830
  }
27563
- function buildReviewRectificationPrompt(failedChecks, story) {
27564
- const errors3 = failedChecks.map((c) => `## ${c.check} errors (exit code ${c.exitCode})
27565
- \`\`\`
27566
- ${c.output}
27567
- \`\`\``).join(`
27568
-
27569
- `);
27570
- const scopeConstraint = story.workdir ? `
27571
-
27572
- IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
27573
- return `You are fixing lint/typecheck errors from a code review.
27574
-
27575
- Story: ${story.title} (${story.id})
27576
-
27577
- The following quality checks failed after implementation:
27578
-
27579
- ${errors3}
27580
-
27581
- Fix ALL errors listed above. Do NOT change test files or test behavior.
27582
- Do NOT add new features \u2014 only fix the quality check errors.
27583
- Commit your fixes when done.${scopeConstraint}`;
27584
- }
27585
27831
  function buildAutofixEscalationPreamble(attempt, maxAttempts, rethinkAtAttempt, urgencyAtAttempt) {
27586
27832
  return buildProgressivePromptPreamble({
27587
27833
  attempt,
@@ -29164,35 +29410,12 @@ async function isGreenfieldStory(story, workdir, testPattern = "**/*.{test,spec}
29164
29410
  }
29165
29411
  var init_greenfield = () => {};
29166
29412
  // src/verification/executor.ts
29167
- async function drainWithDeadline(proc, deadlineMs) {
29168
- const EMPTY = Symbol("timeout");
29169
- const race = (p) => {
29170
- let timerId;
29171
- const timeoutPromise = new Promise((r) => {
29172
- timerId = setTimeout(() => r(EMPTY), deadlineMs);
29173
- });
29174
- return Promise.race([p, timeoutPromise]).finally(() => clearTimeout(timerId));
29175
- };
29176
- let out = "";
29177
- try {
29178
- const stdout = race(new Response(proc.stdout).text());
29179
- const stderr = race(new Response(proc.stderr).text());
29180
- const [o, e] = await Promise.all([stdout, stderr]);
29181
- if (o !== EMPTY)
29182
- out += o;
29183
- if (e !== EMPTY)
29184
- out += (out ? `
29185
- ` : "") + e;
29186
- } catch (error48) {
29187
- const isExpectedStreamError = error48 instanceof TypeError || error48 instanceof Error && /abort|cancel|close|destroy|locked/i.test(error48.message);
29188
- if (!isExpectedStreamError) {
29189
- const { getSafeLogger: getSafeLogger4 } = await Promise.resolve().then(() => (init_logger2(), exports_logger));
29190
- getSafeLogger4()?.debug("executor", "Unexpected error draining process output", {
29191
- error: errorMessage(error48)
29192
- });
29193
- }
29194
- }
29195
- return out;
29413
+ function raceWithDeadline(p, deadlineMs) {
29414
+ const timer = { id: undefined };
29415
+ const timeoutP = new Promise((r) => {
29416
+ timer.id = setTimeout(() => r(DRAIN_TIMEOUT), deadlineMs);
29417
+ });
29418
+ return Promise.race([p, timeoutP]).finally(() => clearTimeout(timer.id));
29196
29419
  }
29197
29420
  function normalizeEnvironment(env2, stripVars) {
29198
29421
  const normalized = { ...env2 };
@@ -29212,6 +29435,8 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
29212
29435
  env: env2 || normalizeEnvironment(process.env),
29213
29436
  cwd: options?.cwd
29214
29437
  });
29438
+ const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
29439
+ const stderrPromise = new Response(proc.stderr).text().catch(() => "");
29215
29440
  const timeoutMs = timeoutSeconds * 1000;
29216
29441
  let timedOut = false;
29217
29442
  const timer = { id: undefined };
@@ -29229,20 +29454,25 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
29229
29454
  killProcessGroup(pid, "SIGTERM");
29230
29455
  await Bun.sleep(gracePeriodMs);
29231
29456
  killProcessGroup(pid, "SIGKILL");
29232
- const partialOutput = await drainWithDeadline(proc, drainTimeoutMs);
29457
+ const [out, err] = await Promise.all([
29458
+ raceWithDeadline(stdoutPromise, drainTimeoutMs),
29459
+ raceWithDeadline(stderrPromise, drainTimeoutMs)
29460
+ ]);
29461
+ const parts = [out !== DRAIN_TIMEOUT ? out : "", err !== DRAIN_TIMEOUT ? err : ""].filter(Boolean);
29462
+ const partialOutput = parts.join(`
29463
+ `) || undefined;
29233
29464
  return {
29234
29465
  success: false,
29235
29466
  timeout: true,
29236
29467
  killed: true,
29237
29468
  childProcessesKilled: true,
29238
- output: partialOutput || undefined,
29469
+ output: partialOutput,
29239
29470
  error: `EXECUTION_TIMEOUT: Verification process exceeded ${timeoutSeconds}s. Process group (PID ${pid}) killed.`,
29240
29471
  countsTowardEscalation: false
29241
29472
  };
29242
29473
  }
29243
- const exitCode = raceResult;
29244
- const stdout = await new Response(proc.stdout).text();
29245
- const stderr = await new Response(proc.stderr).text();
29474
+ const exitCode = typeof raceResult === "number" ? raceResult : 0;
29475
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
29246
29476
  const output = `${stdout}
29247
29477
  ${stderr}`;
29248
29478
  return {
@@ -29288,10 +29518,11 @@ function buildTestCommand(baseCommand, options) {
29288
29518
  }
29289
29519
  return command;
29290
29520
  }
29291
- var _executorDeps, DEFAULT_STRIP_ENV_VARS;
29521
+ var _executorDeps, DRAIN_TIMEOUT, DEFAULT_STRIP_ENV_VARS;
29292
29522
  var init_executor = __esm(() => {
29293
29523
  init_bun_deps();
29294
29524
  _executorDeps = { spawn };
29525
+ DRAIN_TIMEOUT = Symbol("drain-timeout");
29295
29526
  DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
29296
29527
  });
29297
29528
 
@@ -34435,12 +34666,12 @@ function createSignalHandler(ctx) {
34435
34666
  hardDeadline.unref();
34436
34667
  const logger = getSafeLogger();
34437
34668
  logger?.error("crash-recovery", `Received ${signal}, shutting down...`, { signal });
34438
- if (ctx.pidRegistry) {
34439
- await ctx.pidRegistry.killAll();
34440
- }
34441
34669
  if (ctx.onShutdown) {
34442
34670
  await ctx.onShutdown().catch(() => {});
34443
34671
  }
34672
+ if (ctx.pidRegistry) {
34673
+ await ctx.pidRegistry.killAll();
34674
+ }
34444
34675
  ctx.emitError?.(signal.toLowerCase());
34445
34676
  await writeFatalLog(ctx.jsonlFilePath, signal);
34446
34677
  await writeRunComplete(ctx, signal.toLowerCase());
@@ -34460,12 +34691,12 @@ ${error48.stack ?? ""}
34460
34691
  error: error48.message,
34461
34692
  stack: error48.stack
34462
34693
  });
34463
- if (ctx.pidRegistry) {
34464
- await ctx.pidRegistry.killAll();
34465
- }
34466
34694
  if (ctx.onShutdown) {
34467
34695
  await ctx.onShutdown().catch(() => {});
34468
34696
  }
34697
+ if (ctx.pidRegistry) {
34698
+ await ctx.pidRegistry.killAll();
34699
+ }
34469
34700
  ctx.emitError?.("uncaughtException");
34470
34701
  await writeFatalLog(ctx.jsonlFilePath, "uncaughtException", error48);
34471
34702
  await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "uncaughtException", ctx.featureDir);
@@ -34484,12 +34715,12 @@ ${error48.stack ?? ""}
34484
34715
  error: error48.message,
34485
34716
  stack: error48.stack
34486
34717
  });
34487
- if (ctx.pidRegistry) {
34488
- await ctx.pidRegistry.killAll();
34489
- }
34490
34718
  if (ctx.onShutdown) {
34491
34719
  await ctx.onShutdown().catch(() => {});
34492
34720
  }
34721
+ if (ctx.pidRegistry) {
34722
+ await ctx.pidRegistry.killAll();
34723
+ }
34493
34724
  ctx.emitError?.("unhandledRejection");
34494
34725
  await writeFatalLog(ctx.jsonlFilePath, "unhandledRejection", error48);
34495
34726
  await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "unhandledRejection", ctx.featureDir);
@@ -38446,7 +38677,7 @@ async function setupRun(options) {
38446
38677
  },
38447
38678
  onShutdown: async () => {
38448
38679
  const { sweepFeatureSessions: sweepFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
38449
- await sweepFeatureSessions2(workdir, feature).catch(() => {});
38680
+ await sweepFeatureSessions2(workdir, feature, pidRegistry).catch(() => {});
38450
38681
  }
38451
38682
  });
38452
38683
  let prd = await loadPRD(prdPath);
@@ -38469,7 +38700,7 @@ async function setupRun(options) {
38469
38700
  logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
38470
38701
  }
38471
38702
  const { sweepStaleFeatureSessions: sweepStaleFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
38472
- await sweepStaleFeatureSessions2(workdir, feature).catch(() => {});
38703
+ await sweepStaleFeatureSessions2(workdir, feature, undefined, pidRegistry).catch(() => {});
38473
38704
  const lockAcquired = await acquireLock(workdir);
38474
38705
  if (!lockAcquired) {
38475
38706
  logger?.error("execution", "Another nax process is already running in this directory");
@@ -69988,6 +70219,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
69988
70219
  }
69989
70220
  // src/cli/plan.ts
69990
70221
  init_registry();
70222
+ init_decompose();
69991
70223
  import { existsSync as existsSync15 } from "fs";
69992
70224
  import { join as join13 } from "path";
69993
70225
  import { createInterface as createInterface2 } from "readline";
@@ -70534,6 +70766,48 @@ init_bridge_builder();
70534
70766
  init_init();
70535
70767
  init_logger2();
70536
70768
 
70769
+ // src/prd/decompose-mapper.ts
70770
+ init_errors();
70771
+ function mapDecomposedStoriesToUserStories(stories, parentStoryId) {
70772
+ return stories.map((story, entryIndex) => {
70773
+ if (!story.id) {
70774
+ throw new NaxError(`Entry at index ${entryIndex} is missing required field: id`, "DECOMPOSE_VALIDATION_FAILED", {
70775
+ stage: "decompose-mapper",
70776
+ entryIndex,
70777
+ parentStoryId
70778
+ });
70779
+ }
70780
+ if (!story.contextFiles || story.contextFiles.length === 0) {
70781
+ throw new NaxError(`Entry ${entryIndex} (${story.id}) has empty contextFiles`, "DECOMPOSE_VALIDATION_FAILED", {
70782
+ stage: "decompose-mapper",
70783
+ entryIndex,
70784
+ storyId: story.id,
70785
+ parentStoryId
70786
+ });
70787
+ }
70788
+ return {
70789
+ id: story.id,
70790
+ title: story.title,
70791
+ description: story.description,
70792
+ acceptanceCriteria: story.acceptanceCriteria,
70793
+ tags: story.tags,
70794
+ dependencies: story.dependencies,
70795
+ contextFiles: story.contextFiles,
70796
+ status: "pending",
70797
+ passes: false,
70798
+ escalations: [],
70799
+ attempts: 0,
70800
+ parentStoryId,
70801
+ routing: {
70802
+ complexity: story.complexity,
70803
+ testStrategy: story.testStrategy ?? "test-after",
70804
+ reasoning: story.reasoning,
70805
+ modelTier: "balanced"
70806
+ }
70807
+ };
70808
+ });
70809
+ }
70810
+
70537
70811
  // src/prd/schema.ts
70538
70812
  init_test_strategy();
70539
70813
  var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
@@ -70825,26 +71099,45 @@ async function planCommand(workdir, config2, options) {
70825
71099
  timeoutSeconds
70826
71100
  });
70827
71101
  const pidRegistry = new PidRegistry(workdir);
71102
+ let planError = null;
70828
71103
  try {
70829
- await adapter.plan({
70830
- prompt,
70831
- workdir,
70832
- interactive: false,
70833
- timeoutSeconds,
70834
- config: config2,
70835
- modelTier: config2?.plan?.model ?? "balanced",
70836
- dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
70837
- maxInteractionTurns: config2?.agent?.maxInteractionTurns,
70838
- featureName: options.feature,
70839
- pidRegistry,
70840
- sessionRole: "plan"
70841
- });
71104
+ try {
71105
+ await adapter.plan({
71106
+ prompt,
71107
+ workdir,
71108
+ interactive: false,
71109
+ timeoutSeconds,
71110
+ config: config2,
71111
+ modelTier: config2?.plan?.model ?? "balanced",
71112
+ dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
71113
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
71114
+ featureName: options.feature,
71115
+ pidRegistry,
71116
+ sessionRole: "plan"
71117
+ });
71118
+ } catch (err) {
71119
+ planError = err instanceof Error ? err : new Error(String(err));
71120
+ logger?.warn("plan", "ACP auto planning did not complete cleanly; checking for written PRD", {
71121
+ error: planError.message,
71122
+ outputPath
71123
+ });
71124
+ }
70842
71125
  } finally {
70843
71126
  await pidRegistry.killAll().catch(() => {});
70844
71127
  }
70845
71128
  if (!_planDeps.existsSync(outputPath)) {
71129
+ if (planError) {
71130
+ throw new Error(`[plan] ACP planning failed and no PRD was written: ${planError.message}`, {
71131
+ cause: planError
71132
+ });
71133
+ }
70846
71134
  throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
70847
71135
  }
71136
+ if (planError) {
71137
+ logger?.warn("plan", "Proceeding with PRD written by ACP despite incomplete terminal response", {
71138
+ outputPath
71139
+ });
71140
+ }
70848
71141
  rawResponse = await _planDeps.readFile(outputPath);
70849
71142
  } else {
70850
71143
  const timeoutMs = (config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2) * 1000;
@@ -70893,20 +71186,30 @@ async function planCommand(workdir, config2, options) {
70893
71186
  timeoutSeconds
70894
71187
  });
70895
71188
  const planStartTime = Date.now();
71189
+ let planError = null;
70896
71190
  try {
70897
- await adapter.plan({
70898
- prompt,
70899
- workdir,
70900
- interactive: true,
70901
- timeoutSeconds,
70902
- interactionBridge,
70903
- config: config2,
70904
- modelTier: resolvedModel,
70905
- dangerouslySkipPermissions: resolvedPerm.skipPermissions,
70906
- maxInteractionTurns: config2?.agent?.maxInteractionTurns,
70907
- featureName: options.feature,
70908
- pidRegistry
70909
- });
71191
+ try {
71192
+ await adapter.plan({
71193
+ prompt,
71194
+ workdir,
71195
+ interactive: true,
71196
+ timeoutSeconds,
71197
+ interactionBridge,
71198
+ config: config2,
71199
+ modelTier: resolvedModel,
71200
+ dangerouslySkipPermissions: resolvedPerm.skipPermissions,
71201
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
71202
+ featureName: options.feature,
71203
+ pidRegistry,
71204
+ sessionRole: "plan"
71205
+ });
71206
+ } catch (err) {
71207
+ planError = err instanceof Error ? err : new Error(String(err));
71208
+ logger?.warn("plan", "Interactive planning did not complete cleanly; checking for written PRD", {
71209
+ error: planError.message,
71210
+ outputPath
71211
+ });
71212
+ }
70910
71213
  } finally {
70911
71214
  await pidRegistry.killAll().catch(() => {});
70912
71215
  if (interactionChain)
@@ -70914,8 +71217,16 @@ async function planCommand(workdir, config2, options) {
70914
71217
  logger?.info("plan", "Interactive session ended", { durationMs: Date.now() - planStartTime });
70915
71218
  }
70916
71219
  if (!_planDeps.existsSync(outputPath)) {
71220
+ if (planError) {
71221
+ throw new Error(`[plan] Planning failed and no PRD was written: ${planError.message}`, { cause: planError });
71222
+ }
70917
71223
  throw new Error(`[plan] Agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
70918
71224
  }
71225
+ if (planError) {
71226
+ logger?.warn("plan", "Proceeding with PRD written by agent despite incomplete terminal response", {
71227
+ outputPath
71228
+ });
71229
+ }
70919
71230
  return _planDeps.readFile(outputPath);
70920
71231
  }
70921
71232
  const finalPrd = validatePlanOutput(rawResponse, options.feature, branchName);
@@ -71142,58 +71453,6 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
71142
71453
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
71143
71454
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.` : "Output ONLY the JSON object. Do not wrap in markdown code blocks."}`;
71144
71455
  }
71145
- function buildDecomposePrompt2(targetStory, siblings, codebaseContext) {
71146
- const siblingsSummary = siblings.length > 0 ? `
71147
- ## Sibling Stories
71148
-
71149
- ${siblings.map((s) => `- ${s.id}: ${s.title}`).join(`
71150
- `)}
71151
- ` : "";
71152
- return `You are a senior software architect decomposing a complex user story into smaller, implementable sub-stories.
71153
-
71154
- ## Target Story
71155
-
71156
- ${JSON.stringify(targetStory, null, 2)}${siblingsSummary}
71157
- ## Codebase Context
71158
-
71159
- ${codebaseContext}
71160
-
71161
- ${COMPLEXITY_GUIDE}
71162
-
71163
- ${TEST_STRATEGY_GUIDE}
71164
-
71165
- ${GROUPING_RULES}
71166
-
71167
- ${getAcQualityRules()}
71168
-
71169
- ## Output
71170
-
71171
- Return JSON with this exact structure (no markdown, no explanation \u2014 JSON only):
71172
-
71173
- {
71174
- "subStories": [
71175
- {
71176
- "id": "string \u2014 e.g. ${targetStory.id}-A",
71177
- "title": "string",
71178
- "description": "string",
71179
- "acceptanceCriteria": ["string \u2014 behavioral, testable criteria"],
71180
- "contextFiles": ["string \u2014 required, non-empty list of key source files"],
71181
- "tags": ["string"],
71182
- "dependencies": ["string"],
71183
- "status": "pending",
71184
- "passes": false,
71185
- "routing": {
71186
- "complexity": "simple | medium | complex | expert",
71187
- "testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
71188
- "modelTier": "fast | balanced | powerful",
71189
- "reasoning": "string"
71190
- },
71191
- "escalations": [],
71192
- "attempts": 0
71193
- }
71194
- ]
71195
- }`;
71196
- }
71197
71456
  async function planDecomposeCommand(workdir, config2, options) {
71198
71457
  const prdPath = join13(workdir, ".nax", "features", options.feature, "prd.json");
71199
71458
  if (!_planDeps.existsSync(prdPath)) {
@@ -71220,31 +71479,34 @@ async function planDecomposeCommand(workdir, config2, options) {
71220
71479
  const scan = await _planDeps.scanCodebase(workdir);
71221
71480
  const codebaseContext = buildCodebaseContext2(scan);
71222
71481
  const siblings = prd.userStories.filter((s) => s.id !== options.storyId);
71223
- const prompt = buildDecomposePrompt2(targetStory, siblings, codebaseContext);
71224
71482
  const agentName = config2?.autoMode?.defaultAgent ?? "claude";
71225
71483
  const adapter = _planDeps.getAgent(agentName, config2);
71226
71484
  if (!adapter)
71227
71485
  throw new Error(`[decompose] No agent adapter found for '${agentName}'`);
71228
- let decomposeModel;
71229
- try {
71230
- const planTier = config2?.plan?.model ?? "balanced";
71231
- const { resolveModelForAgent: resolveModelForAgent2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
71232
- if (config2?.models) {
71233
- const defaultAgent = config2.autoMode?.defaultAgent ?? "claude";
71234
- decomposeModel = resolveModelForAgent2(config2.models, defaultAgent, planTier, defaultAgent).model;
71235
- }
71236
- } catch {}
71237
- const stages = config2?.debate?.stages;
71238
- const debateEnabled = config2?.debate?.enabled && stages?.decompose?.enabled;
71239
- let rawResponse;
71240
71486
  const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
71241
- const timeoutMs = timeoutSeconds * 1000;
71242
- if (debateEnabled) {
71243
- const stageConfig = stages?.decompose;
71487
+ const maxAcCount = config2?.precheck?.storySizeGate?.maxAcCount ?? Number.POSITIVE_INFINITY;
71488
+ if (typeof adapter.decompose !== "function") {
71489
+ throw new NaxError(`Agent "${agentName}" does not support decompose() required by plan --decompose`, "DECOMPOSE_NOT_SUPPORTED", { stage: "decompose", agent: agentName, storyId: options.storyId });
71490
+ }
71491
+ const debateStages = config2?.debate?.stages;
71492
+ const debateDecompEnabled = config2?.debate?.enabled && debateStages?.decompose?.enabled;
71493
+ let decompStories;
71494
+ if (debateDecompEnabled) {
71495
+ const decomposeStageConfig = debateStages.decompose;
71496
+ const prompt = buildDecomposePrompt({
71497
+ specContent: "",
71498
+ codebaseContext,
71499
+ workdir,
71500
+ targetStory,
71501
+ siblings,
71502
+ featureName: options.feature,
71503
+ storyId: options.storyId,
71504
+ config: config2
71505
+ });
71244
71506
  const debateSession = _planDeps.createDebateSession({
71245
71507
  storyId: options.storyId,
71246
71508
  stage: "decompose",
71247
- stageConfig,
71509
+ stageConfig: decomposeStageConfig,
71248
71510
  config: config2,
71249
71511
  workdir,
71250
71512
  featureName: options.feature,
@@ -71252,52 +71514,24 @@ async function planDecomposeCommand(workdir, config2, options) {
71252
71514
  });
71253
71515
  const debateResult = await debateSession.run(prompt);
71254
71516
  if (debateResult.outcome !== "failed" && debateResult.output) {
71255
- rawResponse = debateResult.output;
71256
- } else {
71257
- const completeResult = await adapter.complete(prompt, {
71258
- model: decomposeModel,
71259
- jsonMode: true,
71260
- workdir,
71261
- sessionRole: "decompose",
71262
- featureName: options.feature,
71263
- storyId: options.storyId,
71264
- timeoutMs
71265
- });
71266
- rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
71517
+ decompStories = parseDecomposeOutput(debateResult.output);
71267
71518
  }
71268
- } else {
71269
- const completeResult = await adapter.complete(prompt, {
71270
- model: decomposeModel,
71271
- jsonMode: true,
71519
+ }
71520
+ if (!decompStories) {
71521
+ const result = await adapter.decompose({
71522
+ specContent: "",
71523
+ codebaseContext,
71272
71524
  workdir,
71273
- sessionRole: "decompose",
71525
+ targetStory,
71526
+ siblings,
71274
71527
  featureName: options.feature,
71275
71528
  storyId: options.storyId,
71276
- timeoutMs
71529
+ config: config2
71277
71530
  });
71278
- rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
71279
- }
71280
- const jsonMatch = rawResponse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
71281
- const cleanedResponse = jsonMatch ? jsonMatch[1] : rawResponse;
71282
- let parsed;
71283
- try {
71284
- parsed = JSON.parse(cleanedResponse.trim());
71285
- } catch (err) {
71286
- throw new NaxError(`Failed to parse decompose response as JSON: ${err.message}
71287
-
71288
- Response (first 500 chars):
71289
- ${rawResponse.slice(0, 500)}`, "DECOMPOSE_PARSE_FAILED", { stage: "decompose", storyId: options.storyId });
71531
+ decompStories = result.stories;
71290
71532
  }
71291
- const subStories = parsed.subStories;
71292
- const maxAcCount = config2?.precheck?.storySizeGate?.maxAcCount ?? Number.POSITIVE_INFINITY;
71293
- for (const sub of subStories) {
71294
- if (!sub.contextFiles || sub.contextFiles.length === 0) {
71295
- throw new NaxError(`Sub-story "${sub.id}" has empty contextFiles`, "DECOMPOSE_VALIDATION_FAILED", {
71296
- stage: "decompose",
71297
- storyId: sub.id
71298
- });
71299
- }
71300
- if (!sub.routing?.complexity || !sub.routing?.testStrategy || !sub.routing?.modelTier) {
71533
+ for (const sub of decompStories) {
71534
+ if (!sub.complexity || !sub.testStrategy) {
71301
71535
  throw new NaxError(`Sub-story "${sub.id}" is missing required routing fields`, "DECOMPOSE_VALIDATION_FAILED", {
71302
71536
  stage: "decompose",
71303
71537
  storyId: sub.id
@@ -71307,8 +71541,8 @@ ${rawResponse.slice(0, 500)}`, "DECOMPOSE_PARSE_FAILED", { stage: "decompose", s
71307
71541
  throw new NaxError(`Sub-story "${sub.id}" has ${sub.acceptanceCriteria.length} ACs, exceeds maxAcCount of ${maxAcCount}`, "DECOMPOSE_VALIDATION_FAILED", { stage: "decompose", storyId: sub.id });
71308
71542
  }
71309
71543
  }
71544
+ const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId);
71310
71545
  const updatedStories = prd.userStories.map((s) => s.id === options.storyId ? { ...s, status: "decomposed" } : s);
71311
- const subStoriesWithParent = subStories.map((s) => ({ ...s, parentStoryId: options.storyId }));
71312
71546
  const originalIndex = updatedStories.findIndex((s) => s.id === options.storyId);
71313
71547
  const finalStories = [
71314
71548
  ...updatedStories.slice(0, originalIndex + 1),
@@ -72987,6 +73221,8 @@ var FIELD_DESCRIPTIONS = {
72987
73221
  "acceptance.generateTests": "Generate acceptance tests during analyze",
72988
73222
  "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
72989
73223
  "acceptance.command": "Override command to run acceptance tests. Use {{FILE}} as placeholder for the test file path (default: 'bun test {{FILE}} --timeout=60000')",
73224
+ "acceptance.model": "Model tier for acceptance generation/refinement LLM calls (fast | balanced | powerful). Default: fast.",
73225
+ "acceptance.refinement": "Enable acceptance criteria refinement step before execution (default: true). Disable to skip refinement and use generated criteria as-is.",
72990
73226
  "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
72991
73227
  context: "Context injection configuration",
72992
73228
  "context.fileInjection": "Mode: 'disabled' (default, MCP-aware agents pull context on-demand) | 'keyword' (legacy git-grep injection for non-MCP agents). Set context.fileInjection in config.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.57.2",
3
+ "version": "0.57.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {