@nathapp/nax 0.70.0-canary.3 → 0.70.0-canary.5

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 +378 -159
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -16852,11 +16852,13 @@ var init_schemas_execution = __esm(() => {
16852
16852
  SmartTestRunnerConfigSchema = exports_external.object({
16853
16853
  enabled: exports_external.boolean().default(true),
16854
16854
  testFilePatterns: exports_external.array(exports_external.string()).optional(),
16855
- fallback: exports_external.enum(["import-grep", "full-suite"]).default("import-grep")
16855
+ fallback: exports_external.enum(["import-grep", "full-suite"]).default("import-grep"),
16856
+ maxScanFiles: exports_external.number().int().min(1).max(5000).default(200)
16856
16857
  });
16857
16858
  SMART_TEST_RUNNER_DEFAULT = {
16858
16859
  enabled: true,
16859
- fallback: "import-grep"
16860
+ fallback: "import-grep",
16861
+ maxScanFiles: 200
16860
16862
  };
16861
16863
  smartTestRunnerFieldSchema = exports_external.preprocess((val) => {
16862
16864
  if (typeof val === "boolean") {
@@ -17665,7 +17667,8 @@ var init_schemas3 = __esm(() => {
17665
17667
  }
17666
17668
  })),
17667
17669
  curator: CuratorConfigSchema.optional(),
17668
- profile: exports_external.string().default("default")
17670
+ profile: exports_external.string().default("default"),
17671
+ profileChain: exports_external.array(exports_external.string()).default([])
17669
17672
  }).refine((data) => data.version === 1, {
17670
17673
  message: "Invalid version: expected 1",
17671
17674
  path: ["version"]
@@ -18728,7 +18731,11 @@ async function loadProfile(profileName, projectRoot) {
18728
18731
  if (!globalExists && !projectExists) {
18729
18732
  const available = await listAvailableProfileNames(projectRoot);
18730
18733
  const availableList = available.length > 0 ? available.join(", ") : "(none)";
18731
- throw new Error(`Profile "${profileName}" not found. Available: ${availableList}`);
18734
+ throw new NaxError(`Profile "${profileName}" not found. Available: ${availableList}`, "PROFILE_NOT_FOUND", {
18735
+ stage: "config",
18736
+ profileName,
18737
+ available
18738
+ });
18732
18739
  }
18733
18740
  let base = {};
18734
18741
  if (globalExists) {
@@ -18760,30 +18767,59 @@ async function loadProfileEnv(profileName, projectRoot) {
18760
18767
  }
18761
18768
  return merged;
18762
18769
  }
18763
- async function resolveProfileName(cliOptions, env2, projectRoot) {
18764
- if (cliOptions.profile) {
18765
- return cliOptions.profile;
18766
- }
18767
- if (env2.NAX_PROFILE) {
18768
- return env2.NAX_PROFILE;
18769
- }
18770
- const projectConfigPath = join2(projectConfigDir(projectRoot), "config.json");
18771
- const projectConfigFile = Bun.file(projectConfigPath);
18772
- if (await projectConfigFile.exists()) {
18773
- const config2 = await projectConfigFile.json();
18774
- if (typeof config2.profile === "string" && config2.profile && config2.profile !== "default") {
18775
- return config2.profile;
18770
+ function parseProfileList(input) {
18771
+ if (input == null)
18772
+ return [];
18773
+ const parts = Array.isArray(input) ? input : [input];
18774
+ const out = [];
18775
+ for (const part of parts) {
18776
+ if (typeof part !== "string")
18777
+ continue;
18778
+ for (const segment of part.split(",")) {
18779
+ const trimmed = segment.trim();
18780
+ if (trimmed)
18781
+ out.push(trimmed);
18776
18782
  }
18777
18783
  }
18778
- const globalConfigPath = join2(globalConfigDir(), "config.json");
18779
- const globalConfigFile = Bun.file(globalConfigPath);
18780
- if (await globalConfigFile.exists()) {
18781
- const config2 = await globalConfigFile.json();
18782
- if (typeof config2.profile === "string" && config2.profile && config2.profile !== "default") {
18783
- return config2.profile;
18784
- }
18784
+ return out;
18785
+ }
18786
+ function profileOverrideFromConfig(config2) {
18787
+ if (config2.profileChain && config2.profileChain.length > 0) {
18788
+ return { profile: config2.profileChain };
18789
+ }
18790
+ if (config2.profile && config2.profile !== "default") {
18791
+ return { profile: [config2.profile] };
18785
18792
  }
18786
- return "default";
18793
+ return;
18794
+ }
18795
+ async function readProfileChainFromConfig(dir) {
18796
+ const configFile = Bun.file(join2(dir, "config.json"));
18797
+ if (!await configFile.exists())
18798
+ return [];
18799
+ const config2 = await configFile.json();
18800
+ return parseProfileList(config2.profile);
18801
+ }
18802
+ function isDefaultOnlyChain(chain) {
18803
+ return chain.length === 0 || chain.length === 1 && chain[0] === "default";
18804
+ }
18805
+ async function resolveProfileNames(cliOptions, env2, projectRoot) {
18806
+ const fromCli = parseProfileList(cliOptions.profile);
18807
+ if (fromCli.length)
18808
+ return fromCli;
18809
+ const fromEnv = parseProfileList(env2.NAX_PROFILE);
18810
+ if (fromEnv.length)
18811
+ return fromEnv;
18812
+ const projectChain = await readProfileChainFromConfig(projectConfigDir(projectRoot));
18813
+ if (!isDefaultOnlyChain(projectChain))
18814
+ return projectChain;
18815
+ const globalChain = await readProfileChainFromConfig(globalConfigDir());
18816
+ if (!isDefaultOnlyChain(globalChain))
18817
+ return globalChain;
18818
+ return ["default"];
18819
+ }
18820
+ async function resolveProfileName(cliOptions, env2, projectRoot) {
18821
+ const chain = await resolveProfileNames(cliOptions, env2, projectRoot);
18822
+ return chain[chain.length - 1] ?? "default";
18787
18823
  }
18788
18824
  async function listAvailableProfileNames(projectRoot) {
18789
18825
  const entries = await listProfiles(projectRoot);
@@ -18811,6 +18847,7 @@ async function listProfiles(projectRoot) {
18811
18847
  return entries;
18812
18848
  }
18813
18849
  var init_profile = __esm(() => {
18850
+ init_errors();
18814
18851
  init_paths();
18815
18852
  });
18816
18853
 
@@ -18993,7 +19030,8 @@ async function loadConfig(startDir, cliOverrides) {
18993
19030
  let rawConfig = structuredClone(DEFAULT_CONFIG);
18994
19031
  const projDir = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
18995
19032
  const projectRoot = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? dirname(startDir) : startDir : process.cwd();
18996
- const profileName = await resolveProfileName(cliOverrides ?? {}, process.env, projectRoot);
19033
+ const profileChain = await resolveProfileNames(cliOverrides ?? {}, process.env, projectRoot);
19034
+ const overlayChain = profileChain.filter((name) => name && name !== "default");
18997
19035
  const globalConfRaw = await loadJsonFile(globalConfigPath(), "config");
18998
19036
  let logger = null;
18999
19037
  try {
@@ -19012,16 +19050,17 @@ async function loadConfig(startDir, cliOverrides) {
19012
19050
  rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
19013
19051
  }
19014
19052
  }
19015
- if (profileName !== "default") {
19016
- const profileData = await loadProfile(profileName, projectRoot);
19053
+ for (const name of overlayChain) {
19054
+ const profileData = await loadProfile(name, projectRoot);
19017
19055
  rawConfig = deepMergeConfig(rawConfig, profileData);
19018
- await loadProfileEnv(profileName, projectRoot);
19056
+ await loadProfileEnv(name, projectRoot);
19019
19057
  }
19020
19058
  if (cliOverrides) {
19021
19059
  rawConfig = deepMergeConfig(rawConfig, cliOverrides);
19022
19060
  }
19023
- rawConfig.profile = profileName;
19024
- const hasMergedConfigs = globalConfRaw || projDir !== null || cliOverrides !== undefined || profileName !== "default";
19061
+ rawConfig.profile = overlayChain.length > 0 ? overlayChain.join("+") : "default";
19062
+ rawConfig.profileChain = overlayChain;
19063
+ const hasMergedConfigs = globalConfRaw || projDir !== null || cliOverrides !== undefined || overlayChain.length > 0;
19025
19064
  if (!hasMergedConfigs) {
19026
19065
  return structuredClone(DEFAULT_CONFIG);
19027
19066
  }
@@ -19051,11 +19090,14 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19051
19090
  const logger = getLogger();
19052
19091
  const resolvedRootConfigPath = resolve3(rootConfigPath);
19053
19092
  const rootNaxDir = dirname(resolvedRootConfigPath);
19054
- const profileKey = cliOverrides?.profile ?? "";
19093
+ const profileKey = parseProfileList(cliOverrides?.profile).join(",");
19055
19094
  const cacheKey = profileKey ? `${resolvedRootConfigPath}:${profileKey}` : resolvedRootConfigPath;
19056
19095
  let rootConfigPromise = _rootConfigCache.get(cacheKey);
19057
19096
  if (!rootConfigPromise) {
19058
- rootConfigPromise = loadConfig(rootNaxDir, cliOverrides);
19097
+ rootConfigPromise = loadConfig(rootNaxDir, cliOverrides).catch((err) => {
19098
+ _rootConfigCache.delete(cacheKey);
19099
+ throw err;
19100
+ });
19059
19101
  if (_rootConfigCache.size >= ROOT_CONFIG_CACHE_MAX) {
19060
19102
  const firstKey = _rootConfigCache.keys().next().value;
19061
19103
  if (firstKey !== undefined)
@@ -19081,22 +19123,29 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19081
19123
  logger.debug("config", "Per-package config loaded", { packageConfigPath, packageDir });
19082
19124
  const { profile: packageProfile, ...packageFields } = packageOverride;
19083
19125
  let merged = mergePackageConfig(rootConfig, packageFields);
19084
- if (packageProfile && packageProfile !== "default") {
19126
+ const packageChain = parseProfileList(packageProfile).filter((name) => name && name !== "default");
19127
+ if (packageChain.length > 0) {
19085
19128
  const packageRoot = join3(repoRoot, packageDir);
19086
- const profileData = await loadProfile(packageProfile, packageRoot);
19087
- const rawMerged = deepMergeConfig(merged, profileData);
19088
- rawMerged.profile = packageProfile;
19129
+ let rawMerged = merged;
19130
+ for (const name of packageChain) {
19131
+ const profileData = await loadProfile(name, packageRoot);
19132
+ rawMerged = deepMergeConfig(rawMerged, profileData);
19133
+ }
19134
+ rawMerged.profile = packageChain.join("+");
19135
+ rawMerged.profileChain = packageChain;
19089
19136
  rejectLegacyAgentKeys(rawMerged);
19090
19137
  rejectLegacyRectificationKeys(rawMerged);
19091
19138
  const result = NaxConfigSchema.safeParse(rawMerged);
19092
- if (result.success) {
19093
- merged = result.data;
19094
- } else {
19095
- logger.warn("config", "Per-package profile failed validation \u2014 using merged config without profile", {
19096
- packageDir,
19097
- packageProfile
19139
+ if (!result.success) {
19140
+ const errors3 = result.error.issues.map((err) => {
19141
+ const path = String(err.path.join("."));
19142
+ return path ? `${path}: ${err.message}` : err.message;
19098
19143
  });
19144
+ throw new NaxError(`Per-package profile "${packageChain.join("+")}" produced an invalid config for package "${packageDir}":
19145
+ ${errors3.join(`
19146
+ `)}`, "PER_PACKAGE_PROFILE_INVALID", { stage: "config", packageDir, profileChain: packageChain });
19099
19147
  }
19148
+ merged = result.data;
19100
19149
  }
19101
19150
  return merged;
19102
19151
  }
@@ -19504,6 +19553,7 @@ __export(exports_config, {
19504
19553
  routingConfigSelector: () => routingConfigSelector,
19505
19554
  reviewConfigSelector: () => reviewConfigSelector,
19506
19555
  resolveTestStrategy: () => resolveTestStrategy,
19556
+ resolveProfileNames: () => resolveProfileNames,
19507
19557
  resolveProfileName: () => resolveProfileName,
19508
19558
  resolveModelForAgent: () => resolveModelForAgent,
19509
19559
  resolveModel: () => resolveModel,
@@ -19514,9 +19564,11 @@ __export(exports_config, {
19514
19564
  qualityConfigSelector: () => qualityConfigSelector,
19515
19565
  promptLoaderConfigSelector: () => promptLoaderConfigSelector,
19516
19566
  projectConfigDir: () => projectConfigDir,
19567
+ profileOverrideFromConfig: () => profileOverrideFromConfig,
19517
19568
  precheckConfigSelector: () => precheckConfigSelector,
19518
19569
  planConfigSelector: () => planConfigSelector,
19519
19570
  pickSelector: () => pickSelector,
19571
+ parseProfileList: () => parseProfileList,
19520
19572
  mergePackageConfig: () => mergePackageConfig,
19521
19573
  loadProfileEnv: () => loadProfileEnv,
19522
19574
  loadProfile: () => loadProfile,
@@ -21286,12 +21338,13 @@ function parseAcpxJsonLine(line, state) {
21286
21338
  }
21287
21339
  return;
21288
21340
  }
21289
- if (event.content && typeof event.content === "string")
21341
+ if (event.result && typeof event.result === "string") {
21342
+ state.text = event.result;
21343
+ } else if (event.content && typeof event.content === "string") {
21290
21344
  state.text += event.content;
21291
- if (event.text && typeof event.text === "string")
21345
+ } else if (event.text && typeof event.text === "string") {
21292
21346
  state.text += event.text;
21293
- if (event.result && typeof event.result === "string")
21294
- state.text = event.result;
21347
+ }
21295
21348
  if (event.cumulative_token_usage)
21296
21349
  state.tokenUsage = event.cumulative_token_usage;
21297
21350
  if (event.usage) {
@@ -21626,7 +21679,11 @@ class AgentManager {
21626
21679
  _registry;
21627
21680
  _unavailable = new Map;
21628
21681
  _prunedFallback = new Set;
21629
- _emitter = new EventEmitter;
21682
+ _emitter = (() => {
21683
+ const ee = new EventEmitter;
21684
+ ee.setMaxListeners(MAX_EMITTER_LISTENERS);
21685
+ return ee;
21686
+ })();
21630
21687
  _logger;
21631
21688
  _middleware;
21632
21689
  _runId;
@@ -22225,6 +22282,9 @@ class AgentManager {
22225
22282
  throw err;
22226
22283
  }
22227
22284
  }
22285
+ close() {
22286
+ this._emitter.removeAllListeners();
22287
+ }
22228
22288
  _resolveRegistry() {
22229
22289
  this._registry ??= createAgentRegistry(this._config);
22230
22290
  return this._registry;
@@ -22233,7 +22293,7 @@ class AgentManager {
22233
22293
  this._emitter.emit(event, payload);
22234
22294
  }
22235
22295
  }
22236
- var _agentManagerDeps;
22296
+ var MAX_EMITTER_LISTENERS = 100, _agentManagerDeps;
22237
22297
  var init_manager = __esm(() => {
22238
22298
  init_errors();
22239
22299
  init_logger2();
@@ -25144,7 +25204,7 @@ function extractSearchTerms(sourceFile) {
25144
25204
  const basename4 = parts[parts.length - 1];
25145
25205
  return [`/${basename4}`, withoutExt];
25146
25206
  }
25147
- async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
25207
+ async function importGrepFallback(sourceFiles, workdir, testFilePatterns, maxScanFiles = MAX_GREP_TEST_FILES) {
25148
25208
  if (sourceFiles.length === 0 || testFilePatterns.length === 0)
25149
25209
  return [];
25150
25210
  const searchTerms = sourceFiles.flatMap(extractSearchTerms);
@@ -25152,11 +25212,11 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
25152
25212
  outer:
25153
25213
  for (const pattern of testFilePatterns) {
25154
25214
  const g = _bunDeps.glob(pattern);
25155
- for await (const file3 of g.scan(workdir)) {
25215
+ for await (const file3 of g.scan({ cwd: workdir, absolute: false })) {
25156
25216
  testFilePaths.push(`${workdir}/${file3}`);
25157
- if (testFilePaths.length >= MAX_GREP_TEST_FILES) {
25217
+ if (testFilePaths.length >= maxScanFiles) {
25158
25218
  getSafeLogger()?.debug("smart-runner", "import-grep glob cap reached \u2014 results truncated", {
25159
- cap: MAX_GREP_TEST_FILES
25219
+ cap: maxScanFiles
25160
25220
  });
25161
25221
  break outer;
25162
25222
  }
@@ -25404,7 +25464,7 @@ async function selectScopedTests(input) {
25404
25464
  if (smartCfg.fallback !== "import-grep") {
25405
25465
  return fullSuite();
25406
25466
  }
25407
- const pass2Files = await _scopedSelectionDeps.importGrepFallback(nonTestFiles, input.workdir, mappingGlobs);
25467
+ const pass2Files = await _scopedSelectionDeps.importGrepFallback(nonTestFiles, input.workdir, mappingGlobs, smartCfg.maxScanFiles);
25408
25468
  if (pass2Files.length > threshold) {
25409
25469
  logger.warn("verify[scoped]", `Scoped test file count ${pass2Files.length} exceeds threshold ${threshold} \u2014 falling back to full suite`, { storyId: input.storyId });
25410
25470
  return fullSuite({ scopeTestFallback: true, thresholdFallback: true });
@@ -25425,7 +25485,8 @@ var init_scoped_selection = __esm(() => {
25425
25485
  DEFAULT_SMART_RUNNER_CONFIG = {
25426
25486
  enabled: true,
25427
25487
  testFilePatterns: [...DEFAULT_TEST_FILE_PATTERNS],
25428
- fallback: "import-grep"
25488
+ fallback: "import-grep",
25489
+ maxScanFiles: MAX_GREP_TEST_FILES
25429
25490
  };
25430
25491
  _scopedSelectionDeps = {
25431
25492
  getChangedNonTestFiles: _smartRunnerDeps.getChangedNonTestFiles,
@@ -27573,7 +27634,7 @@ class SessionScratchProvider {
27573
27634
  if (!dirs || dirs.length === 0) {
27574
27635
  return { chunks: [], pullTools: [] };
27575
27636
  }
27576
- const ignoreMatchers = await resolveNaxIgnorePatterns(request.repoRoot, request.packageDir);
27637
+ const ignoreMatchers = request.naxIgnoreIndex?.getMatchers(request.packageDir) ?? await resolveNaxIgnorePatterns(request.repoRoot, request.packageDir);
27577
27638
  const chunks = [];
27578
27639
  for (const dir of dirs) {
27579
27640
  const chunk = await readScratchDir(dir, request.agentId, ignoreMatchers);
@@ -28553,6 +28614,7 @@ async function scanTestFiles(options) {
28553
28614
  const patterns = deriveTestPatterns(contextFiles, resolvedTestGlobs);
28554
28615
  allowedBasenames = new Set(patterns);
28555
28616
  }
28617
+ const maxScanFiles = options.maxScanFiles ?? DEFAULT_MAX_SCAN_FILES;
28556
28618
  const glob = new Glob2(testPattern);
28557
28619
  const files = [];
28558
28620
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
@@ -28562,6 +28624,13 @@ async function scanTestFiles(options) {
28562
28624
  continue;
28563
28625
  }
28564
28626
  }
28627
+ if (files.length >= maxScanFiles) {
28628
+ getLogger().debug("test-scanner", "Glob cap reached \u2014 results truncated", {
28629
+ cap: maxScanFiles,
28630
+ scanDir
28631
+ });
28632
+ break;
28633
+ }
28565
28634
  const fullPath = path.join(scanDir, filePath);
28566
28635
  try {
28567
28636
  const source = await Bun.file(fullPath).text();
@@ -28658,6 +28727,7 @@ async function generateTestCoverageSummary(options) {
28658
28727
  const tokens = estimateTokens4(summary);
28659
28728
  return { files, totalTests, summary, tokens };
28660
28729
  }
28730
+ var DEFAULT_MAX_SCAN_FILES = 200;
28661
28731
  var init_test_scanner = __esm(() => {
28662
28732
  init_logger2();
28663
28733
  init_conventions();
@@ -28692,12 +28762,14 @@ class TestCoverageProvider {
28692
28762
  const resolved = await _testCoverageProviderDeps.resolveTestFilePatterns(this.config, request.repoRoot, relPackageDir);
28693
28763
  const contextFiles = _testCoverageProviderDeps.getContextFiles(this.story);
28694
28764
  const globs = resolved.patterns ?? resolved.globs;
28765
+ const smartCfg = coerceSmartRunner(this.config.execution?.smartTestRunner);
28695
28766
  const scanOptions = {
28696
28767
  workdir: request.packageDir,
28697
28768
  testDir: tcConfig.testDir,
28698
28769
  maxTokens: tcConfig.maxTokens ?? 500,
28699
28770
  detail: tcConfig.detail ?? "names-and-counts",
28700
28771
  scopeToStory: tcConfig.scopeToStory ?? true,
28772
+ maxScanFiles: smartCfg.maxScanFiles,
28701
28773
  contextFiles,
28702
28774
  resolvedTestGlobs: globs
28703
28775
  };
@@ -28729,6 +28801,7 @@ class TestCoverageProvider {
28729
28801
  }
28730
28802
  var _testCoverageProviderDeps;
28731
28803
  var init_test_coverage = __esm(() => {
28804
+ init_test_runners();
28732
28805
  init_logger2();
28733
28806
  init_prd();
28734
28807
  init_resolver();
@@ -30026,10 +30099,9 @@ async function gitLsFiles2(workdir) {
30026
30099
  stdout: "pipe",
30027
30100
  stderr: "pipe"
30028
30101
  });
30029
- const exitCode = await proc.exited;
30102
+ const [exitCode, output] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
30030
30103
  if (exitCode !== 0)
30031
30104
  return null;
30032
- const output = await new Response(proc.stdout).text();
30033
30105
  return output.split(`
30034
30106
  `).filter(Boolean);
30035
30107
  } catch {
@@ -32092,6 +32164,19 @@ ${STEP3_SHARED_RULES}
32092
32164
  - **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.
32093
32165
  - **Path anchor (CRITICAL \u2014 do NOT deviate)**: Write the test file to this exact path: \`${p.targetTestFilePath}\`. This path is intentional and computed by the orchestrator \u2014 do not change it based on what you observe in the project. In particular: if you see a \`.nax/features/\` directory at the repo root, that is for stories scoped to the repo root. When a story belongs to a specific package (e.g. \`packages/core\`), its acceptance test lives inside that package's \`.nax/features/\` directory so the test runner can resolve the package's imports correctly. The package root is 3 levels above the test file (\`../../../\` relative to the test file).
32094
32166
  - **Process cwd**: When spawning child processes to invoke a CLI or binary, set the working directory to the **package root** (\`join(import.meta.dir, "../../..")\`) as your default \u2014 unless your Step 2 exploration reveals the CLI uses a different working directory convention (e.g. reads config from \`~/.config/\`, or resolves paths relative to a flag value). Always check how the CLI resolves file paths before assuming.${implSection}`;
32167
+ }
32168
+ buildPathCorrection(targetTestFilePath) {
32169
+ return `The acceptance test file was NOT found at the required path. You likely wrote it to a different filename or directory (for example, by renaming a dotfile or replacing dashes with underscores).
32170
+
32171
+ Move (or re-write) the acceptance test you just created so it lives at EXACTLY this path:
32172
+ ${targetTestFilePath}
32173
+
32174
+ Requirements:
32175
+ - The file must be at that exact path \u2014 same directory and same filename, including any leading dot and dashes. Do NOT sanitize, rename, or relocate it.
32176
+ - Preserve the test content you already wrote. Do not regenerate, weaken, or stub the assertions.
32177
+ - If you wrote it somewhere else, delete the misplaced copy after moving it so only the canonical path remains.
32178
+
32179
+ After writing the file to the exact path above, reply with a brief confirmation only.`;
32095
32180
  }
32096
32181
  buildGeneratorFromSpecPrompt(p) {
32097
32182
  return `You are a senior test engineer. Your task is to generate a complete acceptance test file for the "${p.featureName}" feature.
@@ -33154,12 +33239,16 @@ async function collectDiff(workdir, storyGitRef, excludePatterns, options) {
33154
33239
  stdout: "pipe",
33155
33240
  stderr: "pipe"
33156
33241
  });
33157
- const [exitCode, stdout] = await Promise.all([
33242
+ const [exitCode, stdout, stderr] = await Promise.all([
33158
33243
  proc.exited,
33159
33244
  new Response(proc.stdout).text(),
33160
33245
  new Response(proc.stderr).text()
33161
33246
  ]);
33162
- return exitCode === 0 ? stdout : "";
33247
+ if (exitCode !== 0) {
33248
+ getSafeLogger()?.warn("diff-utils", "git diff failed \u2014 skipping review diff", { storyGitRef, stderr });
33249
+ return null;
33250
+ }
33251
+ return stdout;
33163
33252
  }
33164
33253
  async function collectDiffStat(workdir, storyGitRef, options) {
33165
33254
  const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
@@ -33410,6 +33499,9 @@ async function prepareSemanticReviewInput(args) {
33410
33499
  return { effectiveRef, stat, diff: undefined, excludePatterns };
33411
33500
  }
33412
33501
  const rawDiff = await collectDiff(workdir, effectiveRef, excludePatterns, { naxIgnoreIndex, packageDir });
33502
+ if (rawDiff === null) {
33503
+ return { effectiveRef, stat, diff: undefined, excludePatterns, skipReason: "git diff failed" };
33504
+ }
33413
33505
  const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat : undefined);
33414
33506
  if (!diff) {
33415
33507
  return { effectiveRef, stat, diff: undefined, excludePatterns, skipReason: "no production code changes" };
@@ -34759,6 +34851,40 @@ ${outputFormat}`, overridable: false }
34759
34851
  };
34760
34852
  });
34761
34853
 
34854
+ // src/operations/self-heal.ts
34855
+ function makeSelfHealStep(spec) {
34856
+ return {
34857
+ async run(ctx) {
34858
+ const deviations = await spec.detect(ctx.input);
34859
+ if (deviations.length === 0)
34860
+ return null;
34861
+ if (spec.log) {
34862
+ getSafeLogger()?.info(spec.log.kind, spec.log.message, spec.log.meta?.(ctx.input, deviations) ?? {});
34863
+ }
34864
+ return ctx.send(spec.buildRepair(deviations, ctx.input));
34865
+ }
34866
+ };
34867
+ }
34868
+ async function runSelfHealChain(ctx, seed, steps) {
34869
+ let last = seed;
34870
+ let totalCost = seed.estimatedCostUsd ?? 0;
34871
+ for (const step of steps) {
34872
+ try {
34873
+ const turn = await step.run(ctx);
34874
+ if (turn) {
34875
+ totalCost += turn.estimatedCostUsd ?? 0;
34876
+ last = turn;
34877
+ }
34878
+ } catch (err) {
34879
+ getSafeLogger()?.warn("self-heal", "step threw \u2014 skipping", { error: errorMessage(err) });
34880
+ }
34881
+ }
34882
+ return { ...last, estimatedCostUsd: totalCost };
34883
+ }
34884
+ var init_self_heal = __esm(() => {
34885
+ init_logger2();
34886
+ });
34887
+
34762
34888
  // src/operations/plan-refine.ts
34763
34889
  import { join as join21 } from "path";
34764
34890
  function hasToken(text, tokens) {
@@ -34897,6 +35023,28 @@ async function normalizeCreatedContextFiles(prd, workdir, fileExists) {
34897
35023
  return prd;
34898
35024
  return { ...prd, userStories: results.map((r) => r.story) };
34899
35025
  }
35026
+ function verbatimSelfHealStep(builder) {
35027
+ return makeSelfHealStep({
35028
+ detect: (input) => readMissingVerbatimAcs(input),
35029
+ buildRepair: (missing, input) => builder.buildVerbatimRepair(missing, input.outputPath),
35030
+ log: {
35031
+ kind: "plan",
35032
+ message: "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn",
35033
+ meta: (input, missing) => ({ featureName: input.featureName, missingCount: missing.length })
35034
+ }
35035
+ });
35036
+ }
35037
+ function specDriftSelfHealStep(builder) {
35038
+ return makeSelfHealStep({
35039
+ detect: (input) => readSpecDriftViolations(input),
35040
+ buildRepair: (drifted, input) => builder.buildSpecDriftRepair(drifted, input.outputPath),
35041
+ log: {
35042
+ kind: "plan",
35043
+ message: "specGuard: spec-drift violations found \u2014 issuing one repair turn",
35044
+ meta: (input, drifted) => ({ featureName: input.featureName, violationCount: drifted.length })
35045
+ }
35046
+ });
35047
+ }
34900
35048
  var _planRefineDeps, NEGATIVE_PATH_TOKENS, planRefineOp;
34901
35049
  var init_plan_refine = __esm(() => {
34902
35050
  init_retry();
@@ -34906,6 +35054,7 @@ var init_plan_refine = __esm(() => {
34906
35054
  init_prd();
34907
35055
  init_schema2();
34908
35056
  init_prompts();
35057
+ init_self_heal();
34909
35058
  init_verbatim_warn();
34910
35059
  _planRefineDeps = {
34911
35060
  readFile: async (path3) => {
@@ -34985,31 +35134,15 @@ ${outputFormat}`,
34985
35134
  const specGuard = ctx.input.specGuard ?? false;
34986
35135
  const turn1 = await ctx.sendWithParseRetry(initialPrompt);
34987
35136
  const turn2 = await ctx.send(builder.buildRefineContinuation(ctx.input.outputPath, specGuard));
34988
- let totalCost = (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0);
34989
- let last = turn2;
34990
- const missing = await readMissingVerbatimAcs(ctx.input);
34991
- if (missing.length > 0) {
34992
- getSafeLogger()?.info("plan", "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn", {
34993
- featureName: ctx.input.featureName,
34994
- missingCount: missing.length
34995
- });
34996
- const turn3 = await ctx.send(builder.buildVerbatimRepair(missing, ctx.input.outputPath));
34997
- totalCost += turn3.estimatedCostUsd ?? 0;
34998
- last = turn3;
34999
- }
35000
- if (specGuard) {
35001
- const drifted = await readSpecDriftViolations(ctx.input);
35002
- if (drifted.length > 0) {
35003
- getSafeLogger()?.info("plan", "specGuard: spec-drift violations found \u2014 issuing one repair turn", {
35004
- featureName: ctx.input.featureName,
35005
- violationCount: drifted.length
35006
- });
35007
- const turn4 = await ctx.send(builder.buildSpecDriftRepair(drifted, ctx.input.outputPath));
35008
- totalCost += turn4.estimatedCostUsd ?? 0;
35009
- last = turn4;
35010
- }
35011
- }
35012
- return { ...last, estimatedCostUsd: totalCost };
35137
+ const seed = {
35138
+ ...turn2,
35139
+ estimatedCostUsd: (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0)
35140
+ };
35141
+ const steps = [
35142
+ verbatimSelfHealStep(builder),
35143
+ ...specGuard ? [specDriftSelfHealStep(builder)] : []
35144
+ ];
35145
+ return runSelfHealChain(ctx, seed, steps);
35013
35146
  },
35014
35147
  parse(output, input) {
35015
35148
  return validatePlanOutput(output, input.featureName, input.branchName);
@@ -36127,11 +36260,32 @@ function isStubTestContent(content) {
36127
36260
  }
36128
36261
 
36129
36262
  // src/operations/acceptance-generate.ts
36130
- var acceptanceGenerateOp;
36263
+ function pathCorrectionStep() {
36264
+ return makeSelfHealStep({
36265
+ detect: async (input) => await _acceptanceGenerateDeps.fileExists(input.targetTestFilePath) ? [] : [input.targetTestFilePath],
36266
+ buildRepair: (_deviations, input) => new AcceptancePromptBuilder().buildPathCorrection(input.targetTestFilePath),
36267
+ log: {
36268
+ kind: "acceptance",
36269
+ message: "Acceptance test not found at target path \u2014 issuing one corrective turn",
36270
+ meta: (input) => ({ targetTestFilePath: input.targetTestFilePath })
36271
+ }
36272
+ });
36273
+ }
36274
+ var _acceptanceGenerateDeps, acceptanceGenerateOp;
36131
36275
  var init_acceptance_generate = __esm(() => {
36132
36276
  init_generator();
36133
36277
  init_config();
36134
36278
  init_prompts();
36279
+ init_self_heal();
36280
+ _acceptanceGenerateDeps = {
36281
+ fileExists: async (path4) => {
36282
+ try {
36283
+ return await Bun.file(path4).exists();
36284
+ } catch {
36285
+ return false;
36286
+ }
36287
+ }
36288
+ };
36135
36289
  acceptanceGenerateOp = {
36136
36290
  kind: "run",
36137
36291
  name: "acceptance-generate",
@@ -36153,6 +36307,10 @@ var init_acceptance_generate = __esm(() => {
36153
36307
  task: { id: "task", content: prompt, overridable: false }
36154
36308
  };
36155
36309
  },
36310
+ async hopBody(initialPrompt, ctx) {
36311
+ const turn1 = await ctx.sendWithParseRetry(initialPrompt);
36312
+ return runSelfHealChain(ctx, turn1, [pathCorrectionStep()]);
36313
+ },
36156
36314
  parse(output, _input, _ctx) {
36157
36315
  return { testCode: extractTestCode(output) };
36158
36316
  },
@@ -40163,6 +40321,7 @@ var init_operations = __esm(() => {
40163
40321
  init_call();
40164
40322
  init_plan();
40165
40323
  init_plan_refine();
40324
+ init_self_heal();
40166
40325
  init_verbatim_warn();
40167
40326
  init_decompose2();
40168
40327
  init_build_hop_callback();
@@ -40970,10 +41129,9 @@ async function listChangedFiles(workdir, baseRef) {
40970
41129
  stdout: "pipe",
40971
41130
  stderr: "pipe"
40972
41131
  });
40973
- const exitCode = await proc.exited;
41132
+ const [exitCode, output] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
40974
41133
  if (exitCode !== 0)
40975
41134
  return null;
40976
- const output = await new Response(proc.stdout).text();
40977
41135
  return output.split(`
40978
41136
  `).map((line) => line.trim()).filter(Boolean).map(normalizePath3);
40979
41137
  }
@@ -45646,6 +45804,8 @@ class PidRegistry {
45646
45804
  pidsFilePath;
45647
45805
  pids = new Set;
45648
45806
  frozen = false;
45807
+ _writing = false;
45808
+ _pendingWrite = false;
45649
45809
  writeQueueTail = Promise.resolve();
45650
45810
  constructor(workdir, _platform) {
45651
45811
  this.workdir = workdir;
@@ -45943,11 +46103,22 @@ class PidRegistry {
45943
46103
  }
45944
46104
  }
45945
46105
  enqueueWrite() {
45946
- this.writeQueueTail = this.writeQueueTail.then(() => this.writePidsFile().catch((err) => {
46106
+ if (this._writing) {
46107
+ this._pendingWrite = true;
46108
+ return this.writeQueueTail;
46109
+ }
46110
+ this._writing = true;
46111
+ this.writeQueueTail = this.writePidsFile().catch((err) => {
45947
46112
  getSafeLogger()?.warn("pid-registry", "Failed to flush PID file \u2014 on-disk registry may be stale", {
45948
46113
  error: errorMessage(err)
45949
46114
  });
45950
- }));
46115
+ }).then(async () => {
46116
+ this._writing = false;
46117
+ if (this._pendingWrite) {
46118
+ this._pendingWrite = false;
46119
+ await this.enqueueWrite();
46120
+ }
46121
+ });
45951
46122
  return this.writeQueueTail;
45952
46123
  }
45953
46124
  }
@@ -46509,6 +46680,7 @@ class SessionManager {
46509
46680
  }
46510
46681
  this._busySessions.delete(handle.id);
46511
46682
  this._cancelledSessions.delete(handle.id);
46683
+ this._clearWatchdogCancelledCalls(handle.id);
46512
46684
  }
46513
46685
  async sendPrompt(handle, prompt, opts) {
46514
46686
  if (this._cancelledSessions.has(handle.id)) {
@@ -46593,6 +46765,10 @@ class SessionManager {
46593
46765
  sweepOrphans(ttlMs = DEFAULT_ORPHAN_TTL_MS) {
46594
46766
  return sweepOrphansImpl(this._sessions, ttlMs);
46595
46767
  }
46768
+ close() {
46769
+ this._agentStreamUnsubscribe?.();
46770
+ this._agentStreamUnsubscribe = undefined;
46771
+ }
46596
46772
  }
46597
46773
  var NULL_PROTOCOL_IDS;
46598
46774
  var init_manager2 = __esm(() => {
@@ -46857,6 +47033,9 @@ function createRuntime(config2, workdir, opts) {
46857
47033
  if (opts?.parentSignal && parentAbortHandler) {
46858
47034
  opts.parentSignal.removeEventListener("abort", parentAbortHandler);
46859
47035
  }
47036
+ agentManager.close();
47037
+ if (sessionManager instanceof SessionManager)
47038
+ sessionManager.close();
46860
47039
  const results = await Promise.allSettled([promptAuditor.flush(), reviewAuditor.flush(), costAggregator.drain()]);
46861
47040
  for (const r of results) {
46862
47041
  if (r.status === "rejected") {
@@ -52395,17 +52574,18 @@ async function saveRunMetrics(outputDir, runMetrics) {
52395
52574
  }
52396
52575
  }
52397
52576
  const hasTokenData = totalInputTokens > 0 || totalOutputTokens > 0 || totalCacheReadInputTokens > 0 || totalCacheCreationInputTokens > 0;
52398
- if (hasTokenData) {
52399
- runMetrics.totalTokens = new TokenUsage({
52577
+ const finalMetrics = hasTokenData ? {
52578
+ ...runMetrics,
52579
+ totalTokens: new TokenUsage({
52400
52580
  inputTokens: totalInputTokens,
52401
52581
  outputTokens: totalOutputTokens,
52402
52582
  cacheReadInputTokens: totalCacheReadInputTokens,
52403
52583
  cacheCreationInputTokens: totalCacheCreationInputTokens
52404
- });
52405
- }
52584
+ })
52585
+ } : runMetrics;
52406
52586
  const existing = await loadJsonFile(metricsPath, "metrics");
52407
52587
  const allMetrics = Array.isArray(existing) ? existing : [];
52408
- allMetrics.push(runMetrics);
52588
+ allMetrics.push(finalMetrics);
52409
52589
  await saveJsonFile(metricsPath, allMetrics, "metrics");
52410
52590
  }
52411
52591
  async function loadRunMetrics(outputDir) {
@@ -54572,29 +54752,27 @@ async function rollbackToRef(workdir, ref) {
54572
54752
  stdout: "pipe",
54573
54753
  stderr: "pipe"
54574
54754
  });
54575
- const exitCode = await resetProc.exited;
54755
+ const [exitCode, resetStderr] = await Promise.all([resetProc.exited, new Response(resetProc.stderr).text()]);
54576
54756
  if (exitCode !== 0) {
54577
- const stderr = await new Response(resetProc.stderr).text();
54578
- logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
54579
- throw new Error(`Git rollback failed: ${stderr}`);
54757
+ logger.error("tdd", "Failed to rollback git changes", { ref, stderr: resetStderr });
54758
+ throw new Error(`Git rollback failed: ${resetStderr}`);
54580
54759
  }
54581
54760
  const cleanProc = _rollbackDeps.spawn(["git", "clean", "-fd"], {
54582
54761
  cwd: workdir,
54583
54762
  stdout: "pipe",
54584
54763
  stderr: "pipe"
54585
54764
  });
54586
- const cleanExitCode = await cleanProc.exited;
54765
+ const [cleanExitCode, cleanStderr] = await Promise.all([cleanProc.exited, new Response(cleanProc.stderr).text()]);
54587
54766
  if (cleanExitCode !== 0) {
54588
- const stderr = await new Response(cleanProc.stderr).text();
54589
- logger.warn("tdd", "Failed to clean untracked files", { stderr });
54767
+ logger.warn("tdd", "Failed to clean untracked files", { stderr: cleanStderr });
54590
54768
  }
54591
54769
  logger.info("tdd", "Successfully rolled back git changes", { ref });
54592
54770
  }
54593
54771
  async function captureSnapshotRef(workdir, storyId) {
54594
54772
  await _rollbackDeps.autoCommitIfDirty(workdir, "non-blocking-fix-snapshot", "snapshot", storyId);
54595
54773
  const proc = _rollbackDeps.spawn(["git", "rev-parse", "HEAD"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
54596
- const sha = (await new Response(proc.stdout).text()).trim();
54597
- const exitCode = await proc.exited;
54774
+ const [exitCode, shaRaw] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
54775
+ const sha = shaRaw.trim();
54598
54776
  if (exitCode !== 0) {
54599
54777
  throw new NaxError("git rev-parse HEAD failed in non-blocking-fix snapshot", "SNAPSHOT_REF_FAILED", {
54600
54778
  storyId,
@@ -55584,7 +55762,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55584
55762
  if (pkgQuality?.commands?.formatFix || pkgQuality?.commands?.formatFixScoped) {
55585
55763
  strategies.push(makeMechanicalFormatFixStrategy());
55586
55764
  }
55587
- if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
55765
+ const fullSuiteGatePhasePresent = Boolean(inputs.fullSuiteGate) && (isThreeSession || regressionMode === "per-story");
55766
+ const verifyScopedPhasePresent = !isThreeSession && Boolean(inputs.verifyScoped);
55767
+ if (fullSuiteGatePhasePresent || verifyScopedPhasePresent) {
55588
55768
  strategies.push(makeFullSuiteRectifyStrategy(story, config2, sink));
55589
55769
  }
55590
55770
  if (config2.quality.autofix?.enabled !== false) {
@@ -60245,7 +60425,7 @@ var package_default;
60245
60425
  var init_package = __esm(() => {
60246
60426
  package_default = {
60247
60427
  name: "@nathapp/nax",
60248
- version: "0.70.0-canary.3",
60428
+ version: "0.70.0-canary.5",
60249
60429
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60250
60430
  type: "module",
60251
60431
  bin: {
@@ -60340,8 +60520,8 @@ var init_version = __esm(() => {
60340
60520
  NAX_VERSION = package_default.version;
60341
60521
  NAX_COMMIT = (() => {
60342
60522
  try {
60343
- if (/^[0-9a-f]{6,10}$/.test("905b80cf"))
60344
- return "905b80cf";
60523
+ if (/^[0-9a-f]{6,10}$/.test("e8c2ab46"))
60524
+ return "e8c2ab46";
60345
60525
  } catch {}
60346
60526
  try {
60347
60527
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -60361,11 +60541,11 @@ var init_version = __esm(() => {
60361
60541
 
60362
60542
  // src/execution/crash-heartbeat.ts
60363
60543
  import { appendFileSync as appendFileSync2 } from "fs";
60364
- async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60544
+ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60365
60545
  const logger = _heartbeatDeps.getSafeLogger();
60366
- while (heartbeatActive) {
60546
+ while (gen === _heartbeatGen && _heartbeatActive) {
60367
60547
  await _heartbeatDeps.sleep(60000);
60368
- if (!heartbeatActive)
60548
+ if (gen !== _heartbeatGen || !_heartbeatActive)
60369
60549
  break;
60370
60550
  try {
60371
60551
  logger?.debug("crash-recovery", "Heartbeat");
@@ -60394,9 +60574,9 @@ async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFil
60394
60574
  }
60395
60575
  function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60396
60576
  const logger = _heartbeatDeps.getSafeLogger();
60397
- stopHeartbeat();
60398
- heartbeatActive = true;
60399
- heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
60577
+ _heartbeatActive = true;
60578
+ const gen = ++_heartbeatGen;
60579
+ heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
60400
60580
  _heartbeatDeps.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
60401
60581
  error: err instanceof Error ? err.message : String(err)
60402
60582
  });
@@ -60404,12 +60584,13 @@ function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath
60404
60584
  logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
60405
60585
  }
60406
60586
  function stopHeartbeat() {
60407
- if (heartbeatActive) {
60408
- heartbeatActive = false;
60587
+ if (_heartbeatActive) {
60588
+ _heartbeatActive = false;
60589
+ _heartbeatGen++;
60409
60590
  getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
60410
60591
  }
60411
60592
  }
60412
- var _heartbeatDeps, heartbeatActive = false;
60593
+ var _heartbeatDeps, _heartbeatGen = 0, _heartbeatActive = false;
60413
60594
  var init_crash_heartbeat = __esm(() => {
60414
60595
  init_logger2();
60415
60596
  _heartbeatDeps = {
@@ -62576,9 +62757,8 @@ ${missing.join(`
62576
62757
  stdout: "pipe",
62577
62758
  stderr: "pipe"
62578
62759
  });
62579
- const exitCode = await proc.exited;
62760
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62580
62761
  if (exitCode !== 0) {
62581
- const stderr = await new Response(proc.stderr).text();
62582
62762
  throw new Error(`Failed to create worktree: ${stderr || "unknown error"}`);
62583
62763
  }
62584
62764
  } catch (error48) {
@@ -62611,9 +62791,8 @@ ${missing.join(`
62611
62791
  stdout: "pipe",
62612
62792
  stderr: "pipe"
62613
62793
  });
62614
- const exitCode = await proc.exited;
62794
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62615
62795
  if (exitCode !== 0) {
62616
- const stderr = await new Response(proc.stderr).text();
62617
62796
  if (stderr.includes("not found") || stderr.includes("does not exist") || stderr.includes("no such worktree") || stderr.includes("is not a working tree")) {
62618
62797
  throw new Error(`Worktree not found: ${worktreePath}`);
62619
62798
  }
@@ -62631,9 +62810,8 @@ ${missing.join(`
62631
62810
  stdout: "pipe",
62632
62811
  stderr: "pipe"
62633
62812
  });
62634
- const exitCode = await proc.exited;
62813
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62635
62814
  if (exitCode !== 0) {
62636
- const stderr = await new Response(proc.stderr).text();
62637
62815
  if (!stderr.includes("not found")) {
62638
62816
  const logger = getSafeLogger();
62639
62817
  logger?.warn("worktree", `Failed to delete branch ${branchName}`, { stderr });
@@ -62653,12 +62831,14 @@ ${missing.join(`
62653
62831
  stdout: "pipe",
62654
62832
  stderr: "pipe"
62655
62833
  });
62656
- const exitCode = await proc.exited;
62834
+ const [exitCode, stderr, stdout] = await Promise.all([
62835
+ proc.exited,
62836
+ new Response(proc.stderr).text(),
62837
+ new Response(proc.stdout).text()
62838
+ ]);
62657
62839
  if (exitCode !== 0) {
62658
- const stderr = await new Response(proc.stderr).text();
62659
62840
  throw new Error(`Failed to list worktrees: ${stderr || "unknown error"}`);
62660
62841
  }
62661
- const stdout = await new Response(proc.stdout).text();
62662
62842
  return this.parseWorktreeList(stdout);
62663
62843
  } catch (error48) {
62664
62844
  if (error48 instanceof Error) {
@@ -62770,9 +62950,11 @@ class MergeEngine {
62770
62950
  stdout: "pipe",
62771
62951
  stderr: "pipe"
62772
62952
  });
62773
- const exitCode = await mergeProc.exited;
62774
- const stderr = await new Response(mergeProc.stderr).text();
62775
- const stdout = await new Response(mergeProc.stdout).text();
62953
+ const [exitCode, stderr, stdout] = await Promise.all([
62954
+ mergeProc.exited,
62955
+ new Response(mergeProc.stderr).text(),
62956
+ new Response(mergeProc.stdout).text()
62957
+ ]);
62776
62958
  if (exitCode === 0) {
62777
62959
  try {
62778
62960
  await this.worktreeManager.remove(projectRoot, storyId);
@@ -62899,19 +63081,25 @@ ${stderr}`;
62899
63081
  stdout: "pipe",
62900
63082
  stderr: "pipe"
62901
63083
  });
62902
- const exitCode = await currentBranchProc.exited;
63084
+ const [exitCode, currentBranchRaw] = await Promise.all([
63085
+ currentBranchProc.exited,
63086
+ new Response(currentBranchProc.stdout).text()
63087
+ ]);
62903
63088
  if (exitCode !== 0) {
62904
63089
  throw new Error("Failed to get current branch");
62905
63090
  }
62906
- const currentBranch = (await new Response(currentBranchProc.stdout).text()).trim();
63091
+ const currentBranch = currentBranchRaw.trim();
62907
63092
  const rebaseProc = _mergeDeps.spawn(["git", "rebase", currentBranch], {
62908
63093
  cwd: worktreePath,
62909
63094
  stdout: "pipe",
62910
63095
  stderr: "pipe"
62911
63096
  });
62912
- const rebaseExitCode = await rebaseProc.exited;
63097
+ const [rebaseExitCode, rebaseStderr] = await Promise.all([
63098
+ rebaseProc.exited,
63099
+ new Response(rebaseProc.stderr).text()
63100
+ ]);
62913
63101
  if (rebaseExitCode !== 0) {
62914
- const stderr = await new Response(rebaseProc.stderr).text();
63102
+ const stderr = rebaseStderr;
62915
63103
  const abortProc = _mergeDeps.spawn(["git", "rebase", "--abort"], {
62916
63104
  cwd: worktreePath,
62917
63105
  stdout: "pipe",
@@ -62934,11 +63122,10 @@ ${stderr}`;
62934
63122
  stdout: "pipe",
62935
63123
  stderr: "pipe"
62936
63124
  });
62937
- const exitCode = await proc.exited;
63125
+ const [exitCode, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
62938
63126
  if (exitCode !== 0) {
62939
63127
  return [];
62940
63128
  }
62941
- const stdout = await new Response(proc.stdout).text();
62942
63129
  return stdout.trim().split(`
62943
63130
  `).filter((line) => line.length > 0);
62944
63131
  } catch {
@@ -63678,7 +63865,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
63678
63865
  }
63679
63866
  }
63680
63867
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
63681
- const profileOverride = ctx.config.profile && ctx.config.profile !== "default" ? { profile: ctx.config.profile } : undefined;
63868
+ const profileOverride = profileOverrideFromConfig(ctx.config);
63682
63869
  const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join81(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
63683
63870
  let dependencyContext;
63684
63871
  if (ctx.config.execution.storyIsolation === "worktree") {
@@ -63824,6 +64011,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
63824
64011
  }
63825
64012
  var _iterationRunnerDeps;
63826
64013
  var init_iteration_runner = __esm(() => {
64014
+ init_config();
63827
64015
  init_loader();
63828
64016
  init_logger2();
63829
64017
  init_runner4();
@@ -64054,7 +64242,7 @@ async function runParallelBatch(options) {
64054
64242
  worktreePaths.set(story.id, path22.join(workdir, ".nax-wt", story.id));
64055
64243
  }
64056
64244
  const rootConfigPath = path22.join(workdir, ".nax", "config.json");
64057
- const profileOverride = config2.profile && config2.profile !== "default" ? { profile: config2.profile } : undefined;
64245
+ const profileOverride = profileOverrideFromConfig(config2);
64058
64246
  const storyEffectiveConfigs = new Map;
64059
64247
  const configResults = await Promise.allSettled(stories.filter((story) => story.workdir).map(async (story) => {
64060
64248
  try {
@@ -64215,6 +64403,7 @@ async function runParallelBatch(options) {
64215
64403
  }
64216
64404
  var _parallelBatchDeps;
64217
64405
  var init_parallel_batch = __esm(() => {
64406
+ init_config();
64218
64407
  init_loader();
64219
64408
  init_logger2();
64220
64409
  init_dependencies();
@@ -64281,13 +64470,15 @@ async function executeUnified(ctx, initialPrd) {
64281
64470
  };
64282
64471
  for (const fn of _prevRunUnsubscribers)
64283
64472
  fn();
64284
- _prevRunUnsubscribers = [
64473
+ _prevRunUnsubscribers = [];
64474
+ const thisRunUnsubscribers = [
64285
64475
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature),
64286
64476
  wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime),
64287
64477
  wireInteraction(pipelineEventBus, ctx.interactionChain, ctx.config),
64288
64478
  wireEventsWriter(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir),
64289
64479
  wireRegistry(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir, ctx.runtime.outputDir)
64290
64480
  ];
64481
+ _prevRunUnsubscribers = thisRunUnsubscribers;
64291
64482
  pipelineEventBus.emit({
64292
64483
  type: "run:started",
64293
64484
  feature: ctx.feature,
@@ -64304,6 +64495,7 @@ async function executeUnified(ctx, initialPrd) {
64304
64495
  deferredReview
64305
64496
  });
64306
64497
  startHeartbeat(ctx.statusWriter, () => totalCost, () => iterations, ctx.logFilePath);
64498
+ let _executeThrew = false;
64307
64499
  try {
64308
64500
  if (isComplete(prd)) {
64309
64501
  logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
@@ -64710,7 +64902,16 @@ async function executeUnified(ctx, initialPrd) {
64710
64902
  }, ctx.eventEmitter);
64711
64903
  }
64712
64904
  return buildResult2("max-iterations");
64713
- } finally {}
64905
+ } catch (err) {
64906
+ _executeThrew = true;
64907
+ throw err;
64908
+ } finally {
64909
+ if (_executeThrew && _prevRunUnsubscribers === thisRunUnsubscribers) {
64910
+ for (const fn of thisRunUnsubscribers)
64911
+ fn();
64912
+ _prevRunUnsubscribers = [];
64913
+ }
64914
+ }
64714
64915
  }
64715
64916
  function reconcileBatchOutcome(prd, batchResult) {
64716
64917
  for (const story of batchResult.completed) {
@@ -65802,6 +66003,7 @@ async function setupRun(options) {
65802
66003
  const resolvedPatterns = await resolveTestFilePatterns(config2, workdir);
65803
66004
  const isTestFileFn = (filename) => resolvedPatterns.regex.some((re) => re.test(filename));
65804
66005
  const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins, isTestFileFn);
66006
+ clearCache();
65805
66007
  logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
65806
66008
  plugins: pluginRegistry.plugins.map((p) => ({ name: p.name, version: p.version, provides: p.provides }))
65807
66009
  });
@@ -65855,6 +66057,7 @@ async function setupRun(options) {
65855
66057
  var _runSetupDeps;
65856
66058
  var init_run_setup = __esm(() => {
65857
66059
  init_pipeline();
66060
+ init_routing();
65858
66061
  init_test_runners();
65859
66062
  init_paths();
65860
66063
  init_errors();
@@ -97784,8 +97987,9 @@ async function rulesLintCommand(options) {
97784
97987
  init_config();
97785
97988
  init_logger2();
97786
97989
  async function resolveRunProfileOverride(opts) {
97787
- if (opts.cliProfile)
97788
- return opts.cliProfile;
97990
+ const cliChain = parseProfileList(opts.cliProfile);
97991
+ if (cliChain.length > 0)
97992
+ return cliChain;
97789
97993
  if (opts.envProfile)
97790
97994
  return;
97791
97995
  const readJson = opts._readJson ?? (async (path19) => {
@@ -97796,15 +98000,15 @@ async function resolveRunProfileOverride(opts) {
97796
98000
  });
97797
98001
  try {
97798
98002
  const prd = await readJson(opts.prdPath);
97799
- if (prd && typeof prd.routingProfile === "string" && prd.routingProfile.length > 0) {
97800
- const name = prd.routingProfile;
97801
- if (name === "default")
97802
- return name;
98003
+ const rp = prd?.routingProfile;
98004
+ const prdChain = parseProfileList(typeof rp === "string" || Array.isArray(rp) ? rp : undefined);
98005
+ if (prdChain.length > 0) {
97803
98006
  const listNames = opts._listProfileNames ?? (async () => (await listProfiles(opts.projectRoot)).map((p) => p.name));
97804
98007
  const available = await listNames();
97805
- if (available.includes(name))
97806
- return name;
97807
- getSafeLogger()?.warn("run", `PRD was planned with config profile "${name}" but no such profile exists \u2014 continuing with current config resolution`, { storyId: "prd", plannedProfile: name });
98008
+ const missing = prdChain.filter((name) => name !== "default" && !available.includes(name));
98009
+ if (missing.length === 0)
98010
+ return prdChain;
98011
+ getSafeLogger()?.warn("run", `PRD was planned with config profile(s) "${prdChain.join(",")}" but ${missing.join(", ")} not found \u2014 continuing with current config resolution`, { storyId: "prd", plannedProfile: prdChain.join(","), missing });
97808
98012
  }
97809
98013
  } catch {}
97810
98014
  return;
@@ -105191,6 +105395,7 @@ var import_react28 = __toESM(require_react(), 1);
105191
105395
  var import_react35 = __toESM(require_react(), 1);
105192
105396
 
105193
105397
  // src/utils/queue-writer.ts
105398
+ var _writeChains = new Map;
105194
105399
  async function writeQueueCommand(queueFilePath, command) {
105195
105400
  let commandLine2;
105196
105401
  switch (command.type) {
@@ -105208,13 +105413,22 @@ async function writeQueueCommand(queueFilePath, command) {
105208
105413
  throw new Error(`Unhandled queue command: ${_exhaustive}`);
105209
105414
  }
105210
105415
  }
105211
- const file3 = Bun.file(queueFilePath);
105212
- const existingContent = await file3.text().catch(() => "");
105213
- const newContent = existingContent ? `${existingContent.trimEnd()}
105416
+ const chain = _writeChains.get(queueFilePath) ?? Promise.resolve();
105417
+ const next = chain.then(async () => {
105418
+ const existing = await Bun.file(queueFilePath).text().catch(() => "");
105419
+ const content = existing ? `${existing.trimEnd()}
105214
105420
  ${commandLine2}
105215
105421
  ` : `${commandLine2}
105216
105422
  `;
105217
- await Bun.write(queueFilePath, newContent);
105423
+ await Bun.write(queueFilePath, content);
105424
+ });
105425
+ const settled = next.catch(() => {});
105426
+ _writeChains.set(queueFilePath, settled);
105427
+ settled.then(() => {
105428
+ if (_writeChains.get(queueFilePath) === settled)
105429
+ _writeChains.delete(queueFilePath);
105430
+ });
105431
+ await next;
105218
105432
  }
105219
105433
 
105220
105434
  // src/tui/components/CostOverlay.tsx
@@ -106859,6 +107073,9 @@ function renderTui(props) {
106859
107073
  init_version();
106860
107074
  var program2 = new Command;
106861
107075
  program2.name("nax").description("AI Coding Agent Orchestrator \u2014 loops until done").version(NAX_VERSION);
107076
+ function collectProfile(value, previous) {
107077
+ return previous.concat(value);
107078
+ }
106862
107079
  async function promptForConfirmation(question) {
106863
107080
  if (!process.stdin.isTTY) {
106864
107081
  return true;
@@ -107056,7 +107273,7 @@ program2.command("setup").description("Analyze repo and generate .nax/config.jso
107056
107273
  });
107057
107274
  process.exit(exitCode);
107058
107275
  });
107059
- program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile to use (overrides config.json profile)").action(async (options) => {
107276
+ program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).action(async (options) => {
107060
107277
  let workdir;
107061
107278
  try {
107062
107279
  workdir = validateDirectory(options.dir);
@@ -107093,13 +107310,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
107093
107310
  }
107094
107311
  const naxDir = findProjectDir(workdir);
107095
107312
  const cliOverrides = {};
107313
+ const cliProfiles = options.profile ?? [];
107096
107314
  const profileOverride = naxDir ? await resolveRunProfileOverride({
107097
107315
  prdPath: join87(naxDir, "features", options.feature, "prd.json"),
107098
107316
  projectRoot: workdir,
107099
- cliProfile: options.profile,
107317
+ cliProfile: cliProfiles,
107100
107318
  envProfile: process.env.NAX_PROFILE
107101
- }) : options.profile;
107102
- if (profileOverride) {
107319
+ }) : cliProfiles;
107320
+ if (profileOverride && profileOverride.length > 0) {
107103
107321
  cliOverrides.profile = profileOverride;
107104
107322
  }
107105
107323
  const config2 = await loadConfig(naxDir ?? undefined, cliOverrides);
@@ -107400,7 +107618,7 @@ Features:
107400
107618
  }
107401
107619
  console.log();
107402
107620
  });
107403
- program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").option("--from <spec-path>", "Path to spec file (required unless --decompose is used)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false).option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).option("--decompose <storyId>", "Decompose an existing story into sub-stories").option("--profile <name>", "Profile to use (overrides config.json profile)").action(async (description, options) => {
107621
+ program2.command("plan [description]").description("Generate prd.json from a spec file via LLM one-shot call (replaces deprecated 'nax analyze')").option("--from <spec-path>", "Path to spec file (required unless --decompose is used)").requiredOption("-f, --feature <name>", "Feature name (required)").option("--auto", "Run in one-shot LLM mode (alias: --one-shot)", false).option("--one-shot", "Run in one-shot LLM mode (alias: --auto)", false).option("-b, --branch <branch>", "Override default branch name").option("-d, --dir <path>", "Project directory", process.cwd()).option("--decompose <storyId>", "Decompose an existing story into sub-stories").option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).action(async (description, options) => {
107404
107622
  if (description) {
107405
107623
  console.error(source_default.red(`Error: Positional args removed in plan v2.
107406
107624
 
@@ -107420,8 +107638,9 @@ Use: nax plan -f <feature> --from <spec>`));
107420
107638
  process.exit(1);
107421
107639
  }
107422
107640
  const cliOverrides = {};
107423
- if (options.profile) {
107424
- cliOverrides.profile = options.profile;
107641
+ const cliProfiles = options.profile ?? [];
107642
+ if (cliProfiles.length > 0) {
107643
+ cliOverrides.profile = cliProfiles;
107425
107644
  }
107426
107645
  const config2 = await loadConfig(workdir, cliOverrides);
107427
107646
  const featureLogDir = join87(naxDir, "features", options.feature, "plan");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.70.0-canary.3",
3
+ "version": "0.70.0-canary.5",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {