@nathapp/nax 0.70.0-canary.4 → 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 +272 -133
  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 {
@@ -33167,12 +33239,16 @@ async function collectDiff(workdir, storyGitRef, excludePatterns, options) {
33167
33239
  stdout: "pipe",
33168
33240
  stderr: "pipe"
33169
33241
  });
33170
- const [exitCode, stdout] = await Promise.all([
33242
+ const [exitCode, stdout, stderr] = await Promise.all([
33171
33243
  proc.exited,
33172
33244
  new Response(proc.stdout).text(),
33173
33245
  new Response(proc.stderr).text()
33174
33246
  ]);
33175
- 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;
33176
33252
  }
33177
33253
  async function collectDiffStat(workdir, storyGitRef, options) {
33178
33254
  const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
@@ -33423,6 +33499,9 @@ async function prepareSemanticReviewInput(args) {
33423
33499
  return { effectiveRef, stat, diff: undefined, excludePatterns };
33424
33500
  }
33425
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
+ }
33426
33505
  const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat : undefined);
33427
33506
  if (!diff) {
33428
33507
  return { effectiveRef, stat, diff: undefined, excludePatterns, skipReason: "no production code changes" };
@@ -41050,10 +41129,9 @@ async function listChangedFiles(workdir, baseRef) {
41050
41129
  stdout: "pipe",
41051
41130
  stderr: "pipe"
41052
41131
  });
41053
- const exitCode = await proc.exited;
41132
+ const [exitCode, output] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
41054
41133
  if (exitCode !== 0)
41055
41134
  return null;
41056
- const output = await new Response(proc.stdout).text();
41057
41135
  return output.split(`
41058
41136
  `).map((line) => line.trim()).filter(Boolean).map(normalizePath3);
41059
41137
  }
@@ -45726,6 +45804,8 @@ class PidRegistry {
45726
45804
  pidsFilePath;
45727
45805
  pids = new Set;
45728
45806
  frozen = false;
45807
+ _writing = false;
45808
+ _pendingWrite = false;
45729
45809
  writeQueueTail = Promise.resolve();
45730
45810
  constructor(workdir, _platform) {
45731
45811
  this.workdir = workdir;
@@ -46023,11 +46103,22 @@ class PidRegistry {
46023
46103
  }
46024
46104
  }
46025
46105
  enqueueWrite() {
46026
- 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) => {
46027
46112
  getSafeLogger()?.warn("pid-registry", "Failed to flush PID file \u2014 on-disk registry may be stale", {
46028
46113
  error: errorMessage(err)
46029
46114
  });
46030
- }));
46115
+ }).then(async () => {
46116
+ this._writing = false;
46117
+ if (this._pendingWrite) {
46118
+ this._pendingWrite = false;
46119
+ await this.enqueueWrite();
46120
+ }
46121
+ });
46031
46122
  return this.writeQueueTail;
46032
46123
  }
46033
46124
  }
@@ -46589,6 +46680,7 @@ class SessionManager {
46589
46680
  }
46590
46681
  this._busySessions.delete(handle.id);
46591
46682
  this._cancelledSessions.delete(handle.id);
46683
+ this._clearWatchdogCancelledCalls(handle.id);
46592
46684
  }
46593
46685
  async sendPrompt(handle, prompt, opts) {
46594
46686
  if (this._cancelledSessions.has(handle.id)) {
@@ -46673,6 +46765,10 @@ class SessionManager {
46673
46765
  sweepOrphans(ttlMs = DEFAULT_ORPHAN_TTL_MS) {
46674
46766
  return sweepOrphansImpl(this._sessions, ttlMs);
46675
46767
  }
46768
+ close() {
46769
+ this._agentStreamUnsubscribe?.();
46770
+ this._agentStreamUnsubscribe = undefined;
46771
+ }
46676
46772
  }
46677
46773
  var NULL_PROTOCOL_IDS;
46678
46774
  var init_manager2 = __esm(() => {
@@ -46937,6 +47033,9 @@ function createRuntime(config2, workdir, opts) {
46937
47033
  if (opts?.parentSignal && parentAbortHandler) {
46938
47034
  opts.parentSignal.removeEventListener("abort", parentAbortHandler);
46939
47035
  }
47036
+ agentManager.close();
47037
+ if (sessionManager instanceof SessionManager)
47038
+ sessionManager.close();
46940
47039
  const results = await Promise.allSettled([promptAuditor.flush(), reviewAuditor.flush(), costAggregator.drain()]);
46941
47040
  for (const r of results) {
46942
47041
  if (r.status === "rejected") {
@@ -52475,17 +52574,18 @@ async function saveRunMetrics(outputDir, runMetrics) {
52475
52574
  }
52476
52575
  }
52477
52576
  const hasTokenData = totalInputTokens > 0 || totalOutputTokens > 0 || totalCacheReadInputTokens > 0 || totalCacheCreationInputTokens > 0;
52478
- if (hasTokenData) {
52479
- runMetrics.totalTokens = new TokenUsage({
52577
+ const finalMetrics = hasTokenData ? {
52578
+ ...runMetrics,
52579
+ totalTokens: new TokenUsage({
52480
52580
  inputTokens: totalInputTokens,
52481
52581
  outputTokens: totalOutputTokens,
52482
52582
  cacheReadInputTokens: totalCacheReadInputTokens,
52483
52583
  cacheCreationInputTokens: totalCacheCreationInputTokens
52484
- });
52485
- }
52584
+ })
52585
+ } : runMetrics;
52486
52586
  const existing = await loadJsonFile(metricsPath, "metrics");
52487
52587
  const allMetrics = Array.isArray(existing) ? existing : [];
52488
- allMetrics.push(runMetrics);
52588
+ allMetrics.push(finalMetrics);
52489
52589
  await saveJsonFile(metricsPath, allMetrics, "metrics");
52490
52590
  }
52491
52591
  async function loadRunMetrics(outputDir) {
@@ -54652,29 +54752,27 @@ async function rollbackToRef(workdir, ref) {
54652
54752
  stdout: "pipe",
54653
54753
  stderr: "pipe"
54654
54754
  });
54655
- const exitCode = await resetProc.exited;
54755
+ const [exitCode, resetStderr] = await Promise.all([resetProc.exited, new Response(resetProc.stderr).text()]);
54656
54756
  if (exitCode !== 0) {
54657
- const stderr = await new Response(resetProc.stderr).text();
54658
- logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
54659
- 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}`);
54660
54759
  }
54661
54760
  const cleanProc = _rollbackDeps.spawn(["git", "clean", "-fd"], {
54662
54761
  cwd: workdir,
54663
54762
  stdout: "pipe",
54664
54763
  stderr: "pipe"
54665
54764
  });
54666
- const cleanExitCode = await cleanProc.exited;
54765
+ const [cleanExitCode, cleanStderr] = await Promise.all([cleanProc.exited, new Response(cleanProc.stderr).text()]);
54667
54766
  if (cleanExitCode !== 0) {
54668
- const stderr = await new Response(cleanProc.stderr).text();
54669
- logger.warn("tdd", "Failed to clean untracked files", { stderr });
54767
+ logger.warn("tdd", "Failed to clean untracked files", { stderr: cleanStderr });
54670
54768
  }
54671
54769
  logger.info("tdd", "Successfully rolled back git changes", { ref });
54672
54770
  }
54673
54771
  async function captureSnapshotRef(workdir, storyId) {
54674
54772
  await _rollbackDeps.autoCommitIfDirty(workdir, "non-blocking-fix-snapshot", "snapshot", storyId);
54675
54773
  const proc = _rollbackDeps.spawn(["git", "rev-parse", "HEAD"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
54676
- const sha = (await new Response(proc.stdout).text()).trim();
54677
- const exitCode = await proc.exited;
54774
+ const [exitCode, shaRaw] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
54775
+ const sha = shaRaw.trim();
54678
54776
  if (exitCode !== 0) {
54679
54777
  throw new NaxError("git rev-parse HEAD failed in non-blocking-fix snapshot", "SNAPSHOT_REF_FAILED", {
54680
54778
  storyId,
@@ -55664,7 +55762,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55664
55762
  if (pkgQuality?.commands?.formatFix || pkgQuality?.commands?.formatFixScoped) {
55665
55763
  strategies.push(makeMechanicalFormatFixStrategy());
55666
55764
  }
55667
- 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) {
55668
55768
  strategies.push(makeFullSuiteRectifyStrategy(story, config2, sink));
55669
55769
  }
55670
55770
  if (config2.quality.autofix?.enabled !== false) {
@@ -60325,7 +60425,7 @@ var package_default;
60325
60425
  var init_package = __esm(() => {
60326
60426
  package_default = {
60327
60427
  name: "@nathapp/nax",
60328
- version: "0.70.0-canary.4",
60428
+ version: "0.70.0-canary.5",
60329
60429
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60330
60430
  type: "module",
60331
60431
  bin: {
@@ -60420,8 +60520,8 @@ var init_version = __esm(() => {
60420
60520
  NAX_VERSION = package_default.version;
60421
60521
  NAX_COMMIT = (() => {
60422
60522
  try {
60423
- if (/^[0-9a-f]{6,10}$/.test("e2a854e7"))
60424
- return "e2a854e7";
60523
+ if (/^[0-9a-f]{6,10}$/.test("e8c2ab46"))
60524
+ return "e8c2ab46";
60425
60525
  } catch {}
60426
60526
  try {
60427
60527
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -60441,11 +60541,11 @@ var init_version = __esm(() => {
60441
60541
 
60442
60542
  // src/execution/crash-heartbeat.ts
60443
60543
  import { appendFileSync as appendFileSync2 } from "fs";
60444
- async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60544
+ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60445
60545
  const logger = _heartbeatDeps.getSafeLogger();
60446
- while (heartbeatActive) {
60546
+ while (gen === _heartbeatGen && _heartbeatActive) {
60447
60547
  await _heartbeatDeps.sleep(60000);
60448
- if (!heartbeatActive)
60548
+ if (gen !== _heartbeatGen || !_heartbeatActive)
60449
60549
  break;
60450
60550
  try {
60451
60551
  logger?.debug("crash-recovery", "Heartbeat");
@@ -60474,9 +60574,9 @@ async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFil
60474
60574
  }
60475
60575
  function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60476
60576
  const logger = _heartbeatDeps.getSafeLogger();
60477
- stopHeartbeat();
60478
- heartbeatActive = true;
60479
- heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
60577
+ _heartbeatActive = true;
60578
+ const gen = ++_heartbeatGen;
60579
+ heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
60480
60580
  _heartbeatDeps.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
60481
60581
  error: err instanceof Error ? err.message : String(err)
60482
60582
  });
@@ -60484,12 +60584,13 @@ function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath
60484
60584
  logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
60485
60585
  }
60486
60586
  function stopHeartbeat() {
60487
- if (heartbeatActive) {
60488
- heartbeatActive = false;
60587
+ if (_heartbeatActive) {
60588
+ _heartbeatActive = false;
60589
+ _heartbeatGen++;
60489
60590
  getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
60490
60591
  }
60491
60592
  }
60492
- var _heartbeatDeps, heartbeatActive = false;
60593
+ var _heartbeatDeps, _heartbeatGen = 0, _heartbeatActive = false;
60493
60594
  var init_crash_heartbeat = __esm(() => {
60494
60595
  init_logger2();
60495
60596
  _heartbeatDeps = {
@@ -62656,9 +62757,8 @@ ${missing.join(`
62656
62757
  stdout: "pipe",
62657
62758
  stderr: "pipe"
62658
62759
  });
62659
- const exitCode = await proc.exited;
62760
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62660
62761
  if (exitCode !== 0) {
62661
- const stderr = await new Response(proc.stderr).text();
62662
62762
  throw new Error(`Failed to create worktree: ${stderr || "unknown error"}`);
62663
62763
  }
62664
62764
  } catch (error48) {
@@ -62691,9 +62791,8 @@ ${missing.join(`
62691
62791
  stdout: "pipe",
62692
62792
  stderr: "pipe"
62693
62793
  });
62694
- const exitCode = await proc.exited;
62794
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62695
62795
  if (exitCode !== 0) {
62696
- const stderr = await new Response(proc.stderr).text();
62697
62796
  if (stderr.includes("not found") || stderr.includes("does not exist") || stderr.includes("no such worktree") || stderr.includes("is not a working tree")) {
62698
62797
  throw new Error(`Worktree not found: ${worktreePath}`);
62699
62798
  }
@@ -62711,9 +62810,8 @@ ${missing.join(`
62711
62810
  stdout: "pipe",
62712
62811
  stderr: "pipe"
62713
62812
  });
62714
- const exitCode = await proc.exited;
62813
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62715
62814
  if (exitCode !== 0) {
62716
- const stderr = await new Response(proc.stderr).text();
62717
62815
  if (!stderr.includes("not found")) {
62718
62816
  const logger = getSafeLogger();
62719
62817
  logger?.warn("worktree", `Failed to delete branch ${branchName}`, { stderr });
@@ -62733,12 +62831,14 @@ ${missing.join(`
62733
62831
  stdout: "pipe",
62734
62832
  stderr: "pipe"
62735
62833
  });
62736
- 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
+ ]);
62737
62839
  if (exitCode !== 0) {
62738
- const stderr = await new Response(proc.stderr).text();
62739
62840
  throw new Error(`Failed to list worktrees: ${stderr || "unknown error"}`);
62740
62841
  }
62741
- const stdout = await new Response(proc.stdout).text();
62742
62842
  return this.parseWorktreeList(stdout);
62743
62843
  } catch (error48) {
62744
62844
  if (error48 instanceof Error) {
@@ -62850,9 +62950,11 @@ class MergeEngine {
62850
62950
  stdout: "pipe",
62851
62951
  stderr: "pipe"
62852
62952
  });
62853
- const exitCode = await mergeProc.exited;
62854
- const stderr = await new Response(mergeProc.stderr).text();
62855
- 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
+ ]);
62856
62958
  if (exitCode === 0) {
62857
62959
  try {
62858
62960
  await this.worktreeManager.remove(projectRoot, storyId);
@@ -62979,19 +63081,25 @@ ${stderr}`;
62979
63081
  stdout: "pipe",
62980
63082
  stderr: "pipe"
62981
63083
  });
62982
- const exitCode = await currentBranchProc.exited;
63084
+ const [exitCode, currentBranchRaw] = await Promise.all([
63085
+ currentBranchProc.exited,
63086
+ new Response(currentBranchProc.stdout).text()
63087
+ ]);
62983
63088
  if (exitCode !== 0) {
62984
63089
  throw new Error("Failed to get current branch");
62985
63090
  }
62986
- const currentBranch = (await new Response(currentBranchProc.stdout).text()).trim();
63091
+ const currentBranch = currentBranchRaw.trim();
62987
63092
  const rebaseProc = _mergeDeps.spawn(["git", "rebase", currentBranch], {
62988
63093
  cwd: worktreePath,
62989
63094
  stdout: "pipe",
62990
63095
  stderr: "pipe"
62991
63096
  });
62992
- const rebaseExitCode = await rebaseProc.exited;
63097
+ const [rebaseExitCode, rebaseStderr] = await Promise.all([
63098
+ rebaseProc.exited,
63099
+ new Response(rebaseProc.stderr).text()
63100
+ ]);
62993
63101
  if (rebaseExitCode !== 0) {
62994
- const stderr = await new Response(rebaseProc.stderr).text();
63102
+ const stderr = rebaseStderr;
62995
63103
  const abortProc = _mergeDeps.spawn(["git", "rebase", "--abort"], {
62996
63104
  cwd: worktreePath,
62997
63105
  stdout: "pipe",
@@ -63014,11 +63122,10 @@ ${stderr}`;
63014
63122
  stdout: "pipe",
63015
63123
  stderr: "pipe"
63016
63124
  });
63017
- const exitCode = await proc.exited;
63125
+ const [exitCode, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
63018
63126
  if (exitCode !== 0) {
63019
63127
  return [];
63020
63128
  }
63021
- const stdout = await new Response(proc.stdout).text();
63022
63129
  return stdout.trim().split(`
63023
63130
  `).filter((line) => line.length > 0);
63024
63131
  } catch {
@@ -63758,7 +63865,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
63758
63865
  }
63759
63866
  }
63760
63867
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
63761
- const profileOverride = ctx.config.profile && ctx.config.profile !== "default" ? { profile: ctx.config.profile } : undefined;
63868
+ const profileOverride = profileOverrideFromConfig(ctx.config);
63762
63869
  const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join81(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
63763
63870
  let dependencyContext;
63764
63871
  if (ctx.config.execution.storyIsolation === "worktree") {
@@ -63904,6 +64011,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
63904
64011
  }
63905
64012
  var _iterationRunnerDeps;
63906
64013
  var init_iteration_runner = __esm(() => {
64014
+ init_config();
63907
64015
  init_loader();
63908
64016
  init_logger2();
63909
64017
  init_runner4();
@@ -64134,7 +64242,7 @@ async function runParallelBatch(options) {
64134
64242
  worktreePaths.set(story.id, path22.join(workdir, ".nax-wt", story.id));
64135
64243
  }
64136
64244
  const rootConfigPath = path22.join(workdir, ".nax", "config.json");
64137
- const profileOverride = config2.profile && config2.profile !== "default" ? { profile: config2.profile } : undefined;
64245
+ const profileOverride = profileOverrideFromConfig(config2);
64138
64246
  const storyEffectiveConfigs = new Map;
64139
64247
  const configResults = await Promise.allSettled(stories.filter((story) => story.workdir).map(async (story) => {
64140
64248
  try {
@@ -64295,6 +64403,7 @@ async function runParallelBatch(options) {
64295
64403
  }
64296
64404
  var _parallelBatchDeps;
64297
64405
  var init_parallel_batch = __esm(() => {
64406
+ init_config();
64298
64407
  init_loader();
64299
64408
  init_logger2();
64300
64409
  init_dependencies();
@@ -64361,13 +64470,15 @@ async function executeUnified(ctx, initialPrd) {
64361
64470
  };
64362
64471
  for (const fn of _prevRunUnsubscribers)
64363
64472
  fn();
64364
- _prevRunUnsubscribers = [
64473
+ _prevRunUnsubscribers = [];
64474
+ const thisRunUnsubscribers = [
64365
64475
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature),
64366
64476
  wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime),
64367
64477
  wireInteraction(pipelineEventBus, ctx.interactionChain, ctx.config),
64368
64478
  wireEventsWriter(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir),
64369
64479
  wireRegistry(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir, ctx.runtime.outputDir)
64370
64480
  ];
64481
+ _prevRunUnsubscribers = thisRunUnsubscribers;
64371
64482
  pipelineEventBus.emit({
64372
64483
  type: "run:started",
64373
64484
  feature: ctx.feature,
@@ -64384,6 +64495,7 @@ async function executeUnified(ctx, initialPrd) {
64384
64495
  deferredReview
64385
64496
  });
64386
64497
  startHeartbeat(ctx.statusWriter, () => totalCost, () => iterations, ctx.logFilePath);
64498
+ let _executeThrew = false;
64387
64499
  try {
64388
64500
  if (isComplete(prd)) {
64389
64501
  logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
@@ -64790,7 +64902,16 @@ async function executeUnified(ctx, initialPrd) {
64790
64902
  }, ctx.eventEmitter);
64791
64903
  }
64792
64904
  return buildResult2("max-iterations");
64793
- } 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
+ }
64794
64915
  }
64795
64916
  function reconcileBatchOutcome(prd, batchResult) {
64796
64917
  for (const story of batchResult.completed) {
@@ -65882,6 +66003,7 @@ async function setupRun(options) {
65882
66003
  const resolvedPatterns = await resolveTestFilePatterns(config2, workdir);
65883
66004
  const isTestFileFn = (filename) => resolvedPatterns.regex.some((re) => re.test(filename));
65884
66005
  const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins, isTestFileFn);
66006
+ clearCache();
65885
66007
  logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
65886
66008
  plugins: pluginRegistry.plugins.map((p) => ({ name: p.name, version: p.version, provides: p.provides }))
65887
66009
  });
@@ -65935,6 +66057,7 @@ async function setupRun(options) {
65935
66057
  var _runSetupDeps;
65936
66058
  var init_run_setup = __esm(() => {
65937
66059
  init_pipeline();
66060
+ init_routing();
65938
66061
  init_test_runners();
65939
66062
  init_paths();
65940
66063
  init_errors();
@@ -97864,8 +97987,9 @@ async function rulesLintCommand(options) {
97864
97987
  init_config();
97865
97988
  init_logger2();
97866
97989
  async function resolveRunProfileOverride(opts) {
97867
- if (opts.cliProfile)
97868
- return opts.cliProfile;
97990
+ const cliChain = parseProfileList(opts.cliProfile);
97991
+ if (cliChain.length > 0)
97992
+ return cliChain;
97869
97993
  if (opts.envProfile)
97870
97994
  return;
97871
97995
  const readJson = opts._readJson ?? (async (path19) => {
@@ -97876,15 +98000,15 @@ async function resolveRunProfileOverride(opts) {
97876
98000
  });
97877
98001
  try {
97878
98002
  const prd = await readJson(opts.prdPath);
97879
- if (prd && typeof prd.routingProfile === "string" && prd.routingProfile.length > 0) {
97880
- const name = prd.routingProfile;
97881
- if (name === "default")
97882
- return name;
98003
+ const rp = prd?.routingProfile;
98004
+ const prdChain = parseProfileList(typeof rp === "string" || Array.isArray(rp) ? rp : undefined);
98005
+ if (prdChain.length > 0) {
97883
98006
  const listNames = opts._listProfileNames ?? (async () => (await listProfiles(opts.projectRoot)).map((p) => p.name));
97884
98007
  const available = await listNames();
97885
- if (available.includes(name))
97886
- return name;
97887
- 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 });
97888
98012
  }
97889
98013
  } catch {}
97890
98014
  return;
@@ -105271,6 +105395,7 @@ var import_react28 = __toESM(require_react(), 1);
105271
105395
  var import_react35 = __toESM(require_react(), 1);
105272
105396
 
105273
105397
  // src/utils/queue-writer.ts
105398
+ var _writeChains = new Map;
105274
105399
  async function writeQueueCommand(queueFilePath, command) {
105275
105400
  let commandLine2;
105276
105401
  switch (command.type) {
@@ -105288,13 +105413,22 @@ async function writeQueueCommand(queueFilePath, command) {
105288
105413
  throw new Error(`Unhandled queue command: ${_exhaustive}`);
105289
105414
  }
105290
105415
  }
105291
- const file3 = Bun.file(queueFilePath);
105292
- const existingContent = await file3.text().catch(() => "");
105293
- 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()}
105294
105420
  ${commandLine2}
105295
105421
  ` : `${commandLine2}
105296
105422
  `;
105297
- 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;
105298
105432
  }
105299
105433
 
105300
105434
  // src/tui/components/CostOverlay.tsx
@@ -106939,6 +107073,9 @@ function renderTui(props) {
106939
107073
  init_version();
106940
107074
  var program2 = new Command;
106941
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
+ }
106942
107079
  async function promptForConfirmation(question) {
106943
107080
  if (!process.stdin.isTTY) {
106944
107081
  return true;
@@ -107136,7 +107273,7 @@ program2.command("setup").description("Analyze repo and generate .nax/config.jso
107136
107273
  });
107137
107274
  process.exit(exitCode);
107138
107275
  });
107139
- 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) => {
107140
107277
  let workdir;
107141
107278
  try {
107142
107279
  workdir = validateDirectory(options.dir);
@@ -107173,13 +107310,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
107173
107310
  }
107174
107311
  const naxDir = findProjectDir(workdir);
107175
107312
  const cliOverrides = {};
107313
+ const cliProfiles = options.profile ?? [];
107176
107314
  const profileOverride = naxDir ? await resolveRunProfileOverride({
107177
107315
  prdPath: join87(naxDir, "features", options.feature, "prd.json"),
107178
107316
  projectRoot: workdir,
107179
- cliProfile: options.profile,
107317
+ cliProfile: cliProfiles,
107180
107318
  envProfile: process.env.NAX_PROFILE
107181
- }) : options.profile;
107182
- if (profileOverride) {
107319
+ }) : cliProfiles;
107320
+ if (profileOverride && profileOverride.length > 0) {
107183
107321
  cliOverrides.profile = profileOverride;
107184
107322
  }
107185
107323
  const config2 = await loadConfig(naxDir ?? undefined, cliOverrides);
@@ -107480,7 +107618,7 @@ Features:
107480
107618
  }
107481
107619
  console.log();
107482
107620
  });
107483
- 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) => {
107484
107622
  if (description) {
107485
107623
  console.error(source_default.red(`Error: Positional args removed in plan v2.
107486
107624
 
@@ -107500,8 +107638,9 @@ Use: nax plan -f <feature> --from <spec>`));
107500
107638
  process.exit(1);
107501
107639
  }
107502
107640
  const cliOverrides = {};
107503
- if (options.profile) {
107504
- cliOverrides.profile = options.profile;
107641
+ const cliProfiles = options.profile ?? [];
107642
+ if (cliProfiles.length > 0) {
107643
+ cliOverrides.profile = cliProfiles;
107505
107644
  }
107506
107645
  const config2 = await loadConfig(workdir, cliOverrides);
107507
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.4",
3
+ "version": "0.70.0-canary.5",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {