@nathapp/nax 0.68.1 → 0.68.2

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 +402 -135
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17735,19 +17735,30 @@ function formatStoryStart(entry, c, _timestamp, mode) {
17735
17735
  const complexity = typeof data.complexity === "string" ? data.complexity : "unknown";
17736
17736
  const tier = typeof data.modelTier === "string" ? data.modelTier : "unknown";
17737
17737
  const attempt = typeof data.attempt === "number" ? data.attempt : 1;
17738
+ const agent = typeof data.agent === "string" ? data.agent : undefined;
17739
+ const progress = typeof data.storyNumber === "number" && typeof data.storyTotal === "number" ? `${data.storyNumber}/${data.storyTotal}` : undefined;
17738
17740
  const lines = [];
17739
17741
  lines.push("");
17740
17742
  lines.push(c.bold(`${EMOJI.storyStart} ${c.cyan(storyId)}: ${title}`));
17741
17743
  if (mode === "verbose") {
17744
+ if (progress)
17745
+ lines.push(` ${c.gray("\u251C\u2500")} Story: ${c.cyan(progress)}`);
17742
17746
  lines.push(` ${c.gray("\u251C\u2500")} Complexity: ${c.yellow(complexity)}`);
17743
17747
  lines.push(` ${c.gray("\u251C\u2500")} Tier: ${c.magenta(tier)}`);
17748
+ if (agent)
17749
+ lines.push(` ${c.gray("\u251C\u2500")} Agent: ${c.cyan(agent)}`);
17744
17750
  if (attempt > 1) {
17745
17751
  lines.push(` ${c.gray("\u2514\u2500")} Attempt: ${c.yellow(`#${attempt}`)} ${EMOJI.retry}`);
17746
17752
  } else {
17747
17753
  lines.push(` ${c.gray("\u2514\u2500")} Status: ${c.green("starting")}`);
17748
17754
  }
17749
17755
  } else {
17750
- const metadata = [complexity, tier];
17756
+ const metadata = [];
17757
+ if (progress)
17758
+ metadata.push(progress);
17759
+ metadata.push(complexity, tier);
17760
+ if (agent)
17761
+ metadata.push(agent);
17751
17762
  if (attempt > 1)
17752
17763
  metadata.push(`attempt #${attempt} ${EMOJI.retry}`);
17753
17764
  lines.push(` ${c.gray(metadata.join(" \u2022 "))}`);
@@ -17809,24 +17820,19 @@ function formatDefault(entry, c, timestamp, mode) {
17809
17820
  if (entry.storyId) {
17810
17821
  parts.push(c.dim(`[${entry.storyId}]`));
17811
17822
  }
17823
+ if (entry.sessionRole) {
17824
+ parts.push(c.dim(`(${entry.sessionRole})`));
17825
+ }
17812
17826
  parts.push(entry.message);
17813
17827
  let output = parts.join(" ");
17814
17828
  const data = entry.data;
17815
17829
  if (data && typeof data === "object") {
17816
- const meta3 = [];
17817
- if (typeof data.cost === "number" && data.cost > 0)
17818
- meta3.push(`${EMOJI.cost} ${formatCost(data.cost)}`);
17819
- if (typeof data.durationMs === "number" && data.durationMs > 0)
17820
- meta3.push(`${EMOJI.duration} ${formatDuration(data.durationMs)}`);
17821
- if (typeof data.action === "string")
17822
- meta3.push(`action: ${data.action}`);
17823
- if (typeof data.reason === "string" && mode !== "quiet")
17824
- meta3.push(data.reason);
17830
+ const meta3 = buildDefaultMeta(data, mode);
17825
17831
  if (meta3.length > 0) {
17826
17832
  output += ` ${c.gray(meta3.join(" "))}`;
17827
17833
  }
17828
17834
  if (mode === "verbose") {
17829
- const { cost: _c, durationMs: _d, action: _a2, reason: _r, ...filtered } = data;
17835
+ const filtered = stripConsumedMetaFields(data);
17830
17836
  if (Object.keys(filtered).length > 0) {
17831
17837
  output += `
17832
17838
  ${c.gray(JSON.stringify(filtered, null, 2))}`;
@@ -17838,6 +17844,52 @@ ${c.gray(JSON.stringify(filtered, null, 2))}`;
17838
17844
  shouldDisplay: true
17839
17845
  };
17840
17846
  }
17847
+ function buildDefaultMeta(data, mode) {
17848
+ const meta3 = [];
17849
+ const identity = [data.agentName, data.model].filter((v) => typeof v === "string" && v.length > 0);
17850
+ if (identity.length > 0)
17851
+ meta3.push(`${EMOJI.agent} ${identity.join("\xB7")}`);
17852
+ if (typeof data.phaseIndex === "number" && typeof data.totalPhases === "number") {
17853
+ meta3.push(`${data.phaseIndex}/${data.totalPhases}`);
17854
+ }
17855
+ if (typeof data.status === "string")
17856
+ meta3.push(`status: ${data.status}`);
17857
+ if (typeof data.findingsCount === "number")
17858
+ meta3.push(`${data.findingsCount} finding${data.findingsCount === 1 ? "" : "s"}`);
17859
+ const activity = buildActivityMeta(data);
17860
+ if (activity)
17861
+ meta3.push(activity);
17862
+ if (typeof data.cost === "number" && data.cost > 0)
17863
+ meta3.push(`${EMOJI.cost} ${formatCost(data.cost)}`);
17864
+ if (typeof data.durationMs === "number" && data.durationMs > 0)
17865
+ meta3.push(`${EMOJI.duration} ${formatDuration(data.durationMs)}`);
17866
+ if (typeof data.action === "string")
17867
+ meta3.push(`action: ${data.action}`);
17868
+ if (typeof data.reason === "string" && mode !== "quiet")
17869
+ meta3.push(data.reason);
17870
+ return meta3;
17871
+ }
17872
+ function buildActivityMeta(data) {
17873
+ const segments = [];
17874
+ if (typeof data.messageUpdates === "number" && data.messageUpdates > 0)
17875
+ segments.push(`msg ${data.messageUpdates}`);
17876
+ if (typeof data.toolCallUpdates === "number" && data.toolCallUpdates > 0)
17877
+ segments.push(`tools ${data.toolCallUpdates}`);
17878
+ if (typeof data.thinkingUpdates === "number" && data.thinkingUpdates > 0)
17879
+ segments.push(`think ${data.thinkingUpdates}`);
17880
+ if (typeof data.idleMs === "number" && data.idleMs > 0)
17881
+ segments.push(`idle ${formatDuration(data.idleMs)}`);
17882
+ return segments.length > 0 ? segments.join(" ") : null;
17883
+ }
17884
+ function stripConsumedMetaFields(data) {
17885
+ const filtered = {};
17886
+ for (const [key, value] of Object.entries(data)) {
17887
+ if (!CONSUMED_META_KEYS.includes(key)) {
17888
+ filtered[key] = value;
17889
+ }
17890
+ }
17891
+ return filtered;
17892
+ }
17841
17893
  function formatRunSummary(summary, options) {
17842
17894
  const { mode, useColor = true } = options;
17843
17895
  if (mode === "json") {
@@ -17882,9 +17934,26 @@ function createNoopChalk() {
17882
17934
  cyan: noop
17883
17935
  };
17884
17936
  }
17937
+ var CONSUMED_META_KEYS;
17885
17938
  var init_formatter = __esm(() => {
17886
17939
  init_source();
17887
17940
  init_types2();
17941
+ CONSUMED_META_KEYS = [
17942
+ "agentName",
17943
+ "model",
17944
+ "phaseIndex",
17945
+ "totalPhases",
17946
+ "status",
17947
+ "findingsCount",
17948
+ "messageUpdates",
17949
+ "toolCallUpdates",
17950
+ "thinkingUpdates",
17951
+ "idleMs",
17952
+ "cost",
17953
+ "durationMs",
17954
+ "action",
17955
+ "reason"
17956
+ ];
17888
17957
  });
17889
17958
 
17890
17959
  // src/logging/index.ts
@@ -17933,6 +18002,43 @@ var init_formatters = __esm(() => {
17933
18002
  init_source();
17934
18003
  });
17935
18004
 
18005
+ // src/logger/redact.ts
18006
+ function redactString(value) {
18007
+ let out = value;
18008
+ for (const re of SECRET_VALUE_PATTERNS) {
18009
+ re.lastIndex = 0;
18010
+ out = out.replace(re, REDACTED);
18011
+ }
18012
+ return out;
18013
+ }
18014
+ function redactSecrets(input) {
18015
+ if (typeof input === "string")
18016
+ return redactString(input);
18017
+ if (Array.isArray(input))
18018
+ return input.map(redactSecrets);
18019
+ if (input !== null && typeof input === "object") {
18020
+ const out = {};
18021
+ for (const [key, value] of Object.entries(input)) {
18022
+ out[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactSecrets(value);
18023
+ }
18024
+ return out;
18025
+ }
18026
+ return input;
18027
+ }
18028
+ var SECRET_KEY_PATTERN, SECRET_VALUE_PATTERNS, REDACTED = "[REDACTED]";
18029
+ var init_redact = __esm(() => {
18030
+ SECRET_KEY_PATTERN = /(SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)/i;
18031
+ SECRET_VALUE_PATTERNS = [
18032
+ /sk-[A-Za-z0-9_-]{16,}/g,
18033
+ /ghp_[A-Za-z0-9]{16,}/g,
18034
+ /gh[opsu]_[A-Za-z0-9]{16,}/g,
18035
+ /npm_[A-Za-z0-9]{8,}/g,
18036
+ /AKIA[0-9A-Z]{16}/g,
18037
+ /xox[baprs]-[A-Za-z0-9-]{10,}/g,
18038
+ /(?:SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)=[^\s"',]+/gi
18039
+ ];
18040
+ });
18041
+
17936
18042
  // src/logger/logger.ts
17937
18043
  import { mkdirSync } from "fs";
17938
18044
  import { appendFile } from "fs/promises";
@@ -18027,7 +18133,8 @@ ${JSON.stringify(entry.data, null, 2)}`;
18027
18133
  writeToFile(entry) {
18028
18134
  if (!this.filePath)
18029
18135
  return;
18030
- const line = `${formatJsonl(entry)}
18136
+ const safeEntry = entry.data ? { ...entry, data: redactSecrets(entry.data) } : entry;
18137
+ const line = `${formatJsonl(safeEntry)}
18031
18138
  `;
18032
18139
  const filePath = this.filePath;
18033
18140
  this.writeQueueTail = this.writeQueueTail.then(() => appendFile(filePath, line).catch((error48) => {
@@ -18089,6 +18196,7 @@ var LOG_LEVEL_PRIORITY, instance = null, noopLogger;
18089
18196
  var init_logger = __esm(() => {
18090
18197
  init_logging();
18091
18198
  init_formatters();
18199
+ init_redact();
18092
18200
  LOG_LEVEL_PRIORITY = {
18093
18201
  silent: -1,
18094
18202
  error: 0,
@@ -22551,9 +22659,16 @@ async function detectTestFramework(_workdir, language, pkg) {
22551
22659
  }
22552
22660
  return;
22553
22661
  }
22662
+ function clearLanguageCache() {
22663
+ _languageCache.clear();
22664
+ }
22554
22665
  async function detectLanguage(packageDir) {
22666
+ if (_languageCache.has(packageDir))
22667
+ return _languageCache.get(packageDir);
22555
22668
  const pkg = await _detectorDeps.readJson(join5(packageDir, "package.json"));
22556
- return _detectLanguageImpl(packageDir, pkg);
22669
+ const result = await _detectLanguageImpl(packageDir, pkg);
22670
+ _languageCache.set(packageDir, result);
22671
+ return result;
22557
22672
  }
22558
22673
  async function detectLintTool(workdir, language) {
22559
22674
  if (language === "go")
@@ -22581,7 +22696,7 @@ async function detectProjectProfile(workdir, existing) {
22581
22696
  const lintTool = existing.lintTool !== undefined ? existing.lintTool : await detectLintTool(workdir, language);
22582
22697
  return { language, type, testFramework, lintTool };
22583
22698
  }
22584
- var _detectorDeps, WEB_DEPS, API_DEPS;
22699
+ var _detectorDeps, WEB_DEPS, API_DEPS, _languageCache;
22585
22700
  var init_detector = __esm(() => {
22586
22701
  _detectorDeps = {
22587
22702
  async fileExists(path) {
@@ -22602,6 +22717,7 @@ var init_detector = __esm(() => {
22602
22717
  };
22603
22718
  WEB_DEPS = new Set(["react", "next", "vue", "nuxt"]);
22604
22719
  API_DEPS = new Set(["express", "fastify", "hono"]);
22720
+ _languageCache = new Map;
22605
22721
  });
22606
22722
 
22607
22723
  // src/test-runners/conventions.ts
@@ -23730,7 +23846,10 @@ async function detectNaxMonoLayout(workdir) {
23730
23846
  } catch {}
23731
23847
  return dirs;
23732
23848
  }
23733
- async function discoverWorkspacePackages(workdir) {
23849
+ function clearWorkspaceCache() {
23850
+ _workspaceCache.clear();
23851
+ }
23852
+ async function discoverWorkspacePackagesUncached(workdir) {
23734
23853
  const [fromPnpm, fromNpm, fromLerna, fromTurboNx, fromNaxMono] = await Promise.all([
23735
23854
  detectPnpmWorkspace(workdir),
23736
23855
  detectNpmWorkspaces(workdir),
@@ -23749,7 +23868,14 @@ async function discoverWorkspacePackages(workdir) {
23749
23868
  }
23750
23869
  return unique;
23751
23870
  }
23752
- var _workspaceDeps;
23871
+ async function discoverWorkspacePackages(workdir) {
23872
+ if (_workspaceCache.has(workdir))
23873
+ return _workspaceCache.get(workdir) ?? [];
23874
+ const result = await discoverWorkspacePackagesUncached(workdir);
23875
+ _workspaceCache.set(workdir, result);
23876
+ return result;
23877
+ }
23878
+ var _workspaceDeps, _workspaceCache;
23753
23879
  var init_workspace = __esm(() => {
23754
23880
  init_logger2();
23755
23881
  _workspaceDeps = {
@@ -23762,6 +23888,7 @@ var init_workspace = __esm(() => {
23762
23888
  spawn: Bun.spawn,
23763
23889
  glob: (pattern, cwd) => new Bun.Glob(pattern).scan({ cwd, onlyFiles: false })
23764
23890
  };
23891
+ _workspaceCache = new Map;
23765
23892
  });
23766
23893
 
23767
23894
  // src/test-runners/detect.ts
@@ -24625,6 +24752,19 @@ var init_path_filters = __esm(() => {
24625
24752
 
24626
24753
  // src/verification/smart-runner.ts
24627
24754
  import { join as join8, relative as relative3 } from "path";
24755
+ function clearGitRootCache() {
24756
+ _gitRootCache.clear();
24757
+ }
24758
+ async function getGitRootMemo(workdir) {
24759
+ const cached2 = _gitRootCache.get(workdir);
24760
+ if (cached2 !== undefined)
24761
+ return cached2;
24762
+ const result = await _gitUtilDeps.getGitRoot(workdir);
24763
+ if (result !== null && result !== undefined) {
24764
+ _gitRootCache.set(workdir, result);
24765
+ }
24766
+ return result ?? null;
24767
+ }
24628
24768
  function extractPatternSuffix(pattern) {
24629
24769
  const lastStar = pattern.lastIndexOf("*");
24630
24770
  if (lastStar === -1)
@@ -24644,28 +24784,28 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
24644
24784
  return [];
24645
24785
  const searchTerms = sourceFiles.flatMap(extractSearchTerms);
24646
24786
  const testFilePaths = [];
24647
- for (const pattern of testFilePatterns) {
24648
- const glob = _bunDeps.glob(pattern);
24649
- for await (const file3 of glob.scan(workdir)) {
24650
- testFilePaths.push(`${workdir}/${file3}`);
24787
+ outer:
24788
+ for (const pattern of testFilePatterns) {
24789
+ const g = _bunDeps.glob(pattern);
24790
+ for await (const file3 of g.scan(workdir)) {
24791
+ testFilePaths.push(`${workdir}/${file3}`);
24792
+ if (testFilePaths.length >= MAX_GREP_TEST_FILES) {
24793
+ getSafeLogger()?.debug("smart-runner", "import-grep glob cap reached \u2014 results truncated", {
24794
+ cap: MAX_GREP_TEST_FILES
24795
+ });
24796
+ break outer;
24797
+ }
24798
+ }
24651
24799
  }
24652
- }
24653
- const matched = [];
24654
- for (const testFile of testFilePaths) {
24655
- let content;
24800
+ const results = await Promise.all(testFilePaths.map(async (testFile) => {
24656
24801
  try {
24657
- content = await _bunDeps.file(testFile).text();
24802
+ const content = await _bunDeps.file(testFile).text();
24803
+ return searchTerms.some((t) => content.includes(t)) ? testFile : null;
24658
24804
  } catch {
24659
- continue;
24660
- }
24661
- for (const term of searchTerms) {
24662
- if (content.includes(term)) {
24663
- matched.push(testFile);
24664
- break;
24665
- }
24805
+ return null;
24666
24806
  }
24667
- }
24668
- return matched;
24807
+ }));
24808
+ return results.filter((p) => p !== null);
24669
24809
  }
24670
24810
  async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePatterns = [...DEFAULT_TEST_FILE_PATTERNS]) {
24671
24811
  const testSuffixes = [...new Set(testFilePatterns.map(extractPatternSuffix).filter((s) => s !== null))];
@@ -24690,11 +24830,11 @@ async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePat
24690
24830
  }
24691
24831
  candidates.push(`${workdir}/${sourceWithoutExt}${suffix}`);
24692
24832
  }
24693
- for (const candidate of candidates) {
24694
- if (await _bunDeps.file(candidate).exists()) {
24695
- result.push(candidate);
24696
- }
24697
- }
24833
+ const existsFlags = await Promise.all(candidates.map((c) => _bunDeps.file(c).exists()));
24834
+ candidates.forEach((c, i) => {
24835
+ if (existsFlags[i])
24836
+ result.push(c);
24837
+ });
24698
24838
  }
24699
24839
  return result;
24700
24840
  }
@@ -24733,7 +24873,7 @@ async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileR
24733
24873
  const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(effectiveRepoRoot, packageDir);
24734
24874
  let effectivePrefix = packagePrefix;
24735
24875
  if (packagePrefix && repoRoot) {
24736
- const gitRoot = await _gitUtilDeps.getGitRoot(workdir);
24876
+ const gitRoot = await getGitRootMemo(workdir);
24737
24877
  const extraPrefix2 = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
24738
24878
  effectivePrefix = extraPrefix2 ? `${extraPrefix2}/${packagePrefix}` : packagePrefix;
24739
24879
  }
@@ -24760,7 +24900,7 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
24760
24900
  `).filter(Boolean);
24761
24901
  const packageDir = packagePrefix ? join8(repoRoot, packagePrefix) : undefined;
24762
24902
  const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(repoRoot, packageDir);
24763
- const gitRoot = await _gitUtilDeps.getGitRoot(workdir);
24903
+ const gitRoot = await getGitRootMemo(workdir);
24764
24904
  const extraPrefix = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
24765
24905
  const effectivePrefix = packagePrefix ? extraPrefix ? `${extraPrefix}/${packagePrefix}` : packagePrefix : undefined;
24766
24906
  const scopedRaw = effectivePrefix ? lines.filter((f) => f.startsWith(`${effectivePrefix}/`)) : lines;
@@ -24771,8 +24911,9 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
24771
24911
  return [];
24772
24912
  }
24773
24913
  }
24774
- var _bunDeps, _gitUtilDeps, _smartRunnerDeps;
24914
+ var _bunDeps, MAX_GREP_TEST_FILES = 200, _gitUtilDeps, _gitRootCache, _smartRunnerDeps;
24775
24915
  var init_smart_runner = __esm(() => {
24916
+ init_logger2();
24776
24917
  init_conventions();
24777
24918
  init_git();
24778
24919
  init_path_filters();
@@ -24783,6 +24924,7 @@ var init_smart_runner = __esm(() => {
24783
24924
  _gitUtilDeps = {
24784
24925
  getGitRoot
24785
24926
  };
24927
+ _gitRootCache = new Map;
24786
24928
  _smartRunnerDeps = {
24787
24929
  glob: _bunDeps.glob,
24788
24930
  file: _bunDeps.file,
@@ -25140,31 +25282,50 @@ async function resolveSourceGlob(override, packageDir) {
25140
25282
  const language = await _codeNeighborDeps.detectLanguage(packageDir);
25141
25283
  return (language && SOURCE_GLOB_BY_LANGUAGE[language]) ?? FALLBACK_SOURCE_GLOB;
25142
25284
  }
25143
- async function collectNeighbors(filePath, workdir, sourceGlob, maxGlobFiles, extraGlobWorkdirs, siblingTestContext, ignoreMatchers, globCtx) {
25285
+ function scanDirectory(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCtx) {
25286
+ const { files, truncated } = _codeNeighborDeps.glob(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCtx);
25287
+ return { workdir, files, truncated };
25288
+ }
25289
+ async function readCached(absolutePath, cache) {
25290
+ const cached2 = cache.get(absolutePath);
25291
+ if (cached2 !== undefined)
25292
+ return cached2;
25293
+ try {
25294
+ const content = await _codeNeighborDeps.readFile(absolutePath);
25295
+ cache.set(absolutePath, content);
25296
+ return content;
25297
+ } catch {
25298
+ cache.set(absolutePath, "");
25299
+ return null;
25300
+ }
25301
+ }
25302
+ async function collectNeighbors(filePath, workdir, scannedDirs, contentCache, siblingTestContext) {
25144
25303
  const neighbors = new Set;
25145
25304
  let anyTruncated = false;
25146
- if (await _codeNeighborDeps.fileExists(join10(workdir, filePath))) {
25147
- const content = await _codeNeighborDeps.readFile(join10(workdir, filePath));
25148
- for (const spec of parseImportSpecifiers(content)) {
25149
- const resolved = resolveImport(spec, filePath, workdir);
25150
- if (resolved && resolved !== filePath)
25151
- neighbors.add(resolved);
25305
+ const ownAbsPath = join10(workdir, filePath);
25306
+ if (await _codeNeighborDeps.fileExists(ownAbsPath)) {
25307
+ const ownContent = await readCached(ownAbsPath, contentCache);
25308
+ if (ownContent !== null && ownContent.length > 0) {
25309
+ for (const spec of parseImportSpecifiers(ownContent)) {
25310
+ const resolved = resolveImport(spec, filePath, workdir);
25311
+ if (resolved && resolved !== filePath)
25312
+ neighbors.add(resolved);
25313
+ }
25152
25314
  }
25153
25315
  }
25154
25316
  const fileBaseName = (filePath.split("/").pop() ?? filePath).replace(/\.[^.]+$/, "");
25155
25317
  const fileNoExt = filePath.replace(/\.[^.]+$/, "");
25156
- const scanForReverseDeps = async (scanWorkdir) => {
25157
- const { files: srcFiles, truncated } = _codeNeighborDeps.glob(sourceGlob, scanWorkdir, ignoreMatchers, maxGlobFiles, globCtx);
25158
- if (truncated)
25159
- anyTruncated = true;
25160
- for (const srcFile of srcFiles) {
25161
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25162
- break;
25163
- if (srcFile === filePath)
25164
- continue;
25165
- try {
25166
- const content = await _codeNeighborDeps.readFile(join10(scanWorkdir, srcFile));
25167
- if (content.includes(fileBaseName)) {
25318
+ outer:
25319
+ for (const { workdir: scanWorkdir, files: srcFiles, truncated } of scannedDirs) {
25320
+ if (truncated)
25321
+ anyTruncated = true;
25322
+ for (const srcFile of srcFiles) {
25323
+ if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25324
+ break outer;
25325
+ if (srcFile === filePath)
25326
+ continue;
25327
+ const content = await readCached(join10(scanWorkdir, srcFile), contentCache);
25328
+ if (content?.includes(fileBaseName)) {
25168
25329
  for (const spec of parseImportSpecifiers(content)) {
25169
25330
  const resolved = resolveImport(spec, srcFile, scanWorkdir);
25170
25331
  if (resolved === filePath || resolved === fileNoExt) {
@@ -25173,17 +25334,8 @@ async function collectNeighbors(filePath, workdir, sourceGlob, maxGlobFiles, ext
25173
25334
  }
25174
25335
  }
25175
25336
  }
25176
- } catch {}
25177
- }
25178
- };
25179
- await scanForReverseDeps(workdir);
25180
- if (extraGlobWorkdirs) {
25181
- for (const extraDir of extraGlobWorkdirs) {
25182
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25183
- break;
25184
- await scanForReverseDeps(extraDir);
25337
+ }
25185
25338
  }
25186
- }
25187
25339
  if (siblingTestContext && !isTestFile2(filePath, siblingTestContext.regex)) {
25188
25340
  const candidates = deriveSiblingTestCandidates(filePath, siblingTestContext.globs);
25189
25341
  let chosen = null;
@@ -25246,10 +25398,17 @@ class CodeNeighborProvider {
25246
25398
  const ignoreMatchers = request.naxIgnoreIndex?.getMatchers(workdir);
25247
25399
  const sourceGlob = await resolveSourceGlob(this.sourceGlobOverride, request.packageDir);
25248
25400
  const globCtx = { storyId: request.storyId, packageDir: request.packageDir };
25401
+ const scannedDirs = [scanDirectory(sourceGlob, workdir, ignoreMatchers, this.maxGlobFiles, globCtx)];
25402
+ if (extraGlobWorkdirs) {
25403
+ for (const extraDir of extraGlobWorkdirs) {
25404
+ scannedDirs.push(scanDirectory(sourceGlob, extraDir, ignoreMatchers, this.maxGlobFiles, globCtx));
25405
+ }
25406
+ }
25407
+ const contentCache = new Map;
25249
25408
  const sections = [];
25250
25409
  let anyTruncated = false;
25251
25410
  for (const file3 of filesToProcess) {
25252
- const { neighbors, truncated } = await collectNeighbors(file3, workdir, sourceGlob, this.maxGlobFiles, extraGlobWorkdirs, siblingTestContext, ignoreMatchers, globCtx);
25411
+ const { neighbors, truncated } = await collectNeighbors(file3, workdir, scannedDirs, contentCache, siblingTestContext);
25253
25412
  if (truncated)
25254
25413
  anyTruncated = true;
25255
25414
  if (neighbors.length > 0) {
@@ -26167,13 +26326,25 @@ function buildPullToolDescriptors(stageToolNames, pullConfig) {
26167
26326
  const allowed = pullConfig.allowedTools;
26168
26327
  return stageToolNames.filter((name) => allowed.length === 0 || allowed.includes(name)).map((name) => PULL_TOOL_REGISTRY[name]).filter((d) => d !== undefined).map((d) => ({ ...d, maxCallsPerSession: pullConfig.maxCallsPerSession ?? d.maxCallsPerSession }));
26169
26328
  }
26170
- async function fetchWithTimeout(provider, request) {
26329
+ async function fetchWithTimeout(provider, request, timeoutMs = PROVIDER_FETCH_TIMEOUT_MS) {
26330
+ const controller = new AbortController;
26171
26331
  let handle;
26332
+ let timedOut = false;
26172
26333
  const timeout = new Promise((_, reject) => {
26173
- handle = setTimeout(() => reject(new Error(`Provider "${provider.id}" timed out`)), PROVIDER_FETCH_TIMEOUT_MS);
26334
+ handle = setTimeout(() => {
26335
+ timedOut = true;
26336
+ controller.abort();
26337
+ reject(new Error(`Provider "${provider.id}" timed out`));
26338
+ }, timeoutMs);
26339
+ });
26340
+ const fetchPromise = provider.fetch(request, controller.signal).then((result) => result, (err) => {
26341
+ if (timedOut) {
26342
+ return new Promise(() => {});
26343
+ }
26344
+ throw err;
26174
26345
  });
26175
26346
  try {
26176
- return await Promise.race([provider.fetch(request), timeout]);
26347
+ return await Promise.race([fetchPromise, timeout]);
26177
26348
  } finally {
26178
26349
  clearTimeout(handle);
26179
26350
  }
@@ -26812,12 +26983,7 @@ class GitHistoryProvider {
26812
26983
  return { chunks: [], pullTools: [] };
26813
26984
  }
26814
26985
  const filesToProcess = touchedFiles.filter(isRelativeAndSafe).slice(0, MAX_FILES2);
26815
- const sections = [];
26816
- for (const file3 of filesToProcess) {
26817
- const section = await fetchFileHistory(file3, workdir);
26818
- if (section)
26819
- sections.push(section);
26820
- }
26986
+ const sections = (await Promise.all(filesToProcess.map((file3) => fetchFileHistory(file3, workdir)))).filter((section) => section !== null);
26821
26987
  if (sections.length === 0) {
26822
26988
  return { chunks: [], pullTools: [] };
26823
26989
  }
@@ -33277,20 +33443,23 @@ function createDrainDeadline(deadlineMs) {
33277
33443
  };
33278
33444
  }
33279
33445
  async function runQualityCommand(opts) {
33280
- const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2 } = opts;
33446
+ const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2, stripEnvVars } = opts;
33281
33447
  const startTime = Date.now();
33282
33448
  const logger = getSafeLogger();
33283
33449
  logger?.info("quality", `Running ${commandName}`, { storyId, commandName, command, workdir });
33284
33450
  try {
33451
+ const baseEnv = {
33452
+ ...process.env
33453
+ };
33454
+ for (const key of stripEnvVars ?? []) {
33455
+ delete baseEnv[key];
33456
+ }
33285
33457
  const proc = _qualityRunnerDeps.spawn({
33286
33458
  cmd: ["/bin/sh", "-c", command],
33287
33459
  cwd: workdir,
33288
33460
  stdout: "pipe",
33289
33461
  stderr: "pipe",
33290
- env: {
33291
- ...process.env,
33292
- ...env2 ?? {}
33293
- }
33462
+ env: { ...baseEnv, ...env2 ?? {} }
33294
33463
  });
33295
33464
  let timedOut = false;
33296
33465
  let exitedBeforeSigkill = false;
@@ -36870,7 +37039,7 @@ function buildVerifierFindings(verdict, categorization) {
36870
37039
  return [];
36871
37040
  }
36872
37041
  }
36873
- function parseVerdictFromStdout(output, _input, _ctx) {
37042
+ function parseVerdictFromStdout(output, input, _ctx) {
36874
37043
  if (!output || !output.trim()) {
36875
37044
  throw new ParseValidationError("verifier produced no stdout");
36876
37045
  }
@@ -36883,6 +37052,18 @@ function parseVerdictFromStdout(output, _input, _ctx) {
36883
37052
  throw new ParseValidationError("verifier stdout JSON missing required VerifierVerdict fields");
36884
37053
  }
36885
37054
  const categorization = categorizeVerdict(verdict, verdict.tests.allPassing === true);
37055
+ getSafeLogger()?.info("verifier", "Verdict categorized", {
37056
+ storyId: input.story.id,
37057
+ approved: verdict.approved,
37058
+ success: categorization.success,
37059
+ advisoryOverride: verdict.approved === false && categorization.success,
37060
+ testsPassing: verdict.tests.allPassing,
37061
+ passCount: verdict.tests.passCount,
37062
+ failCount: verdict.tests.failCount,
37063
+ testModsDetected: verdict.testModifications.detected,
37064
+ testModsLegitimate: verdict.testModifications.legitimate,
37065
+ ...categorization.failureCategory && { failureCategory: categorization.failureCategory }
37066
+ });
36886
37067
  return {
36887
37068
  success: categorization.success,
36888
37069
  filesChanged: [],
@@ -38235,7 +38416,8 @@ var init_mechanical_lintfix_strategy = __esm(() => {
38235
38416
  commandName: "lintFix",
38236
38417
  command,
38237
38418
  workdir: input.workdir,
38238
- storyId: input.storyId
38419
+ storyId: input.storyId,
38420
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38239
38421
  });
38240
38422
  return { applied: true, exitCode: result.exitCode };
38241
38423
  }
@@ -38293,7 +38475,8 @@ var init_mechanical_formatfix_strategy = __esm(() => {
38293
38475
  commandName: "formatFix",
38294
38476
  command,
38295
38477
  workdir: input.workdir,
38296
- storyId: input.storyId
38478
+ storyId: input.storyId,
38479
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38297
38480
  });
38298
38481
  return { applied: true, exitCode: result.exitCode };
38299
38482
  }
@@ -38326,7 +38509,8 @@ var init_lint_check = __esm(() => {
38326
38509
  commandName: "lint",
38327
38510
  command: command ?? "",
38328
38511
  workdir: input.workdir,
38329
- storyId: input.storyId
38512
+ storyId: input.storyId,
38513
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38330
38514
  });
38331
38515
  if (result.exitCode === 0) {
38332
38516
  return { success: true, findings: [], durationMs: Date.now() - start };
@@ -38575,7 +38759,8 @@ var init_typecheck_check = __esm(() => {
38575
38759
  commandName: "typecheck",
38576
38760
  command: command ?? "",
38577
38761
  workdir: input.workdir,
38578
- storyId: input.storyId
38762
+ storyId: input.storyId,
38763
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38579
38764
  });
38580
38765
  if (result.exitCode === 0) {
38581
38766
  return { success: true, findings: [], durationMs: Date.now() - start };
@@ -39550,13 +39735,14 @@ async function resolveLintScope(args) {
39550
39735
  function resolveScopedTemplate(reviewCommands, qualityCommands) {
39551
39736
  return reviewCommands.lintScoped ?? qualityCommands?.lintScoped;
39552
39737
  }
39553
- async function runLintCommand(workdir, storyId, env2, command) {
39738
+ async function runLintCommand(workdir, storyId, env2, command, stripEnvVars) {
39554
39739
  return runQualityCommand({
39555
39740
  commandName: SCOPED_LINT_CHECK,
39556
39741
  command,
39557
39742
  workdir,
39558
39743
  storyId,
39559
- env: env2
39744
+ env: env2,
39745
+ stripEnvVars
39560
39746
  });
39561
39747
  }
39562
39748
  function toReviewCheck(result) {
@@ -39597,7 +39783,7 @@ async function runScopedLintCheck(args) {
39597
39783
  storyId: args.storyId,
39598
39784
  reason: scope.degradedReason
39599
39785
  });
39600
- const fullResult2 = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand);
39786
+ const fullResult2 = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand, args.stripEnvVars);
39601
39787
  return withLintScope(attachLintFindings(toReviewCheck(fullResult2), args.lintOutputFormat, args.workdir), scope, "degraded");
39602
39788
  }
39603
39789
  logger?.info("review", "lint_scope_empty", { storyId: args.storyId });
@@ -39622,19 +39808,19 @@ async function runScopedLintCheck(args) {
39622
39808
  }
39623
39809
  if (scopedTemplate) {
39624
39810
  const scopedCommand = scopedTemplate.replaceAll("{{files}}", scope.files.map(shellQuotePath4).join(" "));
39625
- const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand);
39811
+ const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand, args.stripEnvVars);
39626
39812
  return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
39627
39813
  }
39628
39814
  if (!scope.degradedReason && isSupportedDerivedScopedCommand(fullLintCommand)) {
39629
39815
  const scopedCommand = appendFilesToCommand(fullLintCommand, scope.files);
39630
- const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand);
39816
+ const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand, args.stripEnvVars);
39631
39817
  return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
39632
39818
  }
39633
39819
  logger?.warn("review", "lint_scope_degraded", {
39634
39820
  storyId: args.storyId,
39635
39821
  reason: scope.degradedReason ?? "unsupported_scoped_command_shape"
39636
39822
  });
39637
- const fullResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand);
39823
+ const fullResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand, args.stripEnvVars);
39638
39824
  if (fullResult.exitCode === 0)
39639
39825
  return withLintScope(toReviewCheck(fullResult), scope, "degraded");
39640
39826
  const parsed = parseLintOutput(fullResult.output, args.lintOutputFormat ?? "auto", {
@@ -40360,8 +40546,8 @@ async function resolveCommand(check2, config2, executionConfig, workdir, quality
40360
40546
  }
40361
40547
  return null;
40362
40548
  }
40363
- async function runCheck(check2, command, workdir, storyId, env2) {
40364
- const result = await runQualityCommand({ commandName: check2, command, workdir, storyId, env: env2 });
40549
+ async function runCheck(check2, command, workdir, storyId, env2, stripEnvVars) {
40550
+ const result = await runQualityCommand({ commandName: check2, command, workdir, storyId, env: env2, stripEnvVars });
40365
40551
  return {
40366
40552
  check: check2,
40367
40553
  command: result.command,
@@ -40567,8 +40753,9 @@ async function runReview(opts) {
40567
40753
  story,
40568
40754
  storyGitRef,
40569
40755
  env: env2,
40756
+ stripEnvVars: naxConfig?.quality?.stripEnvVars ?? [],
40570
40757
  naxIgnoreIndex
40571
- }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2), workdir);
40758
+ }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2, naxConfig?.quality?.stripEnvVars ?? []), workdir);
40572
40759
  checks3.push(result);
40573
40760
  if (result.success) {
40574
40761
  logger?.info("review", `${checkName} passed`, {
@@ -43916,13 +44103,13 @@ var init_factory = __esm(() => {
43916
44103
 
43917
44104
  // src/execution/pid-registry.ts
43918
44105
  import { existsSync as existsSync7 } from "fs";
43919
- import { appendFile as appendFile2 } from "fs/promises";
43920
44106
 
43921
44107
  class PidRegistry {
43922
44108
  workdir;
43923
44109
  pidsFilePath;
43924
44110
  pids = new Set;
43925
44111
  frozen = false;
44112
+ writeQueueTail = Promise.resolve();
43926
44113
  constructor(workdir, _platform) {
43927
44114
  this.workdir = workdir;
43928
44115
  this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
@@ -43943,15 +44130,8 @@ class PidRegistry {
43943
44130
  return;
43944
44131
  }
43945
44132
  this.pids.add(pid);
43946
- const entry = {
43947
- pid,
43948
- spawnedAt: new Date().toISOString(),
43949
- workdir: this.workdir
43950
- };
43951
44133
  try {
43952
- const line = `${JSON.stringify(entry)}
43953
- `;
43954
- await appendFile2(this.pidsFilePath, line);
44134
+ await this.enqueueWrite();
43955
44135
  logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
43956
44136
  } catch (err) {
43957
44137
  logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
@@ -43963,7 +44143,7 @@ class PidRegistry {
43963
44143
  const logger = getSafeLogger();
43964
44144
  this.pids.delete(pid);
43965
44145
  try {
43966
- await this.writePidsFile();
44146
+ await this.enqueueWrite();
43967
44147
  logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
43968
44148
  } catch (err) {
43969
44149
  logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
@@ -43982,8 +44162,8 @@ class PidRegistry {
43982
44162
  const killPromises = pids.map((pid) => this.killPidTree(pid));
43983
44163
  await Promise.allSettled(killPromises);
43984
44164
  try {
43985
- await Bun.write(this.pidsFilePath, "");
43986
44165
  this.pids.clear();
44166
+ await this.enqueueWrite();
43987
44167
  logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
43988
44168
  } catch (err) {
43989
44169
  logger?.warn("pid-registry", "Failed to clear registry file", {
@@ -44202,6 +44382,37 @@ class PidRegistry {
44202
44382
  getPids() {
44203
44383
  return Array.from(this.pids);
44204
44384
  }
44385
+ snapshot() {
44386
+ return Array.from(this.pids);
44387
+ }
44388
+ async flush() {
44389
+ await this.writeQueueTail;
44390
+ }
44391
+ async readPidsFromDisk() {
44392
+ try {
44393
+ if (!existsSync7(this.pidsFilePath))
44394
+ return [];
44395
+ const content = await Bun.file(this.pidsFilePath).text();
44396
+ return content.split(`
44397
+ `).filter((line) => line.trim()).map((line) => {
44398
+ try {
44399
+ return JSON.parse(line).pid;
44400
+ } catch {
44401
+ return null;
44402
+ }
44403
+ }).filter((pid) => pid !== null);
44404
+ } catch {
44405
+ return [];
44406
+ }
44407
+ }
44408
+ enqueueWrite() {
44409
+ this.writeQueueTail = this.writeQueueTail.then(() => this.writePidsFile().catch((err) => {
44410
+ getSafeLogger()?.warn("pid-registry", "Failed to flush PID file \u2014 on-disk registry may be stale", {
44411
+ error: errorMessage(err)
44412
+ });
44413
+ }));
44414
+ return this.writeQueueTail;
44415
+ }
44205
44416
  }
44206
44417
  var PID_REGISTRY_FILE = ".nax-pids", PID_TREE_KILL_GRACE_MS = 250, _pidRegistryDeps;
44207
44418
  var init_pid_registry = __esm(() => {
@@ -52048,7 +52259,7 @@ var init_effectiveness = __esm(() => {
52048
52259
  });
52049
52260
 
52050
52261
  // src/execution/progress.ts
52051
- import { appendFile as appendFile3, mkdir as mkdir7 } from "fs/promises";
52262
+ import { appendFile as appendFile2, mkdir as mkdir7 } from "fs/promises";
52052
52263
  import { join as join44 } from "path";
52053
52264
  async function appendProgress(featureDir, storyId, status, message) {
52054
52265
  await mkdir7(featureDir, { recursive: true });
@@ -52056,7 +52267,7 @@ async function appendProgress(featureDir, storyId, status, message) {
52056
52267
  const timestamp = new Date().toISOString();
52057
52268
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
52058
52269
  `;
52059
- await appendFile3(progressPath, entry);
52270
+ await appendFile2(progressPath, entry);
52060
52271
  }
52061
52272
  var init_progress = () => {};
52062
52273
 
@@ -52163,6 +52374,7 @@ var init_completion = __esm(() => {
52163
52374
  const logger = getLogger();
52164
52375
  const isBatch = ctx.stories.length > 1;
52165
52376
  const sessionCost = ctx.agentResult?.estimatedCostUsd || 0;
52377
+ const persistPrd = ctx.skipPrdPersistence !== true;
52166
52378
  const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
52167
52379
  const storyStartTime = ctx.storyStartTime || new Date().toISOString();
52168
52380
  if (isBatch) {
@@ -52187,7 +52399,9 @@ var init_completion = __esm(() => {
52187
52399
  }
52188
52400
  }
52189
52401
  for (const completedStory of ctx.stories) {
52190
- markStoryPassed(ctx.prd, completedStory.id);
52402
+ if (persistPrd) {
52403
+ markStoryPassed(ctx.prd, completedStory.id);
52404
+ }
52191
52405
  const costPerStory = sessionCost / ctx.stories.length;
52192
52406
  logger.info("completion", "Story passed", {
52193
52407
  storyId: completedStory.id,
@@ -52219,7 +52433,9 @@ var init_completion = __esm(() => {
52219
52433
  }
52220
52434
  }
52221
52435
  }
52222
- await _completionDeps.savePRD(ctx.prd, prdPath);
52436
+ if (persistPrd) {
52437
+ await _completionDeps.savePRD(ctx.prd, prdPath);
52438
+ }
52223
52439
  const updatedCounts = countStories(ctx.prd);
52224
52440
  logger.info("completion", "Progress update", {
52225
52441
  storyId: ctx.story.id,
@@ -53022,7 +53238,7 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
53022
53238
  data.reviewReason = r.reviewReason;
53023
53239
  return { success: success2, data };
53024
53240
  }
53025
- function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage) {
53241
+ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage, progressData = {}) {
53026
53242
  if (isTddPhase)
53027
53243
  return;
53028
53244
  if (opName === "semantic-review" || opName === "adversarial-review")
@@ -53030,7 +53246,8 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
53030
53246
  const built = buildPhaseOutcomeLogData(storyId, opName, output, durationMs);
53031
53247
  if (!built)
53032
53248
  return;
53033
- const { success: success2, data } = built;
53249
+ const { success: success2 } = built;
53250
+ const data = { ...built.data, ...progressData };
53034
53251
  const logger = getSafeLogger();
53035
53252
  const message = formatPhaseResultMessage(opName, success2, stage);
53036
53253
  if (stage === "rectification") {
@@ -53093,17 +53310,21 @@ function logUnifiedReviewPhaseResult(storyId, opName, output) {
53093
53310
  truncated: findingsCount > findingsSummary.length
53094
53311
  });
53095
53312
  }
53096
- async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false) {
53313
+ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false, progress) {
53097
53314
  const logger = getSafeLogger();
53098
53315
  const opName = slot.op.name;
53316
+ const progressData = progress ? { phaseIndex: progress.index, totalPhases: progress.total } : {};
53099
53317
  const isTddPhase = isThreeSession && TDD_OP_NAMES.has(opName);
53100
53318
  const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
53101
53319
  let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
53102
53320
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
53103
53321
  if (isTddPhase) {
53104
- logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName });
53322
+ logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName, ...progressData });
53105
53323
  } else if (isThreeSession && opName === "full-suite-gate") {
53106
- logger?.info("tdd", "-> Running full test suite gate (before Verifier)", { storyId: ctx.storyId });
53324
+ logger?.info("tdd", "-> Running full test suite gate (before Verifier)", {
53325
+ storyId: ctx.storyId,
53326
+ ...progressData
53327
+ });
53107
53328
  }
53108
53329
  logUnifiedReviewPhaseStart(ctx.storyId, opName);
53109
53330
  const phaseStartedAt = Date.now();
@@ -53113,7 +53334,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
53113
53334
  phaseOutputs[opName] = output;
53114
53335
  emitReviewDecision(ctx, opName, output);
53115
53336
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
53116
- logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage);
53337
+ logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
53117
53338
  if (isTddPhase) {
53118
53339
  const durationMs = Date.now() - phaseStartedAt;
53119
53340
  logger?.info("tdd", `Session complete: ${opName}`, {
@@ -53282,9 +53503,13 @@ class ExecutionPlan {
53282
53503
  const phaseOutputs = {};
53283
53504
  const startedAt = Date.now();
53284
53505
  const logger = getSafeLogger();
53285
- for (const phase of collectOrderedPhases(this.state)) {
53506
+ const orderedPhases = collectOrderedPhases(this.state);
53507
+ for (const [phaseIndex, phase] of orderedPhases.entries()) {
53286
53508
  try {
53287
- await runPhase(this.ctx, phase.slot, phaseCosts, phaseOutputs, this.isThreeSession);
53509
+ await runPhase(this.ctx, phase.slot, phaseCosts, phaseOutputs, this.isThreeSession, {
53510
+ index: phaseIndex + 1,
53511
+ total: orderedPhases.length
53512
+ });
53288
53513
  } catch (error48) {
53289
53514
  logger?.error("story-orchestrator", "Phase threw unexpected error", {
53290
53515
  storyId: this.ctx.storyId,
@@ -56949,7 +57174,7 @@ function renderProposals(proposals, runId, observationCount) {
56949
57174
  }
56950
57175
 
56951
57176
  // src/plugins/builtin/curator/rollup.ts
56952
- import { appendFile as appendFile4, mkdir as mkdir10, writeFile } from "fs/promises";
57177
+ import { appendFile as appendFile3, mkdir as mkdir10, writeFile } from "fs/promises";
56953
57178
  import * as path14 from "path";
56954
57179
  async function appendToRollup(observations, rollupPath) {
56955
57180
  try {
@@ -56965,7 +57190,7 @@ async function appendToRollup(observations, rollupPath) {
56965
57190
  const newLines = `${observations.map((o) => JSON.stringify(o)).join(`
56966
57191
  `)}
56967
57192
  `;
56968
- await appendFile4(rollupPath, newLines);
57193
+ await appendFile3(rollupPath, newLines);
56969
57194
  } catch {}
56970
57195
  }
56971
57196
  var init_rollup = () => {};
@@ -57883,7 +58108,7 @@ var package_default;
57883
58108
  var init_package = __esm(() => {
57884
58109
  package_default = {
57885
58110
  name: "@nathapp/nax",
57886
- version: "0.68.1",
58111
+ version: "0.68.2",
57887
58112
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
57888
58113
  type: "module",
57889
58114
  bin: {
@@ -57978,8 +58203,8 @@ var init_version = __esm(() => {
57978
58203
  NAX_VERSION = package_default.version;
57979
58204
  NAX_COMMIT = (() => {
57980
58205
  try {
57981
- if (/^[0-9a-f]{6,10}$/.test("5d69e4e4"))
57982
- return "5d69e4e4";
58206
+ if (/^[0-9a-f]{6,10}$/.test("27a81a5e"))
58207
+ return "27a81a5e";
57983
58208
  } catch {}
57984
58209
  try {
57985
58210
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -59395,6 +59620,9 @@ async function handleRunCompletion(options) {
59395
59620
  if (options.pluginProviderCache) {
59396
59621
  await options.pluginProviderCache.disposeAll();
59397
59622
  }
59623
+ clearLanguageCache();
59624
+ clearWorkspaceCache();
59625
+ clearGitRootCache();
59398
59626
  const finalCounts = countStories(prd);
59399
59627
  const fallbackAggregate = deriveRunFallbackAggregates(allStoryMetrics);
59400
59628
  pipelineEventBus.emit({
@@ -59500,7 +59728,10 @@ var init_run_completion = __esm(() => {
59500
59728
  init_metrics();
59501
59729
  init_event_bus();
59502
59730
  init_prd();
59731
+ init_detector();
59503
59732
  init_scratch_purge();
59733
+ init_workspace();
59734
+ init_smart_runner();
59504
59735
  init_run_regression();
59505
59736
  _runCompletionDeps = {
59506
59737
  runDeferredRegression,
@@ -59598,7 +59829,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
59598
59829
  var DEFAULT_MAX_BATCH_SIZE = 4;
59599
59830
 
59600
59831
  // src/pipeline/subscribers/events-writer.ts
59601
- import { appendFile as appendFile5, mkdir as mkdir14 } from "fs/promises";
59832
+ import { appendFile as appendFile4, mkdir as mkdir14 } from "fs/promises";
59602
59833
  import { basename as basename14, join as join73 } from "path";
59603
59834
  function wireEventsWriter(bus, feature, runId, workdir) {
59604
59835
  const logger = getSafeLogger();
@@ -59613,7 +59844,7 @@ function wireEventsWriter(bus, feature, runId, workdir) {
59613
59844
  await mkdir14(eventsDir, { recursive: true });
59614
59845
  dirReady = true;
59615
59846
  }
59616
- await appendFile5(eventsFile, `${JSON.stringify(line)}
59847
+ await appendFile4(eventsFile, `${JSON.stringify(line)}
59617
59848
  `);
59618
59849
  } catch (err) {
59619
59850
  logger?.warn("events-writer", "Failed to write event line (non-fatal)", {
@@ -61510,9 +61741,13 @@ var exports_parallel_worker = {};
61510
61741
  __export(exports_parallel_worker, {
61511
61742
  executeStoryInWorktree: () => executeStoryInWorktree,
61512
61743
  executeParallelBatch: () => executeParallelBatch,
61744
+ buildWorktreePipelineContext: () => buildWorktreePipelineContext,
61513
61745
  _parallelWorkerDeps: () => _parallelWorkerDeps
61514
61746
  });
61515
61747
  import { join as join79 } from "path";
61748
+ function buildWorktreePipelineContext(base, _story) {
61749
+ return { ...base, prd: structuredClone(base.prd) };
61750
+ }
61516
61751
  async function executeStoryInWorktree(story, worktreePath, dependencyContext, context, routing, eventEmitter) {
61517
61752
  const logger = getSafeLogger();
61518
61753
  try {
@@ -61526,7 +61761,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
61526
61761
  }
61527
61762
  }
61528
61763
  const pipelineContext = {
61529
- ...context,
61764
+ ...buildWorktreePipelineContext(context, story),
61530
61765
  config: context.config,
61531
61766
  rootConfig: context.rootConfig,
61532
61767
  story,
@@ -61826,6 +62061,7 @@ var init_parallel_batch = __esm(() => {
61826
62061
  // src/execution/unified-executor.ts
61827
62062
  var exports_unified_executor = {};
61828
62063
  __export(exports_unified_executor, {
62064
+ reconcileBatchOutcome: () => reconcileBatchOutcome,
61829
62065
  executeUnified: () => executeUnified,
61830
62066
  _unifiedExecutorDeps: () => _unifiedExecutorDeps
61831
62067
  });
@@ -61949,7 +62185,10 @@ async function executeUnified(ctx, initialPrd) {
61949
62185
  const readyStories = getAllReadyStories(prd);
61950
62186
  const batch = _unifiedExecutorDeps.selectIndependentBatch(readyStories, ctx.parallelCount);
61951
62187
  if (batch.length > 1) {
61952
- for (const story of batch) {
62188
+ const batchAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62189
+ const batchCounts = countStories(prd);
62190
+ const batchBaseDone = batchCounts.total - batchCounts.pending;
62191
+ for (const [batchIndex, story] of batch.entries()) {
61953
62192
  const modelTier2 = story.routing?.modelTier ?? ctx.config.autoMode.complexityRouting?.[story.routing?.complexity ?? "medium"] ?? "balanced";
61954
62193
  pipelineEventBus.emit({
61955
62194
  type: "story:started",
@@ -61957,7 +62196,7 @@ async function executeUnified(ctx, initialPrd) {
61957
62196
  story: { id: story.id, title: story.title, status: story.status, attempts: story.attempts },
61958
62197
  workdir: ctx.workdir,
61959
62198
  modelTier: modelTier2,
61960
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62199
+ agent: batchAgent,
61961
62200
  iteration: iterations
61962
62201
  });
61963
62202
  logger?.info("story.start", `${story.title}`, {
@@ -61965,6 +62204,9 @@ async function executeUnified(ctx, initialPrd) {
61965
62204
  storyTitle: story.title,
61966
62205
  complexity: story.routing?.complexity ?? "unknown",
61967
62206
  modelTier: modelTier2,
62207
+ agent: batchAgent,
62208
+ storyNumber: batchBaseDone + batchIndex + 1,
62209
+ storyTotal: batchCounts.total,
61968
62210
  attempt: story.attempts + 1
61969
62211
  });
61970
62212
  }
@@ -61984,6 +62226,7 @@ async function executeUnified(ctx, initialPrd) {
61984
62226
  config: ctx.config,
61985
62227
  rootConfig: ctx.config,
61986
62228
  prd,
62229
+ skipPrdPersistence: true,
61987
62230
  projectDir: ctx.workdir,
61988
62231
  naxIgnoreIndex,
61989
62232
  hooks: ctx.hooks,
@@ -62031,6 +62274,8 @@ async function executeUnified(ctx, initialPrd) {
62031
62274
  abortSignal: ctx.abortSignal
62032
62275
  }, pipelineResult);
62033
62276
  }
62277
+ reconcileBatchOutcome(prd, batchResult);
62278
+ await savePRD(prd, ctx.prdPath);
62034
62279
  await pipelineEventBus.drain();
62035
62280
  totalCost += batchResult.totalCost;
62036
62281
  storiesCompleted += batchResult.completed.length;
@@ -62121,6 +62366,8 @@ async function executeUnified(ctx, initialPrd) {
62121
62366
  }
62122
62367
  }
62123
62368
  const modelTier2 = singleSelection.routing.modelTier;
62369
+ const singleAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62370
+ const singleCounts = countStories(prd);
62124
62371
  pipelineEventBus.emit({
62125
62372
  type: "story:started",
62126
62373
  storyId: singleStory.id,
@@ -62132,7 +62379,7 @@ async function executeUnified(ctx, initialPrd) {
62132
62379
  },
62133
62380
  workdir: ctx.workdir,
62134
62381
  modelTier: modelTier2,
62135
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62382
+ agent: singleAgent,
62136
62383
  iteration: iterations
62137
62384
  });
62138
62385
  logger?.info("story.start", `${singleStory.title}`, {
@@ -62140,6 +62387,9 @@ async function executeUnified(ctx, initialPrd) {
62140
62387
  storyTitle: singleStory.title,
62141
62388
  complexity: singleSelection.routing.complexity ?? "unknown",
62142
62389
  modelTier: modelTier2,
62390
+ agent: singleAgent,
62391
+ storyNumber: singleCounts.total - singleCounts.pending + 1,
62392
+ storyTotal: singleCounts.total,
62143
62393
  attempt: singleStory.attempts + 1
62144
62394
  });
62145
62395
  const singleIter = await _unifiedExecutorDeps.runIteration(ctx, prd, singleSelection, iterations, totalCost, allStoryMetrics);
@@ -62191,6 +62441,8 @@ async function executeUnified(ctx, initialPrd) {
62191
62441
  }
62192
62442
  }
62193
62443
  const modelTier = selection.routing.modelTier;
62444
+ const seqAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62445
+ const seqCounts = countStories(prd);
62194
62446
  pipelineEventBus.emit({
62195
62447
  type: "story:started",
62196
62448
  storyId: selection.story.id,
@@ -62202,7 +62454,7 @@ async function executeUnified(ctx, initialPrd) {
62202
62454
  },
62203
62455
  workdir: ctx.workdir,
62204
62456
  modelTier,
62205
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62457
+ agent: seqAgent,
62206
62458
  iteration: iterations
62207
62459
  });
62208
62460
  logger?.info("story.start", `${selection.story.title}`, {
@@ -62210,6 +62462,9 @@ async function executeUnified(ctx, initialPrd) {
62210
62462
  storyTitle: selection.story.title,
62211
62463
  complexity: selection.routing.complexity ?? "unknown",
62212
62464
  modelTier,
62465
+ agent: seqAgent,
62466
+ storyNumber: seqCounts.total - seqCounts.pending + 1,
62467
+ storyTotal: seqCounts.total,
62213
62468
  attempt: selection.story.attempts + 1
62214
62469
  });
62215
62470
  const iter = await _unifiedExecutorDeps.runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
@@ -62268,6 +62523,18 @@ async function executeUnified(ctx, initialPrd) {
62268
62523
  return buildResult2("max-iterations");
62269
62524
  } finally {}
62270
62525
  }
62526
+ function reconcileBatchOutcome(prd, batchResult) {
62527
+ for (const story of batchResult.completed) {
62528
+ markStoryPassed(prd, story.id);
62529
+ }
62530
+ for (const conflict of batchResult.mergeConflicts) {
62531
+ if (conflict.rectified) {
62532
+ markStoryPassed(prd, conflict.story.id);
62533
+ } else {
62534
+ markStoryFailed(prd, conflict.story.id, undefined, "merge-conflict");
62535
+ }
62536
+ }
62537
+ }
62271
62538
  var TERMINAL_ACTIONS, _unifiedExecutorDeps;
62272
62539
  var init_unified_executor = __esm(() => {
62273
62540
  init_agents();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.68.1",
3
+ "version": "0.68.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {