@nathapp/nax 0.68.0 → 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 +443 -142
  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,
@@ -21971,6 +22079,10 @@ function tryParseLLMJson(text) {
21971
22079
  }
21972
22080
 
21973
22081
  // src/agents/retry/parse-retry.ts
22082
+ function previewOutput(output, maxBytes) {
22083
+ const collapsed = output.replace(/\s+/g, " ").trim();
22084
+ return collapsed.length > maxBytes ? `${collapsed.slice(0, maxBytes)}\u2026` : collapsed;
22085
+ }
21974
22086
  function makeParseRetryStrategy(opts) {
21975
22087
  const parse5 = opts.parse ?? tryParseLLMJson;
21976
22088
  const checkTruncated = opts.looksTruncated ?? looksLikeTruncatedJson;
@@ -22003,16 +22115,19 @@ function makeParseRetryStrategy(opts) {
22003
22115
  const isTruncated = checkTruncated(ctx.lastOutput);
22004
22116
  const nextPrompt = isTruncated ? opts.prompts.truncated() : opts.prompts.invalid();
22005
22117
  const logger = opts._logger ?? getSafeLogger();
22118
+ const preview = opts.outputPreviewBytes && opts.outputPreviewBytes > 0 ? { outputPreview: previewOutput(ctx.lastOutput, opts.outputPreviewBytes) } : {};
22006
22119
  if (isTruncated) {
22007
22120
  logger?.warn(opts.reviewerKind, "JSON parse retry \u2014 likely truncated", {
22008
22121
  storyId: ctx.storyId,
22009
22122
  originalByteSize: ctx.lastOutput.length,
22123
+ ...preview,
22010
22124
  ...opts.logContext
22011
22125
  });
22012
22126
  } else {
22013
22127
  logger?.warn(opts.reviewerKind, "JSON parse retry \u2014 invalid shape", {
22014
22128
  storyId: ctx.storyId,
22015
22129
  originalByteSize: ctx.lastOutput.length,
22130
+ ...preview,
22016
22131
  ...opts.logContext
22017
22132
  });
22018
22133
  }
@@ -22544,9 +22659,16 @@ async function detectTestFramework(_workdir, language, pkg) {
22544
22659
  }
22545
22660
  return;
22546
22661
  }
22662
+ function clearLanguageCache() {
22663
+ _languageCache.clear();
22664
+ }
22547
22665
  async function detectLanguage(packageDir) {
22666
+ if (_languageCache.has(packageDir))
22667
+ return _languageCache.get(packageDir);
22548
22668
  const pkg = await _detectorDeps.readJson(join5(packageDir, "package.json"));
22549
- return _detectLanguageImpl(packageDir, pkg);
22669
+ const result = await _detectLanguageImpl(packageDir, pkg);
22670
+ _languageCache.set(packageDir, result);
22671
+ return result;
22550
22672
  }
22551
22673
  async function detectLintTool(workdir, language) {
22552
22674
  if (language === "go")
@@ -22574,7 +22696,7 @@ async function detectProjectProfile(workdir, existing) {
22574
22696
  const lintTool = existing.lintTool !== undefined ? existing.lintTool : await detectLintTool(workdir, language);
22575
22697
  return { language, type, testFramework, lintTool };
22576
22698
  }
22577
- var _detectorDeps, WEB_DEPS, API_DEPS;
22699
+ var _detectorDeps, WEB_DEPS, API_DEPS, _languageCache;
22578
22700
  var init_detector = __esm(() => {
22579
22701
  _detectorDeps = {
22580
22702
  async fileExists(path) {
@@ -22595,6 +22717,7 @@ var init_detector = __esm(() => {
22595
22717
  };
22596
22718
  WEB_DEPS = new Set(["react", "next", "vue", "nuxt"]);
22597
22719
  API_DEPS = new Set(["express", "fastify", "hono"]);
22720
+ _languageCache = new Map;
22598
22721
  });
22599
22722
 
22600
22723
  // src/test-runners/conventions.ts
@@ -23723,7 +23846,10 @@ async function detectNaxMonoLayout(workdir) {
23723
23846
  } catch {}
23724
23847
  return dirs;
23725
23848
  }
23726
- async function discoverWorkspacePackages(workdir) {
23849
+ function clearWorkspaceCache() {
23850
+ _workspaceCache.clear();
23851
+ }
23852
+ async function discoverWorkspacePackagesUncached(workdir) {
23727
23853
  const [fromPnpm, fromNpm, fromLerna, fromTurboNx, fromNaxMono] = await Promise.all([
23728
23854
  detectPnpmWorkspace(workdir),
23729
23855
  detectNpmWorkspaces(workdir),
@@ -23742,7 +23868,14 @@ async function discoverWorkspacePackages(workdir) {
23742
23868
  }
23743
23869
  return unique;
23744
23870
  }
23745
- 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;
23746
23879
  var init_workspace = __esm(() => {
23747
23880
  init_logger2();
23748
23881
  _workspaceDeps = {
@@ -23755,6 +23888,7 @@ var init_workspace = __esm(() => {
23755
23888
  spawn: Bun.spawn,
23756
23889
  glob: (pattern, cwd) => new Bun.Glob(pattern).scan({ cwd, onlyFiles: false })
23757
23890
  };
23891
+ _workspaceCache = new Map;
23758
23892
  });
23759
23893
 
23760
23894
  // src/test-runners/detect.ts
@@ -24618,6 +24752,19 @@ var init_path_filters = __esm(() => {
24618
24752
 
24619
24753
  // src/verification/smart-runner.ts
24620
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
+ }
24621
24768
  function extractPatternSuffix(pattern) {
24622
24769
  const lastStar = pattern.lastIndexOf("*");
24623
24770
  if (lastStar === -1)
@@ -24637,28 +24784,28 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
24637
24784
  return [];
24638
24785
  const searchTerms = sourceFiles.flatMap(extractSearchTerms);
24639
24786
  const testFilePaths = [];
24640
- for (const pattern of testFilePatterns) {
24641
- const glob = _bunDeps.glob(pattern);
24642
- for await (const file3 of glob.scan(workdir)) {
24643
- 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
+ }
24644
24799
  }
24645
- }
24646
- const matched = [];
24647
- for (const testFile of testFilePaths) {
24648
- let content;
24800
+ const results = await Promise.all(testFilePaths.map(async (testFile) => {
24649
24801
  try {
24650
- content = await _bunDeps.file(testFile).text();
24802
+ const content = await _bunDeps.file(testFile).text();
24803
+ return searchTerms.some((t) => content.includes(t)) ? testFile : null;
24651
24804
  } catch {
24652
- continue;
24653
- }
24654
- for (const term of searchTerms) {
24655
- if (content.includes(term)) {
24656
- matched.push(testFile);
24657
- break;
24658
- }
24805
+ return null;
24659
24806
  }
24660
- }
24661
- return matched;
24807
+ }));
24808
+ return results.filter((p) => p !== null);
24662
24809
  }
24663
24810
  async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePatterns = [...DEFAULT_TEST_FILE_PATTERNS]) {
24664
24811
  const testSuffixes = [...new Set(testFilePatterns.map(extractPatternSuffix).filter((s) => s !== null))];
@@ -24683,11 +24830,11 @@ async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePat
24683
24830
  }
24684
24831
  candidates.push(`${workdir}/${sourceWithoutExt}${suffix}`);
24685
24832
  }
24686
- for (const candidate of candidates) {
24687
- if (await _bunDeps.file(candidate).exists()) {
24688
- result.push(candidate);
24689
- }
24690
- }
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
+ });
24691
24838
  }
24692
24839
  return result;
24693
24840
  }
@@ -24726,7 +24873,7 @@ async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileR
24726
24873
  const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(effectiveRepoRoot, packageDir);
24727
24874
  let effectivePrefix = packagePrefix;
24728
24875
  if (packagePrefix && repoRoot) {
24729
- const gitRoot = await _gitUtilDeps.getGitRoot(workdir);
24876
+ const gitRoot = await getGitRootMemo(workdir);
24730
24877
  const extraPrefix2 = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
24731
24878
  effectivePrefix = extraPrefix2 ? `${extraPrefix2}/${packagePrefix}` : packagePrefix;
24732
24879
  }
@@ -24753,7 +24900,7 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
24753
24900
  `).filter(Boolean);
24754
24901
  const packageDir = packagePrefix ? join8(repoRoot, packagePrefix) : undefined;
24755
24902
  const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(repoRoot, packageDir);
24756
- const gitRoot = await _gitUtilDeps.getGitRoot(workdir);
24903
+ const gitRoot = await getGitRootMemo(workdir);
24757
24904
  const extraPrefix = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
24758
24905
  const effectivePrefix = packagePrefix ? extraPrefix ? `${extraPrefix}/${packagePrefix}` : packagePrefix : undefined;
24759
24906
  const scopedRaw = effectivePrefix ? lines.filter((f) => f.startsWith(`${effectivePrefix}/`)) : lines;
@@ -24764,8 +24911,9 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
24764
24911
  return [];
24765
24912
  }
24766
24913
  }
24767
- var _bunDeps, _gitUtilDeps, _smartRunnerDeps;
24914
+ var _bunDeps, MAX_GREP_TEST_FILES = 200, _gitUtilDeps, _gitRootCache, _smartRunnerDeps;
24768
24915
  var init_smart_runner = __esm(() => {
24916
+ init_logger2();
24769
24917
  init_conventions();
24770
24918
  init_git();
24771
24919
  init_path_filters();
@@ -24776,6 +24924,7 @@ var init_smart_runner = __esm(() => {
24776
24924
  _gitUtilDeps = {
24777
24925
  getGitRoot
24778
24926
  };
24927
+ _gitRootCache = new Map;
24779
24928
  _smartRunnerDeps = {
24780
24929
  glob: _bunDeps.glob,
24781
24930
  file: _bunDeps.file,
@@ -25133,31 +25282,50 @@ async function resolveSourceGlob(override, packageDir) {
25133
25282
  const language = await _codeNeighborDeps.detectLanguage(packageDir);
25134
25283
  return (language && SOURCE_GLOB_BY_LANGUAGE[language]) ?? FALLBACK_SOURCE_GLOB;
25135
25284
  }
25136
- 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) {
25137
25303
  const neighbors = new Set;
25138
25304
  let anyTruncated = false;
25139
- if (await _codeNeighborDeps.fileExists(join10(workdir, filePath))) {
25140
- const content = await _codeNeighborDeps.readFile(join10(workdir, filePath));
25141
- for (const spec of parseImportSpecifiers(content)) {
25142
- const resolved = resolveImport(spec, filePath, workdir);
25143
- if (resolved && resolved !== filePath)
25144
- 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
+ }
25145
25314
  }
25146
25315
  }
25147
25316
  const fileBaseName = (filePath.split("/").pop() ?? filePath).replace(/\.[^.]+$/, "");
25148
25317
  const fileNoExt = filePath.replace(/\.[^.]+$/, "");
25149
- const scanForReverseDeps = async (scanWorkdir) => {
25150
- const { files: srcFiles, truncated } = _codeNeighborDeps.glob(sourceGlob, scanWorkdir, ignoreMatchers, maxGlobFiles, globCtx);
25151
- if (truncated)
25152
- anyTruncated = true;
25153
- for (const srcFile of srcFiles) {
25154
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25155
- break;
25156
- if (srcFile === filePath)
25157
- continue;
25158
- try {
25159
- const content = await _codeNeighborDeps.readFile(join10(scanWorkdir, srcFile));
25160
- 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)) {
25161
25329
  for (const spec of parseImportSpecifiers(content)) {
25162
25330
  const resolved = resolveImport(spec, srcFile, scanWorkdir);
25163
25331
  if (resolved === filePath || resolved === fileNoExt) {
@@ -25166,17 +25334,8 @@ async function collectNeighbors(filePath, workdir, sourceGlob, maxGlobFiles, ext
25166
25334
  }
25167
25335
  }
25168
25336
  }
25169
- } catch {}
25170
- }
25171
- };
25172
- await scanForReverseDeps(workdir);
25173
- if (extraGlobWorkdirs) {
25174
- for (const extraDir of extraGlobWorkdirs) {
25175
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25176
- break;
25177
- await scanForReverseDeps(extraDir);
25337
+ }
25178
25338
  }
25179
- }
25180
25339
  if (siblingTestContext && !isTestFile2(filePath, siblingTestContext.regex)) {
25181
25340
  const candidates = deriveSiblingTestCandidates(filePath, siblingTestContext.globs);
25182
25341
  let chosen = null;
@@ -25239,10 +25398,17 @@ class CodeNeighborProvider {
25239
25398
  const ignoreMatchers = request.naxIgnoreIndex?.getMatchers(workdir);
25240
25399
  const sourceGlob = await resolveSourceGlob(this.sourceGlobOverride, request.packageDir);
25241
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;
25242
25408
  const sections = [];
25243
25409
  let anyTruncated = false;
25244
25410
  for (const file3 of filesToProcess) {
25245
- 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);
25246
25412
  if (truncated)
25247
25413
  anyTruncated = true;
25248
25414
  if (neighbors.length > 0) {
@@ -26160,13 +26326,25 @@ function buildPullToolDescriptors(stageToolNames, pullConfig) {
26160
26326
  const allowed = pullConfig.allowedTools;
26161
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 }));
26162
26328
  }
26163
- async function fetchWithTimeout(provider, request) {
26329
+ async function fetchWithTimeout(provider, request, timeoutMs = PROVIDER_FETCH_TIMEOUT_MS) {
26330
+ const controller = new AbortController;
26164
26331
  let handle;
26332
+ let timedOut = false;
26165
26333
  const timeout = new Promise((_, reject) => {
26166
- 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;
26167
26345
  });
26168
26346
  try {
26169
- return await Promise.race([provider.fetch(request), timeout]);
26347
+ return await Promise.race([fetchPromise, timeout]);
26170
26348
  } finally {
26171
26349
  clearTimeout(handle);
26172
26350
  }
@@ -26805,12 +26983,7 @@ class GitHistoryProvider {
26805
26983
  return { chunks: [], pullTools: [] };
26806
26984
  }
26807
26985
  const filesToProcess = touchedFiles.filter(isRelativeAndSafe).slice(0, MAX_FILES2);
26808
- const sections = [];
26809
- for (const file3 of filesToProcess) {
26810
- const section = await fetchFileHistory(file3, workdir);
26811
- if (section)
26812
- sections.push(section);
26813
- }
26986
+ const sections = (await Promise.all(filesToProcess.map((file3) => fetchFileHistory(file3, workdir)))).filter((section) => section !== null);
26814
26987
  if (sections.length === 0) {
26815
26988
  return { chunks: [], pullTools: [] };
26816
26989
  }
@@ -33270,20 +33443,23 @@ function createDrainDeadline(deadlineMs) {
33270
33443
  };
33271
33444
  }
33272
33445
  async function runQualityCommand(opts) {
33273
- 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;
33274
33447
  const startTime = Date.now();
33275
33448
  const logger = getSafeLogger();
33276
33449
  logger?.info("quality", `Running ${commandName}`, { storyId, commandName, command, workdir });
33277
33450
  try {
33451
+ const baseEnv = {
33452
+ ...process.env
33453
+ };
33454
+ for (const key of stripEnvVars ?? []) {
33455
+ delete baseEnv[key];
33456
+ }
33278
33457
  const proc = _qualityRunnerDeps.spawn({
33279
33458
  cmd: ["/bin/sh", "-c", command],
33280
33459
  cwd: workdir,
33281
33460
  stdout: "pipe",
33282
33461
  stderr: "pipe",
33283
- env: {
33284
- ...process.env,
33285
- ...env2 ?? {}
33286
- }
33462
+ env: { ...baseEnv, ...env2 ?? {} }
33287
33463
  });
33288
33464
  let timedOut = false;
33289
33465
  let exitedBeforeSigkill = false;
@@ -36863,7 +37039,7 @@ function buildVerifierFindings(verdict, categorization) {
36863
37039
  return [];
36864
37040
  }
36865
37041
  }
36866
- function parseVerdictFromStdout(output, _input, _ctx) {
37042
+ function parseVerdictFromStdout(output, input, _ctx) {
36867
37043
  if (!output || !output.trim()) {
36868
37044
  throw new ParseValidationError("verifier produced no stdout");
36869
37045
  }
@@ -36876,6 +37052,18 @@ function parseVerdictFromStdout(output, _input, _ctx) {
36876
37052
  throw new ParseValidationError("verifier stdout JSON missing required VerifierVerdict fields");
36877
37053
  }
36878
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
+ });
36879
37067
  return {
36880
37068
  success: categorization.success,
36881
37069
  filesChanged: [],
@@ -36897,6 +37085,7 @@ var verifierOp;
36897
37085
  var init_verify = __esm(() => {
36898
37086
  init_retry();
36899
37087
  init_config();
37088
+ init_logger2();
36900
37089
  init_tdd_builder();
36901
37090
  init_isolation();
36902
37091
  init_verdict();
@@ -36915,6 +37104,7 @@ var init_verify = __esm(() => {
36915
37104
  },
36916
37105
  reviewerKind: "verifier",
36917
37106
  maxAttempts: 2,
37107
+ outputPreviewBytes: 600,
36918
37108
  prompts: {
36919
37109
  invalid: () => TddPromptBuilder.verdictRetry(),
36920
37110
  truncated: () => TddPromptBuilder.verdictRetryCondensed()
@@ -36943,24 +37133,38 @@ var init_verify = __esm(() => {
36943
37133
  },
36944
37134
  async recover(input, verifyCtx) {
36945
37135
  const packageDir = verifyCtx.packageView.packageDir;
37136
+ const logger = getSafeLogger();
37137
+ const storyId = input.story.id;
36946
37138
  try {
36947
37139
  const verdict = await readVerdict(packageDir);
36948
37140
  if (verdict) {
36949
37141
  const testsAllPassing = verdict.tests.allPassing === true;
36950
37142
  const categorization = categorizeVerdict(verdict, testsAllPassing);
36951
37143
  const isolation = await runVerifierIsolation(input.beforeRef, verifyCtx);
37144
+ const normalizedFindings = buildVerifierFindings(verdict, categorization);
37145
+ logger?.warn("verifier", "Recovered verdict from disk after unparseable stdout", {
37146
+ storyId,
37147
+ packageDir,
37148
+ success: categorization.success,
37149
+ findingsCount: normalizedFindings.length,
37150
+ ...categorization.failureCategory && { failureCategory: categorization.failureCategory }
37151
+ });
36952
37152
  return {
36953
37153
  success: categorization.success,
36954
37154
  filesChanged: [],
36955
37155
  estimatedCostUsd: 0,
36956
37156
  durationMs: 0,
36957
37157
  output: "",
36958
- normalizedFindings: buildVerifierFindings(verdict, categorization),
37158
+ normalizedFindings,
36959
37159
  ...categorization.failureCategory && { failureCategory: categorization.failureCategory },
36960
37160
  ...categorization.reviewReason && { reviewReason: categorization.reviewReason },
36961
37161
  ...isolation && { isolation }
36962
37162
  };
36963
37163
  }
37164
+ logger?.error("verifier", "No usable verdict \u2014 unparseable stdout and no verdict file on disk (fail-closed)", {
37165
+ storyId,
37166
+ packageDir
37167
+ });
36964
37168
  return {
36965
37169
  success: false,
36966
37170
  filesChanged: [],
@@ -38212,7 +38416,8 @@ var init_mechanical_lintfix_strategy = __esm(() => {
38212
38416
  commandName: "lintFix",
38213
38417
  command,
38214
38418
  workdir: input.workdir,
38215
- storyId: input.storyId
38419
+ storyId: input.storyId,
38420
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38216
38421
  });
38217
38422
  return { applied: true, exitCode: result.exitCode };
38218
38423
  }
@@ -38270,7 +38475,8 @@ var init_mechanical_formatfix_strategy = __esm(() => {
38270
38475
  commandName: "formatFix",
38271
38476
  command,
38272
38477
  workdir: input.workdir,
38273
- storyId: input.storyId
38478
+ storyId: input.storyId,
38479
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38274
38480
  });
38275
38481
  return { applied: true, exitCode: result.exitCode };
38276
38482
  }
@@ -38303,7 +38509,8 @@ var init_lint_check = __esm(() => {
38303
38509
  commandName: "lint",
38304
38510
  command: command ?? "",
38305
38511
  workdir: input.workdir,
38306
- storyId: input.storyId
38512
+ storyId: input.storyId,
38513
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38307
38514
  });
38308
38515
  if (result.exitCode === 0) {
38309
38516
  return { success: true, findings: [], durationMs: Date.now() - start };
@@ -38552,7 +38759,8 @@ var init_typecheck_check = __esm(() => {
38552
38759
  commandName: "typecheck",
38553
38760
  command: command ?? "",
38554
38761
  workdir: input.workdir,
38555
- storyId: input.storyId
38762
+ storyId: input.storyId,
38763
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38556
38764
  });
38557
38765
  if (result.exitCode === 0) {
38558
38766
  return { success: true, findings: [], durationMs: Date.now() - start };
@@ -39527,13 +39735,14 @@ async function resolveLintScope(args) {
39527
39735
  function resolveScopedTemplate(reviewCommands, qualityCommands) {
39528
39736
  return reviewCommands.lintScoped ?? qualityCommands?.lintScoped;
39529
39737
  }
39530
- async function runLintCommand(workdir, storyId, env2, command) {
39738
+ async function runLintCommand(workdir, storyId, env2, command, stripEnvVars) {
39531
39739
  return runQualityCommand({
39532
39740
  commandName: SCOPED_LINT_CHECK,
39533
39741
  command,
39534
39742
  workdir,
39535
39743
  storyId,
39536
- env: env2
39744
+ env: env2,
39745
+ stripEnvVars
39537
39746
  });
39538
39747
  }
39539
39748
  function toReviewCheck(result) {
@@ -39574,7 +39783,7 @@ async function runScopedLintCheck(args) {
39574
39783
  storyId: args.storyId,
39575
39784
  reason: scope.degradedReason
39576
39785
  });
39577
- 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);
39578
39787
  return withLintScope(attachLintFindings(toReviewCheck(fullResult2), args.lintOutputFormat, args.workdir), scope, "degraded");
39579
39788
  }
39580
39789
  logger?.info("review", "lint_scope_empty", { storyId: args.storyId });
@@ -39599,19 +39808,19 @@ async function runScopedLintCheck(args) {
39599
39808
  }
39600
39809
  if (scopedTemplate) {
39601
39810
  const scopedCommand = scopedTemplate.replaceAll("{{files}}", scope.files.map(shellQuotePath4).join(" "));
39602
- 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);
39603
39812
  return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
39604
39813
  }
39605
39814
  if (!scope.degradedReason && isSupportedDerivedScopedCommand(fullLintCommand)) {
39606
39815
  const scopedCommand = appendFilesToCommand(fullLintCommand, scope.files);
39607
- 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);
39608
39817
  return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
39609
39818
  }
39610
39819
  logger?.warn("review", "lint_scope_degraded", {
39611
39820
  storyId: args.storyId,
39612
39821
  reason: scope.degradedReason ?? "unsupported_scoped_command_shape"
39613
39822
  });
39614
- 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);
39615
39824
  if (fullResult.exitCode === 0)
39616
39825
  return withLintScope(toReviewCheck(fullResult), scope, "degraded");
39617
39826
  const parsed = parseLintOutput(fullResult.output, args.lintOutputFormat ?? "auto", {
@@ -40337,8 +40546,8 @@ async function resolveCommand(check2, config2, executionConfig, workdir, quality
40337
40546
  }
40338
40547
  return null;
40339
40548
  }
40340
- async function runCheck(check2, command, workdir, storyId, env2) {
40341
- 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 });
40342
40551
  return {
40343
40552
  check: check2,
40344
40553
  command: result.command,
@@ -40544,8 +40753,9 @@ async function runReview(opts) {
40544
40753
  story,
40545
40754
  storyGitRef,
40546
40755
  env: env2,
40756
+ stripEnvVars: naxConfig?.quality?.stripEnvVars ?? [],
40547
40757
  naxIgnoreIndex
40548
- }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2), workdir);
40758
+ }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2, naxConfig?.quality?.stripEnvVars ?? []), workdir);
40549
40759
  checks3.push(result);
40550
40760
  if (result.success) {
40551
40761
  logger?.info("review", `${checkName} passed`, {
@@ -43893,13 +44103,13 @@ var init_factory = __esm(() => {
43893
44103
 
43894
44104
  // src/execution/pid-registry.ts
43895
44105
  import { existsSync as existsSync7 } from "fs";
43896
- import { appendFile as appendFile2 } from "fs/promises";
43897
44106
 
43898
44107
  class PidRegistry {
43899
44108
  workdir;
43900
44109
  pidsFilePath;
43901
44110
  pids = new Set;
43902
44111
  frozen = false;
44112
+ writeQueueTail = Promise.resolve();
43903
44113
  constructor(workdir, _platform) {
43904
44114
  this.workdir = workdir;
43905
44115
  this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
@@ -43920,15 +44130,8 @@ class PidRegistry {
43920
44130
  return;
43921
44131
  }
43922
44132
  this.pids.add(pid);
43923
- const entry = {
43924
- pid,
43925
- spawnedAt: new Date().toISOString(),
43926
- workdir: this.workdir
43927
- };
43928
44133
  try {
43929
- const line = `${JSON.stringify(entry)}
43930
- `;
43931
- await appendFile2(this.pidsFilePath, line);
44134
+ await this.enqueueWrite();
43932
44135
  logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
43933
44136
  } catch (err) {
43934
44137
  logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
@@ -43940,7 +44143,7 @@ class PidRegistry {
43940
44143
  const logger = getSafeLogger();
43941
44144
  this.pids.delete(pid);
43942
44145
  try {
43943
- await this.writePidsFile();
44146
+ await this.enqueueWrite();
43944
44147
  logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
43945
44148
  } catch (err) {
43946
44149
  logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
@@ -43959,8 +44162,8 @@ class PidRegistry {
43959
44162
  const killPromises = pids.map((pid) => this.killPidTree(pid));
43960
44163
  await Promise.allSettled(killPromises);
43961
44164
  try {
43962
- await Bun.write(this.pidsFilePath, "");
43963
44165
  this.pids.clear();
44166
+ await this.enqueueWrite();
43964
44167
  logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
43965
44168
  } catch (err) {
43966
44169
  logger?.warn("pid-registry", "Failed to clear registry file", {
@@ -44179,6 +44382,37 @@ class PidRegistry {
44179
44382
  getPids() {
44180
44383
  return Array.from(this.pids);
44181
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
+ }
44182
44416
  }
44183
44417
  var PID_REGISTRY_FILE = ".nax-pids", PID_TREE_KILL_GRACE_MS = 250, _pidRegistryDeps;
44184
44418
  var init_pid_registry = __esm(() => {
@@ -52025,7 +52259,7 @@ var init_effectiveness = __esm(() => {
52025
52259
  });
52026
52260
 
52027
52261
  // src/execution/progress.ts
52028
- import { appendFile as appendFile3, mkdir as mkdir7 } from "fs/promises";
52262
+ import { appendFile as appendFile2, mkdir as mkdir7 } from "fs/promises";
52029
52263
  import { join as join44 } from "path";
52030
52264
  async function appendProgress(featureDir, storyId, status, message) {
52031
52265
  await mkdir7(featureDir, { recursive: true });
@@ -52033,7 +52267,7 @@ async function appendProgress(featureDir, storyId, status, message) {
52033
52267
  const timestamp = new Date().toISOString();
52034
52268
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
52035
52269
  `;
52036
- await appendFile3(progressPath, entry);
52270
+ await appendFile2(progressPath, entry);
52037
52271
  }
52038
52272
  var init_progress = () => {};
52039
52273
 
@@ -52140,6 +52374,7 @@ var init_completion = __esm(() => {
52140
52374
  const logger = getLogger();
52141
52375
  const isBatch = ctx.stories.length > 1;
52142
52376
  const sessionCost = ctx.agentResult?.estimatedCostUsd || 0;
52377
+ const persistPrd = ctx.skipPrdPersistence !== true;
52143
52378
  const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
52144
52379
  const storyStartTime = ctx.storyStartTime || new Date().toISOString();
52145
52380
  if (isBatch) {
@@ -52164,7 +52399,9 @@ var init_completion = __esm(() => {
52164
52399
  }
52165
52400
  }
52166
52401
  for (const completedStory of ctx.stories) {
52167
- markStoryPassed(ctx.prd, completedStory.id);
52402
+ if (persistPrd) {
52403
+ markStoryPassed(ctx.prd, completedStory.id);
52404
+ }
52168
52405
  const costPerStory = sessionCost / ctx.stories.length;
52169
52406
  logger.info("completion", "Story passed", {
52170
52407
  storyId: completedStory.id,
@@ -52196,7 +52433,9 @@ var init_completion = __esm(() => {
52196
52433
  }
52197
52434
  }
52198
52435
  }
52199
- await _completionDeps.savePRD(ctx.prd, prdPath);
52436
+ if (persistPrd) {
52437
+ await _completionDeps.savePRD(ctx.prd, prdPath);
52438
+ }
52200
52439
  const updatedCounts = countStories(ctx.prd);
52201
52440
  logger.info("completion", "Progress update", {
52202
52441
  storyId: ctx.story.id,
@@ -52981,23 +53220,35 @@ function logUnifiedReviewPhaseStart(storyId, opName) {
52981
53220
  logger?.info("review", "Running adversarial check", { storyId });
52982
53221
  }
52983
53222
  }
52984
- function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage) {
52985
- if (isTddPhase)
52986
- return;
52987
- if (opName === "semantic-review" || opName === "adversarial-review")
52988
- return;
53223
+ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
52989
53224
  if (output === null || output === undefined || typeof output !== "object")
52990
- return;
52991
- const logger = getSafeLogger();
53225
+ return null;
52992
53226
  const r = output;
52993
53227
  const success2 = r.success === true || r.passed === true;
52994
- const findingsCount = Array.isArray(r.findings) ? r.findings.length : undefined;
53228
+ const findingsCount = Array.isArray(r.normalizedFindings) ? r.normalizedFindings.length : Array.isArray(r.findings) ? r.findings.length : undefined;
52995
53229
  const status = typeof r.status === "string" ? r.status : undefined;
52996
53230
  const data = { storyId, phase: opName, durationMs };
52997
53231
  if (findingsCount !== undefined)
52998
53232
  data.findingsCount = findingsCount;
52999
53233
  if (status !== undefined)
53000
53234
  data.status = status;
53235
+ if (typeof r.failureCategory === "string")
53236
+ data.failureCategory = r.failureCategory;
53237
+ if (typeof r.reviewReason === "string")
53238
+ data.reviewReason = r.reviewReason;
53239
+ return { success: success2, data };
53240
+ }
53241
+ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage, progressData = {}) {
53242
+ if (isTddPhase)
53243
+ return;
53244
+ if (opName === "semantic-review" || opName === "adversarial-review")
53245
+ return;
53246
+ const built = buildPhaseOutcomeLogData(storyId, opName, output, durationMs);
53247
+ if (!built)
53248
+ return;
53249
+ const { success: success2 } = built;
53250
+ const data = { ...built.data, ...progressData };
53251
+ const logger = getSafeLogger();
53001
53252
  const message = formatPhaseResultMessage(opName, success2, stage);
53002
53253
  if (stage === "rectification") {
53003
53254
  logger?.info("story-orchestrator", message, data);
@@ -53059,17 +53310,21 @@ function logUnifiedReviewPhaseResult(storyId, opName, output) {
53059
53310
  truncated: findingsCount > findingsSummary.length
53060
53311
  });
53061
53312
  }
53062
- async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false) {
53313
+ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false, progress) {
53063
53314
  const logger = getSafeLogger();
53064
53315
  const opName = slot.op.name;
53316
+ const progressData = progress ? { phaseIndex: progress.index, totalPhases: progress.total } : {};
53065
53317
  const isTddPhase = isThreeSession && TDD_OP_NAMES.has(opName);
53066
53318
  const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
53067
53319
  let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
53068
53320
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
53069
53321
  if (isTddPhase) {
53070
- logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName });
53322
+ logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName, ...progressData });
53071
53323
  } else if (isThreeSession && opName === "full-suite-gate") {
53072
- 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
+ });
53073
53328
  }
53074
53329
  logUnifiedReviewPhaseStart(ctx.storyId, opName);
53075
53330
  const phaseStartedAt = Date.now();
@@ -53079,7 +53334,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
53079
53334
  phaseOutputs[opName] = output;
53080
53335
  emitReviewDecision(ctx, opName, output);
53081
53336
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
53082
- 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);
53083
53338
  if (isTddPhase) {
53084
53339
  const durationMs = Date.now() - phaseStartedAt;
53085
53340
  logger?.info("tdd", `Session complete: ${opName}`, {
@@ -53248,9 +53503,13 @@ class ExecutionPlan {
53248
53503
  const phaseOutputs = {};
53249
53504
  const startedAt = Date.now();
53250
53505
  const logger = getSafeLogger();
53251
- for (const phase of collectOrderedPhases(this.state)) {
53506
+ const orderedPhases = collectOrderedPhases(this.state);
53507
+ for (const [phaseIndex, phase] of orderedPhases.entries()) {
53252
53508
  try {
53253
- 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
+ });
53254
53513
  } catch (error48) {
53255
53514
  logger?.error("story-orchestrator", "Phase threw unexpected error", {
53256
53515
  storyId: this.ctx.storyId,
@@ -56915,7 +57174,7 @@ function renderProposals(proposals, runId, observationCount) {
56915
57174
  }
56916
57175
 
56917
57176
  // src/plugins/builtin/curator/rollup.ts
56918
- import { appendFile as appendFile4, mkdir as mkdir10, writeFile } from "fs/promises";
57177
+ import { appendFile as appendFile3, mkdir as mkdir10, writeFile } from "fs/promises";
56919
57178
  import * as path14 from "path";
56920
57179
  async function appendToRollup(observations, rollupPath) {
56921
57180
  try {
@@ -56931,7 +57190,7 @@ async function appendToRollup(observations, rollupPath) {
56931
57190
  const newLines = `${observations.map((o) => JSON.stringify(o)).join(`
56932
57191
  `)}
56933
57192
  `;
56934
- await appendFile4(rollupPath, newLines);
57193
+ await appendFile3(rollupPath, newLines);
56935
57194
  } catch {}
56936
57195
  }
56937
57196
  var init_rollup = () => {};
@@ -57849,7 +58108,7 @@ var package_default;
57849
58108
  var init_package = __esm(() => {
57850
58109
  package_default = {
57851
58110
  name: "@nathapp/nax",
57852
- version: "0.68.0",
58111
+ version: "0.68.2",
57853
58112
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
57854
58113
  type: "module",
57855
58114
  bin: {
@@ -57944,8 +58203,8 @@ var init_version = __esm(() => {
57944
58203
  NAX_VERSION = package_default.version;
57945
58204
  NAX_COMMIT = (() => {
57946
58205
  try {
57947
- if (/^[0-9a-f]{6,10}$/.test("d56db412"))
57948
- return "d56db412";
58206
+ if (/^[0-9a-f]{6,10}$/.test("27a81a5e"))
58207
+ return "27a81a5e";
57949
58208
  } catch {}
57950
58209
  try {
57951
58210
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -59361,6 +59620,9 @@ async function handleRunCompletion(options) {
59361
59620
  if (options.pluginProviderCache) {
59362
59621
  await options.pluginProviderCache.disposeAll();
59363
59622
  }
59623
+ clearLanguageCache();
59624
+ clearWorkspaceCache();
59625
+ clearGitRootCache();
59364
59626
  const finalCounts = countStories(prd);
59365
59627
  const fallbackAggregate = deriveRunFallbackAggregates(allStoryMetrics);
59366
59628
  pipelineEventBus.emit({
@@ -59466,7 +59728,10 @@ var init_run_completion = __esm(() => {
59466
59728
  init_metrics();
59467
59729
  init_event_bus();
59468
59730
  init_prd();
59731
+ init_detector();
59469
59732
  init_scratch_purge();
59733
+ init_workspace();
59734
+ init_smart_runner();
59470
59735
  init_run_regression();
59471
59736
  _runCompletionDeps = {
59472
59737
  runDeferredRegression,
@@ -59564,7 +59829,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
59564
59829
  var DEFAULT_MAX_BATCH_SIZE = 4;
59565
59830
 
59566
59831
  // src/pipeline/subscribers/events-writer.ts
59567
- import { appendFile as appendFile5, mkdir as mkdir14 } from "fs/promises";
59832
+ import { appendFile as appendFile4, mkdir as mkdir14 } from "fs/promises";
59568
59833
  import { basename as basename14, join as join73 } from "path";
59569
59834
  function wireEventsWriter(bus, feature, runId, workdir) {
59570
59835
  const logger = getSafeLogger();
@@ -59579,7 +59844,7 @@ function wireEventsWriter(bus, feature, runId, workdir) {
59579
59844
  await mkdir14(eventsDir, { recursive: true });
59580
59845
  dirReady = true;
59581
59846
  }
59582
- await appendFile5(eventsFile, `${JSON.stringify(line)}
59847
+ await appendFile4(eventsFile, `${JSON.stringify(line)}
59583
59848
  `);
59584
59849
  } catch (err) {
59585
59850
  logger?.warn("events-writer", "Failed to write event line (non-fatal)", {
@@ -61476,9 +61741,13 @@ var exports_parallel_worker = {};
61476
61741
  __export(exports_parallel_worker, {
61477
61742
  executeStoryInWorktree: () => executeStoryInWorktree,
61478
61743
  executeParallelBatch: () => executeParallelBatch,
61744
+ buildWorktreePipelineContext: () => buildWorktreePipelineContext,
61479
61745
  _parallelWorkerDeps: () => _parallelWorkerDeps
61480
61746
  });
61481
61747
  import { join as join79 } from "path";
61748
+ function buildWorktreePipelineContext(base, _story) {
61749
+ return { ...base, prd: structuredClone(base.prd) };
61750
+ }
61482
61751
  async function executeStoryInWorktree(story, worktreePath, dependencyContext, context, routing, eventEmitter) {
61483
61752
  const logger = getSafeLogger();
61484
61753
  try {
@@ -61492,7 +61761,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
61492
61761
  }
61493
61762
  }
61494
61763
  const pipelineContext = {
61495
- ...context,
61764
+ ...buildWorktreePipelineContext(context, story),
61496
61765
  config: context.config,
61497
61766
  rootConfig: context.rootConfig,
61498
61767
  story,
@@ -61792,6 +62061,7 @@ var init_parallel_batch = __esm(() => {
61792
62061
  // src/execution/unified-executor.ts
61793
62062
  var exports_unified_executor = {};
61794
62063
  __export(exports_unified_executor, {
62064
+ reconcileBatchOutcome: () => reconcileBatchOutcome,
61795
62065
  executeUnified: () => executeUnified,
61796
62066
  _unifiedExecutorDeps: () => _unifiedExecutorDeps
61797
62067
  });
@@ -61915,7 +62185,10 @@ async function executeUnified(ctx, initialPrd) {
61915
62185
  const readyStories = getAllReadyStories(prd);
61916
62186
  const batch = _unifiedExecutorDeps.selectIndependentBatch(readyStories, ctx.parallelCount);
61917
62187
  if (batch.length > 1) {
61918
- 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()) {
61919
62192
  const modelTier2 = story.routing?.modelTier ?? ctx.config.autoMode.complexityRouting?.[story.routing?.complexity ?? "medium"] ?? "balanced";
61920
62193
  pipelineEventBus.emit({
61921
62194
  type: "story:started",
@@ -61923,7 +62196,7 @@ async function executeUnified(ctx, initialPrd) {
61923
62196
  story: { id: story.id, title: story.title, status: story.status, attempts: story.attempts },
61924
62197
  workdir: ctx.workdir,
61925
62198
  modelTier: modelTier2,
61926
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62199
+ agent: batchAgent,
61927
62200
  iteration: iterations
61928
62201
  });
61929
62202
  logger?.info("story.start", `${story.title}`, {
@@ -61931,6 +62204,9 @@ async function executeUnified(ctx, initialPrd) {
61931
62204
  storyTitle: story.title,
61932
62205
  complexity: story.routing?.complexity ?? "unknown",
61933
62206
  modelTier: modelTier2,
62207
+ agent: batchAgent,
62208
+ storyNumber: batchBaseDone + batchIndex + 1,
62209
+ storyTotal: batchCounts.total,
61934
62210
  attempt: story.attempts + 1
61935
62211
  });
61936
62212
  }
@@ -61950,6 +62226,7 @@ async function executeUnified(ctx, initialPrd) {
61950
62226
  config: ctx.config,
61951
62227
  rootConfig: ctx.config,
61952
62228
  prd,
62229
+ skipPrdPersistence: true,
61953
62230
  projectDir: ctx.workdir,
61954
62231
  naxIgnoreIndex,
61955
62232
  hooks: ctx.hooks,
@@ -61997,6 +62274,8 @@ async function executeUnified(ctx, initialPrd) {
61997
62274
  abortSignal: ctx.abortSignal
61998
62275
  }, pipelineResult);
61999
62276
  }
62277
+ reconcileBatchOutcome(prd, batchResult);
62278
+ await savePRD(prd, ctx.prdPath);
62000
62279
  await pipelineEventBus.drain();
62001
62280
  totalCost += batchResult.totalCost;
62002
62281
  storiesCompleted += batchResult.completed.length;
@@ -62087,6 +62366,8 @@ async function executeUnified(ctx, initialPrd) {
62087
62366
  }
62088
62367
  }
62089
62368
  const modelTier2 = singleSelection.routing.modelTier;
62369
+ const singleAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62370
+ const singleCounts = countStories(prd);
62090
62371
  pipelineEventBus.emit({
62091
62372
  type: "story:started",
62092
62373
  storyId: singleStory.id,
@@ -62098,7 +62379,7 @@ async function executeUnified(ctx, initialPrd) {
62098
62379
  },
62099
62380
  workdir: ctx.workdir,
62100
62381
  modelTier: modelTier2,
62101
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62382
+ agent: singleAgent,
62102
62383
  iteration: iterations
62103
62384
  });
62104
62385
  logger?.info("story.start", `${singleStory.title}`, {
@@ -62106,6 +62387,9 @@ async function executeUnified(ctx, initialPrd) {
62106
62387
  storyTitle: singleStory.title,
62107
62388
  complexity: singleSelection.routing.complexity ?? "unknown",
62108
62389
  modelTier: modelTier2,
62390
+ agent: singleAgent,
62391
+ storyNumber: singleCounts.total - singleCounts.pending + 1,
62392
+ storyTotal: singleCounts.total,
62109
62393
  attempt: singleStory.attempts + 1
62110
62394
  });
62111
62395
  const singleIter = await _unifiedExecutorDeps.runIteration(ctx, prd, singleSelection, iterations, totalCost, allStoryMetrics);
@@ -62157,6 +62441,8 @@ async function executeUnified(ctx, initialPrd) {
62157
62441
  }
62158
62442
  }
62159
62443
  const modelTier = selection.routing.modelTier;
62444
+ const seqAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62445
+ const seqCounts = countStories(prd);
62160
62446
  pipelineEventBus.emit({
62161
62447
  type: "story:started",
62162
62448
  storyId: selection.story.id,
@@ -62168,7 +62454,7 @@ async function executeUnified(ctx, initialPrd) {
62168
62454
  },
62169
62455
  workdir: ctx.workdir,
62170
62456
  modelTier,
62171
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62457
+ agent: seqAgent,
62172
62458
  iteration: iterations
62173
62459
  });
62174
62460
  logger?.info("story.start", `${selection.story.title}`, {
@@ -62176,6 +62462,9 @@ async function executeUnified(ctx, initialPrd) {
62176
62462
  storyTitle: selection.story.title,
62177
62463
  complexity: selection.routing.complexity ?? "unknown",
62178
62464
  modelTier,
62465
+ agent: seqAgent,
62466
+ storyNumber: seqCounts.total - seqCounts.pending + 1,
62467
+ storyTotal: seqCounts.total,
62179
62468
  attempt: selection.story.attempts + 1
62180
62469
  });
62181
62470
  const iter = await _unifiedExecutorDeps.runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
@@ -62234,6 +62523,18 @@ async function executeUnified(ctx, initialPrd) {
62234
62523
  return buildResult2("max-iterations");
62235
62524
  } finally {}
62236
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
+ }
62237
62538
  var TERMINAL_ACTIONS, _unifiedExecutorDeps;
62238
62539
  var init_unified_executor = __esm(() => {
62239
62540
  init_agents();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.68.0",
3
+ "version": "0.68.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {