@nathapp/nax 0.70.0-canary.4 → 0.70.0-canary.6

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 +377 -161
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -16828,7 +16828,8 @@ var init_schemas_execution = __esm(() => {
16828
16828
  escalation: exports_external.object({
16829
16829
  enabled: exports_external.boolean(),
16830
16830
  tierOrder: exports_external.array(TierConfigSchema).min(1, { message: "tierOrder must have at least one tier" }),
16831
- escalateEntireBatch: exports_external.boolean().optional()
16831
+ escalateEntireBatch: exports_external.boolean().optional(),
16832
+ resetMode: exports_external.enum(["initial", "last"]).default("initial")
16832
16833
  })
16833
16834
  });
16834
16835
  RectificationConfigSchema = exports_external.object({
@@ -16852,11 +16853,13 @@ var init_schemas_execution = __esm(() => {
16852
16853
  SmartTestRunnerConfigSchema = exports_external.object({
16853
16854
  enabled: exports_external.boolean().default(true),
16854
16855
  testFilePatterns: exports_external.array(exports_external.string()).optional(),
16855
- fallback: exports_external.enum(["import-grep", "full-suite"]).default("import-grep")
16856
+ fallback: exports_external.enum(["import-grep", "full-suite"]).default("import-grep"),
16857
+ maxScanFiles: exports_external.number().int().min(1).max(5000).default(200)
16856
16858
  });
16857
16859
  SMART_TEST_RUNNER_DEFAULT = {
16858
16860
  enabled: true,
16859
- fallback: "import-grep"
16861
+ fallback: "import-grep",
16862
+ maxScanFiles: 200
16860
16863
  };
16861
16864
  smartTestRunnerFieldSchema = exports_external.preprocess((val) => {
16862
16865
  if (typeof val === "boolean") {
@@ -16870,7 +16873,7 @@ var init_schemas_execution = __esm(() => {
16870
16873
  }).superRefine((value, ctx) => {
16871
16874
  if (value.mode !== "provision" && value.setupCommand !== null) {
16872
16875
  ctx.addIssue({
16873
- code: exports_external.ZodIssueCode.custom,
16876
+ code: "custom",
16874
16877
  path: ["setupCommand"],
16875
16878
  message: "execution.worktreeDependencies.setupCommand requires mode 'provision'"
16876
16879
  });
@@ -17352,7 +17355,8 @@ var init_schemas3 = __esm(() => {
17352
17355
  { tier: "balanced", attempts: 3 },
17353
17356
  { tier: "powerful", attempts: 2 }
17354
17357
  ],
17355
- escalateEntireBatch: true
17358
+ escalateEntireBatch: true,
17359
+ resetMode: "initial"
17356
17360
  }
17357
17361
  }),
17358
17362
  routing: RoutingConfigSchema.default({
@@ -17665,7 +17669,8 @@ var init_schemas3 = __esm(() => {
17665
17669
  }
17666
17670
  })),
17667
17671
  curator: CuratorConfigSchema.optional(),
17668
- profile: exports_external.string().default("default")
17672
+ profile: exports_external.string().default("default"),
17673
+ profileChain: exports_external.array(exports_external.string()).default([])
17669
17674
  }).refine((data) => data.version === 1, {
17670
17675
  message: "Invalid version: expected 1",
17671
17676
  path: ["version"]
@@ -18728,7 +18733,11 @@ async function loadProfile(profileName, projectRoot) {
18728
18733
  if (!globalExists && !projectExists) {
18729
18734
  const available = await listAvailableProfileNames(projectRoot);
18730
18735
  const availableList = available.length > 0 ? available.join(", ") : "(none)";
18731
- throw new Error(`Profile "${profileName}" not found. Available: ${availableList}`);
18736
+ throw new NaxError(`Profile "${profileName}" not found. Available: ${availableList}`, "PROFILE_NOT_FOUND", {
18737
+ stage: "config",
18738
+ profileName,
18739
+ available
18740
+ });
18732
18741
  }
18733
18742
  let base = {};
18734
18743
  if (globalExists) {
@@ -18760,30 +18769,59 @@ async function loadProfileEnv(profileName, projectRoot) {
18760
18769
  }
18761
18770
  return merged;
18762
18771
  }
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;
18772
+ function parseProfileList(input) {
18773
+ if (input == null)
18774
+ return [];
18775
+ const parts = Array.isArray(input) ? input : [input];
18776
+ const out = [];
18777
+ for (const part of parts) {
18778
+ if (typeof part !== "string")
18779
+ continue;
18780
+ for (const segment of part.split(",")) {
18781
+ const trimmed = segment.trim();
18782
+ if (trimmed)
18783
+ out.push(trimmed);
18776
18784
  }
18777
18785
  }
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
- }
18786
+ return out;
18787
+ }
18788
+ function profileOverrideFromConfig(config2) {
18789
+ if (config2.profileChain && config2.profileChain.length > 0) {
18790
+ return { profile: config2.profileChain };
18791
+ }
18792
+ if (config2.profile && config2.profile !== "default") {
18793
+ return { profile: [config2.profile] };
18785
18794
  }
18786
- return "default";
18795
+ return;
18796
+ }
18797
+ async function readProfileChainFromConfig(dir) {
18798
+ const configFile = Bun.file(join2(dir, "config.json"));
18799
+ if (!await configFile.exists())
18800
+ return [];
18801
+ const config2 = await configFile.json();
18802
+ return parseProfileList(config2.profile);
18803
+ }
18804
+ function isDefaultOnlyChain(chain) {
18805
+ return chain.length === 0 || chain.length === 1 && chain[0] === "default";
18806
+ }
18807
+ async function resolveProfileNames(cliOptions, env2, projectRoot) {
18808
+ const fromCli = parseProfileList(cliOptions.profile);
18809
+ if (fromCli.length)
18810
+ return fromCli;
18811
+ const fromEnv = parseProfileList(env2.NAX_PROFILE);
18812
+ if (fromEnv.length)
18813
+ return fromEnv;
18814
+ const projectChain = await readProfileChainFromConfig(projectConfigDir(projectRoot));
18815
+ if (!isDefaultOnlyChain(projectChain))
18816
+ return projectChain;
18817
+ const globalChain = await readProfileChainFromConfig(globalConfigDir());
18818
+ if (!isDefaultOnlyChain(globalChain))
18819
+ return globalChain;
18820
+ return ["default"];
18821
+ }
18822
+ async function resolveProfileName(cliOptions, env2, projectRoot) {
18823
+ const chain = await resolveProfileNames(cliOptions, env2, projectRoot);
18824
+ return chain[chain.length - 1] ?? "default";
18787
18825
  }
18788
18826
  async function listAvailableProfileNames(projectRoot) {
18789
18827
  const entries = await listProfiles(projectRoot);
@@ -18811,6 +18849,7 @@ async function listProfiles(projectRoot) {
18811
18849
  return entries;
18812
18850
  }
18813
18851
  var init_profile = __esm(() => {
18852
+ init_errors();
18814
18853
  init_paths();
18815
18854
  });
18816
18855
 
@@ -18993,7 +19032,8 @@ async function loadConfig(startDir, cliOverrides) {
18993
19032
  let rawConfig = structuredClone(DEFAULT_CONFIG);
18994
19033
  const projDir = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
18995
19034
  const projectRoot = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? dirname(startDir) : startDir : process.cwd();
18996
- const profileName = await resolveProfileName(cliOverrides ?? {}, process.env, projectRoot);
19035
+ const profileChain = await resolveProfileNames(cliOverrides ?? {}, process.env, projectRoot);
19036
+ const overlayChain = profileChain.filter((name) => name && name !== "default");
18997
19037
  const globalConfRaw = await loadJsonFile(globalConfigPath(), "config");
18998
19038
  let logger = null;
18999
19039
  try {
@@ -19012,16 +19052,17 @@ async function loadConfig(startDir, cliOverrides) {
19012
19052
  rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
19013
19053
  }
19014
19054
  }
19015
- if (profileName !== "default") {
19016
- const profileData = await loadProfile(profileName, projectRoot);
19055
+ for (const name of overlayChain) {
19056
+ const profileData = await loadProfile(name, projectRoot);
19017
19057
  rawConfig = deepMergeConfig(rawConfig, profileData);
19018
- await loadProfileEnv(profileName, projectRoot);
19058
+ await loadProfileEnv(name, projectRoot);
19019
19059
  }
19020
19060
  if (cliOverrides) {
19021
19061
  rawConfig = deepMergeConfig(rawConfig, cliOverrides);
19022
19062
  }
19023
- rawConfig.profile = profileName;
19024
- const hasMergedConfigs = globalConfRaw || projDir !== null || cliOverrides !== undefined || profileName !== "default";
19063
+ rawConfig.profile = overlayChain.length > 0 ? overlayChain.join("+") : "default";
19064
+ rawConfig.profileChain = overlayChain;
19065
+ const hasMergedConfigs = globalConfRaw || projDir !== null || cliOverrides !== undefined || overlayChain.length > 0;
19025
19066
  if (!hasMergedConfigs) {
19026
19067
  return structuredClone(DEFAULT_CONFIG);
19027
19068
  }
@@ -19051,11 +19092,14 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19051
19092
  const logger = getLogger();
19052
19093
  const resolvedRootConfigPath = resolve3(rootConfigPath);
19053
19094
  const rootNaxDir = dirname(resolvedRootConfigPath);
19054
- const profileKey = cliOverrides?.profile ?? "";
19095
+ const profileKey = parseProfileList(cliOverrides?.profile).join(",");
19055
19096
  const cacheKey = profileKey ? `${resolvedRootConfigPath}:${profileKey}` : resolvedRootConfigPath;
19056
19097
  let rootConfigPromise = _rootConfigCache.get(cacheKey);
19057
19098
  if (!rootConfigPromise) {
19058
- rootConfigPromise = loadConfig(rootNaxDir, cliOverrides);
19099
+ rootConfigPromise = loadConfig(rootNaxDir, cliOverrides).catch((err) => {
19100
+ _rootConfigCache.delete(cacheKey);
19101
+ throw err;
19102
+ });
19059
19103
  if (_rootConfigCache.size >= ROOT_CONFIG_CACHE_MAX) {
19060
19104
  const firstKey = _rootConfigCache.keys().next().value;
19061
19105
  if (firstKey !== undefined)
@@ -19081,22 +19125,29 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19081
19125
  logger.debug("config", "Per-package config loaded", { packageConfigPath, packageDir });
19082
19126
  const { profile: packageProfile, ...packageFields } = packageOverride;
19083
19127
  let merged = mergePackageConfig(rootConfig, packageFields);
19084
- if (packageProfile && packageProfile !== "default") {
19128
+ const packageChain = parseProfileList(packageProfile).filter((name) => name && name !== "default");
19129
+ if (packageChain.length > 0) {
19085
19130
  const packageRoot = join3(repoRoot, packageDir);
19086
- const profileData = await loadProfile(packageProfile, packageRoot);
19087
- const rawMerged = deepMergeConfig(merged, profileData);
19088
- rawMerged.profile = packageProfile;
19131
+ let rawMerged = merged;
19132
+ for (const name of packageChain) {
19133
+ const profileData = await loadProfile(name, packageRoot);
19134
+ rawMerged = deepMergeConfig(rawMerged, profileData);
19135
+ }
19136
+ rawMerged.profile = packageChain.join("+");
19137
+ rawMerged.profileChain = packageChain;
19089
19138
  rejectLegacyAgentKeys(rawMerged);
19090
19139
  rejectLegacyRectificationKeys(rawMerged);
19091
19140
  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
19141
+ if (!result.success) {
19142
+ const errors3 = result.error.issues.map((err) => {
19143
+ const path = String(err.path.join("."));
19144
+ return path ? `${path}: ${err.message}` : err.message;
19098
19145
  });
19146
+ throw new NaxError(`Per-package profile "${packageChain.join("+")}" produced an invalid config for package "${packageDir}":
19147
+ ${errors3.join(`
19148
+ `)}`, "PER_PACKAGE_PROFILE_INVALID", { stage: "config", packageDir, profileChain: packageChain });
19099
19149
  }
19150
+ merged = result.data;
19100
19151
  }
19101
19152
  return merged;
19102
19153
  }
@@ -19504,6 +19555,7 @@ __export(exports_config, {
19504
19555
  routingConfigSelector: () => routingConfigSelector,
19505
19556
  reviewConfigSelector: () => reviewConfigSelector,
19506
19557
  resolveTestStrategy: () => resolveTestStrategy,
19558
+ resolveProfileNames: () => resolveProfileNames,
19507
19559
  resolveProfileName: () => resolveProfileName,
19508
19560
  resolveModelForAgent: () => resolveModelForAgent,
19509
19561
  resolveModel: () => resolveModel,
@@ -19514,9 +19566,11 @@ __export(exports_config, {
19514
19566
  qualityConfigSelector: () => qualityConfigSelector,
19515
19567
  promptLoaderConfigSelector: () => promptLoaderConfigSelector,
19516
19568
  projectConfigDir: () => projectConfigDir,
19569
+ profileOverrideFromConfig: () => profileOverrideFromConfig,
19517
19570
  precheckConfigSelector: () => precheckConfigSelector,
19518
19571
  planConfigSelector: () => planConfigSelector,
19519
19572
  pickSelector: () => pickSelector,
19573
+ parseProfileList: () => parseProfileList,
19520
19574
  mergePackageConfig: () => mergePackageConfig,
19521
19575
  loadProfileEnv: () => loadProfileEnv,
19522
19576
  loadProfile: () => loadProfile,
@@ -21286,12 +21340,13 @@ function parseAcpxJsonLine(line, state) {
21286
21340
  }
21287
21341
  return;
21288
21342
  }
21289
- if (event.content && typeof event.content === "string")
21343
+ if (event.result && typeof event.result === "string") {
21344
+ state.text = event.result;
21345
+ } else if (event.content && typeof event.content === "string") {
21290
21346
  state.text += event.content;
21291
- if (event.text && typeof event.text === "string")
21347
+ } else if (event.text && typeof event.text === "string") {
21292
21348
  state.text += event.text;
21293
- if (event.result && typeof event.result === "string")
21294
- state.text = event.result;
21349
+ }
21295
21350
  if (event.cumulative_token_usage)
21296
21351
  state.tokenUsage = event.cumulative_token_usage;
21297
21352
  if (event.usage) {
@@ -21626,7 +21681,11 @@ class AgentManager {
21626
21681
  _registry;
21627
21682
  _unavailable = new Map;
21628
21683
  _prunedFallback = new Set;
21629
- _emitter = new EventEmitter;
21684
+ _emitter = (() => {
21685
+ const ee = new EventEmitter;
21686
+ ee.setMaxListeners(MAX_EMITTER_LISTENERS);
21687
+ return ee;
21688
+ })();
21630
21689
  _logger;
21631
21690
  _middleware;
21632
21691
  _runId;
@@ -22225,6 +22284,9 @@ class AgentManager {
22225
22284
  throw err;
22226
22285
  }
22227
22286
  }
22287
+ close() {
22288
+ this._emitter.removeAllListeners();
22289
+ }
22228
22290
  _resolveRegistry() {
22229
22291
  this._registry ??= createAgentRegistry(this._config);
22230
22292
  return this._registry;
@@ -22233,7 +22295,7 @@ class AgentManager {
22233
22295
  this._emitter.emit(event, payload);
22234
22296
  }
22235
22297
  }
22236
- var _agentManagerDeps;
22298
+ var MAX_EMITTER_LISTENERS = 100, _agentManagerDeps;
22237
22299
  var init_manager = __esm(() => {
22238
22300
  init_errors();
22239
22301
  init_logger2();
@@ -25144,7 +25206,7 @@ function extractSearchTerms(sourceFile) {
25144
25206
  const basename4 = parts[parts.length - 1];
25145
25207
  return [`/${basename4}`, withoutExt];
25146
25208
  }
25147
- async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
25209
+ async function importGrepFallback(sourceFiles, workdir, testFilePatterns, maxScanFiles = MAX_GREP_TEST_FILES) {
25148
25210
  if (sourceFiles.length === 0 || testFilePatterns.length === 0)
25149
25211
  return [];
25150
25212
  const searchTerms = sourceFiles.flatMap(extractSearchTerms);
@@ -25152,11 +25214,11 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
25152
25214
  outer:
25153
25215
  for (const pattern of testFilePatterns) {
25154
25216
  const g = _bunDeps.glob(pattern);
25155
- for await (const file3 of g.scan(workdir)) {
25217
+ for await (const file3 of g.scan({ cwd: workdir, absolute: false })) {
25156
25218
  testFilePaths.push(`${workdir}/${file3}`);
25157
- if (testFilePaths.length >= MAX_GREP_TEST_FILES) {
25219
+ if (testFilePaths.length >= maxScanFiles) {
25158
25220
  getSafeLogger()?.debug("smart-runner", "import-grep glob cap reached \u2014 results truncated", {
25159
- cap: MAX_GREP_TEST_FILES
25221
+ cap: maxScanFiles
25160
25222
  });
25161
25223
  break outer;
25162
25224
  }
@@ -25404,7 +25466,7 @@ async function selectScopedTests(input) {
25404
25466
  if (smartCfg.fallback !== "import-grep") {
25405
25467
  return fullSuite();
25406
25468
  }
25407
- const pass2Files = await _scopedSelectionDeps.importGrepFallback(nonTestFiles, input.workdir, mappingGlobs);
25469
+ const pass2Files = await _scopedSelectionDeps.importGrepFallback(nonTestFiles, input.workdir, mappingGlobs, smartCfg.maxScanFiles);
25408
25470
  if (pass2Files.length > threshold) {
25409
25471
  logger.warn("verify[scoped]", `Scoped test file count ${pass2Files.length} exceeds threshold ${threshold} \u2014 falling back to full suite`, { storyId: input.storyId });
25410
25472
  return fullSuite({ scopeTestFallback: true, thresholdFallback: true });
@@ -25425,7 +25487,8 @@ var init_scoped_selection = __esm(() => {
25425
25487
  DEFAULT_SMART_RUNNER_CONFIG = {
25426
25488
  enabled: true,
25427
25489
  testFilePatterns: [...DEFAULT_TEST_FILE_PATTERNS],
25428
- fallback: "import-grep"
25490
+ fallback: "import-grep",
25491
+ maxScanFiles: MAX_GREP_TEST_FILES
25429
25492
  };
25430
25493
  _scopedSelectionDeps = {
25431
25494
  getChangedNonTestFiles: _smartRunnerDeps.getChangedNonTestFiles,
@@ -27573,7 +27636,7 @@ class SessionScratchProvider {
27573
27636
  if (!dirs || dirs.length === 0) {
27574
27637
  return { chunks: [], pullTools: [] };
27575
27638
  }
27576
- const ignoreMatchers = await resolveNaxIgnorePatterns(request.repoRoot, request.packageDir);
27639
+ const ignoreMatchers = request.naxIgnoreIndex?.getMatchers(request.packageDir) ?? await resolveNaxIgnorePatterns(request.repoRoot, request.packageDir);
27577
27640
  const chunks = [];
27578
27641
  for (const dir of dirs) {
27579
27642
  const chunk = await readScratchDir(dir, request.agentId, ignoreMatchers);
@@ -27895,7 +27958,8 @@ var init_static_rules = __esm(() => {
27895
27958
 
27896
27959
  // src/prd/types.ts
27897
27960
  function getContextFiles(story) {
27898
- const files = story.contextFiles ?? story.relevantFiles ?? [];
27961
+ const legacyFiles = story.relevantFiles;
27962
+ const files = story.contextFiles ?? legacyFiles ?? [];
27899
27963
  return files.map((f) => typeof f === "string" ? f : f.path);
27900
27964
  }
27901
27965
  function getExpectedFiles(story) {
@@ -28407,16 +28471,28 @@ function markStoryFailed(prd, storyId, failureCategory, failureStage, statusWrit
28407
28471
  }
28408
28472
  }
28409
28473
  }
28410
- function resetFailedStoriesToPending(prd, resetRef = false, storyIsolation) {
28474
+ function resetFailedStoriesToPending(prd, opts = {}) {
28475
+ const { resetRef = false, storyIsolation, resetMode = "initial" } = opts;
28411
28476
  const reset = [];
28412
28477
  for (const story of prd.userStories) {
28413
- if (story.status === "failed") {
28414
- story.status = "pending";
28415
- if (resetRef || storyIsolation === "worktree") {
28416
- story.storyGitRef = undefined;
28417
- }
28418
- reset.push(story);
28478
+ if (story.status !== "failed")
28479
+ continue;
28480
+ story.status = "pending";
28481
+ story.attempts = 0;
28482
+ if (resetMode === "initial" && story.routing) {
28483
+ story.routing = {
28484
+ ...story.routing,
28485
+ ...story.routing.initialModelTier !== undefined && {
28486
+ modelTier: story.routing.initialModelTier
28487
+ },
28488
+ agent: story.routing.initialAgent
28489
+ };
28490
+ story.escalations = [];
28419
28491
  }
28492
+ if (resetRef || storyIsolation === "worktree") {
28493
+ story.storyGitRef = undefined;
28494
+ }
28495
+ reset.push(story);
28420
28496
  }
28421
28497
  return reset;
28422
28498
  }
@@ -28553,6 +28629,7 @@ async function scanTestFiles(options) {
28553
28629
  const patterns = deriveTestPatterns(contextFiles, resolvedTestGlobs);
28554
28630
  allowedBasenames = new Set(patterns);
28555
28631
  }
28632
+ const maxScanFiles = options.maxScanFiles ?? DEFAULT_MAX_SCAN_FILES;
28556
28633
  const glob = new Glob2(testPattern);
28557
28634
  const files = [];
28558
28635
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
@@ -28562,6 +28639,13 @@ async function scanTestFiles(options) {
28562
28639
  continue;
28563
28640
  }
28564
28641
  }
28642
+ if (files.length >= maxScanFiles) {
28643
+ getLogger().debug("test-scanner", "Glob cap reached \u2014 results truncated", {
28644
+ cap: maxScanFiles,
28645
+ scanDir
28646
+ });
28647
+ break;
28648
+ }
28565
28649
  const fullPath = path.join(scanDir, filePath);
28566
28650
  try {
28567
28651
  const source = await Bun.file(fullPath).text();
@@ -28658,6 +28742,7 @@ async function generateTestCoverageSummary(options) {
28658
28742
  const tokens = estimateTokens4(summary);
28659
28743
  return { files, totalTests, summary, tokens };
28660
28744
  }
28745
+ var DEFAULT_MAX_SCAN_FILES = 200;
28661
28746
  var init_test_scanner = __esm(() => {
28662
28747
  init_logger2();
28663
28748
  init_conventions();
@@ -28692,12 +28777,14 @@ class TestCoverageProvider {
28692
28777
  const resolved = await _testCoverageProviderDeps.resolveTestFilePatterns(this.config, request.repoRoot, relPackageDir);
28693
28778
  const contextFiles = _testCoverageProviderDeps.getContextFiles(this.story);
28694
28779
  const globs = resolved.patterns ?? resolved.globs;
28780
+ const smartCfg = coerceSmartRunner(this.config.execution?.smartTestRunner);
28695
28781
  const scanOptions = {
28696
28782
  workdir: request.packageDir,
28697
28783
  testDir: tcConfig.testDir,
28698
28784
  maxTokens: tcConfig.maxTokens ?? 500,
28699
28785
  detail: tcConfig.detail ?? "names-and-counts",
28700
28786
  scopeToStory: tcConfig.scopeToStory ?? true,
28787
+ maxScanFiles: smartCfg.maxScanFiles,
28701
28788
  contextFiles,
28702
28789
  resolvedTestGlobs: globs
28703
28790
  };
@@ -28729,6 +28816,7 @@ class TestCoverageProvider {
28729
28816
  }
28730
28817
  var _testCoverageProviderDeps;
28731
28818
  var init_test_coverage = __esm(() => {
28819
+ init_test_runners();
28732
28820
  init_logger2();
28733
28821
  init_prd();
28734
28822
  init_resolver();
@@ -29602,10 +29690,12 @@ function formatPriorFailures(failures) {
29602
29690
  return "";
29603
29691
  }
29604
29692
  const parts = [];
29605
- parts.push(`## Prior Failures (Structured Context)
29606
- `);
29607
29693
  for (const failure of failures) {
29608
29694
  parts.push(`### Attempt ${failure.attempt} \u2014 ${failure.modelTier}`);
29695
+ if (failure.agent) {
29696
+ const profilePart = failure.agentProfileId ? ` (profile: ${failure.agentProfileId})` : "";
29697
+ parts.push(`**Agent:** ${failure.agent}${profilePart}`);
29698
+ }
29609
29699
  parts.push(`**Stage:** ${failure.stage}`);
29610
29700
  parts.push(`**Summary:** ${failure.summary}`);
29611
29701
  if (failure.testFailures && failure.testFailures.length > 0) {
@@ -29711,10 +29801,14 @@ function formatContextAsMarkdown(built) {
29711
29801
  byType.set(element.type, existing);
29712
29802
  }
29713
29803
  renderSection(sections, byType, "progress", `## Progress
29804
+ `, renderSimple);
29805
+ renderSection(sections, byType, "prior-failures", `## Prior Failures (Structured Context)
29714
29806
  `, renderSimple);
29715
29807
  renderErrorSection(sections, byType);
29716
29808
  renderSection(sections, byType, "test-coverage", "", renderSimple);
29717
29809
  renderSection(sections, byType, "story", `## Current Story
29810
+ `, renderSimple);
29811
+ renderSection(sections, byType, "planning-analysis", `## Planning Analysis
29718
29812
  `, renderSimple);
29719
29813
  renderSection(sections, byType, "dependency", `## Dependency Stories
29720
29814
  `, renderSimple);
@@ -30026,10 +30120,9 @@ async function gitLsFiles2(workdir) {
30026
30120
  stdout: "pipe",
30027
30121
  stderr: "pipe"
30028
30122
  });
30029
- const exitCode = await proc.exited;
30123
+ const [exitCode, output] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
30030
30124
  if (exitCode !== 0)
30031
30125
  return null;
30032
- const output = await new Response(proc.stdout).text();
30033
30126
  return output.split(`
30034
30127
  `).filter(Boolean);
30035
30128
  } catch {
@@ -33167,12 +33260,16 @@ async function collectDiff(workdir, storyGitRef, excludePatterns, options) {
33167
33260
  stdout: "pipe",
33168
33261
  stderr: "pipe"
33169
33262
  });
33170
- const [exitCode, stdout] = await Promise.all([
33263
+ const [exitCode, stdout, stderr] = await Promise.all([
33171
33264
  proc.exited,
33172
33265
  new Response(proc.stdout).text(),
33173
33266
  new Response(proc.stderr).text()
33174
33267
  ]);
33175
- return exitCode === 0 ? stdout : "";
33268
+ if (exitCode !== 0) {
33269
+ getSafeLogger()?.warn("diff-utils", "git diff failed \u2014 skipping review diff", { storyGitRef, stderr });
33270
+ return null;
33271
+ }
33272
+ return stdout;
33176
33273
  }
33177
33274
  async function collectDiffStat(workdir, storyGitRef, options) {
33178
33275
  const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
@@ -33423,6 +33520,9 @@ async function prepareSemanticReviewInput(args) {
33423
33520
  return { effectiveRef, stat, diff: undefined, excludePatterns };
33424
33521
  }
33425
33522
  const rawDiff = await collectDiff(workdir, effectiveRef, excludePatterns, { naxIgnoreIndex, packageDir });
33523
+ if (rawDiff === null) {
33524
+ return { effectiveRef, stat, diff: undefined, excludePatterns, skipReason: "git diff failed" };
33525
+ }
33426
33526
  const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat : undefined);
33427
33527
  if (!diff) {
33428
33528
  return { effectiveRef, stat, diff: undefined, excludePatterns, skipReason: "no production code changes" };
@@ -41050,10 +41150,9 @@ async function listChangedFiles(workdir, baseRef) {
41050
41150
  stdout: "pipe",
41051
41151
  stderr: "pipe"
41052
41152
  });
41053
- const exitCode = await proc.exited;
41153
+ const [exitCode, output] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
41054
41154
  if (exitCode !== 0)
41055
41155
  return null;
41056
- const output = await new Response(proc.stdout).text();
41057
41156
  return output.split(`
41058
41157
  `).map((line) => line.trim()).filter(Boolean).map(normalizePath3);
41059
41158
  }
@@ -45726,6 +45825,8 @@ class PidRegistry {
45726
45825
  pidsFilePath;
45727
45826
  pids = new Set;
45728
45827
  frozen = false;
45828
+ _writing = false;
45829
+ _pendingWrite = false;
45729
45830
  writeQueueTail = Promise.resolve();
45730
45831
  constructor(workdir, _platform) {
45731
45832
  this.workdir = workdir;
@@ -46023,11 +46124,22 @@ class PidRegistry {
46023
46124
  }
46024
46125
  }
46025
46126
  enqueueWrite() {
46026
- this.writeQueueTail = this.writeQueueTail.then(() => this.writePidsFile().catch((err) => {
46127
+ if (this._writing) {
46128
+ this._pendingWrite = true;
46129
+ return this.writeQueueTail;
46130
+ }
46131
+ this._writing = true;
46132
+ this.writeQueueTail = this.writePidsFile().catch((err) => {
46027
46133
  getSafeLogger()?.warn("pid-registry", "Failed to flush PID file \u2014 on-disk registry may be stale", {
46028
46134
  error: errorMessage(err)
46029
46135
  });
46030
- }));
46136
+ }).then(async () => {
46137
+ this._writing = false;
46138
+ if (this._pendingWrite) {
46139
+ this._pendingWrite = false;
46140
+ await this.enqueueWrite();
46141
+ }
46142
+ });
46031
46143
  return this.writeQueueTail;
46032
46144
  }
46033
46145
  }
@@ -46589,6 +46701,7 @@ class SessionManager {
46589
46701
  }
46590
46702
  this._busySessions.delete(handle.id);
46591
46703
  this._cancelledSessions.delete(handle.id);
46704
+ this._clearWatchdogCancelledCalls(handle.id);
46592
46705
  }
46593
46706
  async sendPrompt(handle, prompt, opts) {
46594
46707
  if (this._cancelledSessions.has(handle.id)) {
@@ -46673,6 +46786,10 @@ class SessionManager {
46673
46786
  sweepOrphans(ttlMs = DEFAULT_ORPHAN_TTL_MS) {
46674
46787
  return sweepOrphansImpl(this._sessions, ttlMs);
46675
46788
  }
46789
+ close() {
46790
+ this._agentStreamUnsubscribe?.();
46791
+ this._agentStreamUnsubscribe = undefined;
46792
+ }
46676
46793
  }
46677
46794
  var NULL_PROTOCOL_IDS;
46678
46795
  var init_manager2 = __esm(() => {
@@ -46937,6 +47054,9 @@ function createRuntime(config2, workdir, opts) {
46937
47054
  if (opts?.parentSignal && parentAbortHandler) {
46938
47055
  opts.parentSignal.removeEventListener("abort", parentAbortHandler);
46939
47056
  }
47057
+ agentManager.close();
47058
+ if (sessionManager instanceof SessionManager)
47059
+ sessionManager.close();
46940
47060
  const results = await Promise.allSettled([promptAuditor.flush(), reviewAuditor.flush(), costAggregator.drain()]);
46941
47061
  for (const r of results) {
46942
47062
  if (r.status === "rejected") {
@@ -52413,6 +52533,7 @@ async function collectStoryMetrics(ctx, storyStartTime) {
52413
52533
  startedAt: storyStartTime,
52414
52534
  completedAt: new Date().toISOString(),
52415
52535
  fullSuiteGatePassed,
52536
+ ...ctx.fullSuiteGateFailingFiles && ctx.fullSuiteGateFailingFiles.length > 0 ? { failingTestFiles: ctx.fullSuiteGateFailingFiles } : {},
52416
52537
  runtimeCrashes: ctx.storyRuntimeCrashes ?? 0,
52417
52538
  tokens: agentResult?.tokenUsage ? new TokenUsage({
52418
52539
  inputTokens: agentResult.tokenUsage.inputTokens,
@@ -52475,17 +52596,18 @@ async function saveRunMetrics(outputDir, runMetrics) {
52475
52596
  }
52476
52597
  }
52477
52598
  const hasTokenData = totalInputTokens > 0 || totalOutputTokens > 0 || totalCacheReadInputTokens > 0 || totalCacheCreationInputTokens > 0;
52478
- if (hasTokenData) {
52479
- runMetrics.totalTokens = new TokenUsage({
52599
+ const finalMetrics = hasTokenData ? {
52600
+ ...runMetrics,
52601
+ totalTokens: new TokenUsage({
52480
52602
  inputTokens: totalInputTokens,
52481
52603
  outputTokens: totalOutputTokens,
52482
52604
  cacheReadInputTokens: totalCacheReadInputTokens,
52483
52605
  cacheCreationInputTokens: totalCacheCreationInputTokens
52484
- });
52485
- }
52606
+ })
52607
+ } : runMetrics;
52486
52608
  const existing = await loadJsonFile(metricsPath, "metrics");
52487
52609
  const allMetrics = Array.isArray(existing) ? existing : [];
52488
- allMetrics.push(runMetrics);
52610
+ allMetrics.push(finalMetrics);
52489
52611
  await saveJsonFile(metricsPath, allMetrics, "metrics");
52490
52612
  }
52491
52613
  async function loadRunMetrics(outputDir) {
@@ -54652,29 +54774,27 @@ async function rollbackToRef(workdir, ref) {
54652
54774
  stdout: "pipe",
54653
54775
  stderr: "pipe"
54654
54776
  });
54655
- const exitCode = await resetProc.exited;
54777
+ const [exitCode, resetStderr] = await Promise.all([resetProc.exited, new Response(resetProc.stderr).text()]);
54656
54778
  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}`);
54779
+ logger.error("tdd", "Failed to rollback git changes", { ref, stderr: resetStderr });
54780
+ throw new Error(`Git rollback failed: ${resetStderr}`);
54660
54781
  }
54661
54782
  const cleanProc = _rollbackDeps.spawn(["git", "clean", "-fd"], {
54662
54783
  cwd: workdir,
54663
54784
  stdout: "pipe",
54664
54785
  stderr: "pipe"
54665
54786
  });
54666
- const cleanExitCode = await cleanProc.exited;
54787
+ const [cleanExitCode, cleanStderr] = await Promise.all([cleanProc.exited, new Response(cleanProc.stderr).text()]);
54667
54788
  if (cleanExitCode !== 0) {
54668
- const stderr = await new Response(cleanProc.stderr).text();
54669
- logger.warn("tdd", "Failed to clean untracked files", { stderr });
54789
+ logger.warn("tdd", "Failed to clean untracked files", { stderr: cleanStderr });
54670
54790
  }
54671
54791
  logger.info("tdd", "Successfully rolled back git changes", { ref });
54672
54792
  }
54673
54793
  async function captureSnapshotRef(workdir, storyId) {
54674
54794
  await _rollbackDeps.autoCommitIfDirty(workdir, "non-blocking-fix-snapshot", "snapshot", storyId);
54675
54795
  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;
54796
+ const [exitCode, shaRaw] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
54797
+ const sha = shaRaw.trim();
54678
54798
  if (exitCode !== 0) {
54679
54799
  throw new NaxError("git rev-parse HEAD failed in non-blocking-fix snapshot", "SNAPSHOT_REF_FAILED", {
54680
54800
  storyId,
@@ -54946,6 +55066,15 @@ function extractPhaseFindings(output) {
54946
55066
  const success2 = "success" in record2 ? record2.success === true : ("passed" in record2) ? record2.passed === true : findings.length === 0;
54947
55067
  return success2 ? [] : findings;
54948
55068
  }
55069
+ function gateFailureKeys(gateOutput) {
55070
+ const keys = new Set;
55071
+ for (const f of extractPhaseFindings(gateOutput)) {
55072
+ if (f.source !== "test-runner")
55073
+ continue;
55074
+ keys.add(`${f.file ?? ""}::${f.rule ?? ""}`);
55075
+ }
55076
+ return keys;
55077
+ }
54949
55078
  function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
54950
55079
  if (phase.kind !== "full-suite-gate")
54951
55080
  return false;
@@ -55335,6 +55464,8 @@ class ExecutionPlan {
55335
55464
  break;
55336
55465
  }
55337
55466
  }
55467
+ const gateName = this.state.fullSuiteGate?.slot.op.name;
55468
+ const preRectGateFailureKeys = gateName ? gateFailureKeys(phaseOutputs[gateName]) : new Set;
55338
55469
  const rectResult = await runRectification(this.ctx, this.state, phaseCosts, phaseOutputs);
55339
55470
  if (this.state.rectification && (!rectResult.rectificationExhausted || rectResult.liteScopeIncomplete)) {
55340
55471
  let resumeRectifyUsed = false;
@@ -55428,9 +55559,12 @@ class ExecutionPlan {
55428
55559
  });
55429
55560
  }
55430
55561
  const verifierName = this.state.verifier?.slot.op.name;
55431
- const gateName = this.state.fullSuiteGate?.slot.op.name;
55432
- const verifierPassedSsot = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
55433
- if (verifierPassedSsot && gateName !== undefined && !phasePassed(gateName, phaseOutputs[gateName], this.ctx.storyId)) {
55562
+ const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
55563
+ const gateRegressedDuringRect = gateName !== undefined && [...gateFailureKeys(phaseOutputs[gateName])].some((k) => !preRectGateFailureKeys.has(k));
55564
+ const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
55565
+ if (verifierExplicitlyPassed && gateRegressedDuringRect) {
55566
+ logger?.warn("story-orchestrator", "Gate regressed during rectification after verifier passed \u2014 verifier verdict is stale, failing story", { storyId: this.ctx.storyId, packageDir: this.ctx.packageDir });
55567
+ } else if (verifierPassedSsot && gateName !== undefined && !phasePassed(gateName, phaseOutputs[gateName], this.ctx.storyId)) {
55434
55568
  logger?.warn("story-orchestrator", "Full-suite gate failed but verifier judged story OK \u2014 treating gate failures as unrelated regressions", { storyId: this.ctx.storyId, packageDir: this.ctx.packageDir });
55435
55569
  }
55436
55570
  const success2 = Object.entries(phaseOutputs).every(([name, output]) => {
@@ -55468,7 +55602,8 @@ class ExecutionPlan {
55468
55602
  totalCostUsd,
55469
55603
  durationMs,
55470
55604
  phaseOutputs,
55471
- ...rectResult
55605
+ ...rectResult,
55606
+ gateRegressedDuringRect
55472
55607
  };
55473
55608
  }
55474
55609
  }
@@ -55664,7 +55799,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
55664
55799
  if (pkgQuality?.commands?.formatFix || pkgQuality?.commands?.formatFixScoped) {
55665
55800
  strategies.push(makeMechanicalFormatFixStrategy());
55666
55801
  }
55667
- if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
55802
+ const fullSuiteGatePhasePresent = Boolean(inputs.fullSuiteGate) && (isThreeSession || regressionMode === "per-story");
55803
+ const verifyScopedPhasePresent = !isThreeSession && Boolean(inputs.verifyScoped);
55804
+ if (fullSuiteGatePhasePresent || verifyScopedPhasePresent) {
55668
55805
  strategies.push(makeFullSuiteRectifyStrategy(story, config2, sink));
55669
55806
  }
55670
55807
  if (config2.quality.autofix?.enabled !== false) {
@@ -56117,7 +56254,7 @@ function extractPauseReason(phaseOutputs) {
56117
56254
  }
56118
56255
  return;
56119
56256
  }
56120
- function deriveTddFailureCategory(phaseOutputs, unfixedFindings) {
56257
+ function deriveTddFailureCategory(phaseOutputs, unfixedFindings, gateRegressedDuringRect) {
56121
56258
  const testWriterOutput = phaseOutputs[testWriterOp.name];
56122
56259
  if (testWriterOutput?.success === false) {
56123
56260
  return "session-failure";
@@ -56133,7 +56270,7 @@ function deriveTddFailureCategory(phaseOutputs, unfixedFindings) {
56133
56270
  }
56134
56271
  return "tests-failing";
56135
56272
  }
56136
- const verifierPassed = verifierOutput?.success === true;
56273
+ const verifierPassed = verifierOutput?.success === true && !gateRegressedDuringRect;
56137
56274
  if (!verifierPassed && unfixedFindings && unfixedFindings.length > 0) {
56138
56275
  const rectOutput = phaseOutputs.rectification;
56139
56276
  if (rectOutput?.exitReason && EXHAUSTED_EXIT_REASONS.has(rectOutput.exitReason) && unfixedFindings.some((f) => f.source === "test-runner")) {
@@ -56182,6 +56319,11 @@ async function applyPostRunInspection(ctx, planResult, opts) {
56182
56319
  if (fullSuiteGateOutput?.passed) {
56183
56320
  ctx.fullSuiteGatePassed = true;
56184
56321
  }
56322
+ const gateFailingFiles = [
56323
+ ...new Set((fullSuiteGateOutput?.findings ?? []).map((f) => f.file).filter((f) => !!f))
56324
+ ];
56325
+ if (gateFailingFiles.length > 0)
56326
+ ctx.fullSuiteGateFailingFiles = gateFailingFiles;
56185
56327
  ctx.selfVerification = parseSelfVerificationMarker(agentResult.output ?? "", ctx.workdir);
56186
56328
  const selfVerificationFailed = ctx.selfVerification.lint === "fail" || ctx.selfVerification.typecheck === "fail";
56187
56329
  if (ctx.config.context?.v2?.enabled && ctx.sessionScratchDir) {
@@ -56248,7 +56390,7 @@ async function applyPostRunInspection(ctx, planResult, opts) {
56248
56390
  }
56249
56391
  }
56250
56392
  const pauseReason = extractPauseReason(planResult.phaseOutputs);
56251
- const failureCategory = isTdd && !planResult.success ? deriveTddFailureCategory(planResult.phaseOutputs, planResult.unfixedFindings) : undefined;
56393
+ const failureCategory = isTdd && !planResult.success ? deriveTddFailureCategory(planResult.phaseOutputs, planResult.unfixedFindings, planResult.gateRegressedDuringRect) : undefined;
56252
56394
  if (isTdd && !planResult.success && !failureCategory) {
56253
56395
  const phaseSignals = {};
56254
56396
  for (const [name, output] of Object.entries(planResult.phaseOutputs)) {
@@ -57103,6 +57245,7 @@ var init_routing2 = __esm(() => {
57103
57245
  const neverEscalated = !hasEscalationRecords;
57104
57246
  const initialAgent = ctx.story.routing?.initialAgent ?? (neverEscalated ? routing.agent : undefined);
57105
57247
  const initialProfileId = ctx.story.routing?.initialProfileId ?? (neverEscalated ? ctx.story.routing?.agentProfileId : undefined);
57248
+ const initialModelTier = ctx.story.routing?.initialModelTier ?? (neverEscalated ? routing.modelTier : undefined);
57106
57249
  ctx.story.routing = {
57107
57250
  ...ctx.story.routing ?? {},
57108
57251
  complexity: routing.complexity,
@@ -57112,7 +57255,8 @@ var init_routing2 = __esm(() => {
57112
57255
  modelTier: routing.modelTier,
57113
57256
  ...routing.agent !== undefined && { agent: routing.agent },
57114
57257
  ...initialAgent !== undefined && { initialAgent },
57115
- ...initialProfileId !== undefined && { initialProfileId }
57258
+ ...initialProfileId !== undefined && { initialProfileId },
57259
+ ...initialModelTier !== undefined && { initialModelTier }
57116
57260
  };
57117
57261
  if (ctx.prdPath) {
57118
57262
  await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
@@ -60325,7 +60469,7 @@ var package_default;
60325
60469
  var init_package = __esm(() => {
60326
60470
  package_default = {
60327
60471
  name: "@nathapp/nax",
60328
- version: "0.70.0-canary.4",
60472
+ version: "0.70.0-canary.6",
60329
60473
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60330
60474
  type: "module",
60331
60475
  bin: {
@@ -60420,8 +60564,8 @@ var init_version = __esm(() => {
60420
60564
  NAX_VERSION = package_default.version;
60421
60565
  NAX_COMMIT = (() => {
60422
60566
  try {
60423
- if (/^[0-9a-f]{6,10}$/.test("e2a854e7"))
60424
- return "e2a854e7";
60567
+ if (/^[0-9a-f]{6,10}$/.test("4423f3a5"))
60568
+ return "4423f3a5";
60425
60569
  } catch {}
60426
60570
  try {
60427
60571
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -60441,11 +60585,11 @@ var init_version = __esm(() => {
60441
60585
 
60442
60586
  // src/execution/crash-heartbeat.ts
60443
60587
  import { appendFileSync as appendFileSync2 } from "fs";
60444
- async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60588
+ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60445
60589
  const logger = _heartbeatDeps.getSafeLogger();
60446
- while (heartbeatActive) {
60590
+ while (gen === _heartbeatGen && _heartbeatActive) {
60447
60591
  await _heartbeatDeps.sleep(60000);
60448
- if (!heartbeatActive)
60592
+ if (gen !== _heartbeatGen || !_heartbeatActive)
60449
60593
  break;
60450
60594
  try {
60451
60595
  logger?.debug("crash-recovery", "Heartbeat");
@@ -60474,9 +60618,9 @@ async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFil
60474
60618
  }
60475
60619
  function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
60476
60620
  const logger = _heartbeatDeps.getSafeLogger();
60477
- stopHeartbeat();
60478
- heartbeatActive = true;
60479
- heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
60621
+ _heartbeatActive = true;
60622
+ const gen = ++_heartbeatGen;
60623
+ heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
60480
60624
  _heartbeatDeps.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
60481
60625
  error: err instanceof Error ? err.message : String(err)
60482
60626
  });
@@ -60484,12 +60628,13 @@ function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath
60484
60628
  logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
60485
60629
  }
60486
60630
  function stopHeartbeat() {
60487
- if (heartbeatActive) {
60488
- heartbeatActive = false;
60631
+ if (_heartbeatActive) {
60632
+ _heartbeatActive = false;
60633
+ _heartbeatGen++;
60489
60634
  getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
60490
60635
  }
60491
60636
  }
60492
- var _heartbeatDeps, heartbeatActive = false;
60637
+ var _heartbeatDeps, _heartbeatGen = 0, _heartbeatActive = false;
60493
60638
  var init_crash_heartbeat = __esm(() => {
60494
60639
  init_logger2();
60495
60640
  _heartbeatDeps = {
@@ -61374,6 +61519,15 @@ async function findResponsibleStory(testFile, workdir, passedStories) {
61374
61519
  }
61375
61520
  return;
61376
61521
  }
61522
+ function findResponsibleStoryByTransition(testFile, snapshots) {
61523
+ const ordered = [...snapshots].sort((a, b) => a.completedAt.localeCompare(b.completedAt) || a.storyId.localeCompare(b.storyId));
61524
+ for (const snap of ordered) {
61525
+ if (snap.failingTestFiles?.includes(testFile)) {
61526
+ return snap.storyId;
61527
+ }
61528
+ }
61529
+ return;
61530
+ }
61377
61531
  async function runDeferredRegression(options) {
61378
61532
  const logger = getSafeLogger();
61379
61533
  const { config: config2, prd, workdir, runtime } = options;
@@ -61521,8 +61675,21 @@ async function runDeferredRegression(options) {
61521
61675
  }
61522
61676
  } else {
61523
61677
  const testFilesArray = Array.from(testFilesInFailures);
61678
+ const snapshots = options.storyMetrics ?? [];
61679
+ const passedById = new Map(passedStories.map((s) => [s.id, s]));
61524
61680
  for (const testFile of testFilesArray) {
61525
- const responsibleStory = await findResponsibleStory(testFile, workdir, passedStories);
61681
+ let responsibleStory;
61682
+ const transitionId = findResponsibleStoryByTransition(testFile, snapshots);
61683
+ if (transitionId && passedById.has(transitionId)) {
61684
+ responsibleStory = passedById.get(transitionId);
61685
+ logger?.info("regression", "Mapped test to story via gate transition", {
61686
+ storyId: transitionId,
61687
+ testFile
61688
+ });
61689
+ }
61690
+ if (!responsibleStory) {
61691
+ responsibleStory = await findResponsibleStory(testFile, workdir, passedStories);
61692
+ }
61526
61693
  if (responsibleStory) {
61527
61694
  affectedStories.add(responsibleStory.id);
61528
61695
  affectedStoriesObjs.set(responsibleStory.id, responsibleStory);
@@ -61707,7 +61874,12 @@ async function handleRunCompletion(options) {
61707
61874
  config: config2,
61708
61875
  prd,
61709
61876
  workdir,
61710
- runtime: options.runtime
61877
+ runtime: options.runtime,
61878
+ storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
61879
+ storyId: m.storyId,
61880
+ completedAt: m.completedAt,
61881
+ failingTestFiles: m.failingTestFiles
61882
+ }))
61711
61883
  });
61712
61884
  const lastRunAt = new Date().toISOString();
61713
61885
  logger?.info("regression", "Deferred regression gate completed", {
@@ -62656,9 +62828,8 @@ ${missing.join(`
62656
62828
  stdout: "pipe",
62657
62829
  stderr: "pipe"
62658
62830
  });
62659
- const exitCode = await proc.exited;
62831
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62660
62832
  if (exitCode !== 0) {
62661
- const stderr = await new Response(proc.stderr).text();
62662
62833
  throw new Error(`Failed to create worktree: ${stderr || "unknown error"}`);
62663
62834
  }
62664
62835
  } catch (error48) {
@@ -62691,9 +62862,8 @@ ${missing.join(`
62691
62862
  stdout: "pipe",
62692
62863
  stderr: "pipe"
62693
62864
  });
62694
- const exitCode = await proc.exited;
62865
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62695
62866
  if (exitCode !== 0) {
62696
- const stderr = await new Response(proc.stderr).text();
62697
62867
  if (stderr.includes("not found") || stderr.includes("does not exist") || stderr.includes("no such worktree") || stderr.includes("is not a working tree")) {
62698
62868
  throw new Error(`Worktree not found: ${worktreePath}`);
62699
62869
  }
@@ -62711,9 +62881,8 @@ ${missing.join(`
62711
62881
  stdout: "pipe",
62712
62882
  stderr: "pipe"
62713
62883
  });
62714
- const exitCode = await proc.exited;
62884
+ const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
62715
62885
  if (exitCode !== 0) {
62716
- const stderr = await new Response(proc.stderr).text();
62717
62886
  if (!stderr.includes("not found")) {
62718
62887
  const logger = getSafeLogger();
62719
62888
  logger?.warn("worktree", `Failed to delete branch ${branchName}`, { stderr });
@@ -62733,12 +62902,14 @@ ${missing.join(`
62733
62902
  stdout: "pipe",
62734
62903
  stderr: "pipe"
62735
62904
  });
62736
- const exitCode = await proc.exited;
62905
+ const [exitCode, stderr, stdout] = await Promise.all([
62906
+ proc.exited,
62907
+ new Response(proc.stderr).text(),
62908
+ new Response(proc.stdout).text()
62909
+ ]);
62737
62910
  if (exitCode !== 0) {
62738
- const stderr = await new Response(proc.stderr).text();
62739
62911
  throw new Error(`Failed to list worktrees: ${stderr || "unknown error"}`);
62740
62912
  }
62741
- const stdout = await new Response(proc.stdout).text();
62742
62913
  return this.parseWorktreeList(stdout);
62743
62914
  } catch (error48) {
62744
62915
  if (error48 instanceof Error) {
@@ -62850,9 +63021,11 @@ class MergeEngine {
62850
63021
  stdout: "pipe",
62851
63022
  stderr: "pipe"
62852
63023
  });
62853
- const exitCode = await mergeProc.exited;
62854
- const stderr = await new Response(mergeProc.stderr).text();
62855
- const stdout = await new Response(mergeProc.stdout).text();
63024
+ const [exitCode, stderr, stdout] = await Promise.all([
63025
+ mergeProc.exited,
63026
+ new Response(mergeProc.stderr).text(),
63027
+ new Response(mergeProc.stdout).text()
63028
+ ]);
62856
63029
  if (exitCode === 0) {
62857
63030
  try {
62858
63031
  await this.worktreeManager.remove(projectRoot, storyId);
@@ -62979,19 +63152,25 @@ ${stderr}`;
62979
63152
  stdout: "pipe",
62980
63153
  stderr: "pipe"
62981
63154
  });
62982
- const exitCode = await currentBranchProc.exited;
63155
+ const [exitCode, currentBranchRaw] = await Promise.all([
63156
+ currentBranchProc.exited,
63157
+ new Response(currentBranchProc.stdout).text()
63158
+ ]);
62983
63159
  if (exitCode !== 0) {
62984
63160
  throw new Error("Failed to get current branch");
62985
63161
  }
62986
- const currentBranch = (await new Response(currentBranchProc.stdout).text()).trim();
63162
+ const currentBranch = currentBranchRaw.trim();
62987
63163
  const rebaseProc = _mergeDeps.spawn(["git", "rebase", currentBranch], {
62988
63164
  cwd: worktreePath,
62989
63165
  stdout: "pipe",
62990
63166
  stderr: "pipe"
62991
63167
  });
62992
- const rebaseExitCode = await rebaseProc.exited;
63168
+ const [rebaseExitCode, rebaseStderr] = await Promise.all([
63169
+ rebaseProc.exited,
63170
+ new Response(rebaseProc.stderr).text()
63171
+ ]);
62993
63172
  if (rebaseExitCode !== 0) {
62994
- const stderr = await new Response(rebaseProc.stderr).text();
63173
+ const stderr = rebaseStderr;
62995
63174
  const abortProc = _mergeDeps.spawn(["git", "rebase", "--abort"], {
62996
63175
  cwd: worktreePath,
62997
63176
  stdout: "pipe",
@@ -63014,11 +63193,10 @@ ${stderr}`;
63014
63193
  stdout: "pipe",
63015
63194
  stderr: "pipe"
63016
63195
  });
63017
- const exitCode = await proc.exited;
63196
+ const [exitCode, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
63018
63197
  if (exitCode !== 0) {
63019
63198
  return [];
63020
63199
  }
63021
- const stdout = await new Response(proc.stdout).text();
63022
63200
  return stdout.trim().split(`
63023
63201
  `).filter((line) => line.length > 0);
63024
63202
  } catch {
@@ -63234,13 +63412,17 @@ function buildEscalationFailure(story, currentTier, reviewFindings, cost, pipeli
63234
63412
  summary,
63235
63413
  reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
63236
63414
  cost: cost ?? 0,
63237
- timestamp: new Date().toISOString()
63415
+ timestamp: new Date().toISOString(),
63416
+ ...story.routing?.agent !== undefined ? { agent: story.routing.agent } : {},
63417
+ ...story.routing?.agentProfileId !== undefined ? { agentProfileId: story.routing.agentProfileId } : {}
63238
63418
  };
63239
63419
  }
63240
- function buildEscalationRecord(currentTier, nextTier, reason) {
63420
+ function buildEscalationRecord(currentTier, nextTier, reason, agents) {
63241
63421
  return {
63242
63422
  fromTier: currentTier,
63243
63423
  toTier: nextTier,
63424
+ ...agents?.fromAgent !== undefined ? { fromAgent: agents.fromAgent } : {},
63425
+ ...agents?.toAgent !== undefined ? { toAgent: agents.toAgent } : {},
63244
63426
  reason,
63245
63427
  timestamp: new Date().toISOString()
63246
63428
  };
@@ -63337,7 +63519,7 @@ async function handleTierEscalation(ctx) {
63337
63519
  const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
63338
63520
  const isChangingTier = currentStoryTier !== escalatedTier;
63339
63521
  const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
63340
- const escalationRecord = isChangingTier || shouldSwitchToTestAfter ? buildEscalationRecord(currentStoryTier, shouldSwitchToTestAfter ? currentStoryTier : escalatedTier, ctx.pipelineResult.reason ?? "Escalated to next retry path") : undefined;
63522
+ const escalationRecord = isChangingTier || shouldSwitchToTestAfter ? buildEscalationRecord(currentStoryTier, shouldSwitchToTestAfter ? currentStoryTier : escalatedTier, ctx.pipelineResult.reason ?? "Escalated to next retry path", { fromAgent: s.routing?.agent, toAgent: nextAgent }) : undefined;
63341
63523
  const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost, verifiedPipelineReason, escalateFailureCategory);
63342
63524
  return {
63343
63525
  ...s,
@@ -63758,7 +63940,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
63758
63940
  }
63759
63941
  }
63760
63942
  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;
63943
+ const profileOverride = profileOverrideFromConfig(ctx.config);
63762
63944
  const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join81(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
63763
63945
  let dependencyContext;
63764
63946
  if (ctx.config.execution.storyIsolation === "worktree") {
@@ -63904,6 +64086,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
63904
64086
  }
63905
64087
  var _iterationRunnerDeps;
63906
64088
  var init_iteration_runner = __esm(() => {
64089
+ init_config();
63907
64090
  init_loader();
63908
64091
  init_logger2();
63909
64092
  init_runner4();
@@ -64134,7 +64317,7 @@ async function runParallelBatch(options) {
64134
64317
  worktreePaths.set(story.id, path22.join(workdir, ".nax-wt", story.id));
64135
64318
  }
64136
64319
  const rootConfigPath = path22.join(workdir, ".nax", "config.json");
64137
- const profileOverride = config2.profile && config2.profile !== "default" ? { profile: config2.profile } : undefined;
64320
+ const profileOverride = profileOverrideFromConfig(config2);
64138
64321
  const storyEffectiveConfigs = new Map;
64139
64322
  const configResults = await Promise.allSettled(stories.filter((story) => story.workdir).map(async (story) => {
64140
64323
  try {
@@ -64295,6 +64478,7 @@ async function runParallelBatch(options) {
64295
64478
  }
64296
64479
  var _parallelBatchDeps;
64297
64480
  var init_parallel_batch = __esm(() => {
64481
+ init_config();
64298
64482
  init_loader();
64299
64483
  init_logger2();
64300
64484
  init_dependencies();
@@ -64361,13 +64545,15 @@ async function executeUnified(ctx, initialPrd) {
64361
64545
  };
64362
64546
  for (const fn of _prevRunUnsubscribers)
64363
64547
  fn();
64364
- _prevRunUnsubscribers = [
64548
+ _prevRunUnsubscribers = [];
64549
+ const thisRunUnsubscribers = [
64365
64550
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature),
64366
64551
  wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime),
64367
64552
  wireInteraction(pipelineEventBus, ctx.interactionChain, ctx.config),
64368
64553
  wireEventsWriter(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir),
64369
64554
  wireRegistry(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir, ctx.runtime.outputDir)
64370
64555
  ];
64556
+ _prevRunUnsubscribers = thisRunUnsubscribers;
64371
64557
  pipelineEventBus.emit({
64372
64558
  type: "run:started",
64373
64559
  feature: ctx.feature,
@@ -64384,6 +64570,7 @@ async function executeUnified(ctx, initialPrd) {
64384
64570
  deferredReview
64385
64571
  });
64386
64572
  startHeartbeat(ctx.statusWriter, () => totalCost, () => iterations, ctx.logFilePath);
64573
+ let _executeThrew = false;
64387
64574
  try {
64388
64575
  if (isComplete(prd)) {
64389
64576
  logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
@@ -64790,7 +64977,16 @@ async function executeUnified(ctx, initialPrd) {
64790
64977
  }, ctx.eventEmitter);
64791
64978
  }
64792
64979
  return buildResult2("max-iterations");
64793
- } finally {}
64980
+ } catch (err) {
64981
+ _executeThrew = true;
64982
+ throw err;
64983
+ } finally {
64984
+ if (_executeThrew && _prevRunUnsubscribers === thisRunUnsubscribers) {
64985
+ for (const fn of thisRunUnsubscribers)
64986
+ fn();
64987
+ _prevRunUnsubscribers = [];
64988
+ }
64989
+ }
64794
64990
  }
64795
64991
  function reconcileBatchOutcome(prd, batchResult) {
64796
64992
  for (const story of batchResult.completed) {
@@ -65542,7 +65738,8 @@ async function initializeRun(ctx) {
65542
65738
  prd = await reconcileState(prd, ctx.prdPath, ctx.workdir, ctx.config);
65543
65739
  const resetRef = ctx.config.review?.semantic?.resetRefOnRerun ?? false;
65544
65740
  const storyIsolation = ctx.config.execution.storyIsolation;
65545
- const resetStories = resetFailedStoriesToPending(prd, resetRef, storyIsolation);
65741
+ const resetMode = ctx.config.autoMode.escalation.resetMode;
65742
+ const resetStories = resetFailedStoriesToPending(prd, { resetRef, storyIsolation, resetMode });
65546
65743
  if (resetStories.length > 0) {
65547
65744
  const resetIds = resetStories.map((s) => s.id);
65548
65745
  logger?.info("run-initialization", "Reset failed stories to pending for re-run", { storyIds: resetIds });
@@ -65882,6 +66079,7 @@ async function setupRun(options) {
65882
66079
  const resolvedPatterns = await resolveTestFilePatterns(config2, workdir);
65883
66080
  const isTestFileFn = (filename) => resolvedPatterns.regex.some((re) => re.test(filename));
65884
66081
  const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir, config2.disabledPlugins, isTestFileFn);
66082
+ clearCache();
65885
66083
  logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
65886
66084
  plugins: pluginRegistry.plugins.map((p) => ({ name: p.name, version: p.version, provides: p.provides }))
65887
66085
  });
@@ -65935,6 +66133,7 @@ async function setupRun(options) {
65935
66133
  var _runSetupDeps;
65936
66134
  var init_run_setup = __esm(() => {
65937
66135
  init_pipeline();
66136
+ init_routing();
65938
66137
  init_test_runners();
65939
66138
  init_paths();
65940
66139
  init_errors();
@@ -95771,7 +95970,8 @@ function finalizePrdRouting(prd, agentRouting, profileName) {
95771
95970
  agentProfileId: assignment.agentProfileId,
95772
95971
  profileModelTier: assignment.profileModelTier,
95773
95972
  initialAgent: story.routing?.initialAgent ?? assignment.agent,
95774
- initialProfileId: story.routing?.initialProfileId ?? assignment.agentProfileId
95973
+ initialProfileId: story.routing?.initialProfileId ?? assignment.agentProfileId,
95974
+ initialModelTier: story.routing?.initialModelTier ?? assignment.profileModelTier
95775
95975
  };
95776
95976
  return { ...story, routing };
95777
95977
  });
@@ -97864,8 +98064,9 @@ async function rulesLintCommand(options) {
97864
98064
  init_config();
97865
98065
  init_logger2();
97866
98066
  async function resolveRunProfileOverride(opts) {
97867
- if (opts.cliProfile)
97868
- return opts.cliProfile;
98067
+ const cliChain = parseProfileList(opts.cliProfile);
98068
+ if (cliChain.length > 0)
98069
+ return cliChain;
97869
98070
  if (opts.envProfile)
97870
98071
  return;
97871
98072
  const readJson = opts._readJson ?? (async (path19) => {
@@ -97876,15 +98077,15 @@ async function resolveRunProfileOverride(opts) {
97876
98077
  });
97877
98078
  try {
97878
98079
  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;
98080
+ const rp = prd?.routingProfile;
98081
+ const prdChain = parseProfileList(typeof rp === "string" || Array.isArray(rp) ? rp : undefined);
98082
+ if (prdChain.length > 0) {
97883
98083
  const listNames = opts._listProfileNames ?? (async () => (await listProfiles(opts.projectRoot)).map((p) => p.name));
97884
98084
  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 });
98085
+ const missing = prdChain.filter((name) => name !== "default" && !available.includes(name));
98086
+ if (missing.length === 0)
98087
+ return prdChain;
98088
+ 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
98089
  }
97889
98090
  } catch {}
97890
98091
  return;
@@ -105271,6 +105472,7 @@ var import_react28 = __toESM(require_react(), 1);
105271
105472
  var import_react35 = __toESM(require_react(), 1);
105272
105473
 
105273
105474
  // src/utils/queue-writer.ts
105475
+ var _writeChains = new Map;
105274
105476
  async function writeQueueCommand(queueFilePath, command) {
105275
105477
  let commandLine2;
105276
105478
  switch (command.type) {
@@ -105288,13 +105490,22 @@ async function writeQueueCommand(queueFilePath, command) {
105288
105490
  throw new Error(`Unhandled queue command: ${_exhaustive}`);
105289
105491
  }
105290
105492
  }
105291
- const file3 = Bun.file(queueFilePath);
105292
- const existingContent = await file3.text().catch(() => "");
105293
- const newContent = existingContent ? `${existingContent.trimEnd()}
105493
+ const chain = _writeChains.get(queueFilePath) ?? Promise.resolve();
105494
+ const next = chain.then(async () => {
105495
+ const existing = await Bun.file(queueFilePath).text().catch(() => "");
105496
+ const content = existing ? `${existing.trimEnd()}
105294
105497
  ${commandLine2}
105295
105498
  ` : `${commandLine2}
105296
105499
  `;
105297
- await Bun.write(queueFilePath, newContent);
105500
+ await Bun.write(queueFilePath, content);
105501
+ });
105502
+ const settled = next.catch(() => {});
105503
+ _writeChains.set(queueFilePath, settled);
105504
+ settled.then(() => {
105505
+ if (_writeChains.get(queueFilePath) === settled)
105506
+ _writeChains.delete(queueFilePath);
105507
+ });
105508
+ await next;
105298
105509
  }
105299
105510
 
105300
105511
  // src/tui/components/CostOverlay.tsx
@@ -106939,6 +107150,9 @@ function renderTui(props) {
106939
107150
  init_version();
106940
107151
  var program2 = new Command;
106941
107152
  program2.name("nax").description("AI Coding Agent Orchestrator \u2014 loops until done").version(NAX_VERSION);
107153
+ function collectProfile(value, previous) {
107154
+ return previous.concat(value);
107155
+ }
106942
107156
  async function promptForConfirmation(question) {
106943
107157
  if (!process.stdin.isTTY) {
106944
107158
  return true;
@@ -107136,7 +107350,7 @@ program2.command("setup").description("Analyze repo and generate .nax/config.jso
107136
107350
  });
107137
107351
  process.exit(exitCode);
107138
107352
  });
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) => {
107353
+ 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
107354
  let workdir;
107141
107355
  try {
107142
107356
  workdir = validateDirectory(options.dir);
@@ -107173,13 +107387,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
107173
107387
  }
107174
107388
  const naxDir = findProjectDir(workdir);
107175
107389
  const cliOverrides = {};
107390
+ const cliProfiles = options.profile ?? [];
107176
107391
  const profileOverride = naxDir ? await resolveRunProfileOverride({
107177
107392
  prdPath: join87(naxDir, "features", options.feature, "prd.json"),
107178
107393
  projectRoot: workdir,
107179
- cliProfile: options.profile,
107394
+ cliProfile: cliProfiles,
107180
107395
  envProfile: process.env.NAX_PROFILE
107181
- }) : options.profile;
107182
- if (profileOverride) {
107396
+ }) : cliProfiles;
107397
+ if (profileOverride && profileOverride.length > 0) {
107183
107398
  cliOverrides.profile = profileOverride;
107184
107399
  }
107185
107400
  const config2 = await loadConfig(naxDir ?? undefined, cliOverrides);
@@ -107480,7 +107695,7 @@ Features:
107480
107695
  }
107481
107696
  console.log();
107482
107697
  });
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) => {
107698
+ 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
107699
  if (description) {
107485
107700
  console.error(source_default.red(`Error: Positional args removed in plan v2.
107486
107701
 
@@ -107500,8 +107715,9 @@ Use: nax plan -f <feature> --from <spec>`));
107500
107715
  process.exit(1);
107501
107716
  }
107502
107717
  const cliOverrides = {};
107503
- if (options.profile) {
107504
- cliOverrides.profile = options.profile;
107718
+ const cliProfiles = options.profile ?? [];
107719
+ if (cliProfiles.length > 0) {
107720
+ cliOverrides.profile = cliProfiles;
107505
107721
  }
107506
107722
  const config2 = await loadConfig(workdir, cliOverrides);
107507
107723
  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.6",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {