@nathapp/nax 0.80.0 → 0.80.1

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.
package/dist/nax.js CHANGED
@@ -16891,7 +16891,7 @@ var init_schemas_execution = __esm(() => {
16891
16891
  return val;
16892
16892
  }, SmartTestRunnerConfigSchema).default(SMART_TEST_RUNNER_DEFAULT);
16893
16893
  WorktreeDependenciesConfigSchema = exports_external.object({
16894
- mode: exports_external.enum(["inherit", "provision", "off"]).default("off"),
16894
+ mode: exports_external.enum(["provision", "off"]).default("off"),
16895
16895
  setupCommand: exports_external.string().nullable().default(null)
16896
16896
  }).superRefine((value, ctx) => {
16897
16897
  if (value.mode !== "provision" && value.setupCommand !== null) {
@@ -17924,7 +17924,8 @@ function formatMutationSummary(summaries) {
17924
17924
 
17925
17925
  // src/review/severity.ts
17926
17926
  function isBlockingSeverity(sev, threshold = "error") {
17927
- return (SEVERITY_RANK[sev] ?? 0) >= SEVERITY_RANK[threshold];
17927
+ const rank = SEVERITY_RANK[sev] ?? 0;
17928
+ return rank >= SEVERITY_RANK[threshold];
17928
17929
  }
17929
17930
  var SEVERITY_RANK;
17930
17931
  var init_severity = __esm(() => {
@@ -18257,7 +18258,8 @@ function formatAdvisorySummary(findings, options) {
18257
18258
  return JSON.stringify(findings);
18258
18259
  }
18259
18260
  const c = useColor ? source_default : createNoopChalk();
18260
- const sorted = [...findings].sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0));
18261
+ const rank = SEVERITY_RANK;
18262
+ const sorted = [...findings].sort((a, b) => (rank[b.severity] ?? 0) - (rank[a.severity] ?? 0));
18261
18263
  const lines = [];
18262
18264
  lines.push("");
18263
18265
  lines.push(c.yellow("\u2500".repeat(60)));
@@ -18399,6 +18401,9 @@ function redactString(value) {
18399
18401
  }
18400
18402
  return out;
18401
18403
  }
18404
+ function redactSecrets(input) {
18405
+ return redactValue(input);
18406
+ }
18402
18407
  function redactEntry(entry) {
18403
18408
  return {
18404
18409
  ...entry,
@@ -18691,6 +18696,7 @@ var init_logger = __esm(() => {
18691
18696
  var exports_logger = {};
18692
18697
  __export(exports_logger, {
18693
18698
  resetLogger: () => resetLogger,
18699
+ redactSecrets: () => redactSecrets,
18694
18700
  initLogger: () => initLogger,
18695
18701
  getSafeLogger: () => getSafeLogger,
18696
18702
  getLogger: () => getLogger,
@@ -18702,6 +18708,7 @@ __export(exports_logger, {
18702
18708
  var init_logger2 = __esm(() => {
18703
18709
  init_logger();
18704
18710
  init_formatters();
18711
+ init_redact();
18705
18712
  });
18706
18713
 
18707
18714
  // src/utils/json-file.ts
@@ -18853,6 +18860,17 @@ function _applyRemovedRoutingKeysShim(conf, warn = defaultConfigWarn) {
18853
18860
  }
18854
18861
  return newRouting === routing ? conf : { ...conf, routing: newRouting };
18855
18862
  }
18863
+ function _applyRemovedWorktreeInheritShim(conf, warn = defaultConfigWarn) {
18864
+ const execution = conf.execution;
18865
+ const worktreeDependencies = execution?.worktreeDependencies;
18866
+ if (worktreeDependencies?.mode !== "inherit")
18867
+ return conf;
18868
+ warn('execution.worktreeDependencies.mode="inherit" was removed (issue #574) and is mapped to "off". ' + "For JS/TS repos that is the same behaviour. If this repo needs its own install (a Python venv, bundler, composer), " + 'set mode "provision" with a setupCommand: "inherit" used to fail the run outright in that case, and "off" will now ' + "proceed without installing.");
18869
+ return {
18870
+ ...conf,
18871
+ execution: { ...execution, worktreeDependencies: { ...worktreeDependencies, mode: "off" } }
18872
+ };
18873
+ }
18856
18874
  function applyBatchModeCompat(conf, warn = defaultConfigWarn) {
18857
18875
  const routing = conf.routing;
18858
18876
  const llm = routing?.llm;
@@ -18940,7 +18958,7 @@ function warnSecuritySensitiveOverrides(beforeConfig, afterConfig, warn = defaul
18940
18958
  function applyConfigCompatShims(conf, logger, dedupe) {
18941
18959
  const log = dedupe.wrapLogger(logger);
18942
18960
  const warn = dedupe.warn;
18943
- return _applyLegacyReviewExecutionShim(_applyRemovedRoutingKeysShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(conf, log), log), warn), warn), warn), warn), warn);
18961
+ return _applyRemovedWorktreeInheritShim(_applyLegacyReviewExecutionShim(_applyRemovedRoutingKeysShim(applyRoutingRetryDeprecationWarning(applyBatchModeCompat(applyRemovedStrategyCompat(migrateLegacyReviewModelKey(migrateLegacyTestPattern(conf, log), log), warn), warn), warn), warn), warn), warn);
18944
18962
  }
18945
18963
  var SECURITY_SENSITIVE_KEY_PATHS;
18946
18964
  var init_compat_shims = __esm(() => {
@@ -19511,6 +19529,26 @@ var init_path_security = () => {};
19511
19529
  // src/config/paths.ts
19512
19530
  import { homedir } from "os";
19513
19531
  import { join, resolve as resolve2 } from "path";
19532
+ function validateFeatureId(featureId) {
19533
+ if (!featureId || featureId.length === 0) {
19534
+ throw new NaxError("Feature ID cannot be empty", "INVALID_FEATURE_ID", { stage: "config" });
19535
+ }
19536
+ if (featureId.includes("..")) {
19537
+ throw new NaxError("Feature ID cannot contain path traversal (..)", "INVALID_FEATURE_ID", {
19538
+ stage: "config",
19539
+ featureId
19540
+ });
19541
+ }
19542
+ if (featureId.startsWith("--")) {
19543
+ throw new NaxError("Feature ID cannot start with git flags (--)", "INVALID_FEATURE_ID", {
19544
+ stage: "config",
19545
+ featureId
19546
+ });
19547
+ }
19548
+ if (!FEATURE_ID_PATTERN.test(featureId)) {
19549
+ throw new NaxError(`Feature ID must match pattern [a-zA-Z0-9_][a-zA-Z0-9._-]{0,63}. Got: ${featureId}`, "INVALID_FEATURE_ID", { stage: "config", featureId });
19550
+ }
19551
+ }
19514
19552
  function globalConfigDir() {
19515
19553
  const override = process.env[GLOBAL_CONFIG_DIR_ENV];
19516
19554
  if (override)
@@ -19524,10 +19562,13 @@ function featuresDir(root) {
19524
19562
  return join(root, PROJECT_NAX_DIR, "features");
19525
19563
  }
19526
19564
  function featureDir(root, featureId) {
19565
+ validateFeatureId(featureId);
19527
19566
  return join(featuresDir(root), featureId);
19528
19567
  }
19529
- var GLOBAL_CONFIG_DIR_ENV = "NAX_GLOBAL_CONFIG_DIR", PROJECT_NAX_DIR = ".nax", PROJECT_FEATURES_DIR;
19568
+ var GLOBAL_CONFIG_DIR_ENV = "NAX_GLOBAL_CONFIG_DIR", FEATURE_ID_PATTERN, PROJECT_NAX_DIR = ".nax", PROJECT_FEATURES_DIR;
19530
19569
  var init_paths = __esm(() => {
19570
+ init_errors();
19571
+ FEATURE_ID_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,63}$/;
19531
19572
  PROJECT_FEATURES_DIR = `${PROJECT_NAX_DIR}/features`;
19532
19573
  });
19533
19574
 
@@ -19714,55 +19755,41 @@ function resolveEnvVarsWarnOnFailure(config2, logger, layerName) {
19714
19755
  function globalConfigPath() {
19715
19756
  return join3(globalConfigDir(), "config.json");
19716
19757
  }
19717
- function findProjectDir(startDir = process.cwd()) {
19718
- let dir = resolve3(startDir);
19719
- let depth = 0;
19720
- while (depth < MAX_DIRECTORY_DEPTH) {
19721
- const candidate = join3(dir, PROJECT_NAX_DIR);
19722
- if (existsSync3(join3(candidate, "config.json"))) {
19723
- return candidate;
19724
- }
19725
- const parent = join3(dir, "..");
19726
- if (parent === dir)
19727
- break;
19728
- dir = parent;
19729
- depth++;
19730
- }
19731
- return null;
19732
- }
19733
- async function loadConfig(startDir, cliOverrides) {
19734
- let rawConfig = structuredClone(DEFAULT_CONFIG);
19735
- const warnDedupe = createConfigWarnDedupe();
19758
+ function resolveProjectPaths(startDir) {
19736
19759
  const projDir = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
19737
19760
  const projectRoot = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? dirname(startDir) : startDir : process.cwd();
19738
- const profileChain = await resolveProfileNames(cliOverrides ?? {}, process.env, projectRoot);
19739
- const overlayChain = profileChain.filter((name) => name && name !== "default");
19761
+ return { projDir, projectRoot };
19762
+ }
19763
+ async function applyGlobalLayer(rawConfig, ctx) {
19740
19764
  const globalConfRaw = await loadJsonFile(globalConfigPath(), "config");
19741
- let logger = null;
19742
- try {
19743
- logger = getLogger();
19744
- } catch {}
19745
- let globalLayerConf = null;
19746
- if (globalConfRaw) {
19747
- const { profile: _gProfile, ...globalConfStripped } = globalConfRaw;
19748
- const globalConf = applyConfigCompatShims(globalConfStripped, logger, warnDedupe);
19749
- globalLayerConf = globalConf;
19750
- rawConfig = deepMergeConfig(rawConfig, globalConf);
19751
- }
19752
- if (projDir) {
19753
- const projConf = await loadJsonFile(join3(projDir, "config.json"), "config");
19754
- if (projConf) {
19755
- const { profile: _pProfile, ...projConfStripped } = projConf;
19756
- const resolvedProjConf = applyConfigCompatShims(projConfStripped, logger, warnDedupe);
19757
- const preProjectMergeConfig = rawConfig;
19758
- rawConfig = deepMergeConfig(rawConfig, resolvedProjConf);
19759
- warnSecuritySensitiveOverrides(preProjectMergeConfig, rawConfig, warnDedupe.warn, {
19760
- layerName: "project",
19761
- sourceLayerConf: globalLayerConf
19762
- });
19763
- }
19764
- }
19765
- rawConfig = resolveEnvVarsWarnOnFailure(rawConfig, logger, "global/project config");
19765
+ if (!globalConfRaw)
19766
+ return { rawConfig, globalConfRaw: null, globalLayerConf: null };
19767
+ const { profile: _gProfile, ...globalConfStripped } = globalConfRaw;
19768
+ const globalConf = applyConfigCompatShims(globalConfStripped, ctx.logger, ctx.warnDedupe);
19769
+ return {
19770
+ rawConfig: deepMergeConfig(rawConfig, globalConf),
19771
+ globalConfRaw,
19772
+ globalLayerConf: globalConf
19773
+ };
19774
+ }
19775
+ async function applyProjectLayer(rawConfig, projDir, globalLayerConf, ctx) {
19776
+ if (!projDir)
19777
+ return rawConfig;
19778
+ const projConf = await loadJsonFile(join3(projDir, "config.json"), "config");
19779
+ if (!projConf)
19780
+ return rawConfig;
19781
+ const { profile: _pProfile, ...projConfStripped } = projConf;
19782
+ const resolvedProjConf = applyConfigCompatShims(projConfStripped, ctx.logger, ctx.warnDedupe);
19783
+ const preProjectMergeConfig = rawConfig;
19784
+ const merged = deepMergeConfig(rawConfig, resolvedProjConf);
19785
+ warnSecuritySensitiveOverrides(preProjectMergeConfig, merged, ctx.warnDedupe.warn, {
19786
+ layerName: "project",
19787
+ sourceLayerConf: globalLayerConf
19788
+ });
19789
+ return merged;
19790
+ }
19791
+ async function applyProfileChainLayer(rawConfig, overlayChain, projectRoot, ctx) {
19792
+ let merged = rawConfig;
19766
19793
  for (const name of overlayChain) {
19767
19794
  const profileData = await loadProfile(name, projectRoot);
19768
19795
  const profileEnv = await loadProfileEnv(name, projectRoot);
@@ -19774,36 +19801,36 @@ async function loadConfig(startDir, cliOverrides) {
19774
19801
  const path = err instanceof UnresolvedEnvVarError ? err.path.join(".") : undefined;
19775
19802
  throw new NaxError(`Profile "${name}" references an undefined environment variable${varName ? ` $${varName}` : ""}${path ? ` at "${path}"` : ""}.`, "PROFILE_ENV_VAR_UNRESOLVED", { stage: "config", profileName: name, varName, path, cause: err });
19776
19803
  }
19777
- const shimmedProfileData = applyConfigCompatShims(resolvedProfileData, logger, warnDedupe);
19778
- const preProfileMergeConfig = rawConfig;
19779
- rawConfig = deepMergeConfig(rawConfig, shimmedProfileData);
19780
- warnSecuritySensitiveOverrides(preProfileMergeConfig, rawConfig, warnDedupe.warn, {
19804
+ const shimmedProfileData = applyConfigCompatShims(resolvedProfileData, ctx.logger, ctx.warnDedupe);
19805
+ const preProfileMergeConfig = merged;
19806
+ merged = deepMergeConfig(merged, shimmedProfileData);
19807
+ warnSecuritySensitiveOverrides(preProfileMergeConfig, merged, ctx.warnDedupe.warn, {
19781
19808
  layerName: `profile:${name}`,
19782
19809
  sourceLayerConf: shimmedProfileData
19783
19810
  });
19784
19811
  }
19785
- if (cliOverrides) {
19786
- const shimmedCliOverrides = applyConfigCompatShims(cliOverrides, logger, warnDedupe);
19787
- const preCliMergeConfig = rawConfig;
19788
- rawConfig = deepMergeConfig(rawConfig, shimmedCliOverrides);
19789
- warnSecuritySensitiveOverrides(preCliMergeConfig, rawConfig, warnDedupe.warn, {
19790
- layerName: "CLI override",
19791
- sourceLayerConf: shimmedCliOverrides
19792
- });
19793
- }
19794
- rawConfig.profile = overlayChain.length > 0 ? overlayChain.join("+") : "default";
19795
- rawConfig.profileChain = overlayChain;
19796
- const hasMergedConfigs = globalConfRaw || projDir !== null || cliOverrides !== undefined || overlayChain.length > 0;
19797
- if (!hasMergedConfigs) {
19798
- return structuredClone(DEFAULT_CONFIG);
19799
- }
19812
+ return merged;
19813
+ }
19814
+ function applyCliOverridesLayer(rawConfig, cliOverrides, ctx) {
19815
+ if (!cliOverrides)
19816
+ return rawConfig;
19817
+ const shimmedCliOverrides = applyConfigCompatShims(cliOverrides, ctx.logger, ctx.warnDedupe);
19818
+ const preCliMergeConfig = rawConfig;
19819
+ const merged = deepMergeConfig(rawConfig, shimmedCliOverrides);
19820
+ warnSecuritySensitiveOverrides(preCliMergeConfig, merged, ctx.warnDedupe.warn, {
19821
+ layerName: "CLI override",
19822
+ sourceLayerConf: shimmedCliOverrides
19823
+ });
19824
+ return merged;
19825
+ }
19826
+ function finalizeAndValidateRootConfig(rawConfig) {
19800
19827
  rejectLegacyAgentKeys(rawConfig);
19801
19828
  rejectLegacyRectificationKeys(rawConfig);
19802
19829
  rejectDeadQualityFlags(rawConfig);
19803
19830
  rejectUnimplementedScopedProfile(rawConfig);
19804
19831
  rejectUnimplementedPermissionsBlock(rawConfig);
19805
- rawConfig = stripRemovedNoOpKeys(rawConfig, defaultConfigWarn);
19806
- const result = NaxConfigSchema.safeParse(rawConfig);
19832
+ const stripped = stripRemovedNoOpKeys(rawConfig, defaultConfigWarn);
19833
+ const result = NaxConfigSchema.safeParse(stripped);
19807
19834
  if (!result.success) {
19808
19835
  const errors3 = result.error.issues.map((err) => {
19809
19836
  const path = String(err.path.join("."));
@@ -19815,6 +19842,47 @@ ${errors3.join(`
19815
19842
  }
19816
19843
  return result.data;
19817
19844
  }
19845
+ function findProjectDir(startDir = process.cwd()) {
19846
+ let dir = resolve3(startDir);
19847
+ let depth = 0;
19848
+ while (depth < MAX_DIRECTORY_DEPTH) {
19849
+ const candidate = join3(dir, PROJECT_NAX_DIR);
19850
+ if (existsSync3(join3(candidate, "config.json"))) {
19851
+ return candidate;
19852
+ }
19853
+ const parent = join3(dir, "..");
19854
+ if (parent === dir)
19855
+ break;
19856
+ dir = parent;
19857
+ depth++;
19858
+ }
19859
+ return null;
19860
+ }
19861
+ async function loadConfig(startDir, cliOverrides) {
19862
+ let rawConfig = structuredClone(DEFAULT_CONFIG);
19863
+ const warnDedupe = createConfigWarnDedupe();
19864
+ const { projDir, projectRoot } = resolveProjectPaths(startDir);
19865
+ const profileChain = await resolveProfileNames(cliOverrides ?? {}, process.env, projectRoot);
19866
+ const overlayChain = profileChain.filter((name) => name && name !== "default");
19867
+ let logger = null;
19868
+ try {
19869
+ logger = getLogger();
19870
+ } catch {}
19871
+ const ctx = { logger, warnDedupe };
19872
+ const globalLayer = await applyGlobalLayer(rawConfig, ctx);
19873
+ rawConfig = globalLayer.rawConfig;
19874
+ rawConfig = await applyProjectLayer(rawConfig, projDir, globalLayer.globalLayerConf, ctx);
19875
+ rawConfig = resolveEnvVarsWarnOnFailure(rawConfig, logger, "global/project config");
19876
+ rawConfig = await applyProfileChainLayer(rawConfig, overlayChain, projectRoot, ctx);
19877
+ rawConfig = applyCliOverridesLayer(rawConfig, cliOverrides, ctx);
19878
+ rawConfig.profile = overlayChain.length > 0 ? overlayChain.join("+") : "default";
19879
+ rawConfig.profileChain = overlayChain;
19880
+ const hasMergedConfigs = globalLayer.globalConfRaw || projDir !== null || cliOverrides !== undefined || overlayChain.length > 0;
19881
+ if (!hasMergedConfigs) {
19882
+ return structuredClone(DEFAULT_CONFIG);
19883
+ }
19884
+ return finalizeAndValidateRootConfig(rawConfig);
19885
+ }
19818
19886
  async function loadPackageOverride(repoRoot, packageDir) {
19819
19887
  const packageConfigPath = join3(repoRoot, PROJECT_NAX_DIR, "mono", packageDir, "config.json");
19820
19888
  const override = await loadJsonFile(packageConfigPath, "config");
@@ -19859,8 +19927,10 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19859
19927
  }
19860
19928
  logger.debug("config", "Per-package config loaded", { packageConfigPath, packageDir });
19861
19929
  const { profile: packageProfile, ...packageFields } = packageOverride;
19862
- let merged = mergePackageConfig(rootConfig, packageFields);
19863
- merged = stripRemovedNoOpKeys(merged, defaultConfigWarn);
19930
+ const warnDedupe = createConfigWarnDedupe();
19931
+ const shimmedPackageFields = applyConfigCompatShims(packageFields, logger, warnDedupe);
19932
+ let merged = mergePackageConfig(rootConfig, shimmedPackageFields);
19933
+ merged = stripRemovedNoOpKeys(merged, warnDedupe.warn);
19864
19934
  const envResolvedMerged = resolveEnvVarsWarnOnFailure(merged, logger, `per-package config (${packageDir})`);
19865
19935
  const packageChain = parseProfileList(packageProfile).filter((name) => name && name !== "default");
19866
19936
  let rawMerged = envResolvedMerged;
@@ -19877,7 +19947,8 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19877
19947
  const path = err instanceof UnresolvedEnvVarError ? err.path.join(".") : undefined;
19878
19948
  throw new NaxError(`Per-package profile "${name}" (${packageDir}) references an undefined environment variable${varName ? ` $${varName}` : ""}${path ? ` at "${path}"` : ""}.`, "PROFILE_ENV_VAR_UNRESOLVED", { stage: "config", profileName: name, packageDir, varName, path, cause: err });
19879
19949
  }
19880
- rawMerged = deepMergeConfig(rawMerged, resolvedProfileData);
19950
+ const shimmedProfileData = applyConfigCompatShims(resolvedProfileData, logger, warnDedupe);
19951
+ rawMerged = deepMergeConfig(rawMerged, shimmedProfileData);
19881
19952
  }
19882
19953
  rawMerged.profile = packageChain.join("+");
19883
19954
  rawMerged.profileChain = packageChain;
@@ -19887,7 +19958,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
19887
19958
  rejectDeadQualityFlags(rawMerged);
19888
19959
  rejectUnimplementedScopedProfile(rawMerged);
19889
19960
  rejectUnimplementedPermissionsBlock(rawMerged);
19890
- rawMerged = stripRemovedNoOpKeys(rawMerged, defaultConfigWarn);
19961
+ rawMerged = stripRemovedNoOpKeys(rawMerged, warnDedupe.warn);
19891
19962
  const result = NaxConfigSchema.safeParse(rawMerged);
19892
19963
  if (!result.success) {
19893
19964
  const errors3 = result.error.issues.map((err) => {
@@ -29603,7 +29674,7 @@ function scanDirectory(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCt
29603
29674
  return { workdir, files, truncated };
29604
29675
  }
29605
29676
  async function collectNeighbors(filePath, workdir, scannedDirs, contentCacheState, siblingTestContext) {
29606
- const neighbors = new Set;
29677
+ const forwardNeighbors = new Set;
29607
29678
  let anyTruncated = false;
29608
29679
  const ownAbsPath = join14(workdir, filePath);
29609
29680
  if (await _codeNeighborDeps.fileExists(ownAbsPath)) {
@@ -29612,18 +29683,19 @@ async function collectNeighbors(filePath, workdir, scannedDirs, contentCacheStat
29612
29683
  for (const spec of parseImportSpecifiers(ownContent)) {
29613
29684
  const resolved = resolveImport(spec, filePath, workdir);
29614
29685
  if (resolved && resolved !== filePath)
29615
- neighbors.add(resolved);
29686
+ forwardNeighbors.add(resolved);
29616
29687
  }
29617
29688
  }
29618
29689
  }
29619
29690
  const fileBaseName = (filePath.split("/").pop() ?? filePath).replace(/\.[^.]+$/, "");
29620
29691
  const fileNoExt = filePath.replace(/\.[^.]+$/, "");
29692
+ const reverseNeighbors = new Set;
29621
29693
  outer:
29622
29694
  for (const { workdir: scanWorkdir, files: srcFiles, truncated } of scannedDirs) {
29623
29695
  if (truncated)
29624
29696
  anyTruncated = true;
29625
29697
  for (const srcFile of srcFiles) {
29626
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
29698
+ if (reverseNeighbors.size >= MAX_NEIGHBORS_PER_FILE)
29627
29699
  break outer;
29628
29700
  if (srcFile === filePath)
29629
29701
  continue;
@@ -29632,13 +29704,26 @@ async function collectNeighbors(filePath, workdir, scannedDirs, contentCacheStat
29632
29704
  for (const spec of parseImportSpecifiers(content)) {
29633
29705
  const resolved = resolveImport(spec, srcFile, scanWorkdir);
29634
29706
  if (resolved === filePath || resolved === fileNoExt) {
29635
- neighbors.add(srcFile);
29707
+ reverseNeighbors.add(srcFile);
29636
29708
  break;
29637
29709
  }
29638
29710
  }
29639
29711
  }
29640
29712
  }
29641
29713
  }
29714
+ const minReverseSlots = Math.min(reverseNeighbors.size, Math.ceil(MAX_NEIGHBORS_PER_FILE / 2));
29715
+ const forwardSlots = MAX_NEIGHBORS_PER_FILE - minReverseSlots;
29716
+ const neighbors = new Set;
29717
+ for (const f of forwardNeighbors) {
29718
+ if (neighbors.size >= forwardSlots)
29719
+ break;
29720
+ neighbors.add(f);
29721
+ }
29722
+ for (const r of reverseNeighbors) {
29723
+ if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
29724
+ break;
29725
+ neighbors.add(r);
29726
+ }
29642
29727
  if (siblingTestContext && !isTestFile2(filePath, siblingTestContext.regex)) {
29643
29728
  const candidates = deriveSiblingTestCandidates(filePath, siblingTestContext.globs);
29644
29729
  let chosen = null;
@@ -35269,6 +35354,47 @@ var init_canonical_rules_cache = __esm(() => {
35269
35354
  canonicalRulesCache = new Map;
35270
35355
  });
35271
35356
 
35357
+ // src/context/engine/providers/static-rules-budget-notice.ts
35358
+ function buildSectionBudgetPressure(allSections, budgetResult) {
35359
+ const { droppedIds, overageTokens } = budgetResult;
35360
+ const droppedCount = droppedIds.length;
35361
+ if (droppedCount === 0 && overageTokens <= 0)
35362
+ return null;
35363
+ const kept = new Set(budgetResult.retainedSections);
35364
+ let droppedTokens = 0;
35365
+ for (const section of allSections) {
35366
+ if (!kept.has(section)) {
35367
+ droppedTokens += section.tokens;
35368
+ }
35369
+ }
35370
+ return { overageTokens, droppedCount, droppedTokens, droppedIds };
35371
+ }
35372
+ function budgetNoticeText(droppedCount, droppedIds) {
35373
+ let detail = "";
35374
+ if (droppedIds && droppedIds.length > 0) {
35375
+ const shown = droppedIds.slice(0, MAX_LISTED_DROPPED_IDS);
35376
+ const remainder = droppedIds.length - shown.length;
35377
+ detail = ` (${shown.join(", ")}${remainder > 0 ? `, +${remainder} more` : ""})`;
35378
+ }
35379
+ return `Note: rule budget exceeded \u2014 ${droppedCount} lower-priority section(s) dropped${detail}. Increase \`context.v2.rules.rulesShare\` / \`context.v2.rules.budgetTokens\`, or set \`context.v2.rules.enforceBudget: false\`, to see the full ruleset.`;
35380
+ }
35381
+ function buildBudgetNoticeChunk(storyId, droppedCount, droppedIds) {
35382
+ const content = `> ${budgetNoticeText(droppedCount, droppedIds)}`;
35383
+ return {
35384
+ id: `static-rules:__budget-notice__:${storyId}`,
35385
+ kind: "static",
35386
+ scope: "project",
35387
+ role: ["all"],
35388
+ content,
35389
+ tokens: estimateTokens2(content),
35390
+ rawScore: 1
35391
+ };
35392
+ }
35393
+ var MAX_LISTED_DROPPED_IDS = 10;
35394
+ var init_static_rules_budget_notice = __esm(() => {
35395
+ init_optimizer();
35396
+ });
35397
+
35272
35398
  // src/context/engine/providers/static-rules.ts
35273
35399
  import { createHash as createHash10 } from "crypto";
35274
35400
  import { join as join20, relative as relative7 } from "path";
@@ -35349,20 +35475,6 @@ function ruleMatchesPackage(paths, repoRoot, packageDir) {
35349
35475
  const patterns = paths.map((p) => globToRegex3(normalizePath2(p)));
35350
35476
  return patterns.some((pattern) => pattern.test(rel) || pattern.test(`${rel}/`));
35351
35477
  }
35352
- function buildSectionBudgetPressure(allSections, budgetResult) {
35353
- const { droppedIds, overageTokens } = budgetResult;
35354
- const droppedCount = droppedIds.length;
35355
- if (droppedCount === 0 && overageTokens <= 0)
35356
- return null;
35357
- const kept = new Set(budgetResult.retainedSections);
35358
- let droppedTokens = 0;
35359
- for (const section of allSections) {
35360
- if (!kept.has(section)) {
35361
- droppedTokens += section.tokens;
35362
- }
35363
- }
35364
- return { overageTokens, droppedCount, droppedTokens, droppedIds };
35365
- }
35366
35478
 
35367
35479
  class StaticRulesProvider {
35368
35480
  id = "static-rules";
@@ -35516,8 +35628,9 @@ class StaticRulesProvider {
35516
35628
  });
35517
35629
  }
35518
35630
  const emptyPressure = buildSectionBudgetPressure(allSections, budgetResult);
35631
+ const emptyChunks = this.enforceBudget && budgetResult.droppedIds.length > 0 ? [buildBudgetNoticeChunk(request.storyId, budgetResult.droppedIds.length, budgetResult.droppedIds)] : [];
35519
35632
  return {
35520
- chunks: [],
35633
+ chunks: emptyChunks,
35521
35634
  pullTools: [],
35522
35635
  ...emptyPressure && { budgetPressure: emptyPressure },
35523
35636
  scopingReport
@@ -35549,6 +35662,9 @@ ${section.content}`;
35549
35662
  totalCanonicalRules: mergedRules.length,
35550
35663
  files: effectiveSections.map((s) => s.rulePath ?? "unknown")
35551
35664
  });
35665
+ if (this.enforceBudget && budgetResult.droppedIds.length > 0) {
35666
+ chunks.push(buildBudgetNoticeChunk(request.storyId, budgetResult.droppedIds.length, budgetResult.droppedIds));
35667
+ }
35552
35668
  const pressure = buildSectionBudgetPressure(allSections, budgetResult);
35553
35669
  return {
35554
35670
  chunks,
@@ -35664,6 +35780,7 @@ var init_static_rules = __esm(() => {
35664
35780
  init_optimizer();
35665
35781
  init_canonical_loader();
35666
35782
  init_canonical_rules_cache();
35783
+ init_static_rules_budget_notice();
35667
35784
  init_canonical_rules_cache();
35668
35785
  _staticRulesDeps = {
35669
35786
  readFile: async (path4) => Bun.file(path4).text(),
@@ -36312,7 +36429,17 @@ function toAbsolutePath2(projectDir, pathValue) {
36312
36429
  }
36313
36430
  async function discoverSessionScratchDirsOnDisk(projectDir, featureName, storyId, ttlMs) {
36314
36431
  const logger = getLogger();
36315
- const sessionsRoot = join22(featureDir(projectDir, featureName), "sessions");
36432
+ let sessionsRoot;
36433
+ try {
36434
+ sessionsRoot = join22(featureDir(projectDir, featureName), "sessions");
36435
+ } catch (err) {
36436
+ logger.debug("context-v2", "discoverSessionScratchDirsOnDisk: invalid featureName \u2014 treating as no sessions", {
36437
+ storyId,
36438
+ featureName,
36439
+ error: errorMessage(err)
36440
+ });
36441
+ return [];
36442
+ }
36316
36443
  let entries;
36317
36444
  try {
36318
36445
  entries = await _stageAssemblerDeps.readdir(sessionsRoot);
@@ -48822,31 +48949,6 @@ var init_review_iteration_store = __esm(() => {
48822
48949
  init_findings();
48823
48950
  });
48824
48951
 
48825
- // src/review/adversarial-audit-event.ts
48826
- function recordAdversarialAudit(opts) {
48827
- opts.runtime?.dispatchEvents.emitReviewDecision({
48828
- kind: "review-decision",
48829
- reviewer: "adversarial",
48830
- workdir: opts.workdir,
48831
- projectDir: opts.projectDir,
48832
- storyId: opts.storyId,
48833
- featureName: opts.featureName,
48834
- timestamp: Date.now(),
48835
- parsed: opts.parsed,
48836
- looksLikeFail: opts.looksLikeFail,
48837
- failOpen: opts.failOpen,
48838
- passed: opts.passed,
48839
- passReason: opts.passReason,
48840
- blockingThreshold: opts.blockingThreshold,
48841
- result: opts.result,
48842
- advisoryFindings: opts.advisoryFindings,
48843
- acks: opts.acks,
48844
- diffAvailable: opts.diffAvailable,
48845
- adversarialDropAnalysis: opts.adversarialDropAnalysis,
48846
- adversarialAcceptAnalysis: opts.adversarialAcceptAnalysis
48847
- });
48848
- }
48849
-
48850
48952
  // src/review/adversarial-counterfactual-telemetry.ts
48851
48953
  function buildCounterfactualTelemetry({
48852
48954
  acDropped,
@@ -48884,172 +48986,30 @@ var init_adversarial_counterfactual_telemetry = __esm(() => {
48884
48986
  init_ac_structural_counterfactual();
48885
48987
  });
48886
48988
 
48887
- // src/review/diff-utils.ts
48888
- var {spawn: spawn3 } = globalThis.Bun;
48889
- async function resolveNaxIgnorePathspecExcludes(workdir, options) {
48890
- if (options?.naxIgnoreIndex)
48891
- return options.naxIgnoreIndex.toPathspecExcludes(options.packageDir);
48892
- const matchers = await resolveNaxIgnorePatterns(workdir, options?.packageDir);
48893
- const pathspec = new Set;
48894
- for (const matcher of matchers)
48895
- pathspec.add(`:!${matcher.pattern}`);
48896
- return [...pathspec];
48897
- }
48898
- async function collectDiff(workdir, storyGitRef, excludePatterns, options) {
48899
- const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
48900
- const merged = [...new Set([...excludePatterns, ...naxIgnoreExcludes, ...ALWAYS_EXCLUDED])];
48901
- const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`, "--", ".", ...merged];
48902
- const proc = _diffUtilsDeps.spawn({
48903
- cmd,
48904
- cwd: workdir,
48905
- stdout: "pipe",
48906
- stderr: "pipe"
48907
- });
48908
- const [exitCode, stdout, stderr] = await Promise.all([
48909
- proc.exited,
48910
- new Response(proc.stdout).text(),
48911
- new Response(proc.stderr).text()
48912
- ]);
48913
- if (exitCode !== 0) {
48914
- getSafeLogger()?.warn("diff-utils", "git diff failed \u2014 skipping review diff", { storyGitRef, stderr });
48915
- return null;
48916
- }
48917
- return stdout;
48918
- }
48919
- async function collectDiffStat(workdir, storyGitRef, options) {
48920
- const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
48921
- const merged = [...new Set([...naxIgnoreExcludes, ...ALWAYS_EXCLUDED])];
48922
- const proc = _diffUtilsDeps.spawn({
48923
- cmd: ["git", "diff", "--stat", `${storyGitRef}..HEAD`, "--", ".", ...merged],
48924
- cwd: workdir,
48925
- stdout: "pipe",
48926
- stderr: "pipe"
48927
- });
48928
- const [exitCode, stdout] = await Promise.all([
48929
- proc.exited,
48930
- new Response(proc.stdout).text(),
48931
- new Response(proc.stderr).text()
48932
- ]);
48933
- return exitCode === 0 ? stdout.trim() : "";
48934
- }
48935
- function truncateDiff(diff, stat4) {
48936
- if (diff.length <= DIFF_CAP_BYTES) {
48937
- return diff;
48938
- }
48939
- const truncated = diff.slice(0, DIFF_CAP_BYTES);
48940
- const visibleFiles = (truncated.match(/^diff --git/gm) ?? []).length;
48941
- const totalFiles = (diff.match(/^diff --git/gm) ?? []).length;
48942
- const statPreamble = stat4 ? `## File Summary (all changed files)
48943
- ${stat4}
48944
-
48945
- ## Diff (truncated \u2014 ${visibleFiles}/${totalFiles} files shown)
48946
- ` : "";
48947
- return `${statPreamble}${truncated}
48948
- ... (truncated at ${DIFF_CAP_BYTES} bytes, showing ${visibleFiles}/${totalFiles} files)`;
48949
- }
48950
- async function resolveEffectiveRef(workdir, storyGitRef, storyId) {
48951
- const logger = getSafeLogger();
48952
- if (storyGitRef && await _diffUtilsDeps.isGitRefValid(workdir, storyGitRef)) {
48953
- return storyGitRef;
48954
- }
48955
- const fallback = await _diffUtilsDeps.getMergeBase(workdir);
48956
- if (fallback) {
48957
- logger?.info("review", "storyGitRef missing or invalid \u2014 using merge-base fallback", {
48958
- storyId,
48959
- storyGitRef,
48960
- fallback
48961
- });
48962
- return fallback;
48963
- }
48964
- return;
48965
- }
48966
- function buildSuffixStrippers(testFilePatterns) {
48967
- if (!testFilePatterns || testFilePatterns.length === 0)
48968
- return DEFAULT_SUFFIX_STRIPPERS;
48969
- const regexes = [];
48970
- for (const pattern of testFilePatterns) {
48971
- const lastStar = pattern.lastIndexOf("*");
48972
- if (lastStar === -1)
48973
- continue;
48974
- const suffix = pattern.slice(lastStar + 1);
48975
- if (suffix.length > 0) {
48976
- regexes.push(new RegExp(`${suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`));
48977
- }
48978
- }
48979
- return regexes.length > 0 ? regexes : DEFAULT_SUFFIX_STRIPPERS;
48980
- }
48981
- function stripTestSuffix(base, strippers) {
48982
- for (const re of strippers) {
48983
- const stripped = base.replace(re, "");
48984
- if (stripped !== base)
48985
- return stripped;
48986
- }
48987
- return base;
48988
- }
48989
- async function computeTestInventory(workdir, storyGitRef, testFilePatterns, options) {
48990
- const proc = _diffUtilsDeps.spawn({
48991
- cmd: ["git", "diff", "--name-only", "--diff-filter=A", `${storyGitRef}..HEAD`],
48992
- cwd: workdir,
48993
- stdout: "pipe",
48994
- stderr: "pipe"
48995
- });
48996
- const [exitCode, stdout] = await Promise.all([
48997
- proc.exited,
48998
- new Response(proc.stdout).text(),
48999
- new Response(proc.stderr).text()
49000
- ]);
49001
- if (exitCode !== 0) {
49002
- return { addedTestFiles: [], newSourceFilesWithoutTests: [] };
49003
- }
49004
- const addedFiles = stdout.trim().split(`
49005
- `).filter(Boolean);
49006
- const ignoreMatchers = options?.naxIgnoreIndex?.getMatchers(options.packageDir) ?? await resolveNaxIgnorePatterns(workdir, options?.packageDir);
49007
- const visibleAddedFiles = filterNaxInternalPaths(addedFiles, ignoreMatchers);
49008
- const addedTestFiles = visibleAddedFiles.filter((f) => isTestFile(f, testFilePatterns));
49009
- const addedSourceFiles = visibleAddedFiles.filter((f) => !isTestFile(f, testFilePatterns));
49010
- const suffixStrippers = buildSuffixStrippers(testFilePatterns);
49011
- const testFileBasenames = new Set(addedTestFiles.map((f) => {
49012
- const base = f.split("/").at(-1) ?? f;
49013
- return stripTestSuffix(base, suffixStrippers);
49014
- }));
49015
- const newSourceFilesWithoutTests = addedSourceFiles.filter((f) => {
49016
- const base = (f.split("/").at(-1) ?? f).replace(/\.(ts|js|tsx|jsx|go)$/, "");
49017
- return !testFileBasenames.has(base);
49018
- });
49019
- return { addedTestFiles, newSourceFilesWithoutTests };
49020
- }
49021
- async function collectDiffFileList(workdir, storyGitRef, options) {
49022
- const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
49023
- const merged = [...new Set([...naxIgnoreExcludes, ...ALWAYS_EXCLUDED])];
49024
- const proc = _diffUtilsDeps.spawn({
49025
- cmd: ["git", "diff", "--name-only", `${storyGitRef}..HEAD`, "--", ".", ...merged],
49026
- cwd: workdir,
49027
- stdout: "pipe",
49028
- stderr: "pipe"
48989
+ // src/review/adversarial-audit-event.ts
48990
+ function recordAdversarialAudit(opts) {
48991
+ opts.runtime?.dispatchEvents.emitReviewDecision({
48992
+ kind: "review-decision",
48993
+ reviewer: "adversarial",
48994
+ workdir: opts.workdir,
48995
+ projectDir: opts.projectDir,
48996
+ storyId: opts.storyId,
48997
+ featureName: opts.featureName,
48998
+ timestamp: Date.now(),
48999
+ parsed: opts.parsed,
49000
+ looksLikeFail: opts.looksLikeFail,
49001
+ failOpen: opts.failOpen,
49002
+ passed: opts.passed,
49003
+ passReason: opts.passReason,
49004
+ blockingThreshold: opts.blockingThreshold,
49005
+ result: opts.result,
49006
+ advisoryFindings: opts.advisoryFindings,
49007
+ acks: opts.acks,
49008
+ diffAvailable: opts.diffAvailable,
49009
+ adversarialDropAnalysis: opts.adversarialDropAnalysis,
49010
+ adversarialAcceptAnalysis: opts.adversarialAcceptAnalysis
49029
49011
  });
49030
- const [exitCode, stdout] = await Promise.all([
49031
- proc.exited,
49032
- new Response(proc.stdout).text(),
49033
- new Response(proc.stderr).text()
49034
- ]);
49035
- if (exitCode !== 0)
49036
- return;
49037
- return stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
49038
49012
  }
49039
- var DIFF_CAP_BYTES = 51200, ALWAYS_EXCLUDED, _diffUtilsDeps, DEFAULT_SUFFIX_STRIPPERS;
49040
- var init_diff_utils = __esm(() => {
49041
- init_logger2();
49042
- init_test_runners();
49043
- init_git();
49044
- init_path_filters();
49045
- ALWAYS_EXCLUDED = [":!.nax/", ":!.nax-pids"];
49046
- _diffUtilsDeps = {
49047
- spawn: spawn3,
49048
- isGitRefValid,
49049
- getMergeBase
49050
- };
49051
- DEFAULT_SUFFIX_STRIPPERS = [/\.(test|spec)\.(ts|js|tsx|jsx)$/, /_test\.go$/];
49052
- });
49053
49013
 
49054
49014
  // src/review/finding-projection.ts
49055
49015
  function narrowSeverity(raw) {
@@ -49134,6 +49094,505 @@ var init_finding_projection = __esm(() => {
49134
49094
  };
49135
49095
  });
49136
49096
 
49097
+ // src/review/adversarial-outcomes.ts
49098
+ function skipResult(output, startTime) {
49099
+ return {
49100
+ check: "adversarial",
49101
+ success: true,
49102
+ command: "",
49103
+ exitCode: 0,
49104
+ output,
49105
+ durationMs: Date.now() - startTime
49106
+ };
49107
+ }
49108
+ function buildFeatureCtxBlock(contextBundle, featureContextMarkdown) {
49109
+ if (contextBundle) {
49110
+ const md = contextBundle.pushMarkdown.trim();
49111
+ return md ? `${md}
49112
+
49113
+ ---
49114
+
49115
+ ` : "";
49116
+ }
49117
+ if (featureContextMarkdown) {
49118
+ const filtered = filterContextByRole(featureContextMarkdown, "reviewer-adversarial");
49119
+ if (filtered.trim())
49120
+ return `${filtered}
49121
+
49122
+ ---
49123
+
49124
+ `;
49125
+ }
49126
+ return "";
49127
+ }
49128
+ function catchDispatchFailure(err, ctx) {
49129
+ const { runtime, workdir, projectDir, storyId, featureName, blockingThreshold, startTime, logger } = ctx;
49130
+ logger?.warn("adversarial", "LLM call failed \u2014 fail-open", { storyId, cause: String(err) });
49131
+ recordAdversarialAudit({
49132
+ runtime,
49133
+ workdir,
49134
+ projectDir,
49135
+ storyId,
49136
+ featureName,
49137
+ parsed: false,
49138
+ looksLikeFail: false,
49139
+ failOpen: true,
49140
+ passed: true,
49141
+ blockingThreshold,
49142
+ result: null
49143
+ });
49144
+ return {
49145
+ check: "adversarial",
49146
+ success: true,
49147
+ failOpen: true,
49148
+ command: "",
49149
+ exitCode: 0,
49150
+ output: `skipped: LLM call failed \u2014 ${String(err)}`,
49151
+ durationMs: Date.now() - startTime
49152
+ };
49153
+ }
49154
+ function handleRetryExhaustedFailOpen(ctx) {
49155
+ const { runtime, workdir, projectDir, storyId, featureName, blockingThreshold, startTime, logger } = ctx;
49156
+ logger?.warn("adversarial", "Retry exhausted \u2014 fail-open", { storyId });
49157
+ recordAdversarialAudit({
49158
+ runtime,
49159
+ workdir,
49160
+ projectDir,
49161
+ storyId,
49162
+ featureName,
49163
+ parsed: false,
49164
+ looksLikeFail: false,
49165
+ failOpen: true,
49166
+ passed: true,
49167
+ blockingThreshold,
49168
+ result: null
49169
+ });
49170
+ return {
49171
+ check: "adversarial",
49172
+ success: true,
49173
+ failOpen: true,
49174
+ command: "",
49175
+ exitCode: 0,
49176
+ output: "adversarial review: could not parse LLM response (fail-open)",
49177
+ durationMs: Date.now() - startTime
49178
+ };
49179
+ }
49180
+ function handleTruncatedLooksLikeFail(ctx) {
49181
+ const { runtime, workdir, projectDir, storyId, featureName, blockingThreshold, startTime, logger } = ctx;
49182
+ logger?.warn("adversarial", "LLM returned truncated JSON with passed:false \u2014 treating as failure", { storyId });
49183
+ recordAdversarialAudit({
49184
+ runtime,
49185
+ workdir,
49186
+ projectDir,
49187
+ storyId,
49188
+ featureName,
49189
+ parsed: false,
49190
+ looksLikeFail: true,
49191
+ failOpen: false,
49192
+ passed: false,
49193
+ blockingThreshold,
49194
+ result: null
49195
+ });
49196
+ return {
49197
+ check: "adversarial",
49198
+ success: false,
49199
+ command: "",
49200
+ exitCode: 1,
49201
+ output: "adversarial review: LLM response truncated but indicated failure (passed:false found in partial response)",
49202
+ durationMs: Date.now() - startTime
49203
+ };
49204
+ }
49205
+ function classifyAdversarialFindings(opResult, blockingThreshold, priorAdversarialIterations, adversarialConfig, resolvedTestPatterns) {
49206
+ const threshold = blockingThreshold ?? "error";
49207
+ const allFindings = opResult.findings;
49208
+ const patterns = resolvedTestPatterns?.regex ?? [];
49209
+ const testFileMatch = (file3) => patterns.some((re) => re.test(file3));
49210
+ const recurrenceCfg = adversarialConfig.recurrenceDemotion ?? { enabled: true, maxBlockingRounds: 2 };
49211
+ const {
49212
+ blocking: blockingFindings,
49213
+ advisory: advisoryOnly,
49214
+ demoted
49215
+ } = classifyRecurrence(allFindings, priorAdversarialIterations ?? [], recurrenceCfg, testFileMatch, threshold);
49216
+ const advisoryFindings = [...advisoryOnly, ...demoted];
49217
+ const advisoryReviewFindings = [
49218
+ ...llmFindingsToReviewFindings(advisoryOnly, { source: "adversarial-review", isTestFile: testFileMatch }),
49219
+ ...tagCoverageGap(llmFindingsToReviewFindings(demoted, { source: "adversarial-review", isTestFile: testFileMatch }))
49220
+ ];
49221
+ const advisoryFindingsAsFindings = [
49222
+ ...toAdversarialReviewFindings(advisoryOnly, { isTestFile: testFileMatch }),
49223
+ ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
49224
+ ];
49225
+ return {
49226
+ threshold,
49227
+ allFindings,
49228
+ testFileMatch,
49229
+ blockingFindings,
49230
+ advisoryFindings,
49231
+ advisoryReviewFindings,
49232
+ advisoryFindingsAsFindings,
49233
+ acDropped: opResult.acDropped ?? [],
49234
+ acks: opResult.acks
49235
+ };
49236
+ }
49237
+ async function resolveDiffFileSet(diff, workdir, projectDir, effectiveRef, naxIgnoreIndex, collectDiffFileListFn) {
49238
+ if (diff && diff.length > 0) {
49239
+ return { diffFiles: extractDiffFiles(diff), diffAvailable: true };
49240
+ }
49241
+ const repoRoot = projectDir ?? workdir;
49242
+ const packageDir = workdir !== repoRoot ? workdir : undefined;
49243
+ const list = await collectDiffFileListFn(workdir, effectiveRef, { naxIgnoreIndex, packageDir });
49244
+ if (list === undefined)
49245
+ return { diffFiles: new Set, diffAvailable: false };
49246
+ return { diffFiles: new Set(list), diffAvailable: true };
49247
+ }
49248
+ function buildBlockingFailureResult(ctx, classification, telemetry, diffAvailable, durationMs) {
49249
+ const { runtime, workdir, projectDir, storyId, featureName, logger } = ctx;
49250
+ const { threshold, allFindings, testFileMatch, blockingFindings, advisoryFindings, advisoryReviewFindings, acks } = classification;
49251
+ logger?.warn("review", `Adversarial review failed: ${blockingFindings.length} blocking findings`, {
49252
+ storyId,
49253
+ durationMs,
49254
+ findings: blockingFindings.map((f) => ({
49255
+ severity: f.severity,
49256
+ category: f.category,
49257
+ file: f.file,
49258
+ line: f.line,
49259
+ issue: f.issue
49260
+ }))
49261
+ });
49262
+ recordAdversarialAudit({
49263
+ runtime,
49264
+ workdir,
49265
+ projectDir,
49266
+ storyId,
49267
+ featureName,
49268
+ parsed: true,
49269
+ failOpen: false,
49270
+ passed: false,
49271
+ blockingThreshold: threshold,
49272
+ result: {
49273
+ passed: false,
49274
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
49275
+ },
49276
+ advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
49277
+ diffAvailable,
49278
+ adversarialDropAnalysis: telemetry.adversarialDropAnalysis,
49279
+ adversarialAcceptAnalysis: telemetry.adversarialAcceptAnalysis,
49280
+ acks
49281
+ });
49282
+ return {
49283
+ check: "adversarial",
49284
+ success: false,
49285
+ command: "",
49286
+ exitCode: 1,
49287
+ output: `Adversarial review failed:
49288
+
49289
+ ${formatFindings(blockingFindings)}`,
49290
+ durationMs,
49291
+ findings: toAdversarialReviewFindings(blockingFindings, { isTestFile: testFileMatch }),
49292
+ advisoryFindings: advisoryFindings.length > 0 ? classification.advisoryFindingsAsFindings : undefined,
49293
+ cost: 0
49294
+ };
49295
+ }
49296
+ function buildHallucinatedAcQuoteResult(ctx, classification, telemetry, diffAvailable, durationMs) {
49297
+ const { runtime, workdir, projectDir, storyId, featureName, logger } = ctx;
49298
+ const { threshold, testFileMatch, acDropped, acks, advisoryFindingsAsFindings } = classification;
49299
+ const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })), { isTestFile: testFileMatch });
49300
+ const allAdvisory = [...advisoryFindingsAsFindings, ...demotedFindings];
49301
+ logger?.warn("review", "Adversarial review passed: all blocking findings discarded as hallucinated AC quotes", {
49302
+ storyId,
49303
+ durationMs,
49304
+ droppedCount: acDropped.length,
49305
+ drops: acDropped.map((d) => ({ file: d.finding.file, issue: d.finding.issue }))
49306
+ });
49307
+ recordAdversarialAudit({
49308
+ runtime,
49309
+ workdir,
49310
+ projectDir,
49311
+ storyId,
49312
+ featureName,
49313
+ parsed: true,
49314
+ acks,
49315
+ failOpen: false,
49316
+ passed: true,
49317
+ passReason: "ac_quote_not_substring_demoted",
49318
+ blockingThreshold: threshold,
49319
+ result: { passed: true, findings: [] },
49320
+ advisoryFindings: allAdvisory.length > 0 ? allAdvisory : undefined,
49321
+ diffAvailable,
49322
+ adversarialDropAnalysis: telemetry.adversarialDropAnalysis,
49323
+ adversarialAcceptAnalysis: []
49324
+ });
49325
+ return {
49326
+ check: "adversarial",
49327
+ success: true,
49328
+ passReason: "ac_quote_not_substring_demoted",
49329
+ command: "",
49330
+ exitCode: 0,
49331
+ output: `Adversarial review passed: ${acDropped.length} blocking finding(s) demoted to advisory \u2014 all cited AC quotes were fabricated and could not be validated.`,
49332
+ durationMs,
49333
+ advisoryFindings: allAdvisory.length > 0 ? allAdvisory : undefined,
49334
+ cost: 0
49335
+ };
49336
+ }
49337
+ function buildUngroundedFailClosedResult(ctx, classification, telemetry, diffAvailable, durationMs) {
49338
+ const { runtime, workdir, projectDir, storyId, featureName, logger } = ctx;
49339
+ const { threshold, acDropped, acks, advisoryFindings, advisoryReviewFindings, advisoryFindingsAsFindings } = classification;
49340
+ logger?.warn("review", "Adversarial review fail-closed: blocking findings dropped as ungrounded", {
49341
+ storyId,
49342
+ durationMs,
49343
+ droppedCount: acDropped.length,
49344
+ dropCodes: acDropped.map((d) => d.code)
49345
+ });
49346
+ const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
49347
+ `);
49348
+ recordAdversarialAudit({
49349
+ runtime,
49350
+ workdir,
49351
+ projectDir,
49352
+ storyId,
49353
+ featureName,
49354
+ parsed: true,
49355
+ acks,
49356
+ failOpen: false,
49357
+ passed: false,
49358
+ blockingThreshold: threshold,
49359
+ result: { passed: false, findings: [] },
49360
+ advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
49361
+ diffAvailable,
49362
+ adversarialDropAnalysis: telemetry.adversarialDropAnalysis,
49363
+ adversarialAcceptAnalysis: []
49364
+ });
49365
+ return {
49366
+ check: "adversarial",
49367
+ success: false,
49368
+ command: "",
49369
+ exitCode: 1,
49370
+ output: `Adversarial review failed: ${acDropped.length} blocking finding(s) dropped as ungrounded \u2014 the model emitted "passed: false" with concerns it could not ground in any acceptance criterion. Drops:
49371
+
49372
+ ${dropSummary}`,
49373
+ durationMs,
49374
+ advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
49375
+ cost: 0
49376
+ };
49377
+ }
49378
+ function buildPassedResult(ctx, classification, telemetry, diffAvailable, durationMs) {
49379
+ const { runtime, workdir, projectDir, storyId, featureName, logger } = ctx;
49380
+ const {
49381
+ threshold,
49382
+ allFindings,
49383
+ testFileMatch,
49384
+ advisoryFindings,
49385
+ advisoryReviewFindings,
49386
+ advisoryFindingsAsFindings,
49387
+ acks
49388
+ } = classification;
49389
+ logger?.info("review", "Adversarial review passed", { storyId, durationMs });
49390
+ recordAdversarialAudit({
49391
+ runtime,
49392
+ workdir,
49393
+ projectDir,
49394
+ storyId,
49395
+ featureName,
49396
+ parsed: true,
49397
+ acks,
49398
+ failOpen: false,
49399
+ passed: true,
49400
+ blockingThreshold: threshold,
49401
+ result: {
49402
+ passed: true,
49403
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
49404
+ },
49405
+ advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
49406
+ diffAvailable,
49407
+ adversarialDropAnalysis: telemetry.adversarialDropAnalysis,
49408
+ adversarialAcceptAnalysis: []
49409
+ });
49410
+ return {
49411
+ check: "adversarial",
49412
+ success: true,
49413
+ command: "",
49414
+ exitCode: 0,
49415
+ output: allFindings.length === 0 ? "Adversarial review passed" : "Adversarial review passed (all findings were advisory \u2014 below blocking threshold)",
49416
+ durationMs,
49417
+ advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
49418
+ cost: 0
49419
+ };
49420
+ }
49421
+ var init_adversarial_outcomes = __esm(() => {
49422
+ init_context();
49423
+ init_diff_files();
49424
+ init_adversarial_helpers();
49425
+ init_finding_projection();
49426
+ init_recurrence_demotion();
49427
+ });
49428
+
49429
+ // src/review/diff-utils.ts
49430
+ var {spawn: spawn3 } = globalThis.Bun;
49431
+ async function resolveNaxIgnorePathspecExcludes(workdir, options) {
49432
+ if (options?.naxIgnoreIndex)
49433
+ return options.naxIgnoreIndex.toPathspecExcludes(options.packageDir);
49434
+ const matchers = await resolveNaxIgnorePatterns(workdir, options?.packageDir);
49435
+ const pathspec = new Set;
49436
+ for (const matcher of matchers)
49437
+ pathspec.add(`:!${matcher.pattern}`);
49438
+ return [...pathspec];
49439
+ }
49440
+ async function collectDiff(workdir, storyGitRef, excludePatterns, options) {
49441
+ const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
49442
+ const merged = [...new Set([...excludePatterns, ...naxIgnoreExcludes, ...ALWAYS_EXCLUDED])];
49443
+ const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`, "--", ".", ...merged];
49444
+ const proc = _diffUtilsDeps.spawn({
49445
+ cmd,
49446
+ cwd: workdir,
49447
+ stdout: "pipe",
49448
+ stderr: "pipe"
49449
+ });
49450
+ const [exitCode, stdout, stderr] = await Promise.all([
49451
+ proc.exited,
49452
+ new Response(proc.stdout).text(),
49453
+ new Response(proc.stderr).text()
49454
+ ]);
49455
+ if (exitCode !== 0) {
49456
+ getSafeLogger()?.warn("diff-utils", "git diff failed \u2014 skipping review diff", { storyGitRef, stderr });
49457
+ return null;
49458
+ }
49459
+ return stdout;
49460
+ }
49461
+ async function collectDiffStat(workdir, storyGitRef, options) {
49462
+ const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
49463
+ const merged = [...new Set([...naxIgnoreExcludes, ...ALWAYS_EXCLUDED])];
49464
+ const proc = _diffUtilsDeps.spawn({
49465
+ cmd: ["git", "diff", "--stat", `${storyGitRef}..HEAD`, "--", ".", ...merged],
49466
+ cwd: workdir,
49467
+ stdout: "pipe",
49468
+ stderr: "pipe"
49469
+ });
49470
+ const [exitCode, stdout] = await Promise.all([
49471
+ proc.exited,
49472
+ new Response(proc.stdout).text(),
49473
+ new Response(proc.stderr).text()
49474
+ ]);
49475
+ return exitCode === 0 ? stdout.trim() : "";
49476
+ }
49477
+ function truncateDiff(diff, stat4) {
49478
+ if (diff.length <= DIFF_CAP_BYTES) {
49479
+ return diff;
49480
+ }
49481
+ const truncated = diff.slice(0, DIFF_CAP_BYTES);
49482
+ const visibleFiles = (truncated.match(/^diff --git/gm) ?? []).length;
49483
+ const totalFiles = (diff.match(/^diff --git/gm) ?? []).length;
49484
+ const statPreamble = stat4 ? `## File Summary (all changed files)
49485
+ ${stat4}
49486
+
49487
+ ## Diff (truncated \u2014 ${visibleFiles}/${totalFiles} files shown)
49488
+ ` : "";
49489
+ return `${statPreamble}${truncated}
49490
+ ... (truncated at ${DIFF_CAP_BYTES} bytes, showing ${visibleFiles}/${totalFiles} files)`;
49491
+ }
49492
+ async function resolveEffectiveRef(workdir, storyGitRef, storyId) {
49493
+ const logger = getSafeLogger();
49494
+ if (storyGitRef && await _diffUtilsDeps.isGitRefValid(workdir, storyGitRef)) {
49495
+ return storyGitRef;
49496
+ }
49497
+ const fallback = await _diffUtilsDeps.getMergeBase(workdir);
49498
+ if (fallback) {
49499
+ logger?.info("review", "storyGitRef missing or invalid \u2014 using merge-base fallback", {
49500
+ storyId,
49501
+ storyGitRef,
49502
+ fallback
49503
+ });
49504
+ return fallback;
49505
+ }
49506
+ return;
49507
+ }
49508
+ function buildSuffixStrippers(testFilePatterns) {
49509
+ if (!testFilePatterns || testFilePatterns.length === 0)
49510
+ return DEFAULT_SUFFIX_STRIPPERS;
49511
+ const regexes = [];
49512
+ for (const pattern of testFilePatterns) {
49513
+ const lastStar = pattern.lastIndexOf("*");
49514
+ if (lastStar === -1)
49515
+ continue;
49516
+ const suffix = pattern.slice(lastStar + 1);
49517
+ if (suffix.length > 0) {
49518
+ regexes.push(new RegExp(`${suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`));
49519
+ }
49520
+ }
49521
+ return regexes.length > 0 ? regexes : DEFAULT_SUFFIX_STRIPPERS;
49522
+ }
49523
+ function stripTestSuffix(base, strippers) {
49524
+ for (const re of strippers) {
49525
+ const stripped = base.replace(re, "");
49526
+ if (stripped !== base)
49527
+ return stripped;
49528
+ }
49529
+ return base;
49530
+ }
49531
+ async function computeTestInventory(workdir, storyGitRef, testFilePatterns, options) {
49532
+ const proc = _diffUtilsDeps.spawn({
49533
+ cmd: ["git", "diff", "--name-only", "--diff-filter=A", `${storyGitRef}..HEAD`],
49534
+ cwd: workdir,
49535
+ stdout: "pipe",
49536
+ stderr: "pipe"
49537
+ });
49538
+ const [exitCode, stdout] = await Promise.all([
49539
+ proc.exited,
49540
+ new Response(proc.stdout).text(),
49541
+ new Response(proc.stderr).text()
49542
+ ]);
49543
+ if (exitCode !== 0) {
49544
+ return { addedTestFiles: [], newSourceFilesWithoutTests: [] };
49545
+ }
49546
+ const addedFiles = stdout.trim().split(`
49547
+ `).filter(Boolean);
49548
+ const ignoreMatchers = options?.naxIgnoreIndex?.getMatchers(options.packageDir) ?? await resolveNaxIgnorePatterns(workdir, options?.packageDir);
49549
+ const visibleAddedFiles = filterNaxInternalPaths(addedFiles, ignoreMatchers);
49550
+ const addedTestFiles = visibleAddedFiles.filter((f) => isTestFile(f, testFilePatterns));
49551
+ const addedSourceFiles = visibleAddedFiles.filter((f) => !isTestFile(f, testFilePatterns));
49552
+ const suffixStrippers = buildSuffixStrippers(testFilePatterns);
49553
+ const testFileBasenames = new Set(addedTestFiles.map((f) => {
49554
+ const base = f.split("/").at(-1) ?? f;
49555
+ return stripTestSuffix(base, suffixStrippers);
49556
+ }));
49557
+ const newSourceFilesWithoutTests = addedSourceFiles.filter((f) => {
49558
+ const base = (f.split("/").at(-1) ?? f).replace(/\.(ts|js|tsx|jsx|go)$/, "");
49559
+ return !testFileBasenames.has(base);
49560
+ });
49561
+ return { addedTestFiles, newSourceFilesWithoutTests };
49562
+ }
49563
+ async function collectDiffFileList(workdir, storyGitRef, options) {
49564
+ const naxIgnoreExcludes = await resolveNaxIgnorePathspecExcludes(workdir, options);
49565
+ const merged = [...new Set([...naxIgnoreExcludes, ...ALWAYS_EXCLUDED])];
49566
+ const proc = _diffUtilsDeps.spawn({
49567
+ cmd: ["git", "diff", "--name-only", `${storyGitRef}..HEAD`, "--", ".", ...merged],
49568
+ cwd: workdir,
49569
+ stdout: "pipe",
49570
+ stderr: "pipe"
49571
+ });
49572
+ const [exitCode, stdout] = await Promise.all([
49573
+ proc.exited,
49574
+ new Response(proc.stdout).text(),
49575
+ new Response(proc.stderr).text()
49576
+ ]);
49577
+ if (exitCode !== 0)
49578
+ return;
49579
+ return stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
49580
+ }
49581
+ var DIFF_CAP_BYTES = 51200, ALWAYS_EXCLUDED, _diffUtilsDeps, DEFAULT_SUFFIX_STRIPPERS;
49582
+ var init_diff_utils = __esm(() => {
49583
+ init_logger2();
49584
+ init_test_runners();
49585
+ init_git();
49586
+ init_path_filters();
49587
+ ALWAYS_EXCLUDED = [":!.nax/", ":!.nax-pids"];
49588
+ _diffUtilsDeps = {
49589
+ spawn: spawn3,
49590
+ isGitRefValid,
49591
+ getMergeBase
49592
+ };
49593
+ DEFAULT_SUFFIX_STRIPPERS = [/\.(test|spec)\.(ts|js|tsx|jsx)$/, /_test\.go$/];
49594
+ });
49595
+
49137
49596
  // src/review/prepare-inputs.ts
49138
49597
  import { relative as relative11, sep as sep3 } from "path";
49139
49598
  function derivePackageDirs(workdir, projectDir) {
@@ -49282,7 +49741,7 @@ var package_default;
49282
49741
  var init_package = __esm(() => {
49283
49742
  package_default = {
49284
49743
  name: "@nathapp/nax",
49285
- version: "0.80.0",
49744
+ version: "0.80.1",
49286
49745
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
49287
49746
  type: "module",
49288
49747
  bin: {
@@ -49397,8 +49856,8 @@ var init_version = __esm(() => {
49397
49856
  NAX_VERSION = package_default.version;
49398
49857
  NAX_COMMIT = (() => {
49399
49858
  try {
49400
- if (/^[0-9a-f]{6,10}$/.test("b4df507e"))
49401
- return "b4df507e";
49859
+ if (/^[0-9a-f]{6,10}$/.test("8daaf106"))
49860
+ return "8daaf106";
49402
49861
  } catch {}
49403
49862
  try {
49404
49863
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -49603,14 +50062,7 @@ async function runAdversarialReview(opts) {
49603
50062
  adversarialConfig
49604
50063
  });
49605
50064
  if (prepared.skipReason === "no git ref") {
49606
- return {
49607
- check: "adversarial",
49608
- success: true,
49609
- command: "",
49610
- exitCode: 0,
49611
- output: "skipped: no git ref",
49612
- durationMs: Date.now() - startTime
49613
- };
50065
+ return skipResult("skipped: no git ref", startTime);
49614
50066
  }
49615
50067
  const diffMode = adversarialConfig.diffMode ?? "ref";
49616
50068
  logger?.info("review", "Running adversarial check", {
@@ -49619,24 +50071,10 @@ async function runAdversarialReview(opts) {
49619
50071
  diffMode
49620
50072
  });
49621
50073
  if (prepared.skipReason === "no changes detected") {
49622
- return {
49623
- check: "adversarial",
49624
- success: true,
49625
- command: "",
49626
- exitCode: 0,
49627
- output: "skipped: no changes detected",
49628
- durationMs: Date.now() - startTime
49629
- };
50074
+ return skipResult("skipped: no changes detected", startTime);
49630
50075
  }
49631
50076
  if (prepared.skipReason === "no code changes") {
49632
- return {
49633
- check: "adversarial",
49634
- success: true,
49635
- command: "",
49636
- exitCode: 0,
49637
- output: "skipped: no code changes",
49638
- durationMs: Date.now() - startTime
49639
- };
50077
+ return skipResult("skipped: no code changes", startTime);
49640
50078
  }
49641
50079
  const effectiveRef = prepared.effectiveRef;
49642
50080
  const stat4 = prepared.stat;
@@ -49650,37 +50088,22 @@ async function runAdversarialReview(opts) {
49650
50088
  storyId: story.id,
49651
50089
  model: adversarialConfig.model
49652
50090
  });
49653
- return {
49654
- check: "adversarial",
49655
- success: true,
49656
- command: "",
49657
- exitCode: 0,
49658
- output: "skipped: no agent available for model tier",
49659
- durationMs: Date.now() - startTime
49660
- };
49661
- }
49662
- let featureCtxBlock = "";
49663
- if (contextBundle) {
49664
- const md = contextBundle.pushMarkdown.trim();
49665
- if (md)
49666
- featureCtxBlock = `${md}
49667
-
49668
- ---
49669
-
49670
- `;
49671
- } else if (featureContextMarkdown) {
49672
- const filtered = filterContextByRole(featureContextMarkdown, "reviewer-adversarial");
49673
- if (filtered.trim())
49674
- featureCtxBlock = `${filtered}
49675
-
49676
- ---
49677
-
49678
- `;
50091
+ return skipResult("skipped: no agent available for model tier", startTime);
49679
50092
  }
50093
+ const featureCtxBlock = buildFeatureCtxBlock(contextBundle, featureContextMarkdown);
49680
50094
  if (!runtime) {
49681
50095
  throw new NaxError("runtime required \u2014 legacy agentManager.run path removed (ADR-019 Wave 3, issue #762)", "DISPATCH_NO_RUNTIME", { stage: "review-adversarial", storyId: story.id });
49682
50096
  }
49683
- const llmCost = 0;
50097
+ const outcomeCtx = {
50098
+ runtime,
50099
+ workdir,
50100
+ projectDir,
50101
+ storyId: story.id,
50102
+ featureName,
50103
+ blockingThreshold,
50104
+ startTime,
50105
+ logger
50106
+ };
49684
50107
  const callCtx = {
49685
50108
  runtime,
49686
50109
  packageView: runtime.packages.resolve(workdir),
@@ -49711,81 +50134,12 @@ async function runAdversarialReview(opts) {
49711
50134
  resolvedTestPatterns
49712
50135
  });
49713
50136
  } catch (err) {
49714
- logger?.warn("adversarial", "LLM call failed \u2014 fail-open", { storyId: story.id, cause: String(err) });
49715
- recordAdversarialAudit({
49716
- runtime,
49717
- workdir,
49718
- projectDir,
49719
- storyId: story.id,
49720
- featureName,
49721
- parsed: false,
49722
- looksLikeFail: false,
49723
- failOpen: true,
49724
- passed: true,
49725
- blockingThreshold,
49726
- result: null
49727
- });
49728
- return {
49729
- check: "adversarial",
49730
- success: true,
49731
- failOpen: true,
49732
- command: "",
49733
- exitCode: 0,
49734
- output: `skipped: LLM call failed \u2014 ${String(err)}`,
49735
- durationMs: Date.now() - startTime
49736
- };
49737
- }
49738
- if (opResult.failOpen) {
49739
- logger?.warn("adversarial", "Retry exhausted \u2014 fail-open", { storyId: story.id });
49740
- recordAdversarialAudit({
49741
- runtime,
49742
- workdir,
49743
- projectDir,
49744
- storyId: story.id,
49745
- featureName,
49746
- parsed: false,
49747
- looksLikeFail: false,
49748
- failOpen: true,
49749
- passed: true,
49750
- blockingThreshold,
49751
- result: null
49752
- });
49753
- return {
49754
- check: "adversarial",
49755
- success: true,
49756
- failOpen: true,
49757
- command: "",
49758
- exitCode: 0,
49759
- output: "adversarial review: could not parse LLM response (fail-open)",
49760
- durationMs: Date.now() - startTime
49761
- };
49762
- }
49763
- if (opResult.looksLikeFail) {
49764
- logger?.warn("adversarial", "LLM returned truncated JSON with passed:false \u2014 treating as failure", {
49765
- storyId: story.id
49766
- });
49767
- recordAdversarialAudit({
49768
- runtime,
49769
- workdir,
49770
- projectDir,
49771
- storyId: story.id,
49772
- featureName,
49773
- parsed: false,
49774
- looksLikeFail: true,
49775
- failOpen: false,
49776
- passed: false,
49777
- blockingThreshold,
49778
- result: null
49779
- });
49780
- return {
49781
- check: "adversarial",
49782
- success: false,
49783
- command: "",
49784
- exitCode: 1,
49785
- output: "adversarial review: LLM response truncated but indicated failure (passed:false found in partial response)",
49786
- durationMs: Date.now() - startTime
49787
- };
50137
+ return catchDispatchFailure(err, outcomeCtx);
49788
50138
  }
50139
+ if (opResult.failOpen)
50140
+ return handleRetryExhaustedFailOpen(outcomeCtx);
50141
+ if (opResult.looksLikeFail)
50142
+ return handleTruncatedLooksLikeFail(outcomeCtx);
49789
50143
  if (opResult.repromptEvent) {
49790
50144
  runtime.dispatchEvents.emitReviewReprompt({
49791
50145
  kind: "review-reprompt-on-drop",
@@ -49796,44 +50150,9 @@ async function runAdversarialReview(opts) {
49796
50150
  costUsd: opResult.repromptEvent.costUsd
49797
50151
  });
49798
50152
  }
49799
- const threshold = blockingThreshold ?? "error";
49800
- const allFindings = opResult.findings;
49801
- const patterns = resolvedTestPatterns?.regex ?? [];
49802
- const testFileMatch = (file3) => patterns.some((re) => re.test(file3));
49803
- const recurrenceCfg = adversarialConfig.recurrenceDemotion ?? { enabled: true, maxBlockingRounds: 2 };
49804
- const {
49805
- blocking: blockingFindings,
49806
- advisory: advisoryOnly,
49807
- demoted
49808
- } = classifyRecurrence(allFindings, priorAdversarialIterations ?? [], recurrenceCfg, testFileMatch, threshold);
49809
- const advisoryFindings = [...advisoryOnly, ...demoted];
49810
- const advisoryReviewFindings = [
49811
- ...llmFindingsToReviewFindings(advisoryOnly, { source: "adversarial-review", isTestFile: testFileMatch }),
49812
- ...tagCoverageGap(llmFindingsToReviewFindings(demoted, { source: "adversarial-review", isTestFile: testFileMatch }))
49813
- ];
49814
- const advisoryFindingsAsFindings = [
49815
- ...toAdversarialReviewFindings(advisoryOnly, { isTestFile: testFileMatch }),
49816
- ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
49817
- ];
49818
- const acDropped = opResult.acDropped ?? [];
49819
- const acks = opResult.acks;
49820
- let diffFiles;
49821
- let diffAvailable;
49822
- if (diff && diff.length > 0) {
49823
- diffFiles = extractDiffFiles(diff);
49824
- diffAvailable = true;
49825
- } else {
49826
- const repoRoot = projectDir ?? workdir;
49827
- const packageDir = workdir !== repoRoot ? workdir : undefined;
49828
- const list = await _adversarialDeps.collectDiffFileList(workdir, effectiveRef, { naxIgnoreIndex, packageDir });
49829
- if (list === undefined) {
49830
- diffFiles = new Set;
49831
- diffAvailable = false;
49832
- } else {
49833
- diffFiles = new Set(list);
49834
- diffAvailable = true;
49835
- }
49836
- }
50153
+ const classification = classifyAdversarialFindings(opResult, blockingThreshold, priorAdversarialIterations, adversarialConfig, resolvedTestPatterns);
50154
+ const { threshold, blockingFindings, advisoryFindings, acDropped } = classification;
50155
+ const { diffFiles, diffAvailable } = await resolveDiffFileSet(diff, workdir, projectDir, effectiveRef, naxIgnoreIndex, _adversarialDeps.collectDiffFileList);
49837
50156
  const { adversarialDropAnalysis, adversarialAcceptAnalysis } = buildCounterfactualTelemetry({
49838
50157
  acDropped,
49839
50158
  blockingFindings,
@@ -49852,177 +50171,28 @@ async function runAdversarialReview(opts) {
49852
50171
  });
49853
50172
  }
49854
50173
  const durationMs = Date.now() - startTime;
50174
+ const telemetry = { adversarialDropAnalysis, adversarialAcceptAnalysis };
49855
50175
  if (blockingFindings.length > 0) {
49856
- logger?.warn("review", `Adversarial review failed: ${blockingFindings.length} blocking findings`, {
49857
- storyId: story.id,
49858
- durationMs,
49859
- findings: blockingFindings.map((f) => ({
49860
- severity: f.severity,
49861
- category: f.category,
49862
- file: f.file,
49863
- line: f.line,
49864
- issue: f.issue
49865
- }))
49866
- });
49867
- recordAdversarialAudit({
49868
- runtime,
49869
- workdir,
49870
- projectDir,
49871
- storyId: story.id,
49872
- featureName,
49873
- parsed: true,
49874
- failOpen: false,
49875
- passed: false,
49876
- blockingThreshold: threshold,
49877
- result: {
49878
- passed: false,
49879
- findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
49880
- },
49881
- advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
49882
- diffAvailable,
49883
- adversarialDropAnalysis,
49884
- adversarialAcceptAnalysis,
49885
- acks
49886
- });
49887
- const output = blockingFindings.length > 0 ? `Adversarial review failed:
49888
-
49889
- ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)";
49890
- return {
49891
- check: "adversarial",
49892
- success: false,
49893
- command: "",
49894
- exitCode: 1,
49895
- output,
49896
- durationMs,
49897
- findings: blockingFindings.length > 0 ? toAdversarialReviewFindings(blockingFindings, { isTestFile: testFileMatch }) : undefined,
49898
- advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
49899
- cost: llmCost
49900
- };
50176
+ return buildBlockingFailureResult(outcomeCtx, classification, telemetry, diffAvailable, durationMs);
49901
50177
  }
49902
50178
  if (!opResult.modelPassed && acDropped.length > 0) {
49903
50179
  if (acDropped.every((d) => d.code === "ac_quote_not_substring")) {
49904
- const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })), { isTestFile: testFileMatch });
49905
- const allAdvisory = [...advisoryFindingsAsFindings, ...demotedFindings];
49906
- logger?.warn("review", "Adversarial review passed: all blocking findings discarded as hallucinated AC quotes", {
49907
- storyId: story.id,
49908
- durationMs,
49909
- droppedCount: acDropped.length,
49910
- drops: acDropped.map((d) => ({ file: d.finding.file, issue: d.finding.issue }))
49911
- });
49912
- recordAdversarialAudit({
49913
- runtime,
49914
- workdir,
49915
- projectDir,
49916
- storyId: story.id,
49917
- featureName,
49918
- parsed: true,
49919
- acks,
49920
- failOpen: false,
49921
- passed: true,
49922
- passReason: "ac_quote_not_substring_demoted",
49923
- blockingThreshold: threshold,
49924
- result: { passed: true, findings: [] },
49925
- advisoryFindings: allAdvisory.length > 0 ? allAdvisory : undefined,
49926
- diffAvailable,
49927
- adversarialDropAnalysis,
49928
- adversarialAcceptAnalysis: []
49929
- });
49930
- return {
49931
- check: "adversarial",
49932
- success: true,
49933
- passReason: "ac_quote_not_substring_demoted",
49934
- command: "",
49935
- exitCode: 0,
49936
- output: `Adversarial review passed: ${acDropped.length} blocking finding(s) demoted to advisory \u2014 all cited AC quotes were fabricated and could not be validated.`,
49937
- durationMs,
49938
- advisoryFindings: allAdvisory.length > 0 ? allAdvisory : undefined,
49939
- cost: llmCost
49940
- };
50180
+ return buildHallucinatedAcQuoteResult(outcomeCtx, classification, telemetry, diffAvailable, durationMs);
49941
50181
  }
49942
- logger?.warn("review", "Adversarial review fail-closed: blocking findings dropped as ungrounded", {
49943
- storyId: story.id,
49944
- durationMs,
49945
- droppedCount: acDropped.length,
49946
- dropCodes: acDropped.map((d) => d.code)
49947
- });
49948
- const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
49949
- `);
49950
- recordAdversarialAudit({
49951
- runtime,
49952
- workdir,
49953
- projectDir,
49954
- storyId: story.id,
49955
- featureName,
49956
- parsed: true,
49957
- acks,
49958
- failOpen: false,
49959
- passed: false,
49960
- blockingThreshold: threshold,
49961
- result: { passed: false, findings: [] },
49962
- advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
49963
- diffAvailable,
49964
- adversarialDropAnalysis,
49965
- adversarialAcceptAnalysis: []
49966
- });
49967
- return {
49968
- check: "adversarial",
49969
- success: false,
49970
- command: "",
49971
- exitCode: 1,
49972
- output: `Adversarial review failed: ${acDropped.length} blocking finding(s) dropped as ungrounded \u2014 the model emitted "passed: false" with concerns it could not ground in any acceptance criterion. Drops:
49973
-
49974
- ${dropSummary}`,
49975
- durationMs,
49976
- advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
49977
- cost: llmCost
49978
- };
50182
+ return buildUngroundedFailClosedResult(outcomeCtx, classification, telemetry, diffAvailable, durationMs);
49979
50183
  }
49980
- logger?.info("review", "Adversarial review passed", { storyId: story.id, durationMs });
49981
- recordAdversarialAudit({
49982
- runtime,
49983
- workdir,
49984
- projectDir,
49985
- storyId: story.id,
49986
- featureName,
49987
- parsed: true,
49988
- acks,
49989
- failOpen: false,
49990
- passed: true,
49991
- blockingThreshold: threshold,
49992
- result: {
49993
- passed: true,
49994
- findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review", isTestFile: testFileMatch })
49995
- },
49996
- advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
49997
- diffAvailable,
49998
- adversarialDropAnalysis,
49999
- adversarialAcceptAnalysis: []
50000
- });
50001
- return {
50002
- check: "adversarial",
50003
- success: true,
50004
- command: "",
50005
- exitCode: 0,
50006
- output: allFindings.length === 0 ? "Adversarial review passed" : "Adversarial review passed (all findings were advisory \u2014 below blocking threshold)",
50007
- durationMs,
50008
- advisoryFindings: advisoryFindings.length > 0 ? advisoryFindingsAsFindings : undefined,
50009
- cost: llmCost
50010
- };
50184
+ return buildPassedResult(outcomeCtx, classification, telemetry, diffAvailable, durationMs);
50011
50185
  }
50012
50186
  var _adversarialDeps;
50013
50187
  var init_adversarial = __esm(() => {
50014
- init_context();
50015
50188
  init_errors();
50016
50189
  init_logger2();
50017
50190
  init_adversarial_review();
50018
50191
  init_call();
50019
- init_diff_files();
50020
50192
  init_adversarial_counterfactual_telemetry();
50021
- init_adversarial_helpers();
50193
+ init_adversarial_outcomes();
50022
50194
  init_diff_utils();
50023
- init_finding_projection();
50024
50195
  init_prepare_inputs();
50025
- init_recurrence_demotion();
50026
50196
  init_review_audit();
50027
50197
  _adversarialDeps = {
50028
50198
  writeReviewAudit,
@@ -50664,7 +50834,37 @@ var init_semantic_debate = __esm(() => {
50664
50834
  init_semantic_helpers();
50665
50835
  });
50666
50836
 
50667
- // src/review/semantic.ts
50837
+ // src/review/semantic-outcomes.ts
50838
+ function skipResult2(output, startTime) {
50839
+ return {
50840
+ check: "semantic",
50841
+ success: true,
50842
+ command: "",
50843
+ exitCode: 0,
50844
+ output,
50845
+ durationMs: Date.now() - startTime
50846
+ };
50847
+ }
50848
+ function buildFeatureCtxBlock2(contextBundle, featureContextMarkdown) {
50849
+ if (contextBundle) {
50850
+ const md = contextBundle.pushMarkdown.trim();
50851
+ return md ? `${md}
50852
+
50853
+ ---
50854
+
50855
+ ` : "";
50856
+ }
50857
+ if (featureContextMarkdown) {
50858
+ const filtered = filterContextByRole(featureContextMarkdown, "reviewer-semantic");
50859
+ if (filtered.trim())
50860
+ return `${filtered}
50861
+
50862
+ ---
50863
+
50864
+ `;
50865
+ }
50866
+ return "";
50867
+ }
50668
50868
  function recordSemanticAudit(opts) {
50669
50869
  opts.runtime?.dispatchEvents.emitReviewDecision({
50670
50870
  kind: "review-decision",
@@ -50684,6 +50884,216 @@ function recordSemanticAudit(opts) {
50684
50884
  acks: opts.acks
50685
50885
  });
50686
50886
  }
50887
+ function catchDispatchFailure2(err, ctx) {
50888
+ const { runtime, workdir, projectDir, storyId, featureName, blockingThreshold, startTime, logger } = ctx;
50889
+ logger?.warn("semantic", "LLM call failed \u2014 fail-open", { storyId, cause: String(err) });
50890
+ recordSemanticAudit({
50891
+ runtime,
50892
+ workdir,
50893
+ projectDir,
50894
+ storyId,
50895
+ featureName,
50896
+ parsed: false,
50897
+ looksLikeFail: false,
50898
+ failOpen: true,
50899
+ passed: true,
50900
+ blockingThreshold,
50901
+ result: null
50902
+ });
50903
+ return {
50904
+ check: "semantic",
50905
+ success: true,
50906
+ failOpen: true,
50907
+ command: "",
50908
+ exitCode: 0,
50909
+ output: `skipped: LLM call failed \u2014 ${String(err)}`,
50910
+ durationMs: Date.now() - startTime
50911
+ };
50912
+ }
50913
+ function handleRetryExhaustedFailOpen2(ctx) {
50914
+ const { runtime, workdir, projectDir, storyId, featureName, blockingThreshold, startTime, logger } = ctx;
50915
+ logger?.warn("semantic", "Retry exhausted \u2014 fail-open", { storyId });
50916
+ recordSemanticAudit({
50917
+ runtime,
50918
+ workdir,
50919
+ projectDir,
50920
+ storyId,
50921
+ featureName,
50922
+ parsed: false,
50923
+ looksLikeFail: false,
50924
+ failOpen: true,
50925
+ passed: true,
50926
+ blockingThreshold,
50927
+ result: null
50928
+ });
50929
+ return {
50930
+ check: "semantic",
50931
+ success: true,
50932
+ failOpen: true,
50933
+ command: "",
50934
+ exitCode: 0,
50935
+ output: "semantic review: could not parse LLM response (fail-open)",
50936
+ durationMs: Date.now() - startTime
50937
+ };
50938
+ }
50939
+ function handleTruncatedLooksLikeFail2(ctx) {
50940
+ const { runtime, workdir, projectDir, storyId, featureName, blockingThreshold, startTime, logger } = ctx;
50941
+ logger?.warn("semantic", "LLM returned truncated JSON with passed:false \u2014 treating as failure", { storyId });
50942
+ recordSemanticAudit({
50943
+ runtime,
50944
+ workdir,
50945
+ projectDir,
50946
+ storyId,
50947
+ featureName,
50948
+ parsed: false,
50949
+ looksLikeFail: true,
50950
+ failOpen: false,
50951
+ passed: false,
50952
+ blockingThreshold,
50953
+ result: null
50954
+ });
50955
+ return {
50956
+ check: "semantic",
50957
+ success: false,
50958
+ command: "",
50959
+ exitCode: 1,
50960
+ output: "semantic review: LLM response truncated but indicated failure (passed:false found in partial response)",
50961
+ durationMs: Date.now() - startTime
50962
+ };
50963
+ }
50964
+ function classifySemanticFindings(opResult, blockingThreshold) {
50965
+ const threshold = blockingThreshold ?? "error";
50966
+ const allFindings = opResult.findings;
50967
+ const blockingFindings = allFindings.filter((f) => isBlockingSeverity(f.severity, threshold));
50968
+ const advisoryFindings = allFindings.filter((f) => !isBlockingSeverity(f.severity, threshold));
50969
+ return {
50970
+ threshold,
50971
+ allFindings,
50972
+ acks: opResult.acks,
50973
+ blockingFindings,
50974
+ advisoryFindings
50975
+ };
50976
+ }
50977
+ function buildBlockingFailureResult2(ctx, classification, durationMs) {
50978
+ const { runtime, workdir, projectDir, storyId, featureName, logger, testFileMatch } = ctx;
50979
+ const { threshold, allFindings, acks, blockingFindings, advisoryFindings } = classification;
50980
+ logger?.warn("review", `Semantic review failed: ${blockingFindings.length} blocking findings`, {
50981
+ storyId,
50982
+ durationMs
50983
+ });
50984
+ logger?.debug("review", "Semantic review findings", {
50985
+ storyId,
50986
+ findings: blockingFindings.map((f) => ({
50987
+ severity: f.severity,
50988
+ file: f.file,
50989
+ line: f.line,
50990
+ issue: f.issue,
50991
+ suggestion: f.suggestion
50992
+ }))
50993
+ });
50994
+ const output = `Semantic review failed:
50995
+
50996
+ ${formatFindings2(blockingFindings)}`;
50997
+ recordSemanticAudit({
50998
+ runtime,
50999
+ workdir,
51000
+ projectDir,
51001
+ storyId,
51002
+ featureName,
51003
+ parsed: true,
51004
+ failOpen: false,
51005
+ passed: false,
51006
+ blockingThreshold: threshold,
51007
+ acks,
51008
+ result: {
51009
+ passed: false,
51010
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
51011
+ },
51012
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
51013
+ });
51014
+ return {
51015
+ check: "semantic",
51016
+ success: false,
51017
+ command: "",
51018
+ exitCode: 1,
51019
+ output,
51020
+ durationMs,
51021
+ findings: toReviewFindings(blockingFindings, { isTestFile: testFileMatch }),
51022
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
51023
+ cost: 0
51024
+ };
51025
+ }
51026
+ function buildAcIndexDroppedFailClosedResult(ctx, classification, durationMs) {
51027
+ const { runtime, workdir, projectDir, storyId, featureName, logger, testFileMatch } = ctx;
51028
+ const { threshold, acks, advisoryFindings } = classification;
51029
+ logger?.warn("review", "Semantic review fail-closed: blocking findings dropped (acIndex invalid)", {
51030
+ storyId,
51031
+ durationMs
51032
+ });
51033
+ recordSemanticAudit({
51034
+ runtime,
51035
+ workdir,
51036
+ projectDir,
51037
+ storyId,
51038
+ featureName,
51039
+ parsed: true,
51040
+ acks,
51041
+ failOpen: false,
51042
+ passed: false,
51043
+ blockingThreshold: threshold,
51044
+ result: { passed: false, findings: [] },
51045
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
51046
+ });
51047
+ return {
51048
+ check: "semantic",
51049
+ success: false,
51050
+ command: "",
51051
+ exitCode: 1,
51052
+ output: 'Semantic review failed: blocking finding(s) were dropped \u2014 acIndex was missing or out of range. The model emitted "passed: false" without valid AC attribution.',
51053
+ durationMs,
51054
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
51055
+ cost: 0
51056
+ };
51057
+ }
51058
+ function buildPassedResult2(ctx, classification, durationMs) {
51059
+ const { runtime, workdir, projectDir, storyId, featureName, logger, testFileMatch } = ctx;
51060
+ const { threshold, allFindings, acks, advisoryFindings } = classification;
51061
+ logger?.info("review", "Semantic review passed", { storyId, durationMs });
51062
+ recordSemanticAudit({
51063
+ runtime,
51064
+ workdir,
51065
+ projectDir,
51066
+ storyId,
51067
+ featureName,
51068
+ parsed: true,
51069
+ acks,
51070
+ failOpen: false,
51071
+ passed: true,
51072
+ blockingThreshold: threshold,
51073
+ result: {
51074
+ passed: true,
51075
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
51076
+ },
51077
+ advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
51078
+ });
51079
+ return {
51080
+ check: "semantic",
51081
+ success: true,
51082
+ command: "",
51083
+ exitCode: 0,
51084
+ output: allFindings.length === 0 ? "Semantic review passed" : "Semantic review passed (all findings were advisory \u2014 below blocking threshold)",
51085
+ durationMs,
51086
+ advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
51087
+ cost: 0
51088
+ };
51089
+ }
51090
+ var init_semantic_outcomes = __esm(() => {
51091
+ init_context();
51092
+ init_finding_projection();
51093
+ init_semantic_helpers();
51094
+ });
51095
+
51096
+ // src/review/semantic.ts
50687
51097
  async function runSemanticReview(opts) {
50688
51098
  const {
50689
51099
  workdir,
@@ -50721,14 +51131,7 @@ async function runSemanticReview(opts) {
50721
51131
  semanticConfig
50722
51132
  });
50723
51133
  if (prepared.skipReason === "no git ref") {
50724
- return {
50725
- check: "semantic",
50726
- success: true,
50727
- command: "",
50728
- exitCode: 0,
50729
- output: "skipped: no git ref",
50730
- durationMs: Date.now() - startTime
50731
- };
51134
+ return skipResult2("skipped: no git ref", startTime);
50732
51135
  }
50733
51136
  const diffMode = semanticConfig.diffMode ?? "ref";
50734
51137
  logger?.info("review", "Running semantic check", {
@@ -50738,24 +51141,10 @@ async function runSemanticReview(opts) {
50738
51141
  configProvided: !!naxConfig
50739
51142
  });
50740
51143
  if (prepared.skipReason === "no changes detected") {
50741
- return {
50742
- check: "semantic",
50743
- success: true,
50744
- command: "",
50745
- exitCode: 0,
50746
- output: "skipped: no changes detected",
50747
- durationMs: Date.now() - startTime
50748
- };
51144
+ return skipResult2("skipped: no changes detected", startTime);
50749
51145
  }
50750
51146
  if (prepared.skipReason === "no production code changes") {
50751
- return {
50752
- check: "semantic",
50753
- success: true,
50754
- command: "",
50755
- exitCode: 0,
50756
- output: "skipped: no production code changes",
50757
- durationMs: Date.now() - startTime
50758
- };
51147
+ return skipResult2("skipped: no production code changes", startTime);
50759
51148
  }
50760
51149
  const effectiveRef = prepared.effectiveRef;
50761
51150
  const stat4 = prepared.stat;
@@ -50767,33 +51156,9 @@ async function runSemanticReview(opts) {
50767
51156
  storyId: story.id,
50768
51157
  model: semanticConfig.model
50769
51158
  });
50770
- return {
50771
- check: "semantic",
50772
- success: true,
50773
- command: "",
50774
- exitCode: 0,
50775
- output: "skipped: no agent available for model tier",
50776
- durationMs: Date.now() - startTime
50777
- };
50778
- }
50779
- let featureCtxBlock = "";
50780
- if (contextBundle) {
50781
- const md = contextBundle.pushMarkdown.trim();
50782
- if (md)
50783
- featureCtxBlock = `${md}
50784
-
50785
- ---
50786
-
50787
- `;
50788
- } else if (featureContextMarkdown) {
50789
- const filtered = filterContextByRole(featureContextMarkdown, "reviewer-semantic");
50790
- if (filtered.trim())
50791
- featureCtxBlock = `${filtered}
50792
-
50793
- ---
50794
-
50795
- `;
51159
+ return skipResult2("skipped: no agent available for model tier", startTime);
50796
51160
  }
51161
+ const featureCtxBlock = buildFeatureCtxBlock2(contextBundle, featureContextMarkdown);
50797
51162
  const basePrompt = new ReviewPromptBuilder().buildSemanticReviewPrompt(story, semanticConfig, {
50798
51163
  mode: diffMode,
50799
51164
  diff,
@@ -50845,7 +51210,17 @@ async function runSemanticReview(opts) {
50845
51210
  if (!runtime) {
50846
51211
  throw new NaxError("runtime required \u2014 legacy agentManager.run path removed (ADR-019 Wave 3, issue #762)", "DISPATCH_NO_RUNTIME", { stage: "review-semantic", storyId: story.id });
50847
51212
  }
50848
- const llmCost = 0;
51213
+ const outcomeCtx = {
51214
+ runtime,
51215
+ workdir,
51216
+ projectDir,
51217
+ storyId: story.id,
51218
+ featureName,
51219
+ blockingThreshold,
51220
+ startTime,
51221
+ logger,
51222
+ testFileMatch
51223
+ };
50849
51224
  const callCtx = {
50850
51225
  runtime,
50851
51226
  packageView: runtime.packages.resolve(workdir),
@@ -50872,81 +51247,12 @@ async function runSemanticReview(opts) {
50872
51247
  blockingThreshold
50873
51248
  });
50874
51249
  } catch (err) {
50875
- logger?.warn("semantic", "LLM call failed \u2014 fail-open", { storyId: story.id, cause: String(err) });
50876
- recordSemanticAudit({
50877
- runtime,
50878
- workdir,
50879
- projectDir,
50880
- storyId: story.id,
50881
- featureName,
50882
- parsed: false,
50883
- looksLikeFail: false,
50884
- failOpen: true,
50885
- passed: true,
50886
- blockingThreshold,
50887
- result: null
50888
- });
50889
- return {
50890
- check: "semantic",
50891
- success: true,
50892
- failOpen: true,
50893
- command: "",
50894
- exitCode: 0,
50895
- output: `skipped: LLM call failed \u2014 ${String(err)}`,
50896
- durationMs: Date.now() - startTime
50897
- };
50898
- }
50899
- if (opResult.failOpen) {
50900
- logger?.warn("semantic", "Retry exhausted \u2014 fail-open", { storyId: story.id });
50901
- recordSemanticAudit({
50902
- runtime,
50903
- workdir,
50904
- projectDir,
50905
- storyId: story.id,
50906
- featureName,
50907
- parsed: false,
50908
- looksLikeFail: false,
50909
- failOpen: true,
50910
- passed: true,
50911
- blockingThreshold,
50912
- result: null
50913
- });
50914
- return {
50915
- check: "semantic",
50916
- success: true,
50917
- failOpen: true,
50918
- command: "",
50919
- exitCode: 0,
50920
- output: "semantic review: could not parse LLM response (fail-open)",
50921
- durationMs: Date.now() - startTime
50922
- };
50923
- }
50924
- if (opResult.looksLikeFail) {
50925
- logger?.warn("semantic", "LLM returned truncated JSON with passed:false \u2014 treating as failure", {
50926
- storyId: story.id
50927
- });
50928
- recordSemanticAudit({
50929
- runtime,
50930
- workdir,
50931
- projectDir,
50932
- storyId: story.id,
50933
- featureName,
50934
- parsed: false,
50935
- looksLikeFail: true,
50936
- failOpen: false,
50937
- passed: false,
50938
- blockingThreshold,
50939
- result: null
50940
- });
50941
- return {
50942
- check: "semantic",
50943
- success: false,
50944
- command: "",
50945
- exitCode: 1,
50946
- output: "semantic review: LLM response truncated but indicated failure (passed:false found in partial response)",
50947
- durationMs: Date.now() - startTime
50948
- };
51250
+ return catchDispatchFailure2(err, outcomeCtx);
50949
51251
  }
51252
+ if (opResult.failOpen)
51253
+ return handleRetryExhaustedFailOpen2(outcomeCtx);
51254
+ if (opResult.looksLikeFail)
51255
+ return handleTruncatedLooksLikeFail2(outcomeCtx);
50950
51256
  if (opResult.repromptEvent) {
50951
51257
  runtime.dispatchEvents.emitReviewReprompt({
50952
51258
  kind: "review-reprompt-on-drop",
@@ -50957,138 +51263,35 @@ async function runSemanticReview(opts) {
50957
51263
  costUsd: opResult.repromptEvent.costUsd
50958
51264
  });
50959
51265
  }
50960
- const threshold = blockingThreshold ?? "error";
50961
- const allFindings = opResult.findings;
50962
- const acks = opResult.acks;
50963
- const blockingFindings = allFindings.filter((f) => isBlockingSeverity(f.severity, threshold));
50964
- const advisoryFindings = allFindings.filter((f) => !isBlockingSeverity(f.severity, threshold));
51266
+ const classification = classifySemanticFindings(opResult, blockingThreshold);
51267
+ const { allFindings, blockingFindings, advisoryFindings } = classification;
50965
51268
  if (advisoryFindings.length > 0) {
50966
- logger?.debug("review", `Semantic review: ${advisoryFindings.length} advisory findings (below threshold '${threshold}')`, {
51269
+ logger?.debug("review", `Semantic review: ${advisoryFindings.length} advisory findings (below threshold '${classification.threshold}')`, {
50967
51270
  storyId: story.id,
50968
51271
  findings: advisoryFindings.map((f) => ({ severity: f.severity, file: f.file, issue: f.issue }))
50969
51272
  });
50970
51273
  }
50971
51274
  const durationMs = Date.now() - startTime;
50972
51275
  if (blockingFindings.length > 0) {
50973
- logger?.warn("review", `Semantic review failed: ${blockingFindings.length} blocking findings`, {
50974
- storyId: story.id,
50975
- durationMs
50976
- });
50977
- logger?.debug("review", "Semantic review findings", {
50978
- storyId: story.id,
50979
- findings: blockingFindings.map((f) => ({
50980
- severity: f.severity,
50981
- file: f.file,
50982
- line: f.line,
50983
- issue: f.issue,
50984
- suggestion: f.suggestion
50985
- }))
50986
- });
50987
- const output = `Semantic review failed:
50988
-
50989
- ${formatFindings2(blockingFindings)}`;
50990
- recordSemanticAudit({
50991
- runtime,
50992
- workdir,
50993
- projectDir,
50994
- storyId: story.id,
50995
- featureName,
50996
- parsed: true,
50997
- failOpen: false,
50998
- passed: false,
50999
- blockingThreshold: threshold,
51000
- acks,
51001
- result: {
51002
- passed: false,
51003
- findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
51004
- },
51005
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
51006
- });
51007
- return {
51008
- check: "semantic",
51009
- success: false,
51010
- command: "",
51011
- exitCode: 1,
51012
- output,
51013
- durationMs,
51014
- findings: toReviewFindings(blockingFindings, { isTestFile: testFileMatch }),
51015
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
51016
- cost: llmCost
51017
- };
51276
+ return buildBlockingFailureResult2(outcomeCtx, classification, durationMs);
51018
51277
  }
51019
51278
  if (!opResult.passed && allFindings.length === 0) {
51020
- logger?.warn("review", "Semantic review fail-closed: blocking findings dropped (acIndex invalid)", {
51021
- storyId: story.id,
51022
- durationMs
51023
- });
51024
- recordSemanticAudit({
51025
- runtime,
51026
- workdir,
51027
- projectDir,
51028
- storyId: story.id,
51029
- featureName,
51030
- parsed: true,
51031
- acks,
51032
- failOpen: false,
51033
- passed: false,
51034
- blockingThreshold: threshold,
51035
- result: { passed: false, findings: [] },
51036
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
51037
- });
51038
- return {
51039
- check: "semantic",
51040
- success: false,
51041
- command: "",
51042
- exitCode: 1,
51043
- output: 'Semantic review failed: blocking finding(s) were dropped \u2014 acIndex was missing or out of range. The model emitted "passed: false" without valid AC attribution.',
51044
- durationMs,
51045
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
51046
- cost: llmCost
51047
- };
51279
+ return buildAcIndexDroppedFailClosedResult(outcomeCtx, classification, durationMs);
51048
51280
  }
51049
- logger?.info("review", "Semantic review passed", { storyId: story.id, durationMs });
51050
- recordSemanticAudit({
51051
- runtime,
51052
- workdir,
51053
- projectDir,
51054
- storyId: story.id,
51055
- featureName,
51056
- parsed: true,
51057
- acks,
51058
- failOpen: false,
51059
- passed: true,
51060
- blockingThreshold: threshold,
51061
- result: {
51062
- passed: true,
51063
- findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
51064
- },
51065
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review", isTestFile: testFileMatch }) : undefined
51066
- });
51067
- return {
51068
- check: "semantic",
51069
- success: true,
51070
- command: "",
51071
- exitCode: 0,
51072
- output: allFindings.length === 0 ? "Semantic review passed" : "Semantic review passed (all findings were advisory \u2014 below blocking threshold)",
51073
- durationMs,
51074
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings, { isTestFile: testFileMatch }) : undefined,
51075
- cost: llmCost
51076
- };
51281
+ return buildPassedResult2(outcomeCtx, classification, durationMs);
51077
51282
  }
51078
51283
  var _semanticDeps;
51079
51284
  var init_semantic = __esm(() => {
51080
- init_context();
51081
51285
  init_debate();
51082
51286
  init_errors();
51083
51287
  init_logger2();
51084
51288
  init_call();
51085
51289
  init_semantic_review();
51086
51290
  init_prompts();
51087
- init_finding_projection();
51088
51291
  init_prepare_inputs();
51089
51292
  init_review_audit();
51090
51293
  init_semantic_debate();
51091
- init_semantic_helpers();
51294
+ init_semantic_outcomes();
51092
51295
  _semanticDeps = {
51093
51296
  createDebateRunner: (opts) => new DebateRunner(opts),
51094
51297
  writeReviewAudit,
@@ -51181,68 +51384,155 @@ async function getUncommittedFilesImpl(workdir) {
51181
51384
  return [];
51182
51385
  }
51183
51386
  }
51184
- async function runReview(opts) {
51387
+ async function guardUncommittedFiles(workdir, storyId, runtime, naxIgnoreIndex, logger) {
51388
+ await autoCommitIfDirty(workdir, "review", "agent", storyId ?? "review", runtime?.dirtyWorktrees);
51389
+ const allUncommittedFiles = await _reviewGitDeps.getUncommittedFiles(workdir);
51390
+ const afterRuntimeFilter = allUncommittedFiles.filter((f) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(f)));
51391
+ const uncommittedFiles = naxIgnoreIndex ? naxIgnoreIndex.filter(afterRuntimeFilter, workdir) : afterRuntimeFilter;
51392
+ if (uncommittedFiles.length > 0) {
51393
+ const fileList = uncommittedFiles.join(", ");
51394
+ logger?.warn("review", `Uncommitted changes detected before review (proceeding): ${fileList}`, {
51395
+ storyId,
51396
+ uncommittedCount: uncommittedFiles.length
51397
+ });
51398
+ }
51399
+ }
51400
+ function buildReviewStory(storyId, story) {
51401
+ return {
51402
+ id: storyId ?? "",
51403
+ title: story?.title ?? "",
51404
+ description: story?.description ?? "",
51405
+ acceptanceCriteria: story?.acceptanceCriteria ?? []
51406
+ };
51407
+ }
51408
+ async function runSemanticCheck(opts) {
51185
51409
  const {
51186
- config: config2,
51187
51410
  workdir,
51188
- executionConfig,
51189
- qualityCommands,
51411
+ storyGitRef,
51412
+ story,
51190
51413
  storyId,
51414
+ config: config2,
51415
+ agentManager,
51416
+ naxConfig,
51417
+ featureName,
51418
+ priorSemanticIterations,
51419
+ featureContextMarkdown,
51420
+ contextBundles,
51421
+ projectDir,
51422
+ naxIgnoreIndex,
51423
+ runtime
51424
+ } = opts;
51425
+ const semanticCfg = config2.semantic ?? {
51426
+ model: "balanced",
51427
+ diffMode: "ref",
51428
+ resetRefOnRerun: false,
51429
+ rules: [],
51430
+ timeoutMs: 600000
51431
+ };
51432
+ const runSemantic = _reviewSemanticDeps.runSemanticReview;
51433
+ return runSemantic({
51434
+ workdir,
51435
+ storyGitRef,
51436
+ story: buildReviewStory(storyId, story),
51437
+ semanticConfig: semanticCfg,
51438
+ agentManager,
51439
+ naxConfig,
51440
+ featureName,
51441
+ priorSemanticIterations,
51442
+ blockingThreshold: config2.blockingThreshold,
51443
+ featureContextMarkdown,
51444
+ contextBundle: contextBundles?.semantic,
51445
+ projectDir,
51446
+ naxIgnoreIndex,
51447
+ runtime
51448
+ });
51449
+ }
51450
+ async function runAdversarialCheck(opts) {
51451
+ const {
51452
+ workdir,
51191
51453
  storyGitRef,
51192
51454
  story,
51455
+ storyId,
51456
+ config: config2,
51193
51457
  agentManager,
51194
51458
  naxConfig,
51195
- retrySkipChecks,
51196
51459
  featureName,
51197
51460
  priorFailures,
51198
- priorSemanticIterations,
51199
51461
  featureContextMarkdown,
51200
51462
  contextBundles,
51201
51463
  projectDir,
51202
- env: env2,
51203
51464
  naxIgnoreIndex,
51204
51465
  runtime,
51205
51466
  priorAdversarialIterations
51206
51467
  } = opts;
51468
+ const adversarialCfg = config2.adversarial ?? {
51469
+ model: "balanced",
51470
+ diffMode: "ref",
51471
+ rules: [],
51472
+ timeoutMs: 600000,
51473
+ parallel: false,
51474
+ maxConcurrentSessions: 2
51475
+ };
51476
+ const runAdversarial = _reviewAdversarialDeps.runAdversarialReview;
51477
+ return runAdversarial({
51478
+ workdir,
51479
+ storyGitRef,
51480
+ story: buildReviewStory(storyId, story),
51481
+ adversarialConfig: adversarialCfg,
51482
+ agentManager,
51483
+ config: naxConfig,
51484
+ featureName,
51485
+ priorFailures,
51486
+ blockingThreshold: config2.blockingThreshold,
51487
+ featureContextMarkdown,
51488
+ contextBundle: contextBundles?.adversarial,
51489
+ projectDir,
51490
+ naxIgnoreIndex,
51491
+ runtime,
51492
+ priorAdversarialIterations
51493
+ });
51494
+ }
51495
+ async function runMechanicalCheck(checkName, opts) {
51496
+ const {
51497
+ config: config2,
51498
+ workdir,
51499
+ executionConfig,
51500
+ qualityCommands,
51501
+ storyId,
51502
+ story,
51503
+ storyGitRef,
51504
+ naxConfig,
51505
+ projectDir,
51506
+ env: env2,
51507
+ naxIgnoreIndex
51508
+ } = opts;
51509
+ const command = await resolveCommand(checkName, config2, executionConfig, workdir, qualityCommands);
51510
+ if (command === null) {
51511
+ getSafeLogger()?.warn("review", `Skipping ${checkName} check (command not configured or disabled)`);
51512
+ return null;
51513
+ }
51514
+ return checkName === "lint" ? await _reviewLintDeps.runScopedLintCheck({
51515
+ resolvedLintCommand: command,
51516
+ configCommands: config2.commands,
51517
+ qualityCommands,
51518
+ lintOutputFormat: naxConfig?.quality?.lintOutput?.format ?? "auto",
51519
+ workdir,
51520
+ projectDir,
51521
+ storyId,
51522
+ story,
51523
+ storyGitRef,
51524
+ env: env2,
51525
+ stripEnvVars: naxConfig?.quality?.stripEnvVars ?? [],
51526
+ naxIgnoreIndex
51527
+ }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2, naxConfig?.quality?.stripEnvVars ?? []), workdir);
51528
+ }
51529
+ async function runReview(opts) {
51530
+ const { config: config2, workdir, storyId, retrySkipChecks, naxIgnoreIndex, runtime } = opts;
51207
51531
  const startTime = Date.now();
51208
51532
  const logger = getSafeLogger();
51209
51533
  const checks3 = [];
51210
51534
  let firstFailure;
51211
- await autoCommitIfDirty(workdir, "review", "agent", storyId ?? "review", runtime?.dirtyWorktrees);
51212
- const allUncommittedFiles = await _reviewGitDeps.getUncommittedFiles(workdir);
51213
- const NAX_RUNTIME_PATTERNS = [
51214
- /nax\.lock$/,
51215
- /nax\/metrics\.json$/,
51216
- /nax\/status\.json$/,
51217
- /nax\/features\/[^/]+\/status\.json$/,
51218
- /nax\/features\/[^/]+\/prd\.json$/,
51219
- /nax\/features\/[^/]+\/runs\//,
51220
- /nax\/features\/[^/]+\/plan\//,
51221
- /nax\/features\/[^/]+\/acp-sessions\.json$/,
51222
- /nax\/features\/[^/]+\/interactions\//,
51223
- /nax\/features\/[^/]+\/progress\.txt$/,
51224
- /nax\/features\/[^/]+\/acceptance-refined\.json$/,
51225
- /nax\/features\/[^/]+\/stories\/[^/]+\/context-manifest-[^/]+\.json$/,
51226
- /nax\/features\/[^/]+\/stories\/[^/]+\/rebuild-manifest\.json$/,
51227
- /\.nax-verifier-verdict\.json$/,
51228
- /\.nax-pids$/,
51229
- /\.nax-wt\//,
51230
- /\.nax-acceptance[^/]*$/,
51231
- /_nax_acceptance_test\.py$/,
51232
- /_nax_suggested_test\.py$/,
51233
- /(?:^|\/)test\/.*\.jsonl$/,
51234
- /(?:^|\/)coverage\//,
51235
- /\.lcov$/
51236
- ];
51237
- const afterRuntimeFilter = allUncommittedFiles.filter((f) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(f)));
51238
- const uncommittedFiles = naxIgnoreIndex ? naxIgnoreIndex.filter(afterRuntimeFilter, workdir) : afterRuntimeFilter;
51239
- if (uncommittedFiles.length > 0) {
51240
- const fileList = uncommittedFiles.join(", ");
51241
- logger?.warn("review", `Uncommitted changes detected before review (proceeding): ${fileList}`, {
51242
- storyId,
51243
- uncommittedCount: uncommittedFiles.length
51244
- });
51245
- }
51535
+ await guardUncommittedFiles(workdir, storyId, runtime, naxIgnoreIndex, logger);
51246
51536
  for (const checkName of config2.checks) {
51247
51537
  if (retrySkipChecks?.has(checkName)) {
51248
51538
  getSafeLogger()?.debug("review", `Skipping ${checkName} check (already passed in previous review pass)`, {
@@ -51250,37 +51540,8 @@ async function runReview(opts) {
51250
51540
  });
51251
51541
  continue;
51252
51542
  }
51253
- if (checkName === "semantic") {
51254
- const semanticStory = {
51255
- id: storyId ?? "",
51256
- title: story?.title ?? "",
51257
- description: story?.description ?? "",
51258
- acceptanceCriteria: story?.acceptanceCriteria ?? []
51259
- };
51260
- const semanticCfg = config2.semantic ?? {
51261
- model: "balanced",
51262
- diffMode: "ref",
51263
- resetRefOnRerun: false,
51264
- rules: [],
51265
- timeoutMs: 600000
51266
- };
51267
- const runSemantic = _reviewSemanticDeps.runSemanticReview;
51268
- const result2 = await runSemantic({
51269
- workdir,
51270
- storyGitRef,
51271
- story: semanticStory,
51272
- semanticConfig: semanticCfg,
51273
- agentManager,
51274
- naxConfig,
51275
- featureName,
51276
- priorSemanticIterations,
51277
- blockingThreshold: config2.blockingThreshold,
51278
- featureContextMarkdown,
51279
- contextBundle: contextBundles?.semantic,
51280
- projectDir,
51281
- naxIgnoreIndex,
51282
- runtime
51283
- });
51543
+ if (checkName === "semantic" || checkName === "adversarial") {
51544
+ const result2 = checkName === "semantic" ? await runSemanticCheck(opts) : await runAdversarialCheck(opts);
51284
51545
  checks3.push(result2);
51285
51546
  if (!result2.success && !firstFailure) {
51286
51547
  firstFailure = `${checkName} failed`;
@@ -51290,67 +51551,9 @@ async function runReview(opts) {
51290
51551
  }
51291
51552
  continue;
51292
51553
  }
51293
- if (checkName === "adversarial") {
51294
- const adversarialStory = {
51295
- id: storyId ?? "",
51296
- title: story?.title ?? "",
51297
- description: story?.description ?? "",
51298
- acceptanceCriteria: story?.acceptanceCriteria ?? []
51299
- };
51300
- const adversarialCfg = config2.adversarial ?? {
51301
- model: "balanced",
51302
- diffMode: "ref",
51303
- rules: [],
51304
- timeoutMs: 600000,
51305
- parallel: false,
51306
- maxConcurrentSessions: 2
51307
- };
51308
- const runAdversarial = _reviewAdversarialDeps.runAdversarialReview;
51309
- const result2 = await runAdversarial({
51310
- workdir,
51311
- storyGitRef,
51312
- story: adversarialStory,
51313
- adversarialConfig: adversarialCfg,
51314
- agentManager,
51315
- config: naxConfig,
51316
- featureName,
51317
- priorFailures,
51318
- blockingThreshold: config2.blockingThreshold,
51319
- featureContextMarkdown,
51320
- contextBundle: contextBundles?.adversarial,
51321
- projectDir,
51322
- naxIgnoreIndex,
51323
- runtime,
51324
- priorAdversarialIterations
51325
- });
51326
- checks3.push(result2);
51327
- if (!result2.success && !firstFailure) {
51328
- firstFailure = `${checkName} failed`;
51329
- }
51330
- if (!result2.success) {
51331
- break;
51332
- }
51333
- continue;
51334
- }
51335
- const command = await resolveCommand(checkName, config2, executionConfig, workdir, qualityCommands);
51336
- if (command === null) {
51337
- getSafeLogger()?.warn("review", `Skipping ${checkName} check (command not configured or disabled)`);
51554
+ const result = await runMechanicalCheck(checkName, opts);
51555
+ if (result === null)
51338
51556
  continue;
51339
- }
51340
- const result = checkName === "lint" ? await _reviewLintDeps.runScopedLintCheck({
51341
- resolvedLintCommand: command,
51342
- configCommands: config2.commands,
51343
- qualityCommands,
51344
- lintOutputFormat: naxConfig?.quality?.lintOutput?.format ?? "auto",
51345
- workdir,
51346
- projectDir,
51347
- storyId,
51348
- story,
51349
- storyGitRef,
51350
- env: env2,
51351
- stripEnvVars: naxConfig?.quality?.stripEnvVars ?? [],
51352
- naxIgnoreIndex
51353
- }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2, naxConfig?.quality?.stripEnvVars ?? []), workdir);
51354
51557
  checks3.push(result);
51355
51558
  if (result.success) {
51356
51559
  logger?.info("review", `${checkName} passed`, {
@@ -51379,7 +51582,7 @@ async function runReview(opts) {
51379
51582
  failureReason: firstFailure
51380
51583
  };
51381
51584
  }
51382
- var _reviewSemanticDeps, _reviewAdversarialDeps, _reviewLintDeps, _reviewRunnerDeps, _reviewGitDeps;
51585
+ var _reviewSemanticDeps, _reviewAdversarialDeps, _reviewLintDeps, _reviewRunnerDeps, _reviewGitDeps, NAX_RUNTIME_PATTERNS;
51383
51586
  var init_runner2 = __esm(() => {
51384
51587
  init_logger2();
51385
51588
  init_quality();
@@ -51405,6 +51608,30 @@ var init_runner2 = __esm(() => {
51405
51608
  _reviewGitDeps = {
51406
51609
  getUncommittedFiles: getUncommittedFilesImpl
51407
51610
  };
51611
+ NAX_RUNTIME_PATTERNS = [
51612
+ /nax\.lock$/,
51613
+ /nax\/metrics\.json$/,
51614
+ /nax\/status\.json$/,
51615
+ /nax\/features\/[^/]+\/status\.json$/,
51616
+ /nax\/features\/[^/]+\/prd\.json$/,
51617
+ /nax\/features\/[^/]+\/runs\//,
51618
+ /nax\/features\/[^/]+\/plan\//,
51619
+ /nax\/features\/[^/]+\/acp-sessions\.json$/,
51620
+ /nax\/features\/[^/]+\/interactions\//,
51621
+ /nax\/features\/[^/]+\/progress\.txt$/,
51622
+ /nax\/features\/[^/]+\/acceptance-refined\.json$/,
51623
+ /nax\/features\/[^/]+\/stories\/[^/]+\/context-manifest-[^/]+\.json$/,
51624
+ /nax\/features\/[^/]+\/stories\/[^/]+\/rebuild-manifest\.json$/,
51625
+ /\.nax-verifier-verdict\.json$/,
51626
+ /\.nax-pids$/,
51627
+ /\.nax-wt\//,
51628
+ /\.nax-acceptance[^/]*$/,
51629
+ /_nax_acceptance_test\.py$/,
51630
+ /_nax_suggested_test\.py$/,
51631
+ /(?:^|\/)test\/.*\.jsonl$/,
51632
+ /(?:^|\/)coverage\//,
51633
+ /\.lcov$/
51634
+ ];
51408
51635
  });
51409
51636
 
51410
51637
  // src/review/index.ts
@@ -54284,18 +54511,18 @@ class PromptAuditor {
54284
54511
  }
54285
54512
  this._dirCreated = true;
54286
54513
  }
54514
+ const safeEntry = redactSecrets(entry);
54287
54515
  try {
54288
- await _promptAuditorDeps.appendLine(this._jsonlPath, `${JSON.stringify(entry)}
54516
+ await _promptAuditorDeps.appendLine(this._jsonlPath, `${JSON.stringify(safeEntry)}
54289
54517
  `);
54290
54518
  } catch (err) {
54291
54519
  throw tagAuditError(err, "jsonl");
54292
54520
  }
54293
54521
  if (!("prompt" in entry) || !("response" in entry))
54294
54522
  return;
54295
- const auditEntry = entry;
54296
- const filename = deriveTxtFilename(auditEntry);
54523
+ const filename = deriveTxtFilename(entry);
54297
54524
  try {
54298
- await _promptAuditorDeps.write(join37(this._featureDir, filename), buildTxtContent(auditEntry));
54525
+ await _promptAuditorDeps.write(join37(this._featureDir, filename), buildTxtContent(safeEntry));
54299
54526
  } catch (err) {
54300
54527
  throw tagAuditError(err, "txt");
54301
54528
  }
@@ -60089,7 +60316,7 @@ async function checkWorkingTreeClean(workdir) {
60089
60316
  const exitCode = await proc.exited;
60090
60317
  const lines = output.trim() === "" ? [] : output.split(`
60091
60318
  `).filter(Boolean);
60092
- const nonNaxDirtyFiles = lines.filter((line) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(line)));
60319
+ const nonNaxDirtyFiles = lines.filter((line) => !NAX_RUNTIME_PATTERNS2.some((pattern) => pattern.test(line)));
60093
60320
  const passed = exitCode === 0 && nonNaxDirtyFiles.length === 0;
60094
60321
  return {
60095
60322
  name: "working-tree-clean",
@@ -60120,9 +60347,9 @@ async function checkGitUserConfigured(workdir) {
60120
60347
  message: passed ? "Git user is configured" : !hasName && !hasEmail ? "Git user.name and user.email not configured" : !hasName ? "Git user.name not configured" : "Git user.email not configured"
60121
60348
  };
60122
60349
  }
60123
- var NAX_RUNTIME_PATTERNS;
60350
+ var NAX_RUNTIME_PATTERNS2;
60124
60351
  var init_checks_git = __esm(() => {
60125
- NAX_RUNTIME_PATTERNS = [
60352
+ NAX_RUNTIME_PATTERNS2 = [
60126
60353
  /^.{2} nax\.lock$/,
60127
60354
  /^.{2} \.nax\/$/,
60128
60355
  /^.{2} \.nax\/metrics\.json$/,
@@ -66527,7 +66754,7 @@ function assemblePlanInputs(story, config2, resolvedTestPatterns) {
66527
66754
  function hasReviewEscalation(story) {
66528
66755
  return (story.priorFailures ?? []).some((f) => f.stage === "review");
66529
66756
  }
66530
- function buildFeatureCtxBlock(ctx, role) {
66757
+ function buildFeatureCtxBlock3(ctx, role) {
66531
66758
  const bundleMarkdown = ctx.contextBundle?.pushMarkdown.trim();
66532
66759
  if (bundleMarkdown) {
66533
66760
  return `${bundleMarkdown}
@@ -66628,7 +66855,7 @@ async function assemblePlanInputsFromCtx(ctx) {
66628
66855
  stat: prepared.stat,
66629
66856
  diff: prepared.diff,
66630
66857
  excludePatterns: prepared.excludePatterns,
66631
- featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-semantic"),
66858
+ featureCtxBlock: buildFeatureCtxBlock3(ctx, "reviewer-semantic"),
66632
66859
  resolvedTestPatterns,
66633
66860
  blockingThreshold: ctx.config.review.blockingThreshold,
66634
66861
  _refresh: {
@@ -66665,7 +66892,7 @@ async function assemblePlanInputsFromCtx(ctx) {
66665
66892
  excludePatterns: prepared.excludePatterns,
66666
66893
  testGlobs: prepared.testGlobs,
66667
66894
  refExcludePatterns: prepared.refExcludePatterns,
66668
- featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-adversarial"),
66895
+ featureCtxBlock: buildFeatureCtxBlock3(ctx, "reviewer-adversarial"),
66669
66896
  resolvedTestPatterns,
66670
66897
  blockingThreshold: ctx.config.review.blockingThreshold,
66671
66898
  _refresh: {
@@ -71426,7 +71653,6 @@ var init_types11 = __esm(() => {
71426
71653
  });
71427
71654
 
71428
71655
  // src/worktree/dependencies.ts
71429
- import { existsSync as existsSync20 } from "fs";
71430
71656
  import { join as join66 } from "path";
71431
71657
  async function prepareWorktreeDependencies(options) {
71432
71658
  const mode = options.config.execution.worktreeDependencies.mode;
@@ -71434,8 +71660,6 @@ async function prepareWorktreeDependencies(options) {
71434
71660
  switch (mode) {
71435
71661
  case "off":
71436
71662
  return { cwd: resolvedCwd };
71437
- case "inherit":
71438
- return resolveInheritedDependencies(options, resolvedCwd);
71439
71663
  case "provision":
71440
71664
  return provisionDependencies(options.config, options.worktreeRoot, resolvedCwd);
71441
71665
  }
@@ -71443,20 +71667,10 @@ async function prepareWorktreeDependencies(options) {
71443
71667
  function resolveDependencyCwd(options) {
71444
71668
  return options.storyWorkdir ? join66(options.worktreeRoot, options.storyWorkdir) : options.worktreeRoot;
71445
71669
  }
71446
- function resolveInheritedDependencies(options, resolvedCwd) {
71447
- if (hasDependencyManifests(options.worktreeRoot, resolvedCwd)) {
71448
- throw new WorktreeDependencyPreparationError(`[worktree-deps] inherit mode is unsupported for dependency-managed worktrees in phase 1. Use mode "provision" with execution.worktreeDependencies.setupCommand, or switch to "off".`, "inherit");
71449
- }
71450
- return { cwd: resolvedCwd };
71451
- }
71452
- function hasDependencyManifests(worktreeRoot, resolvedCwd) {
71453
- const directories = resolvedCwd === worktreeRoot ? [worktreeRoot] : [worktreeRoot, resolvedCwd];
71454
- return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(join66(directory, filename))));
71455
- }
71456
71670
  async function provisionDependencies(config2, worktreeRoot, resolvedCwd) {
71457
71671
  const setupCommand = config2.execution.worktreeDependencies.setupCommand;
71458
71672
  if (!setupCommand) {
71459
- throw new WorktreeDependencyPreparationError("[worktree-deps] provision mode requires execution.worktreeDependencies.setupCommand in phase 1.", "provision");
71673
+ throw new WorktreeDependencyPreparationError("[worktree-deps] provision mode requires execution.worktreeDependencies.setupCommand.", "provision");
71460
71674
  }
71461
71675
  const argv = parseCommandToArgv(setupCommand);
71462
71676
  if (argv.length === 0) {
@@ -71479,36 +71693,62 @@ async function provisionDependencies(config2, worktreeRoot, resolvedCwd) {
71479
71693
  }
71480
71694
  return { cwd: resolvedCwd };
71481
71695
  }
71482
- var PHASE_ONE_INHERIT_UNSUPPORTED_FILES, _worktreeDependencyDeps;
71696
+ var _worktreeDependencyDeps;
71483
71697
  var init_dependencies = __esm(() => {
71484
71698
  init_bun_deps();
71485
71699
  init_command_argv();
71486
71700
  init_types11();
71487
- PHASE_ONE_INHERIT_UNSUPPORTED_FILES = [
71488
- "package.json",
71489
- "bun.lock",
71490
- "bun.lockb",
71491
- "package-lock.json",
71492
- "pnpm-lock.yaml",
71493
- "yarn.lock",
71494
- "requirements.txt",
71495
- "pyproject.toml",
71496
- "Cargo.toml",
71497
- "go.mod",
71498
- "Gemfile",
71499
- "composer.json",
71500
- "pom.xml",
71501
- "build.gradle",
71502
- "build.gradle.kts"
71503
- ];
71504
71701
  _worktreeDependencyDeps = {
71505
- existsSync: existsSync20,
71506
71702
  spawn
71507
71703
  };
71508
71704
  });
71509
71705
 
71510
71706
  // src/utils/gitignore.ts
71511
- var NAX_GITIGNORE_ENTRIES;
71707
+ function activeIgnoreLines(content) {
71708
+ return content.split(`
71709
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
71710
+ }
71711
+ function hasOpinionOn(activeLines, entry) {
71712
+ return activeLines.has(entry) || activeLines.has(`!${entry}`);
71713
+ }
71714
+ async function patchIgnoreFile(filePath, entries, options = {}) {
71715
+ const { footer = "", sectionComment = "# nax - generated files" } = options;
71716
+ const header = options.header ?? `${sectionComment}
71717
+ `;
71718
+ const file3 = Bun.file(filePath);
71719
+ const existing = await file3.exists() ? await file3.text() : "";
71720
+ const isNew = existing.trim().length === 0;
71721
+ const active = new Set(activeIgnoreLines(existing));
71722
+ const missing = entries.filter((entry) => !hasOpinionOn(active, entry));
71723
+ if (missing.length === 0)
71724
+ return { created: false, added: [] };
71725
+ if (isNew) {
71726
+ await Bun.write(filePath, `${header}${missing.join(`
71727
+ `)}
71728
+ ${footer}`);
71729
+ return { created: true, added: [...missing] };
71730
+ }
71731
+ const separator = existing.endsWith(`
71732
+ `) ? "" : `
71733
+ `;
71734
+ await Bun.write(filePath, `${existing}${separator}
71735
+ ${sectionComment}
71736
+ ${missing.join(`
71737
+ `)}
71738
+ `);
71739
+ return { created: false, added: [...missing] };
71740
+ }
71741
+ var NAX_GITIGNORE_ENTRIES, NAX_NAXIGNORE_ENTRIES, NAX_NAXIGNORE_HEADER = `# nax - paths excluded from context, review
71742
+ # and verification scanning.
71743
+ # gitignore syntax. Also honoured per-package.
71744
+
71745
+ `, NAX_NAXIGNORE_SUGGESTIONS = `
71746
+ # Uncomment what applies to this repo:
71747
+ # examples/
71748
+ # fixtures/
71749
+ # vendor/
71750
+ # *.generated.*
71751
+ `;
71512
71752
  var init_gitignore = __esm(() => {
71513
71753
  init_config();
71514
71754
  NAX_GITIGNORE_ENTRIES = [
@@ -71533,10 +71773,17 @@ var init_gitignore = __esm(() => {
71533
71773
  ".nax/finish-audit/",
71534
71774
  ".nax/mutation-journal/"
71535
71775
  ];
71776
+ NAX_NAXIGNORE_ENTRIES = [
71777
+ ".nax/",
71778
+ "dist/",
71779
+ "build/",
71780
+ "coverage/",
71781
+ "node_modules/"
71782
+ ];
71536
71783
  });
71537
71784
 
71538
71785
  // src/worktree/manager.ts
71539
- import { existsSync as existsSync21, symlinkSync } from "fs";
71786
+ import { existsSync as existsSync20, symlinkSync } from "fs";
71540
71787
  import { mkdir as mkdir12 } from "fs/promises";
71541
71788
  import { join as join67 } from "path";
71542
71789
 
@@ -71548,7 +71795,7 @@ class WorktreeManager {
71548
71795
  try {
71549
71796
  await mkdir12(infoDir, { recursive: true });
71550
71797
  let existing = "";
71551
- if (existsSync21(excludePath)) {
71798
+ if (existsSync20(excludePath)) {
71552
71799
  existing = await Bun.file(excludePath).text();
71553
71800
  }
71554
71801
  const missing = NAX_GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
@@ -71635,7 +71882,7 @@ ${missing.join(`
71635
71882
  });
71636
71883
  }
71637
71884
  const envSource = join67(projectRoot, ".env");
71638
- if (existsSync21(envSource)) {
71885
+ if (existsSync20(envSource)) {
71639
71886
  const envTarget = join67(worktreePath, ".env");
71640
71887
  try {
71641
71888
  symlinkSync(envSource, envTarget, "file");
@@ -72575,7 +72822,7 @@ var init_pipeline_result_handler = __esm(() => {
72575
72822
  });
72576
72823
 
72577
72824
  // src/execution/iteration-runner.ts
72578
- import { existsSync as existsSync22 } from "fs";
72825
+ import { existsSync as existsSync21 } from "fs";
72579
72826
  import { join as join70 } from "path";
72580
72827
  function releaseHeavyPipelineContext(ctx) {
72581
72828
  ctx.agentResult = undefined;
@@ -72792,7 +73039,7 @@ var init_iteration_runner = __esm(() => {
72792
73039
  loadConfigForWorkdir,
72793
73040
  prepareWorktreeDependencies,
72794
73041
  runPipeline,
72795
- existsSync: existsSync22,
73042
+ existsSync: existsSync21,
72796
73043
  worktreeManager: new WorktreeManager
72797
73044
  };
72798
73045
  });
@@ -75550,7 +75797,14 @@ async function defaultReadResult(resultPath) {
75550
75797
  const f = Bun.file(resultPath);
75551
75798
  if (!await f.exists())
75552
75799
  return null;
75553
- return JSON.parse(await f.text());
75800
+ try {
75801
+ return JSON.parse(await f.text());
75802
+ } catch (err) {
75803
+ getSafeLogger()?.warn("plugins", `nax-finish: malformed result file at ${resultPath}`, {
75804
+ error: errorMessage(err)
75805
+ });
75806
+ return null;
75807
+ }
75554
75808
  }
75555
75809
  async function defaultClearResult(resultPath) {
75556
75810
  const file3 = Bun.file(resultPath);
@@ -75741,6 +75995,7 @@ async function finalizeFinishOutcome(options) {
75741
75995
  }
75742
75996
  var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
75743
75997
  var init_nax_finish = __esm(() => {
75998
+ init_logger2();
75744
75999
  init_config2();
75745
76000
  init_telegram2();
75746
76001
  init_telegram2();
@@ -77353,12 +77608,19 @@ function resolveModulePath(modulePath, projectRoot) {
77353
77608
  }
77354
77609
  return path25.resolve(modulePath);
77355
77610
  }
77356
- async function loadAndValidatePlugin(initialModulePath, config2, allowedRoots = [], originalPath) {
77611
+ async function loadAndValidatePlugin(initialModulePath, config2, allowedRoots, originalPath) {
77357
77612
  let attemptedPath = initialModulePath;
77358
77613
  try {
77359
77614
  let modulePath = initialModulePath;
77360
77615
  const isFilePath = modulePath.startsWith("/") || modulePath.startsWith("./") || modulePath.startsWith("../");
77361
- if (isFilePath && allowedRoots.length > 0) {
77616
+ if (isFilePath) {
77617
+ if (allowedRoots.length === 0) {
77618
+ const msg = `no allowed roots configured for file-path plugin module '${initialModulePath}'`;
77619
+ const logger = getSafeLogger6();
77620
+ logger?.error("plugins", `Security: ${msg}`);
77621
+ _pluginErrorSink(`[plugins] Security: ${msg}`);
77622
+ return null;
77623
+ }
77362
77624
  const validation = validateModulePath(modulePath, allowedRoots);
77363
77625
  if (!validation.valid) {
77364
77626
  const logger = getSafeLogger6();
@@ -77632,14 +77894,6 @@ var init_status_writer = __esm(() => {
77632
77894
  });
77633
77895
 
77634
77896
  // src/cli/init-context.ts
77635
- var exports_init_context = {};
77636
- __export(exports_init_context, {
77637
- scanProject: () => scanProject,
77638
- initPackage: () => initPackage,
77639
- initContext: () => initContext,
77640
- generatePackageContextTemplate: () => generatePackageContextTemplate,
77641
- generateContextTemplate: () => generateContextTemplate
77642
- });
77643
77897
  import { basename as basename16, join as join78 } from "path";
77644
77898
  async function bunFileExists(path26) {
77645
77899
  return Bun.file(path26).exists();
@@ -77893,11 +78147,11 @@ var init_init_context = __esm(() => {
77893
78147
  });
77894
78148
 
77895
78149
  // src/cli/init-detect.ts
77896
- import { existsSync as existsSync23, readFileSync } from "fs";
78150
+ import { existsSync as existsSync22, readFileSync } from "fs";
77897
78151
  import { join as join79 } from "path";
77898
78152
  function readPackageJson(projectRoot) {
77899
78153
  const pkgPath = join79(projectRoot, "package.json");
77900
- if (!existsSync23(pkgPath))
78154
+ if (!existsSync22(pkgPath))
77901
78155
  return;
77902
78156
  try {
77903
78157
  return JSON.parse(readFileSync(pkgPath, "utf-8"));
@@ -77939,41 +78193,41 @@ function detectStack(projectRoot) {
77939
78193
  };
77940
78194
  }
77941
78195
  function detectRuntime(projectRoot) {
77942
- if (existsSync23(join79(projectRoot, "bun.lockb")) || existsSync23(join79(projectRoot, "bunfig.toml"))) {
78196
+ if (existsSync22(join79(projectRoot, "bun.lockb")) || existsSync22(join79(projectRoot, "bunfig.toml"))) {
77943
78197
  return "bun";
77944
78198
  }
77945
- if (existsSync23(join79(projectRoot, "package-lock.json")) || existsSync23(join79(projectRoot, "yarn.lock")) || existsSync23(join79(projectRoot, "pnpm-lock.yaml"))) {
78199
+ if (existsSync22(join79(projectRoot, "package-lock.json")) || existsSync22(join79(projectRoot, "yarn.lock")) || existsSync22(join79(projectRoot, "pnpm-lock.yaml"))) {
77946
78200
  return "node";
77947
78201
  }
77948
78202
  return "unknown";
77949
78203
  }
77950
78204
  function detectLanguage2(projectRoot) {
77951
- if (existsSync23(join79(projectRoot, "tsconfig.json")))
78205
+ if (existsSync22(join79(projectRoot, "tsconfig.json")))
77952
78206
  return "typescript";
77953
- if (existsSync23(join79(projectRoot, "pyproject.toml")) || existsSync23(join79(projectRoot, "setup.py"))) {
78207
+ if (existsSync22(join79(projectRoot, "pyproject.toml")) || existsSync22(join79(projectRoot, "setup.py"))) {
77954
78208
  return "python";
77955
78209
  }
77956
- if (existsSync23(join79(projectRoot, "Cargo.toml")))
78210
+ if (existsSync22(join79(projectRoot, "Cargo.toml")))
77957
78211
  return "rust";
77958
- if (existsSync23(join79(projectRoot, "go.mod")))
78212
+ if (existsSync22(join79(projectRoot, "go.mod")))
77959
78213
  return "go";
77960
78214
  return "unknown";
77961
78215
  }
77962
78216
  function detectLinter(projectRoot) {
77963
- if (existsSync23(join79(projectRoot, "biome.json")) || existsSync23(join79(projectRoot, "biome.jsonc"))) {
78217
+ if (existsSync22(join79(projectRoot, "biome.json")) || existsSync22(join79(projectRoot, "biome.jsonc"))) {
77964
78218
  return "biome";
77965
78219
  }
77966
- if (existsSync23(join79(projectRoot, ".eslintrc.json")) || existsSync23(join79(projectRoot, ".eslintrc.js")) || existsSync23(join79(projectRoot, "eslint.config.js"))) {
78220
+ if (existsSync22(join79(projectRoot, ".eslintrc.json")) || existsSync22(join79(projectRoot, ".eslintrc.js")) || existsSync22(join79(projectRoot, "eslint.config.js"))) {
77967
78221
  return "eslint";
77968
78222
  }
77969
78223
  return "unknown";
77970
78224
  }
77971
78225
  function detectMonorepo(projectRoot) {
77972
- if (existsSync23(join79(projectRoot, "turbo.json")))
78226
+ if (existsSync22(join79(projectRoot, "turbo.json")))
77973
78227
  return "turborepo";
77974
- if (existsSync23(join79(projectRoot, "nx.json")))
78228
+ if (existsSync22(join79(projectRoot, "nx.json")))
77975
78229
  return "nx";
77976
- if (existsSync23(join79(projectRoot, "pnpm-workspace.yaml")))
78230
+ if (existsSync22(join79(projectRoot, "pnpm-workspace.yaml")))
77977
78231
  return "pnpm-workspaces";
77978
78232
  const pkg = readPackageJson(projectRoot);
77979
78233
  if (pkg?.workspaces)
@@ -78115,7 +78369,7 @@ __export(exports_init, {
78115
78369
  checkInitCollision: () => checkInitCollision,
78116
78370
  _initDeps: () => _initDeps
78117
78371
  });
78118
- import { existsSync as existsSync24 } from "fs";
78372
+ import { existsSync as existsSync23 } from "fs";
78119
78373
  import { mkdir as mkdir15 } from "fs/promises";
78120
78374
  import { join as join80 } from "path";
78121
78375
  function validateProjectName(name) {
@@ -78154,24 +78408,32 @@ async function checkInitCollision(name, currentWorkdir, currentRemote) {
78154
78408
  async function updateGitignore(projectRoot) {
78155
78409
  const logger = getLogger();
78156
78410
  const gitignorePath = join80(projectRoot, ".gitignore");
78157
- let existing = "";
78158
- if (existsSync24(gitignorePath)) {
78159
- existing = await Bun.file(gitignorePath).text();
78160
- }
78161
- const missingEntries = NAX_GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
78162
- if (missingEntries.length === 0) {
78411
+ const result = await patchIgnoreFile(gitignorePath, NAX_GITIGNORE_ENTRIES);
78412
+ if (result.added.length === 0) {
78163
78413
  logger.info("init", ".gitignore already has nax entries", { path: gitignorePath });
78164
78414
  return;
78165
78415
  }
78166
- const naxSection = `
78167
- # nax \u2014 generated files
78168
- ${missingEntries.join(`
78169
- `)}
78170
- `;
78171
- await Bun.write(gitignorePath, existing + naxSection);
78172
78416
  logger.info("init", "Updated .gitignore with nax entries", {
78173
78417
  path: gitignorePath,
78174
- added: missingEntries
78418
+ created: result.created,
78419
+ added: result.added
78420
+ });
78421
+ }
78422
+ async function updateNaxignore(projectRoot) {
78423
+ const logger = getLogger();
78424
+ const naxignorePath = join80(projectRoot, ".naxignore");
78425
+ const result = await patchIgnoreFile(naxignorePath, NAX_NAXIGNORE_ENTRIES, {
78426
+ header: NAX_NAXIGNORE_HEADER,
78427
+ footer: NAX_NAXIGNORE_SUGGESTIONS,
78428
+ sectionComment: "# nax - scanning exclusions"
78429
+ });
78430
+ if (result.added.length === 0) {
78431
+ logger.info("init", ".naxignore already has nax entries", { path: naxignorePath });
78432
+ return;
78433
+ }
78434
+ logger.info("init", result.created ? "Created .naxignore" : "Updated .naxignore with nax entries", {
78435
+ path: naxignorePath,
78436
+ added: result.added
78175
78437
  });
78176
78438
  }
78177
78439
  function buildConstitution(stack) {
@@ -78235,12 +78497,12 @@ function buildConstitution(stack) {
78235
78497
  async function initGlobal() {
78236
78498
  const logger = getLogger();
78237
78499
  const globalDir = globalConfigDir();
78238
- if (!existsSync24(globalDir)) {
78500
+ if (!existsSync23(globalDir)) {
78239
78501
  await mkdir15(globalDir, { recursive: true });
78240
78502
  logger.info("init", "Created global config directory", { path: globalDir });
78241
78503
  }
78242
78504
  const configPath = join80(globalDir, "config.json");
78243
- if (!existsSync24(configPath)) {
78505
+ if (!existsSync23(configPath)) {
78244
78506
  await Bun.write(configPath, `${JSON.stringify(MINIMAL_GLOBAL_CONFIG, null, 2)}
78245
78507
  `);
78246
78508
  logger.info("init", "Created global config", { path: configPath });
@@ -78248,14 +78510,14 @@ async function initGlobal() {
78248
78510
  logger.info("init", "Global config already exists", { path: configPath });
78249
78511
  }
78250
78512
  const constitutionPath = join80(globalDir, "constitution.md");
78251
- if (!existsSync24(constitutionPath)) {
78513
+ if (!existsSync23(constitutionPath)) {
78252
78514
  await Bun.write(constitutionPath, buildConstitution({ runtime: "unknown", language: "unknown", linter: "unknown", monorepo: "none" }));
78253
78515
  logger.info("init", "Created global constitution", { path: constitutionPath });
78254
78516
  } else {
78255
78517
  logger.info("init", "Global constitution already exists", { path: constitutionPath });
78256
78518
  }
78257
78519
  const hooksDir = join80(globalDir, "hooks");
78258
- if (!existsSync24(hooksDir)) {
78520
+ if (!existsSync23(hooksDir)) {
78259
78521
  await mkdir15(hooksDir, { recursive: true });
78260
78522
  logger.info("init", "Created global hooks directory", { path: hooksDir });
78261
78523
  } else {
@@ -78300,7 +78562,7 @@ async function initProject(projectRoot, options) {
78300
78562
  `), "INIT_NAME_COLLISION", { stage: "init", name: detectedName });
78301
78563
  }
78302
78564
  }
78303
- if (!existsSync24(projectDir)) {
78565
+ if (!existsSync23(projectDir)) {
78304
78566
  await mkdir15(projectDir, { recursive: true });
78305
78567
  logger.info("init", "Created project config directory", { path: projectDir });
78306
78568
  }
@@ -78316,7 +78578,7 @@ async function initProject(projectRoot, options) {
78316
78578
  monorepo: stack.monorepo
78317
78579
  });
78318
78580
  const configPath = join80(projectDir, "config.json");
78319
- if (!existsSync24(configPath)) {
78581
+ if (!existsSync23(configPath)) {
78320
78582
  await Bun.write(configPath, `${JSON.stringify(projectConfig, null, 2)}
78321
78583
  `);
78322
78584
  logger.info("init", "Created project config", { path: configPath });
@@ -78325,35 +78587,44 @@ async function initProject(projectRoot, options) {
78325
78587
  }
78326
78588
  await initContext(projectRoot, { force: options?.force });
78327
78589
  const constitutionPath = join80(projectDir, "constitution.md");
78328
- if (!existsSync24(constitutionPath) || options?.force) {
78590
+ if (!existsSync23(constitutionPath) || options?.force) {
78329
78591
  await Bun.write(constitutionPath, buildConstitution(stack));
78330
78592
  logger.info("init", "Created project constitution", { path: constitutionPath });
78331
78593
  } else {
78332
78594
  logger.info("init", "Project constitution already exists", { path: constitutionPath });
78333
78595
  }
78334
- const hooksDir = join80(projectDir, "hooks");
78335
- if (!existsSync24(hooksDir)) {
78336
- await mkdir15(hooksDir, { recursive: true });
78337
- logger.info("init", "Created project hooks directory", { path: hooksDir });
78338
- } else {
78339
- logger.info("init", "Project hooks directory already exists", { path: hooksDir });
78596
+ for (const [label, dir] of [
78597
+ ["hooks", join80(projectDir, "hooks")],
78598
+ ["features", featuresDir(projectRoot)]
78599
+ ]) {
78600
+ if (!existsSync23(dir)) {
78601
+ await mkdir15(dir, { recursive: true });
78602
+ logger.info("init", `Created project ${label} directory`, { path: dir });
78603
+ } else {
78604
+ logger.info("init", `Project ${label} directory already exists`, { path: dir });
78605
+ }
78340
78606
  }
78341
78607
  await updateGitignore(projectRoot);
78342
- await promptsInitCommand({ workdir: projectRoot, force: false, autoWireConfig: false });
78608
+ await updateNaxignore(projectRoot);
78343
78609
  _initDeps.log(`
78344
78610
  [OK] nax init complete. Created files:`);
78345
78611
  _initDeps.log(" - .nax/config.json");
78346
78612
  _initDeps.log(" - .nax/context.md");
78347
78613
  _initDeps.log(" - .nax/constitution.md");
78348
78614
  _initDeps.log(" - .nax/hooks/");
78349
- _initDeps.log(" - .nax/templates/");
78615
+ _initDeps.log(` - ${PROJECT_FEATURES_DIR}/`);
78616
+ _initDeps.log(" - .gitignore (nax entries)");
78617
+ _initDeps.log(" - .naxignore");
78350
78618
  _initDeps.log(`
78351
78619
  Next steps:`);
78352
78620
  _initDeps.log(" 1. Review .nax/context.md and fill in TODOs");
78353
- _initDeps.log(" 2. Review .nax/config.json and adjust quality commands");
78354
- _initDeps.log(" 3. Run: nax generate");
78355
- _initDeps.log(" 4. Run: nax plan");
78356
- _initDeps.log(" 5. Run: nax run");
78621
+ _initDeps.log(" 2. Review .naxignore and add paths nax should not scan");
78622
+ _initDeps.log(" 3. Review .nax/config.json and adjust quality commands");
78623
+ _initDeps.log(" 4. Run: nax generate");
78624
+ _initDeps.log(" 5. Run: nax plan");
78625
+ _initDeps.log(" 6. Run: nax run");
78626
+ _initDeps.log(`
78627
+ Optional: nax prompts --init (scaffold overridable prompt templates)`);
78357
78628
  logger.info("init", "Project config initialized successfully", { path: projectDir });
78358
78629
  }
78359
78630
  async function initCommand(options = {}) {
@@ -78361,7 +78632,7 @@ async function initCommand(options = {}) {
78361
78632
  await initGlobal();
78362
78633
  } else if (options.package) {
78363
78634
  const projectRoot = options.projectRoot ?? process.cwd();
78364
- await initPackage(projectRoot, options.package);
78635
+ await initPackage(projectRoot, options.package, options.force);
78365
78636
  _initDeps.log(`
78366
78637
  [OK] Package scaffold created.`);
78367
78638
  _initDeps.log(` Created: .nax/mono/${options.package}/context.md`);
@@ -78383,7 +78654,6 @@ var init_init2 = __esm(() => {
78383
78654
  init_gitignore();
78384
78655
  init_init_context();
78385
78656
  init_init_detect();
78386
- init_prompts2();
78387
78657
  _initDeps = {
78388
78658
  log: console.log.bind(console)
78389
78659
  };
@@ -78398,11 +78668,11 @@ __export(exports_migrate, {
78398
78668
  migrateCommand: () => migrateCommand,
78399
78669
  detectGeneratedContent: () => detectGeneratedContent
78400
78670
  });
78401
- import { existsSync as existsSync25 } from "fs";
78671
+ import { existsSync as existsSync24 } from "fs";
78402
78672
  import { mkdir as mkdir16, readdir as readdir4, rename as rename5 } from "fs/promises";
78403
78673
  import path26 from "path";
78404
78674
  async function detectGeneratedContent(naxDir) {
78405
- if (!existsSync25(naxDir))
78675
+ if (!existsSync24(naxDir))
78406
78676
  return [];
78407
78677
  const candidates = [];
78408
78678
  let entries = [];
@@ -78417,7 +78687,7 @@ async function detectGeneratedContent(naxDir) {
78417
78687
  }
78418
78688
  }
78419
78689
  const featuresDir2 = path26.join(naxDir, "features");
78420
- if (existsSync25(featuresDir2)) {
78690
+ if (existsSync24(featuresDir2)) {
78421
78691
  let featureDirs = [];
78422
78692
  try {
78423
78693
  featureDirs = await readdir4(featuresDir2);
@@ -78479,7 +78749,7 @@ async function migrateCommand(options) {
78479
78749
  });
78480
78750
  }
78481
78751
  const src = path26.join(globalConfigDir(), options.reclaim);
78482
- if (!existsSync25(src)) {
78752
+ if (!existsSync24(src)) {
78483
78753
  throw new NaxError(`Nothing to reclaim: ~/.nax/${options.reclaim} does not exist`, "MIGRATE_RECLAIM_NOT_FOUND", {
78484
78754
  stage: "migrate",
78485
78755
  name: options.reclaim
@@ -78525,7 +78795,7 @@ async function migrateCommand(options) {
78525
78795
  }
78526
78796
  const naxDir = path26.join(options.workdir, ".nax");
78527
78797
  const configPath = path26.join(naxDir, "config.json");
78528
- if (!existsSync25(configPath)) {
78798
+ if (!existsSync24(configPath)) {
78529
78799
  throw new NaxError("No .nax/config.json found \u2014 run nax init first", "MIGRATE_NO_CONFIG", {
78530
78800
  stage: "migrate",
78531
78801
  workdir: options.workdir
@@ -78560,7 +78830,7 @@ async function migrateCommand(options) {
78560
78830
  for (const candidate of candidates) {
78561
78831
  const dest = path26.join(destBase, candidate.name);
78562
78832
  await mkdir16(path26.dirname(dest), { recursive: true });
78563
- if (existsSync25(dest)) {
78833
+ if (existsSync24(dest)) {
78564
78834
  throw new NaxError(`Migration conflict: destination already exists.
78565
78835
  Source: ${candidate.srcPath}
78566
78836
  Destination: ${dest}
@@ -80458,380 +80728,6 @@ var init_pipeline = __esm(() => {
80458
80728
  init_reporters();
80459
80729
  });
80460
80730
 
80461
- // src/cli/prompts-shared.ts
80462
- function buildFrontmatter(story, ctx, role) {
80463
- const lines = [];
80464
- lines.push(`storyId: ${story.id}`);
80465
- lines.push(`title: "${story.title}"`);
80466
- lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
80467
- lines.push(`modelTier: ${ctx.routing.modelTier}`);
80468
- if (role) {
80469
- lines.push(`role: ${role}`);
80470
- }
80471
- const builtContext = ctx.builtContext;
80472
- const contextTokens = builtContext?.totalTokens ?? 0;
80473
- const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
80474
- lines.push(`contextTokens: ${contextTokens}`);
80475
- lines.push(`promptTokens: ${promptTokens}`);
80476
- if (story.dependencies && story.dependencies.length > 0) {
80477
- lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
80478
- }
80479
- lines.push("contextElements:");
80480
- if (builtContext) {
80481
- for (const element of builtContext.elements) {
80482
- lines.push(` - type: ${element.type}`);
80483
- if (element.storyId) {
80484
- lines.push(` storyId: ${element.storyId}`);
80485
- }
80486
- if (element.filePath) {
80487
- lines.push(` filePath: ${element.filePath}`);
80488
- }
80489
- lines.push(` tokens: ${element.tokens}`);
80490
- }
80491
- }
80492
- if (builtContext?.truncated) {
80493
- lines.push("truncated: true");
80494
- }
80495
- return `${lines.join(`
80496
- `)}
80497
- `;
80498
- }
80499
-
80500
- // src/cli/prompts-tdd.ts
80501
- import { join as join82 } from "path";
80502
- async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
80503
- const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
80504
- TddPromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
80505
- TddPromptBuilder.for("implementer", { variant: "standard" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
80506
- TddPromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build()
80507
- ]);
80508
- const sessions = [
80509
- { role: "test-writer", prompt: testWriterPrompt },
80510
- { role: "implementer", prompt: implementerPrompt },
80511
- { role: "verifier", prompt: verifierPrompt }
80512
- ];
80513
- for (const session of sessions) {
80514
- const frontmatter = buildFrontmatter(story, ctx, session.role);
80515
- const fullOutput = `---
80516
- ${frontmatter}---
80517
-
80518
- ${session.prompt}`;
80519
- if (outputDir) {
80520
- const promptFile = join82(outputDir, `${story.id}.${session.role}.md`);
80521
- await Bun.write(promptFile, fullOutput);
80522
- logger.info("cli", "Written TDD prompt file", {
80523
- storyId: story.id,
80524
- role: session.role,
80525
- promptFile
80526
- });
80527
- } else {
80528
- console.log(`
80529
- ${"=".repeat(80)}`);
80530
- console.log(`Story: ${story.id} \u2014 ${story.title} [${session.role}]`);
80531
- console.log("=".repeat(80));
80532
- console.log(fullOutput);
80533
- }
80534
- }
80535
- if (outputDir && ctx.contextMarkdown) {
80536
- const contextFile = join82(outputDir, `${story.id}.context.md`);
80537
- const frontmatter = buildFrontmatter(story, ctx);
80538
- const contextOutput = `---
80539
- ${frontmatter}---
80540
-
80541
- ${ctx.contextMarkdown}`;
80542
- await Bun.write(contextFile, contextOutput);
80543
- }
80544
- }
80545
- var init_prompts_tdd = __esm(() => {
80546
- init_prompts();
80547
- });
80548
-
80549
- // src/cli/prompts-main.ts
80550
- import { existsSync as existsSync26, mkdirSync as mkdirSync4 } from "fs";
80551
- import { join as join83 } from "path";
80552
- async function promptsCommand(options) {
80553
- const logger = getLogger();
80554
- const { feature, workdir, config: config2, storyId, outputDir } = options;
80555
- const naxDir = join83(workdir, ".nax");
80556
- if (!existsSync26(naxDir)) {
80557
- throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
80558
- }
80559
- const featureDir2 = join83(naxDir, "features", feature);
80560
- const prdPath = join83(featureDir2, "prd.json");
80561
- if (!existsSync26(prdPath)) {
80562
- throw new Error(`Feature "${feature}" not found or missing prd.json`);
80563
- }
80564
- const prd = await loadPRD(prdPath);
80565
- const runtime = _promptsMainDeps.createRuntime(config2, workdir);
80566
- try {
80567
- const stories = storyId ? prd.userStories.filter((s) => s.id === storyId) : prd.userStories;
80568
- if (stories.length === 0) {
80569
- throw new Error(storyId ? `Story "${storyId}" not found in feature "${feature}"` : `No stories found in feature "${feature}"`);
80570
- }
80571
- if (outputDir) {
80572
- mkdirSync4(outputDir, { recursive: true });
80573
- }
80574
- logger.info("cli", "Assembling prompts", {
80575
- feature,
80576
- storyCount: stories.length,
80577
- outputMode: outputDir ? "files" : "stdout"
80578
- });
80579
- const processedStories = [];
80580
- const promptPipeline = [routingStage, constitutionStage, contextStage, promptStage];
80581
- for (const story of stories) {
80582
- const ctx = {
80583
- config: config2,
80584
- rootConfig: config2,
80585
- prd,
80586
- story,
80587
- stories: [story],
80588
- routing: {
80589
- complexity: "simple",
80590
- modelTier: "fast",
80591
- testStrategy: "test-after",
80592
- reasoning: "Placeholder routing"
80593
- },
80594
- projectDir: workdir,
80595
- workdir,
80596
- featureDir: featureDir2,
80597
- hooks: { hooks: {} },
80598
- agentManager: runtime.agentManager,
80599
- sessionManager: runtime.sessionManager,
80600
- runtime,
80601
- abortSignal: runtime.signal
80602
- };
80603
- const result = await runPipeline(promptPipeline, ctx);
80604
- if (!result.success) {
80605
- logger.warn("cli", "Failed to assemble prompt for story", {
80606
- storyId: story.id,
80607
- reason: result.reason
80608
- });
80609
- continue;
80610
- }
80611
- if (ctx.routing.testStrategy === "three-session-tdd") {
80612
- await handleThreeSessionTddPrompts(story, ctx, outputDir, logger);
80613
- processedStories.push(story.id);
80614
- continue;
80615
- }
80616
- if (!ctx.prompt) {
80617
- logger.warn("cli", "No prompt generated for story", {
80618
- storyId: story.id
80619
- });
80620
- continue;
80621
- }
80622
- const frontmatter = buildFrontmatter(story, ctx);
80623
- const fullOutput = `---
80624
- ${frontmatter}---
80625
-
80626
- ${ctx.prompt}`;
80627
- if (outputDir) {
80628
- const promptFile = join83(outputDir, `${story.id}.prompt.md`);
80629
- await Bun.write(promptFile, fullOutput);
80630
- if (ctx.contextMarkdown) {
80631
- const contextFile = join83(outputDir, `${story.id}.context.md`);
80632
- const contextOutput = `---
80633
- ${frontmatter}---
80634
-
80635
- ${ctx.contextMarkdown}`;
80636
- await Bun.write(contextFile, contextOutput);
80637
- }
80638
- logger.info("cli", "Written prompt files", {
80639
- storyId: story.id,
80640
- promptFile
80641
- });
80642
- } else {
80643
- console.log(`
80644
- ${"=".repeat(80)}`);
80645
- console.log(`Story: ${story.id} \u2014 ${story.title}`);
80646
- console.log("=".repeat(80));
80647
- console.log(fullOutput);
80648
- }
80649
- processedStories.push(story.id);
80650
- }
80651
- logger.info("cli", "Prompt assembly complete", {
80652
- processedCount: processedStories.length
80653
- });
80654
- return processedStories;
80655
- } finally {
80656
- await runtime.close();
80657
- }
80658
- }
80659
- var _promptsMainDeps;
80660
- var init_prompts_main = __esm(() => {
80661
- init_logger2();
80662
- init_pipeline();
80663
- init_stages();
80664
- init_prd();
80665
- init_runtime();
80666
- init_prompts_tdd();
80667
- _promptsMainDeps = { createRuntime };
80668
- });
80669
-
80670
- // src/cli/prompts-init.ts
80671
- import { existsSync as existsSync27, mkdirSync as mkdirSync5 } from "fs";
80672
- import { join as join84 } from "path";
80673
- async function promptsInitCommand(options) {
80674
- const { workdir, force = false, autoWireConfig = true } = options;
80675
- const templatesDir = join84(workdir, ".nax", "templates");
80676
- mkdirSync5(templatesDir, { recursive: true });
80677
- const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync27(join84(templatesDir, f)));
80678
- if (existingFiles.length > 0 && !force) {
80679
- _promptsInitDeps.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
80680
- Pass --force to overwrite existing templates.`);
80681
- return [];
80682
- }
80683
- const written = [];
80684
- for (const template of TEMPLATE_ROLES) {
80685
- const filePath = join84(templatesDir, template.file);
80686
- const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
80687
- const content = TEMPLATE_HEADER + roleBody;
80688
- await Bun.write(filePath, content);
80689
- written.push(filePath);
80690
- }
80691
- _promptsInitDeps.log(`[OK] Written ${written.length} template files to nax/templates/:`);
80692
- for (const filePath of written) {
80693
- _promptsInitDeps.log(` - ${filePath.replace(`${workdir}/`, "")}`);
80694
- }
80695
- if (autoWireConfig) {
80696
- await autoWirePromptsConfig(workdir);
80697
- }
80698
- return written;
80699
- }
80700
- async function autoWirePromptsConfig(workdir) {
80701
- const configPath = join84(workdir, "nax.config.json");
80702
- if (!existsSync27(configPath)) {
80703
- const exampleConfig = JSON.stringify({
80704
- prompts: {
80705
- overrides: {
80706
- "test-writer": ".nax/templates/test-writer.md",
80707
- implementer: ".nax/templates/implementer.md",
80708
- verifier: ".nax/templates/verifier.md",
80709
- "single-session": ".nax/templates/single-session.md",
80710
- "tdd-simple": ".nax/templates/tdd-simple.md"
80711
- }
80712
- }
80713
- }, null, 2);
80714
- _promptsInitDeps.log(`
80715
- No nax.config.json found. To activate overrides, create nax/config.json with:
80716
- ${exampleConfig}`);
80717
- return;
80718
- }
80719
- const configFile = Bun.file(configPath);
80720
- const configContent = await configFile.text();
80721
- const config2 = JSON.parse(configContent);
80722
- if (config2.prompts?.overrides && Object.keys(config2.prompts.overrides).length > 0) {
80723
- _promptsInitDeps.log(`[INFO] prompts.overrides already configured in nax.config.json. Skipping auto-wiring.
80724
- ` + " To reset overrides, remove the prompts.overrides section and re-run this command.");
80725
- return;
80726
- }
80727
- const overrides = {
80728
- "test-writer": ".nax/templates/test-writer.md",
80729
- implementer: ".nax/templates/implementer.md",
80730
- verifier: ".nax/templates/verifier.md",
80731
- "single-session": ".nax/templates/single-session.md",
80732
- "tdd-simple": ".nax/templates/tdd-simple.md"
80733
- };
80734
- if (!config2.prompts) {
80735
- config2.prompts = {};
80736
- }
80737
- config2.prompts.overrides = overrides;
80738
- const updatedConfig = formatConfigJson(config2);
80739
- await Bun.write(configPath, updatedConfig);
80740
- _promptsInitDeps.log("[OK] Auto-wired prompts.overrides in nax.config.json");
80741
- }
80742
- function formatConfigJson(config2) {
80743
- const lines = ["{"];
80744
- const keys = Object.keys(config2);
80745
- for (let i = 0;i < keys.length; i++) {
80746
- const key = keys[i];
80747
- const value = config2[key];
80748
- const isLast = i === keys.length - 1;
80749
- if (key === "prompts" && typeof value === "object" && value !== null) {
80750
- const promptsObj = value;
80751
- if (promptsObj.overrides) {
80752
- const overridesJson = JSON.stringify(promptsObj.overrides);
80753
- lines.push(` "${key}": { "overrides": ${overridesJson} }${isLast ? "" : ","}`);
80754
- } else {
80755
- lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
80756
- }
80757
- } else {
80758
- lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
80759
- }
80760
- }
80761
- lines.push("}");
80762
- return lines.join(`
80763
- `);
80764
- }
80765
- var _promptsInitDeps, TEMPLATE_ROLES, TEMPLATE_HEADER = `<!--
80766
- This file controls the role-body section of the nax prompt for this role.
80767
- Edit the content below to customize the task instructions given to the agent.
80768
-
80769
- NON-OVERRIDABLE SECTIONS (always injected by nax, cannot be changed here):
80770
- - Isolation rules (scope, file access boundaries)
80771
- - Story context (acceptance criteria, description, dependencies)
80772
- - Conventions (project coding standards)
80773
-
80774
- To activate overrides, add to your nax/config.json:
80775
- { "prompts": { "overrides": { "<role>": ".nax/templates/<role>.md" } } }
80776
- -->
80777
-
80778
- `;
80779
- var init_prompts_init = __esm(() => {
80780
- init_role_task();
80781
- _promptsInitDeps = {
80782
- log: console.log.bind(console),
80783
- warn: console.warn.bind(console)
80784
- };
80785
- TEMPLATE_ROLES = [
80786
- { file: "test-writer.md", role: "test-writer" },
80787
- { file: "implementer.md", role: "implementer", variant: "standard" },
80788
- { file: "verifier.md", role: "verifier" },
80789
- { file: "single-session.md", role: "single-session" },
80790
- { file: "tdd-simple.md", role: "tdd-simple" }
80791
- ];
80792
- });
80793
-
80794
- // src/cli/prompts-export.ts
80795
- async function exportPromptCommand(options) {
80796
- const { role, out } = options;
80797
- if (!VALID_EXPORT_ROLES.includes(role)) {
80798
- console.error(`[ERROR] Invalid role: "${role}". Valid roles: ${VALID_EXPORT_ROLES.join(", ")}`);
80799
- process.exit(1);
80800
- }
80801
- const stubStory = {
80802
- id: "EXAMPLE",
80803
- title: "Example story",
80804
- description: "Story ID: EXAMPLE. This is a placeholder story used to demonstrate the default prompt.",
80805
- acceptanceCriteria: ["AC-1: Example criterion"],
80806
- tags: [],
80807
- dependencies: [],
80808
- status: "pending",
80809
- passes: false,
80810
- escalations: [],
80811
- attempts: 0
80812
- };
80813
- const prompt = await TddPromptBuilder.for(role).story(stubStory).build();
80814
- if (out) {
80815
- await Bun.write(out, prompt);
80816
- console.log(`[OK] Exported prompt for "${role}" to ${out}`);
80817
- } else {
80818
- console.log(prompt);
80819
- }
80820
- }
80821
- var VALID_EXPORT_ROLES;
80822
- var init_prompts_export = __esm(() => {
80823
- init_prompts();
80824
- VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"];
80825
- });
80826
-
80827
- // src/cli/prompts.ts
80828
- var init_prompts2 = __esm(() => {
80829
- init_prompts_main();
80830
- init_prompts_init();
80831
- init_prompts_export();
80832
- init_prompts_tdd();
80833
- });
80834
-
80835
80731
  // src/cli/setup-analyze.ts
80836
80732
  import { join as join85 } from "path";
80837
80733
  async function detectPackageManager(workdir) {
@@ -111331,7 +111227,7 @@ var init_curator2 = __esm(() => {
111331
111227
 
111332
111228
  // bin/nax.ts
111333
111229
  init_source();
111334
- import { existsSync as existsSync41, mkdirSync as mkdirSync8 } from "fs";
111230
+ import { existsSync as existsSync40, mkdirSync as mkdirSync8 } from "fs";
111335
111231
  import { homedir as homedir3 } from "os";
111336
111232
  import { basename as basename24, join as join115 } from "path";
111337
111233
 
@@ -112130,9 +112026,361 @@ async function runsShowCommand(options) {
112130
112026
  });
112131
112027
  }
112132
112028
  }
112029
+ // src/cli/prompts-main.ts
112030
+ init_logger2();
112031
+ init_pipeline();
112032
+ init_stages();
112033
+ init_prd();
112034
+ init_runtime();
112035
+ import { existsSync as existsSync25, mkdirSync as mkdirSync4 } from "fs";
112036
+ import { join as join83 } from "path";
112037
+
112038
+ // src/cli/prompts-shared.ts
112039
+ function buildFrontmatter(story, ctx, role) {
112040
+ const lines = [];
112041
+ lines.push(`storyId: ${story.id}`);
112042
+ lines.push(`title: "${story.title}"`);
112043
+ lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
112044
+ lines.push(`modelTier: ${ctx.routing.modelTier}`);
112045
+ if (role) {
112046
+ lines.push(`role: ${role}`);
112047
+ }
112048
+ const builtContext = ctx.builtContext;
112049
+ const contextTokens = builtContext?.totalTokens ?? 0;
112050
+ const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
112051
+ lines.push(`contextTokens: ${contextTokens}`);
112052
+ lines.push(`promptTokens: ${promptTokens}`);
112053
+ if (story.dependencies && story.dependencies.length > 0) {
112054
+ lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
112055
+ }
112056
+ lines.push("contextElements:");
112057
+ if (builtContext) {
112058
+ for (const element of builtContext.elements) {
112059
+ lines.push(` - type: ${element.type}`);
112060
+ if (element.storyId) {
112061
+ lines.push(` storyId: ${element.storyId}`);
112062
+ }
112063
+ if (element.filePath) {
112064
+ lines.push(` filePath: ${element.filePath}`);
112065
+ }
112066
+ lines.push(` tokens: ${element.tokens}`);
112067
+ }
112068
+ }
112069
+ if (builtContext?.truncated) {
112070
+ lines.push("truncated: true");
112071
+ }
112072
+ return `${lines.join(`
112073
+ `)}
112074
+ `;
112075
+ }
112133
112076
 
112077
+ // src/cli/prompts-tdd.ts
112078
+ init_prompts();
112079
+ import { join as join82 } from "path";
112080
+ async function handleThreeSessionTddPrompts(story, ctx, outputDir, logger) {
112081
+ const [testWriterPrompt, implementerPrompt, verifierPrompt] = await Promise.all([
112082
+ TddPromptBuilder.for("test-writer", { isolation: "strict" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
112083
+ TddPromptBuilder.for("implementer", { variant: "standard" }).withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build(),
112084
+ TddPromptBuilder.for("verifier").withLoader(ctx.workdir, ctx.config).story(story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(ctx.config.quality?.commands?.test).build()
112085
+ ]);
112086
+ const sessions = [
112087
+ { role: "test-writer", prompt: testWriterPrompt },
112088
+ { role: "implementer", prompt: implementerPrompt },
112089
+ { role: "verifier", prompt: verifierPrompt }
112090
+ ];
112091
+ for (const session of sessions) {
112092
+ const frontmatter = buildFrontmatter(story, ctx, session.role);
112093
+ const fullOutput = `---
112094
+ ${frontmatter}---
112095
+
112096
+ ${session.prompt}`;
112097
+ if (outputDir) {
112098
+ const promptFile = join82(outputDir, `${story.id}.${session.role}.md`);
112099
+ await Bun.write(promptFile, fullOutput);
112100
+ logger.info("cli", "Written TDD prompt file", {
112101
+ storyId: story.id,
112102
+ role: session.role,
112103
+ promptFile
112104
+ });
112105
+ } else {
112106
+ console.log(`
112107
+ ${"=".repeat(80)}`);
112108
+ console.log(`Story: ${story.id} \u2014 ${story.title} [${session.role}]`);
112109
+ console.log("=".repeat(80));
112110
+ console.log(fullOutput);
112111
+ }
112112
+ }
112113
+ if (outputDir && ctx.contextMarkdown) {
112114
+ const contextFile = join82(outputDir, `${story.id}.context.md`);
112115
+ const frontmatter = buildFrontmatter(story, ctx);
112116
+ const contextOutput = `---
112117
+ ${frontmatter}---
112118
+
112119
+ ${ctx.contextMarkdown}`;
112120
+ await Bun.write(contextFile, contextOutput);
112121
+ }
112122
+ }
112123
+
112124
+ // src/cli/prompts-main.ts
112125
+ var _promptsMainDeps = { createRuntime };
112126
+ async function promptsCommand(options) {
112127
+ const logger = getLogger();
112128
+ const { feature, workdir, config: config2, storyId, outputDir } = options;
112129
+ const naxDir = join83(workdir, ".nax");
112130
+ if (!existsSync25(naxDir)) {
112131
+ throw new Error(`.nax directory not found. Run 'nax init' first in ${workdir}`);
112132
+ }
112133
+ const featureDir2 = join83(naxDir, "features", feature);
112134
+ const prdPath = join83(featureDir2, "prd.json");
112135
+ if (!existsSync25(prdPath)) {
112136
+ throw new Error(`Feature "${feature}" not found or missing prd.json`);
112137
+ }
112138
+ const prd = await loadPRD(prdPath);
112139
+ const runtime = _promptsMainDeps.createRuntime(config2, workdir);
112140
+ try {
112141
+ const stories = storyId ? prd.userStories.filter((s) => s.id === storyId) : prd.userStories;
112142
+ if (stories.length === 0) {
112143
+ throw new Error(storyId ? `Story "${storyId}" not found in feature "${feature}"` : `No stories found in feature "${feature}"`);
112144
+ }
112145
+ if (outputDir) {
112146
+ mkdirSync4(outputDir, { recursive: true });
112147
+ }
112148
+ logger.info("cli", "Assembling prompts", {
112149
+ feature,
112150
+ storyCount: stories.length,
112151
+ outputMode: outputDir ? "files" : "stdout"
112152
+ });
112153
+ const processedStories = [];
112154
+ const promptPipeline = [routingStage, constitutionStage, contextStage, promptStage];
112155
+ for (const story of stories) {
112156
+ const ctx = {
112157
+ config: config2,
112158
+ rootConfig: config2,
112159
+ prd,
112160
+ story,
112161
+ stories: [story],
112162
+ routing: {
112163
+ complexity: "simple",
112164
+ modelTier: "fast",
112165
+ testStrategy: "test-after",
112166
+ reasoning: "Placeholder routing"
112167
+ },
112168
+ projectDir: workdir,
112169
+ workdir,
112170
+ featureDir: featureDir2,
112171
+ hooks: { hooks: {} },
112172
+ agentManager: runtime.agentManager,
112173
+ sessionManager: runtime.sessionManager,
112174
+ runtime,
112175
+ abortSignal: runtime.signal
112176
+ };
112177
+ const result = await runPipeline(promptPipeline, ctx);
112178
+ if (!result.success) {
112179
+ logger.warn("cli", "Failed to assemble prompt for story", {
112180
+ storyId: story.id,
112181
+ reason: result.reason
112182
+ });
112183
+ continue;
112184
+ }
112185
+ if (ctx.routing.testStrategy === "three-session-tdd") {
112186
+ await handleThreeSessionTddPrompts(story, ctx, outputDir, logger);
112187
+ processedStories.push(story.id);
112188
+ continue;
112189
+ }
112190
+ if (!ctx.prompt) {
112191
+ logger.warn("cli", "No prompt generated for story", {
112192
+ storyId: story.id
112193
+ });
112194
+ continue;
112195
+ }
112196
+ const frontmatter = buildFrontmatter(story, ctx);
112197
+ const fullOutput = `---
112198
+ ${frontmatter}---
112199
+
112200
+ ${ctx.prompt}`;
112201
+ if (outputDir) {
112202
+ const promptFile = join83(outputDir, `${story.id}.prompt.md`);
112203
+ await Bun.write(promptFile, fullOutput);
112204
+ if (ctx.contextMarkdown) {
112205
+ const contextFile = join83(outputDir, `${story.id}.context.md`);
112206
+ const contextOutput = `---
112207
+ ${frontmatter}---
112208
+
112209
+ ${ctx.contextMarkdown}`;
112210
+ await Bun.write(contextFile, contextOutput);
112211
+ }
112212
+ logger.info("cli", "Written prompt files", {
112213
+ storyId: story.id,
112214
+ promptFile
112215
+ });
112216
+ } else {
112217
+ console.log(`
112218
+ ${"=".repeat(80)}`);
112219
+ console.log(`Story: ${story.id} \u2014 ${story.title}`);
112220
+ console.log("=".repeat(80));
112221
+ console.log(fullOutput);
112222
+ }
112223
+ processedStories.push(story.id);
112224
+ }
112225
+ logger.info("cli", "Prompt assembly complete", {
112226
+ processedCount: processedStories.length
112227
+ });
112228
+ return processedStories;
112229
+ } finally {
112230
+ await runtime.close();
112231
+ }
112232
+ }
112233
+ // src/cli/prompts-init.ts
112234
+ init_role_task();
112235
+ import { existsSync as existsSync26, mkdirSync as mkdirSync5 } from "fs";
112236
+ import { join as join84 } from "path";
112237
+ var _promptsInitDeps = {
112238
+ log: console.log.bind(console),
112239
+ warn: console.warn.bind(console)
112240
+ };
112241
+ var TEMPLATE_ROLES = [
112242
+ { file: "test-writer.md", role: "test-writer" },
112243
+ { file: "implementer.md", role: "implementer", variant: "standard" },
112244
+ { file: "verifier.md", role: "verifier" },
112245
+ { file: "single-session.md", role: "single-session" },
112246
+ { file: "tdd-simple.md", role: "tdd-simple" }
112247
+ ];
112248
+ var TEMPLATE_HEADER = `<!--
112249
+ This file controls the role-body section of the nax prompt for this role.
112250
+ Edit the content below to customize the task instructions given to the agent.
112251
+
112252
+ NON-OVERRIDABLE SECTIONS (always injected by nax, cannot be changed here):
112253
+ - Isolation rules (scope, file access boundaries)
112254
+ - Story context (acceptance criteria, description, dependencies)
112255
+ - Conventions (project coding standards)
112256
+
112257
+ To activate overrides, add to your nax/config.json:
112258
+ { "prompts": { "overrides": { "<role>": ".nax/templates/<role>.md" } } }
112259
+ -->
112260
+
112261
+ `;
112262
+ async function promptsInitCommand(options) {
112263
+ const { workdir, force = false, autoWireConfig = true } = options;
112264
+ const templatesDir = join84(workdir, ".nax", "templates");
112265
+ mkdirSync5(templatesDir, { recursive: true });
112266
+ const existingFiles = TEMPLATE_ROLES.map((t) => t.file).filter((f) => existsSync26(join84(templatesDir, f)));
112267
+ if (existingFiles.length > 0 && !force) {
112268
+ _promptsInitDeps.warn(`[WARN] nax/templates/ already contains files: ${existingFiles.join(", ")}. No files overwritten.
112269
+ Pass --force to overwrite existing templates.`);
112270
+ return [];
112271
+ }
112272
+ const written = [];
112273
+ for (const template of TEMPLATE_ROLES) {
112274
+ const filePath = join84(templatesDir, template.file);
112275
+ const roleBody = template.role === "implementer" ? buildRoleTaskSection(template.role, template.variant) : buildRoleTaskSection(template.role);
112276
+ const content = TEMPLATE_HEADER + roleBody;
112277
+ await Bun.write(filePath, content);
112278
+ written.push(filePath);
112279
+ }
112280
+ _promptsInitDeps.log(`[OK] Written ${written.length} template files to nax/templates/:`);
112281
+ for (const filePath of written) {
112282
+ _promptsInitDeps.log(` - ${filePath.replace(`${workdir}/`, "")}`);
112283
+ }
112284
+ if (autoWireConfig) {
112285
+ await autoWirePromptsConfig(workdir);
112286
+ }
112287
+ return written;
112288
+ }
112289
+ async function autoWirePromptsConfig(workdir) {
112290
+ const configPath = join84(workdir, "nax.config.json");
112291
+ if (!existsSync26(configPath)) {
112292
+ const exampleConfig = JSON.stringify({
112293
+ prompts: {
112294
+ overrides: {
112295
+ "test-writer": ".nax/templates/test-writer.md",
112296
+ implementer: ".nax/templates/implementer.md",
112297
+ verifier: ".nax/templates/verifier.md",
112298
+ "single-session": ".nax/templates/single-session.md",
112299
+ "tdd-simple": ".nax/templates/tdd-simple.md"
112300
+ }
112301
+ }
112302
+ }, null, 2);
112303
+ _promptsInitDeps.log(`
112304
+ No nax.config.json found. To activate overrides, create nax/config.json with:
112305
+ ${exampleConfig}`);
112306
+ return;
112307
+ }
112308
+ const configFile = Bun.file(configPath);
112309
+ const configContent = await configFile.text();
112310
+ const config2 = JSON.parse(configContent);
112311
+ if (config2.prompts?.overrides && Object.keys(config2.prompts.overrides).length > 0) {
112312
+ _promptsInitDeps.log(`[INFO] prompts.overrides already configured in nax.config.json. Skipping auto-wiring.
112313
+ ` + " To reset overrides, remove the prompts.overrides section and re-run this command.");
112314
+ return;
112315
+ }
112316
+ const overrides = {
112317
+ "test-writer": ".nax/templates/test-writer.md",
112318
+ implementer: ".nax/templates/implementer.md",
112319
+ verifier: ".nax/templates/verifier.md",
112320
+ "single-session": ".nax/templates/single-session.md",
112321
+ "tdd-simple": ".nax/templates/tdd-simple.md"
112322
+ };
112323
+ if (!config2.prompts) {
112324
+ config2.prompts = {};
112325
+ }
112326
+ config2.prompts.overrides = overrides;
112327
+ const updatedConfig = formatConfigJson(config2);
112328
+ await Bun.write(configPath, updatedConfig);
112329
+ _promptsInitDeps.log("[OK] Auto-wired prompts.overrides in nax.config.json");
112330
+ }
112331
+ function formatConfigJson(config2) {
112332
+ const lines = ["{"];
112333
+ const keys = Object.keys(config2);
112334
+ for (let i = 0;i < keys.length; i++) {
112335
+ const key = keys[i];
112336
+ const value = config2[key];
112337
+ const isLast = i === keys.length - 1;
112338
+ if (key === "prompts" && typeof value === "object" && value !== null) {
112339
+ const promptsObj = value;
112340
+ if (promptsObj.overrides) {
112341
+ const overridesJson = JSON.stringify(promptsObj.overrides);
112342
+ lines.push(` "${key}": { "overrides": ${overridesJson} }${isLast ? "" : ","}`);
112343
+ } else {
112344
+ lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
112345
+ }
112346
+ } else {
112347
+ lines.push(` "${key}": ${JSON.stringify(value)}${isLast ? "" : ","}`);
112348
+ }
112349
+ }
112350
+ lines.push("}");
112351
+ return lines.join(`
112352
+ `);
112353
+ }
112354
+ // src/cli/prompts-export.ts
112355
+ init_prompts();
112356
+ var VALID_EXPORT_ROLES = ["test-writer", "implementer", "verifier", "single-session", "tdd-simple"];
112357
+ async function exportPromptCommand(options) {
112358
+ const { role, out } = options;
112359
+ if (!VALID_EXPORT_ROLES.includes(role)) {
112360
+ console.error(`[ERROR] Invalid role: "${role}". Valid roles: ${VALID_EXPORT_ROLES.join(", ")}`);
112361
+ process.exit(1);
112362
+ }
112363
+ const stubStory = {
112364
+ id: "EXAMPLE",
112365
+ title: "Example story",
112366
+ description: "Story ID: EXAMPLE. This is a placeholder story used to demonstrate the default prompt.",
112367
+ acceptanceCriteria: ["AC-1: Example criterion"],
112368
+ tags: [],
112369
+ dependencies: [],
112370
+ status: "pending",
112371
+ passes: false,
112372
+ escalations: [],
112373
+ attempts: 0
112374
+ };
112375
+ const prompt = await TddPromptBuilder.for(role).story(stubStory).build();
112376
+ if (out) {
112377
+ await Bun.write(out, prompt);
112378
+ console.log(`[OK] Exported prompt for "${role}" to ${out}`);
112379
+ } else {
112380
+ console.log(prompt);
112381
+ }
112382
+ }
112134
112383
  // src/cli/index.ts
112135
- init_prompts2();
112136
112384
  init_init2();
112137
112385
  init_setup();
112138
112386
  init_setup_write();
@@ -112207,7 +112455,7 @@ function pad(str, width) {
112207
112455
  init_source();
112208
112456
  init_loader();
112209
112457
  init_generator2();
112210
- import { existsSync as existsSync28 } from "fs";
112458
+ import { existsSync as existsSync27 } from "fs";
112211
112459
  import { join as join90 } from "path";
112212
112460
  var VALID_AGENTS = ["claude", "codex", "opencode", "cursor", "windsurf", "aider", "gemini"];
112213
112461
  async function generateCommand(options) {
@@ -112274,7 +112522,7 @@ async function generateCommand(options) {
112274
112522
  const contextPath = options.context ? join90(workdir, options.context) : join90(workdir, ".nax/context.md");
112275
112523
  const outputDir = options.output ? join90(workdir, options.output) : workdir;
112276
112524
  const autoInject = !options.noAutoInject;
112277
- if (!existsSync28(contextPath)) {
112525
+ if (!existsSync27(contextPath)) {
112278
112526
  console.error(source_default.red(`\u2717 Context file not found: ${contextPath}`));
112279
112527
  console.error(source_default.yellow(" Create .nax/context.md first, or run `nax init` to scaffold it."));
112280
112528
  process.exit(1);
@@ -112377,7 +112625,7 @@ async function generateCommand(options) {
112377
112625
  }
112378
112626
  // src/cli/config-display.ts
112379
112627
  init_loader();
112380
- import { existsSync as existsSync30 } from "fs";
112628
+ import { existsSync as existsSync29 } from "fs";
112381
112629
  import { join as join93 } from "path";
112382
112630
 
112383
112631
  // src/cli/config-descriptions.ts
@@ -112442,6 +112690,9 @@ var FIELD_DESCRIPTIONS = {
112442
112690
  "execution.mutationCheck.maxMutants": "Mutants tested per story (default: 3). Candidates are gathered across all changed files and sampled evenly, so raising this widens coverage at a cost of one scoped test run per mutant.",
112443
112691
  "execution.mutationCheck.timeoutSeconds": "Per-mutant scoped test-run timeout in seconds (default: 60). Worst-case added wall clock per story is maxMutants x timeoutSeconds.",
112444
112692
  "execution.storyIsolation": 'Story isolation mode. "shared" (default): all stories run on the main branch. "worktree": each story runs in an isolated git worktree (.nax-wt/<storyId>/); passed stories merge into main, failed commits never reach main.',
112693
+ "execution.worktreeDependencies": 'How a story worktree gets its dependencies (only applies when storyIsolation="worktree"). "off" (default): install nothing. Because worktrees live at <projectRoot>/.nax-wt/<storyId>/ \u2014 inside the project root \u2014 Node/Bun resolution walks up to the root node_modules, so JS/TS repos need nothing here. "provision": run execution.worktreeDependencies.setupCommand from the worktree root before the story starts (required for ecosystems with no upward resolution: a Python venv, bundler, composer).',
112694
+ "execution.worktreeDependencies.mode": 'Dependency preparation strategy for story worktrees: "off" (default, install nothing) or "provision" (run setupCommand first).',
112695
+ "execution.worktreeDependencies.setupCommand": 'Install command run from the worktree root before a story starts, e.g. "bun install --frozen-lockfile". Rejected unless mode is "provision".',
112445
112696
  quality: "Quality gate configuration",
112446
112697
  "quality.commands": "Custom quality commands",
112447
112698
  "quality.commands.typecheck": "Custom typecheck command",
@@ -112625,10 +112876,10 @@ function deepEqual(a, b) {
112625
112876
  init_defaults();
112626
112877
  init_loader();
112627
112878
  init_merger();
112628
- import { existsSync as existsSync29 } from "fs";
112879
+ import { existsSync as existsSync28 } from "fs";
112629
112880
  import { join as join91 } from "path";
112630
112881
  async function loadConfigFile(path32) {
112631
- if (!existsSync29(path32))
112882
+ if (!existsSync28(path32))
112632
112883
  return null;
112633
112884
  try {
112634
112885
  return await Bun.file(path32).json();
@@ -112825,7 +113076,7 @@ function determineConfigSources() {
112825
113076
  };
112826
113077
  }
112827
113078
  function fileExists(path32) {
112828
- return existsSync30(path32);
113079
+ return existsSync29(path32);
112829
113080
  }
112830
113081
  function displayConfigWithDescriptions(obj, path32, sources, indent = 0) {
112831
113082
  const indentStr = " ".repeat(indent);
@@ -113070,7 +113321,7 @@ function pad2(str, width) {
113070
113321
  init_source();
113071
113322
  init_engine();
113072
113323
  init_effectiveness();
113073
- import { existsSync as existsSync31 } from "fs";
113324
+ import { existsSync as existsSync30 } from "fs";
113074
113325
 
113075
113326
  // src/context/engine/effectiveness-eval.ts
113076
113327
  init_errors();
@@ -113320,7 +113571,7 @@ async function contextInspectCommand(options) {
113320
113571
  }
113321
113572
  }
113322
113573
  var _effectivenessEvalDeps2 = {
113323
- existsSync: existsSync31,
113574
+ existsSync: existsSync30,
113324
113575
  readLabels: async (path32) => {
113325
113576
  return await Bun.file(path32).text();
113326
113577
  },
@@ -113409,11 +113660,11 @@ init_config();
113409
113660
  init_fragments();
113410
113661
  init_logger2();
113411
113662
  init_source();
113412
- import { existsSync as existsSync32 } from "fs";
113663
+ import { existsSync as existsSync31 } from "fs";
113413
113664
  import { join as join94 } from "path";
113414
113665
  var _contextFragmentsDeps = {
113415
113666
  loadPRD: async (path32) => {
113416
- if (!existsSync32(path32)) {
113667
+ if (!existsSync31(path32)) {
113417
113668
  return { kind: "missing" };
113418
113669
  }
113419
113670
  try {
@@ -114066,7 +114317,7 @@ async function resolveRunProfileOverride(opts) {
114066
114317
  // src/cli/features-resolve.ts
114067
114318
  init_config();
114068
114319
  init_test_runners();
114069
- import { existsSync as existsSync34, readdirSync as readdirSync6 } from "fs";
114320
+ import { existsSync as existsSync33, readdirSync as readdirSync6 } from "fs";
114070
114321
  import { join as join99, relative as relative17 } from "path";
114071
114322
 
114072
114323
  // src/cli/features-acceptance.ts
@@ -114074,7 +114325,7 @@ init_acceptance2();
114074
114325
  init_config();
114075
114326
  init_logger2();
114076
114327
  init_prd();
114077
- import { existsSync as existsSync33 } from "fs";
114328
+ import { existsSync as existsSync32 } from "fs";
114078
114329
  import { join as join98, relative as relative16 } from "path";
114079
114330
  async function resolveFeatureAcceptance(featureName, workdir) {
114080
114331
  let enabled = true;
@@ -114090,7 +114341,7 @@ async function resolveFeatureAcceptance(featureName, workdir) {
114090
114341
  return { status: "disabled", enabled: false, groups: [] };
114091
114342
  }
114092
114343
  const prdPath = join98(naxDir, "features", featureName, "prd.json");
114093
- if (!existsSync33(prdPath)) {
114344
+ if (!existsSync32(prdPath)) {
114094
114345
  return { status: "no-prd", enabled, groups: [] };
114095
114346
  }
114096
114347
  const prd = await loadPRD(prdPath);
@@ -114134,7 +114385,7 @@ async function resolveTestPatterns(workdir) {
114134
114385
  }
114135
114386
  }
114136
114387
  async function isNonEmptyFile(absolutePath) {
114137
- if (!existsSync34(absolutePath))
114388
+ if (!existsSync33(absolutePath))
114138
114389
  return false;
114139
114390
  const content = await Bun.file(absolutePath).text();
114140
114391
  return content.trim().length > 0;
@@ -114159,7 +114410,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
114159
114410
  return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
114160
114411
  }
114161
114412
  const docsSpecsDir = join99(repoRoot, "docs", "specs");
114162
- if (existsSync34(docsSpecsDir)) {
114413
+ if (existsSync33(docsSpecsDir)) {
114163
114414
  const glob = new Bun.Glob(`*${name}*.md`);
114164
114415
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
114165
114416
  const abs = join99(docsSpecsDir, match);
@@ -114175,20 +114426,20 @@ async function searchSpecSource(naxDir, repoRoot, name) {
114175
114426
  const prdRel = relative17(repoRoot, prdAbs);
114176
114427
  if (!checked.includes(prdRel))
114177
114428
  checked.push(prdRel);
114178
- if (existsSync34(prdAbs)) {
114429
+ if (existsSync33(prdAbs)) {
114179
114430
  return { source: { kind: "prd", path: prdRel }, checked };
114180
114431
  }
114181
114432
  return { source: null, checked };
114182
114433
  }
114183
114434
  function discoverCandidates(naxDir) {
114184
114435
  const featuresDir2 = join99(naxDir, "features");
114185
- if (!existsSync34(featuresDir2))
114436
+ if (!existsSync33(featuresDir2))
114186
114437
  return [];
114187
114438
  return readdirSync6(featuresDir2, { withFileTypes: true }).filter((e) => {
114188
114439
  if (!e.isDirectory())
114189
114440
  return false;
114190
114441
  const dir = join99(featuresDir2, e.name);
114191
- return existsSync34(join99(dir, "prd.json")) || existsSync34(join99(dir, "spec.md"));
114442
+ return existsSync33(join99(dir, "prd.json")) || existsSync33(join99(dir, "spec.md"));
114192
114443
  }).map((e) => e.name).sort();
114193
114444
  }
114194
114445
  async function resolveFeatureSpec(name, workdir) {
@@ -114202,7 +114453,7 @@ async function resolveFeatureSpec(name, workdir) {
114202
114453
  const repoRoot = join99(naxDir, "..");
114203
114454
  if (name !== undefined && (name.startsWith("./") || name.startsWith("/") || name.endsWith(".md"))) {
114204
114455
  const abs = name.startsWith("/") ? name : join99(workdir, name);
114205
- if (!existsSync34(abs)) {
114456
+ if (!existsSync33(abs)) {
114206
114457
  return {
114207
114458
  status: "missing",
114208
114459
  featureName: null,
@@ -114242,7 +114493,7 @@ async function resolveFeatureSpec(name, workdir) {
114242
114493
  };
114243
114494
  }
114244
114495
  const featureDir2 = join99(naxDir, "features", name);
114245
- if (existsSync34(featureDir2)) {
114496
+ if (existsSync33(featureDir2)) {
114246
114497
  return {
114247
114498
  status: "missing",
114248
114499
  featureName: name,
@@ -114624,7 +114875,7 @@ async function detectCommand(options) {
114624
114875
 
114625
114876
  // src/commands/logs.ts
114626
114877
  init_common();
114627
- import { existsSync as existsSync36 } from "fs";
114878
+ import { existsSync as existsSync35 } from "fs";
114628
114879
  import { join as join104 } from "path";
114629
114880
 
114630
114881
  // src/commands/logs-formatter.ts
@@ -114635,7 +114886,7 @@ import { join as join103 } from "path";
114635
114886
 
114636
114887
  // src/commands/logs-reader.ts
114637
114888
  init_paths3();
114638
- import { existsSync as existsSync35, readdirSync as readdirSync7 } from "fs";
114889
+ import { existsSync as existsSync34, readdirSync as readdirSync7 } from "fs";
114639
114890
  import { readdir as readdir6 } from "fs/promises";
114640
114891
  import { join as join102 } from "path";
114641
114892
  var _logsReaderDeps = {
@@ -114675,7 +114926,7 @@ async function resolveRunFileFromRegistry(runId) {
114675
114926
  if (!matched) {
114676
114927
  throw new Error(`Run not found in registry: ${runId}`);
114677
114928
  }
114678
- if (!existsSync35(matched.eventsDir)) {
114929
+ if (!existsSync34(matched.eventsDir)) {
114679
114930
  console.log(`Log directory unavailable for run: ${runId}`);
114680
114931
  return null;
114681
114932
  }
@@ -114896,7 +115147,7 @@ async function logsCommand(options) {
114896
115147
  const featureName = resolveSingleFeature(naxDir, "pass -r <runId> (see `nax runs list`)");
114897
115148
  const featureDir2 = join104(naxDir, "features", featureName);
114898
115149
  const runsDir = join104(featureDir2, "runs");
114899
- if (!existsSync36(runsDir)) {
115150
+ if (!existsSync35(runsDir)) {
114900
115151
  throw new Error(`No runs directory found for feature: ${featureName}`);
114901
115152
  }
114902
115153
  if (options.list) {
@@ -114920,7 +115171,7 @@ init_config();
114920
115171
  init_prd();
114921
115172
  init_precheck();
114922
115173
  init_common();
114923
- import { existsSync as existsSync37 } from "fs";
115174
+ import { existsSync as existsSync36 } from "fs";
114924
115175
  import { join as join105 } from "path";
114925
115176
  async function precheckCommand(options) {
114926
115177
  const resolved = resolveProject2({
@@ -114945,11 +115196,11 @@ async function precheckCommand(options) {
114945
115196
  }
114946
115197
  const featureDir2 = join105(naxDir, "features", featureName);
114947
115198
  const prdPath = join105(featureDir2, "prd.json");
114948
- if (!existsSync37(featureDir2)) {
115199
+ if (!existsSync36(featureDir2)) {
114949
115200
  console.error(source_default.red(`Feature not found: ${featureName}`));
114950
115201
  process.exit(1);
114951
115202
  }
114952
- if (!existsSync37(prdPath)) {
115203
+ if (!existsSync36(prdPath)) {
114953
115204
  console.error(source_default.red(`Missing prd.json for feature: ${featureName}`));
114954
115205
  console.error(source_default.dim(`Run: nax plan -f ${featureName} --from spec.md --auto`));
114955
115206
  process.exit(EXIT_CODES.INVALID_PRD);
@@ -114966,7 +115217,7 @@ async function precheckCommand(options) {
114966
115217
  // src/commands/replay.ts
114967
115218
  init_errors();
114968
115219
  init_metrics();
114969
- import { existsSync as existsSync38 } from "fs";
115220
+ import { existsSync as existsSync37 } from "fs";
114970
115221
  import { dirname as dirname19 } from "path";
114971
115222
 
114972
115223
  // src/replay/discovery.ts
@@ -115253,7 +115504,7 @@ function renderReport(timeline, options = {}) {
115253
115504
 
115254
115505
  // src/commands/replay.ts
115255
115506
  async function readJsonlLenient(path32) {
115256
- if (!existsSync38(path32))
115507
+ if (!existsSync37(path32))
115257
115508
  return [];
115258
115509
  const content = await Bun.file(path32).text();
115259
115510
  const lines = content.split(`
@@ -115270,7 +115521,7 @@ async function readJsonlLenient(path32) {
115270
115521
  return entries;
115271
115522
  }
115272
115523
  async function readJsonOrUndefined(path32) {
115273
- if (!existsSync38(path32))
115524
+ if (!existsSync37(path32))
115274
115525
  return;
115275
115526
  try {
115276
115527
  return await Bun.file(path32).json();
@@ -115362,12 +115613,12 @@ init_paths();
115362
115613
  init_errors();
115363
115614
  init_checkpoint();
115364
115615
  init_runtime();
115365
- import { existsSync as existsSync39 } from "fs";
115616
+ import { existsSync as existsSync38 } from "fs";
115366
115617
  import { basename as basename21, join as join107 } from "path";
115367
115618
  async function defaultCheckpointExists(featureDir2) {
115368
- if (!featureDir2 || !existsSync39(featureDir2))
115619
+ if (!featureDir2 || !existsSync38(featureDir2))
115369
115620
  return false;
115370
- return existsSync39(join107(featureDir2, "checkpoint.jsonl"));
115621
+ return existsSync38(join107(featureDir2, "checkpoint.jsonl"));
115371
115622
  }
115372
115623
  async function defaultLoadCheckpoints(featureDir2) {
115373
115624
  return loadCheckpoints(featureDir2);
@@ -115417,7 +115668,7 @@ function registerResumeCommand(program2) {
115417
115668
  const { findProjectDir: findProjectDir2 } = await Promise.resolve().then(() => (init_config(), exports_config));
115418
115669
  const { run: run2 } = await Promise.resolve().then(() => (init_execution2(), exports_execution));
115419
115670
  const { applyResumeModeDeps: applyResumeModeDeps2 } = await Promise.resolve().then(() => (init_checkpoint(), exports_checkpoint));
115420
- const { existsSync: existsSync40, mkdirSync: mkdirSync8 } = await import("fs");
115671
+ const { existsSync: existsSync39, mkdirSync: mkdirSync8 } = await import("fs");
115421
115672
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
115422
115673
  const { loadPRD: loadPRD2 } = await Promise.resolve().then(() => (init_prd(), exports_prd));
115423
115674
  const { loadHooksConfig: loadHooksConfig2 } = await Promise.resolve().then(() => (init_hooks(), exports_hooks));
@@ -115434,7 +115685,7 @@ function registerResumeCommand(program2) {
115434
115685
  runInvocation: async (feature, opts) => {
115435
115686
  const config2 = await loadConfig2(naxDir ?? undefined);
115436
115687
  const prdPath = join107(opts.featureDir ?? "", "prd.json");
115437
- if (!existsSync40(prdPath)) {
115688
+ if (!existsSync39(prdPath)) {
115438
115689
  process.stderr.write(`Feature "${feature}" not found or missing prd.json
115439
115690
  `);
115440
115691
  return 1;
@@ -123569,151 +123820,18 @@ program2.command("init").description("Initialize nax in the current project").op
123569
123820
  console.error(source_default.red(`Invalid directory: ${err.message}`));
123570
123821
  process.exit(1);
123571
123822
  }
123572
- if (options.package) {
123573
- const { initPackage: initPkg } = await Promise.resolve().then(() => (init_init_context(), exports_init_context));
123574
- try {
123575
- await initPkg(workdir, options.package, options.force);
123576
- console.log(source_default.green(`
123577
- [OK] Package scaffold created.`));
123578
- console.log(source_default.dim(` Created: ${options.package}/nax/context.md`));
123579
- console.log(source_default.dim(`
123580
- Next: nax generate --package ${options.package}`));
123581
- } catch (err) {
123582
- console.error(source_default.red(`Error: ${err.message}`));
123583
- process.exit(1);
123584
- }
123585
- return;
123586
- }
123587
- const naxDir = join115(workdir, ".nax");
123588
- if (existsSync41(naxDir) && !options.force) {
123589
- console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
123590
- return;
123591
- }
123592
- if (options.name) {
123593
- const { validateProjectName: validateProjectName2, checkInitCollision: checkInitCollision2 } = await Promise.resolve().then(() => (init_init2(), exports_init));
123594
- const nameValidation = validateProjectName2(options.name);
123595
- if (!nameValidation.valid) {
123596
- console.error(source_default.red(`Invalid project name "${options.name}": ${nameValidation.error}`));
123597
- process.exit(1);
123598
- }
123599
- if (!options.force) {
123600
- const collision = await checkInitCollision2(options.name, workdir, null);
123601
- if (collision.collision && collision.existing) {
123602
- console.error(source_default.red([
123603
- `Project name collision: "${options.name}"`,
123604
- ` This project: ${workdir}`,
123605
- ` Already in use: ${collision.existing.workdir} (last run: ${collision.existing.lastSeen})`,
123606
- " Resolve:",
123607
- " 1. Rename: choose a different --name",
123608
- ` 2. Reclaim: nax migrate --reclaim ${options.name}`,
123609
- ` 3. Merge: nax migrate --merge ${options.name}`
123610
- ].join(`
123611
- `)));
123612
- process.exit(1);
123613
- }
123614
- }
123615
- }
123616
- mkdirSync8(join115(naxDir, "features"), { recursive: true });
123617
- mkdirSync8(join115(naxDir, "hooks"), { recursive: true });
123618
- const initConfig = options.name ? { ...DEFAULT_CONFIG, name: options.name } : DEFAULT_CONFIG;
123619
- await Bun.write(join115(naxDir, "config.json"), JSON.stringify(initConfig, null, 2));
123620
- await Bun.write(join115(naxDir, "hooks.json"), JSON.stringify({
123621
- hooks: {
123622
- "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
123623
- "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
123624
- "on-pause": { command: 'echo "nax paused: $NAX_REASON"', enabled: false },
123625
- "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
123626
- }
123627
- }, null, 2));
123628
- await Bun.write(join115(naxDir, ".gitignore"), `# nax temp files
123629
- *.tmp
123630
- .paused.json
123631
- .nax-verifier-verdict.json
123632
- `);
123633
- await Bun.write(join115(naxDir, "context.md"), `# Project Context
123634
-
123635
- This document defines coding standards, architectural decisions, and forbidden patterns for this project.
123636
- Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
123637
-
123638
- > Project metadata (dependencies, commands) is auto-injected by \`nax generate\`.
123639
-
123640
- ## Coding Standards
123641
-
123642
- - Follow the project's existing code style and conventions
123643
- - Write clear, self-documenting code with meaningful names
123644
- - Keep functions small and focused (single responsibility)
123645
- - Prefer immutability over mutation
123646
- - Use consistent formatting throughout the codebase
123647
-
123648
- ## Testing Requirements
123649
-
123650
- - All new code must include tests
123651
- - Tests should cover happy paths, edge cases, and error conditions
123652
- - Aim for high test coverage (80%+ recommended)
123653
- - Tests must pass before marking a story as complete
123654
- - Before writing tests, read existing test files to understand what is already covered
123655
- - Do not duplicate test coverage that prior stories already wrote
123656
- - Focus on testing NEW behavior introduced by this story
123657
-
123658
- ## Architecture Rules
123659
-
123660
- - Follow the project's existing architecture patterns
123661
- - Each module should have a clear, single purpose
123662
- - Avoid tight coupling between modules
123663
- - Use dependency injection where appropriate
123664
- - Document architectural decisions in comments or docs
123665
-
123666
- ## Forbidden Patterns
123667
-
123668
- - No hardcoded secrets, API keys, or credentials
123669
- - No console.log in production code (use proper logging)
123670
- - No \`any\` types in TypeScript (use proper typing)
123671
- - No commented-out code (use version control instead)
123672
- - No large files (split into smaller, focused modules)
123673
-
123674
- ## Commit Standards
123675
-
123676
- - Write clear, descriptive commit messages
123677
- - Follow conventional commits format (feat:, fix:, refactor:, etc.)
123678
- - Commit early and often with atomic changes
123679
- - Reference story IDs in commit messages
123680
-
123681
- ## Documentation
123682
-
123683
- - Add JSDoc comments for public APIs
123684
- - Update README when adding new features
123685
- - Document complex algorithms or business logic
123686
- - Keep documentation up-to-date with code changes
123687
-
123688
- ---
123689
-
123690
- **Note:** Customize this file to match your project's specific needs.
123691
- `);
123823
+ const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init2(), exports_init));
123692
123824
  try {
123693
- await promptsInitCommand({
123694
- workdir,
123825
+ await initCommand2({
123826
+ projectRoot: workdir,
123827
+ name: options.name,
123695
123828
  force: options.force,
123696
- autoWireConfig: false
123829
+ package: options.package
123697
123830
  });
123698
123831
  } catch (err) {
123699
- console.error(source_default.red(`Failed to initialize templates: ${err.message}`));
123832
+ console.error(source_default.red(`Error: ${err.message}`));
123700
123833
  process.exit(1);
123701
123834
  }
123702
- console.log(source_default.green("\u2705 Initialized nax"));
123703
- console.log(source_default.dim(` ${naxDir}/`));
123704
- console.log(source_default.dim(" \u251C\u2500\u2500 config.json"));
123705
- console.log(source_default.dim(" \u251C\u2500\u2500 context.md"));
123706
- console.log(source_default.dim(" \u251C\u2500\u2500 hooks.json"));
123707
- console.log(source_default.dim(" \u251C\u2500\u2500 features/"));
123708
- console.log(source_default.dim(" \u251C\u2500\u2500 hooks/"));
123709
- console.log(source_default.dim(" \u2514\u2500\u2500 templates/"));
123710
- console.log(source_default.dim(" \u251C\u2500\u2500 test-writer.md"));
123711
- console.log(source_default.dim(" \u251C\u2500\u2500 implementer.md"));
123712
- console.log(source_default.dim(" \u251C\u2500\u2500 verifier.md"));
123713
- console.log(source_default.dim(" \u251C\u2500\u2500 single-session.md"));
123714
- console.log(source_default.dim(" \u2514\u2500\u2500 tdd-simple.md"));
123715
- console.log(source_default.dim(`
123716
- Next: nax features create <name>`));
123717
123835
  });
123718
123836
  program2.command("setup").description("Analyze repo and generate .nax/config.json via LLM").option("-d, --dir <path>", "Project directory", process.cwd()).option("-a, --agent <name>", "Force a specific agent").option("--fill-scripts", "Add missing quality-gate scripts to package.json", false).option("--dry-run", "Preview planned config without writing files", false).option("--force", "Overwrite existing .nax/config.json", false).action(async (options) => {
123719
123837
  let workdir;
@@ -123793,7 +123911,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
123793
123911
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
123794
123912
  process.exit(1);
123795
123913
  }
123796
- if (options.from && !existsSync41(options.from)) {
123914
+ if (options.from && !existsSync40(options.from)) {
123797
123915
  console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
123798
123916
  process.exit(1);
123799
123917
  }
@@ -123837,7 +123955,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
123837
123955
  const featureDir2 = join115(naxDir, "features", options.feature);
123838
123956
  const prdPath = join115(featureDir2, "prd.json");
123839
123957
  if (options.plan && options.from) {
123840
- if (existsSync41(prdPath) && !options.force) {
123958
+ if (existsSync40(prdPath) && !options.force) {
123841
123959
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
123842
123960
  console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
123843
123961
  process.exit(1);
@@ -123901,7 +124019,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
123901
124019
  process.exit(1);
123902
124020
  }
123903
124021
  }
123904
- if (!existsSync41(prdPath)) {
124022
+ if (!existsSync40(prdPath)) {
123905
124023
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
123906
124024
  process.exit(1);
123907
124025
  }
@@ -124040,7 +124158,7 @@ Scheduled run cancelled.`));
124040
124158
  }
124041
124159
  const latestSymlink = join115(runsDir, "latest.jsonl");
124042
124160
  try {
124043
- if (existsSync41(latestSymlink)) {
124161
+ if (existsSync40(latestSymlink)) {
124044
124162
  Bun.spawnSync(["rm", latestSymlink]);
124045
124163
  }
124046
124164
  Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
@@ -124200,7 +124318,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
124200
124318
  process.exit(1);
124201
124319
  }
124202
124320
  const featuresDir2 = join115(naxDir, "features");
124203
- if (!existsSync41(featuresDir2)) {
124321
+ if (!existsSync40(featuresDir2)) {
124204
124322
  console.log(source_default.dim("No features yet."));
124205
124323
  return;
124206
124324
  }
@@ -124215,7 +124333,7 @@ Features:
124215
124333
  `));
124216
124334
  for (const name of entries) {
124217
124335
  const prdPath = join115(featuresDir2, name, "prd.json");
124218
- if (existsSync41(prdPath)) {
124336
+ if (existsSync40(prdPath)) {
124219
124337
  const prd = await loadPRD(prdPath);
124220
124338
  const c = countStories(prd);
124221
124339
  console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);