@nathapp/nax 0.68.1 → 0.68.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +673 -159
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17203,7 +17203,8 @@ var init_schemas_review = __esm(() => {
17203
17203
  requote: true,
17204
17204
  maxRequotes: 5
17205
17205
  }),
17206
- excludePatterns: exports_external.array(exports_external.string()).optional()
17206
+ excludePatterns: exports_external.array(exports_external.string()).optional(),
17207
+ demandInspectionTrail: exports_external.boolean().default(true)
17207
17208
  });
17208
17209
  AdversarialReviewConfigSchema = exports_external.object({
17209
17210
  model: ConfiguredModelSchema.default("balanced"),
@@ -17214,6 +17215,7 @@ var init_schemas_review = __esm(() => {
17214
17215
  parallel: exports_external.boolean().default(false),
17215
17216
  maxConcurrentSessions: exports_external.number().int().min(1).max(4).default(2),
17216
17217
  acRegroundOnDrop: exports_external.boolean().default(true),
17218
+ demandInspectionTrail: exports_external.boolean().default(true),
17217
17219
  substantiation: exports_external.object({
17218
17220
  requote: exports_external.boolean().default(true),
17219
17221
  maxRequotes: exports_external.number().int().min(0).default(5)
@@ -17428,6 +17430,7 @@ var init_schemas3 = __esm(() => {
17428
17430
  resetRefOnRerun: false,
17429
17431
  rules: [],
17430
17432
  timeoutMs: 600000,
17433
+ demandInspectionTrail: true,
17431
17434
  substantiation: {
17432
17435
  requote: true,
17433
17436
  maxRequotes: 5
@@ -17451,6 +17454,7 @@ var init_schemas3 = __esm(() => {
17451
17454
  parallel: false,
17452
17455
  maxConcurrentSessions: 2,
17453
17456
  acRegroundOnDrop: true,
17457
+ demandInspectionTrail: true,
17454
17458
  substantiation: {
17455
17459
  requote: true,
17456
17460
  maxRequotes: 5
@@ -17735,19 +17739,30 @@ function formatStoryStart(entry, c, _timestamp, mode) {
17735
17739
  const complexity = typeof data.complexity === "string" ? data.complexity : "unknown";
17736
17740
  const tier = typeof data.modelTier === "string" ? data.modelTier : "unknown";
17737
17741
  const attempt = typeof data.attempt === "number" ? data.attempt : 1;
17742
+ const agent = typeof data.agent === "string" ? data.agent : undefined;
17743
+ const progress = typeof data.storyNumber === "number" && typeof data.storyTotal === "number" ? `${data.storyNumber}/${data.storyTotal}` : undefined;
17738
17744
  const lines = [];
17739
17745
  lines.push("");
17740
17746
  lines.push(c.bold(`${EMOJI.storyStart} ${c.cyan(storyId)}: ${title}`));
17741
17747
  if (mode === "verbose") {
17748
+ if (progress)
17749
+ lines.push(` ${c.gray("\u251C\u2500")} Story: ${c.cyan(progress)}`);
17742
17750
  lines.push(` ${c.gray("\u251C\u2500")} Complexity: ${c.yellow(complexity)}`);
17743
17751
  lines.push(` ${c.gray("\u251C\u2500")} Tier: ${c.magenta(tier)}`);
17752
+ if (agent)
17753
+ lines.push(` ${c.gray("\u251C\u2500")} Agent: ${c.cyan(agent)}`);
17744
17754
  if (attempt > 1) {
17745
17755
  lines.push(` ${c.gray("\u2514\u2500")} Attempt: ${c.yellow(`#${attempt}`)} ${EMOJI.retry}`);
17746
17756
  } else {
17747
17757
  lines.push(` ${c.gray("\u2514\u2500")} Status: ${c.green("starting")}`);
17748
17758
  }
17749
17759
  } else {
17750
- const metadata = [complexity, tier];
17760
+ const metadata = [];
17761
+ if (progress)
17762
+ metadata.push(progress);
17763
+ metadata.push(complexity, tier);
17764
+ if (agent)
17765
+ metadata.push(agent);
17751
17766
  if (attempt > 1)
17752
17767
  metadata.push(`attempt #${attempt} ${EMOJI.retry}`);
17753
17768
  lines.push(` ${c.gray(metadata.join(" \u2022 "))}`);
@@ -17809,24 +17824,19 @@ function formatDefault(entry, c, timestamp, mode) {
17809
17824
  if (entry.storyId) {
17810
17825
  parts.push(c.dim(`[${entry.storyId}]`));
17811
17826
  }
17827
+ if (entry.sessionRole) {
17828
+ parts.push(c.dim(`(${entry.sessionRole})`));
17829
+ }
17812
17830
  parts.push(entry.message);
17813
17831
  let output = parts.join(" ");
17814
17832
  const data = entry.data;
17815
17833
  if (data && typeof data === "object") {
17816
- const meta3 = [];
17817
- if (typeof data.cost === "number" && data.cost > 0)
17818
- meta3.push(`${EMOJI.cost} ${formatCost(data.cost)}`);
17819
- if (typeof data.durationMs === "number" && data.durationMs > 0)
17820
- meta3.push(`${EMOJI.duration} ${formatDuration(data.durationMs)}`);
17821
- if (typeof data.action === "string")
17822
- meta3.push(`action: ${data.action}`);
17823
- if (typeof data.reason === "string" && mode !== "quiet")
17824
- meta3.push(data.reason);
17834
+ const meta3 = buildDefaultMeta(data, mode);
17825
17835
  if (meta3.length > 0) {
17826
17836
  output += ` ${c.gray(meta3.join(" "))}`;
17827
17837
  }
17828
17838
  if (mode === "verbose") {
17829
- const { cost: _c, durationMs: _d, action: _a2, reason: _r, ...filtered } = data;
17839
+ const filtered = stripConsumedMetaFields(data);
17830
17840
  if (Object.keys(filtered).length > 0) {
17831
17841
  output += `
17832
17842
  ${c.gray(JSON.stringify(filtered, null, 2))}`;
@@ -17838,6 +17848,52 @@ ${c.gray(JSON.stringify(filtered, null, 2))}`;
17838
17848
  shouldDisplay: true
17839
17849
  };
17840
17850
  }
17851
+ function buildDefaultMeta(data, mode) {
17852
+ const meta3 = [];
17853
+ const identity = [data.agentName, data.model].filter((v) => typeof v === "string" && v.length > 0);
17854
+ if (identity.length > 0)
17855
+ meta3.push(`${EMOJI.agent} ${identity.join("\xB7")}`);
17856
+ if (typeof data.phaseIndex === "number" && typeof data.totalPhases === "number") {
17857
+ meta3.push(`${data.phaseIndex}/${data.totalPhases}`);
17858
+ }
17859
+ if (typeof data.status === "string")
17860
+ meta3.push(`status: ${data.status}`);
17861
+ if (typeof data.findingsCount === "number")
17862
+ meta3.push(`${data.findingsCount} finding${data.findingsCount === 1 ? "" : "s"}`);
17863
+ const activity = buildActivityMeta(data);
17864
+ if (activity)
17865
+ meta3.push(activity);
17866
+ if (typeof data.cost === "number" && data.cost > 0)
17867
+ meta3.push(`${EMOJI.cost} ${formatCost(data.cost)}`);
17868
+ if (typeof data.durationMs === "number" && data.durationMs > 0)
17869
+ meta3.push(`${EMOJI.duration} ${formatDuration(data.durationMs)}`);
17870
+ if (typeof data.action === "string")
17871
+ meta3.push(`action: ${data.action}`);
17872
+ if (typeof data.reason === "string" && mode !== "quiet")
17873
+ meta3.push(data.reason);
17874
+ return meta3;
17875
+ }
17876
+ function buildActivityMeta(data) {
17877
+ const segments = [];
17878
+ if (typeof data.messageUpdates === "number" && data.messageUpdates > 0)
17879
+ segments.push(`msg ${data.messageUpdates}`);
17880
+ if (typeof data.toolCallUpdates === "number" && data.toolCallUpdates > 0)
17881
+ segments.push(`tools ${data.toolCallUpdates}`);
17882
+ if (typeof data.thinkingUpdates === "number" && data.thinkingUpdates > 0)
17883
+ segments.push(`think ${data.thinkingUpdates}`);
17884
+ if (typeof data.idleMs === "number" && data.idleMs > 0)
17885
+ segments.push(`idle ${formatDuration(data.idleMs)}`);
17886
+ return segments.length > 0 ? segments.join(" ") : null;
17887
+ }
17888
+ function stripConsumedMetaFields(data) {
17889
+ const filtered = {};
17890
+ for (const [key, value] of Object.entries(data)) {
17891
+ if (!CONSUMED_META_KEYS.includes(key)) {
17892
+ filtered[key] = value;
17893
+ }
17894
+ }
17895
+ return filtered;
17896
+ }
17841
17897
  function formatRunSummary(summary, options) {
17842
17898
  const { mode, useColor = true } = options;
17843
17899
  if (mode === "json") {
@@ -17882,9 +17938,26 @@ function createNoopChalk() {
17882
17938
  cyan: noop
17883
17939
  };
17884
17940
  }
17941
+ var CONSUMED_META_KEYS;
17885
17942
  var init_formatter = __esm(() => {
17886
17943
  init_source();
17887
17944
  init_types2();
17945
+ CONSUMED_META_KEYS = [
17946
+ "agentName",
17947
+ "model",
17948
+ "phaseIndex",
17949
+ "totalPhases",
17950
+ "status",
17951
+ "findingsCount",
17952
+ "messageUpdates",
17953
+ "toolCallUpdates",
17954
+ "thinkingUpdates",
17955
+ "idleMs",
17956
+ "cost",
17957
+ "durationMs",
17958
+ "action",
17959
+ "reason"
17960
+ ];
17888
17961
  });
17889
17962
 
17890
17963
  // src/logging/index.ts
@@ -17933,6 +18006,43 @@ var init_formatters = __esm(() => {
17933
18006
  init_source();
17934
18007
  });
17935
18008
 
18009
+ // src/logger/redact.ts
18010
+ function redactString(value) {
18011
+ let out = value;
18012
+ for (const re of SECRET_VALUE_PATTERNS) {
18013
+ re.lastIndex = 0;
18014
+ out = out.replace(re, REDACTED);
18015
+ }
18016
+ return out;
18017
+ }
18018
+ function redactSecrets(input) {
18019
+ if (typeof input === "string")
18020
+ return redactString(input);
18021
+ if (Array.isArray(input))
18022
+ return input.map(redactSecrets);
18023
+ if (input !== null && typeof input === "object") {
18024
+ const out = {};
18025
+ for (const [key, value] of Object.entries(input)) {
18026
+ out[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactSecrets(value);
18027
+ }
18028
+ return out;
18029
+ }
18030
+ return input;
18031
+ }
18032
+ var SECRET_KEY_PATTERN, SECRET_VALUE_PATTERNS, REDACTED = "[REDACTED]";
18033
+ var init_redact = __esm(() => {
18034
+ SECRET_KEY_PATTERN = /(SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)/i;
18035
+ SECRET_VALUE_PATTERNS = [
18036
+ /sk-[A-Za-z0-9_-]{16,}/g,
18037
+ /ghp_[A-Za-z0-9]{16,}/g,
18038
+ /gh[opsu]_[A-Za-z0-9]{16,}/g,
18039
+ /npm_[A-Za-z0-9]{8,}/g,
18040
+ /AKIA[0-9A-Z]{16}/g,
18041
+ /xox[baprs]-[A-Za-z0-9-]{10,}/g,
18042
+ /(?:SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)=[^\s"',]+/gi
18043
+ ];
18044
+ });
18045
+
17936
18046
  // src/logger/logger.ts
17937
18047
  import { mkdirSync } from "fs";
17938
18048
  import { appendFile } from "fs/promises";
@@ -18027,7 +18137,8 @@ ${JSON.stringify(entry.data, null, 2)}`;
18027
18137
  writeToFile(entry) {
18028
18138
  if (!this.filePath)
18029
18139
  return;
18030
- const line = `${formatJsonl(entry)}
18140
+ const safeEntry = entry.data ? { ...entry, data: redactSecrets(entry.data) } : entry;
18141
+ const line = `${formatJsonl(safeEntry)}
18031
18142
  `;
18032
18143
  const filePath = this.filePath;
18033
18144
  this.writeQueueTail = this.writeQueueTail.then(() => appendFile(filePath, line).catch((error48) => {
@@ -18089,6 +18200,7 @@ var LOG_LEVEL_PRIORITY, instance = null, noopLogger;
18089
18200
  var init_logger = __esm(() => {
18090
18201
  init_logging();
18091
18202
  init_formatters();
18203
+ init_redact();
18092
18204
  LOG_LEVEL_PRIORITY = {
18093
18205
  silent: -1,
18094
18206
  error: 0,
@@ -19070,7 +19182,7 @@ GOOD (write ACs like these):
19070
19182
 
19071
19183
  When a spec is provided, these rules govern acceptance criteria generation:
19072
19184
 
19073
- 1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria\`, verbatim or lightly rephrased for testability. Never silently drop a spec AC.
19185
+ 1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria\`. Never silently drop a spec AC. ACs the spec tags \`[verbatim]\` (typically executable greps, file-existence checks, regex/count assertions, or architectural invariants) MUST be copied **character-for-character** into an \`acceptanceCriteria\` entry \u2014 preserve every backtick-quoted command, file path, regex, and count exactly; do not paraphrase, retag, split, or move them into a description. Untagged ACs may be lightly rephrased for testability, but must retain the same assertion and concrete identifiers.
19074
19186
  2. **Do not invent spec ACs.** If you identify useful behavioral edge cases or negative paths that the spec did not explicitly list, place them in \`suggestedCriteria\` (a string array on the same story object) \u2014 never in \`acceptanceCriteria\`. These go through a separate hardening pass.
19075
19187
  3. **Respect story scope.** Each story's criteria must only cover what the spec says for that story. Do not assign criteria that belong to a different story's scope (wrong feature area, wrong file, wrong dependency chain).
19076
19188
  4. **\`suggestedCriteria\` format.** Each element must be a plain behavioral assertion \u2014 an observable output, return value, state change, or error condition that a test can assert. Never include implementation details (imports, internal structure), design suggestions, or vague descriptions.
@@ -22551,9 +22663,16 @@ async function detectTestFramework(_workdir, language, pkg) {
22551
22663
  }
22552
22664
  return;
22553
22665
  }
22666
+ function clearLanguageCache() {
22667
+ _languageCache.clear();
22668
+ }
22554
22669
  async function detectLanguage(packageDir) {
22670
+ if (_languageCache.has(packageDir))
22671
+ return _languageCache.get(packageDir);
22555
22672
  const pkg = await _detectorDeps.readJson(join5(packageDir, "package.json"));
22556
- return _detectLanguageImpl(packageDir, pkg);
22673
+ const result = await _detectLanguageImpl(packageDir, pkg);
22674
+ _languageCache.set(packageDir, result);
22675
+ return result;
22557
22676
  }
22558
22677
  async function detectLintTool(workdir, language) {
22559
22678
  if (language === "go")
@@ -22581,7 +22700,7 @@ async function detectProjectProfile(workdir, existing) {
22581
22700
  const lintTool = existing.lintTool !== undefined ? existing.lintTool : await detectLintTool(workdir, language);
22582
22701
  return { language, type, testFramework, lintTool };
22583
22702
  }
22584
- var _detectorDeps, WEB_DEPS, API_DEPS;
22703
+ var _detectorDeps, WEB_DEPS, API_DEPS, _languageCache;
22585
22704
  var init_detector = __esm(() => {
22586
22705
  _detectorDeps = {
22587
22706
  async fileExists(path) {
@@ -22602,6 +22721,7 @@ var init_detector = __esm(() => {
22602
22721
  };
22603
22722
  WEB_DEPS = new Set(["react", "next", "vue", "nuxt"]);
22604
22723
  API_DEPS = new Set(["express", "fastify", "hono"]);
22724
+ _languageCache = new Map;
22605
22725
  });
22606
22726
 
22607
22727
  // src/test-runners/conventions.ts
@@ -23730,7 +23850,10 @@ async function detectNaxMonoLayout(workdir) {
23730
23850
  } catch {}
23731
23851
  return dirs;
23732
23852
  }
23733
- async function discoverWorkspacePackages(workdir) {
23853
+ function clearWorkspaceCache() {
23854
+ _workspaceCache.clear();
23855
+ }
23856
+ async function discoverWorkspacePackagesUncached(workdir) {
23734
23857
  const [fromPnpm, fromNpm, fromLerna, fromTurboNx, fromNaxMono] = await Promise.all([
23735
23858
  detectPnpmWorkspace(workdir),
23736
23859
  detectNpmWorkspaces(workdir),
@@ -23749,7 +23872,14 @@ async function discoverWorkspacePackages(workdir) {
23749
23872
  }
23750
23873
  return unique;
23751
23874
  }
23752
- var _workspaceDeps;
23875
+ async function discoverWorkspacePackages(workdir) {
23876
+ if (_workspaceCache.has(workdir))
23877
+ return _workspaceCache.get(workdir) ?? [];
23878
+ const result = await discoverWorkspacePackagesUncached(workdir);
23879
+ _workspaceCache.set(workdir, result);
23880
+ return result;
23881
+ }
23882
+ var _workspaceDeps, _workspaceCache;
23753
23883
  var init_workspace = __esm(() => {
23754
23884
  init_logger2();
23755
23885
  _workspaceDeps = {
@@ -23762,6 +23892,7 @@ var init_workspace = __esm(() => {
23762
23892
  spawn: Bun.spawn,
23763
23893
  glob: (pattern, cwd) => new Bun.Glob(pattern).scan({ cwd, onlyFiles: false })
23764
23894
  };
23895
+ _workspaceCache = new Map;
23765
23896
  });
23766
23897
 
23767
23898
  // src/test-runners/detect.ts
@@ -24625,6 +24756,19 @@ var init_path_filters = __esm(() => {
24625
24756
 
24626
24757
  // src/verification/smart-runner.ts
24627
24758
  import { join as join8, relative as relative3 } from "path";
24759
+ function clearGitRootCache() {
24760
+ _gitRootCache.clear();
24761
+ }
24762
+ async function getGitRootMemo(workdir) {
24763
+ const cached2 = _gitRootCache.get(workdir);
24764
+ if (cached2 !== undefined)
24765
+ return cached2;
24766
+ const result = await _gitUtilDeps.getGitRoot(workdir);
24767
+ if (result !== null && result !== undefined) {
24768
+ _gitRootCache.set(workdir, result);
24769
+ }
24770
+ return result ?? null;
24771
+ }
24628
24772
  function extractPatternSuffix(pattern) {
24629
24773
  const lastStar = pattern.lastIndexOf("*");
24630
24774
  if (lastStar === -1)
@@ -24644,28 +24788,28 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
24644
24788
  return [];
24645
24789
  const searchTerms = sourceFiles.flatMap(extractSearchTerms);
24646
24790
  const testFilePaths = [];
24647
- for (const pattern of testFilePatterns) {
24648
- const glob = _bunDeps.glob(pattern);
24649
- for await (const file3 of glob.scan(workdir)) {
24650
- testFilePaths.push(`${workdir}/${file3}`);
24791
+ outer:
24792
+ for (const pattern of testFilePatterns) {
24793
+ const g = _bunDeps.glob(pattern);
24794
+ for await (const file3 of g.scan(workdir)) {
24795
+ testFilePaths.push(`${workdir}/${file3}`);
24796
+ if (testFilePaths.length >= MAX_GREP_TEST_FILES) {
24797
+ getSafeLogger()?.debug("smart-runner", "import-grep glob cap reached \u2014 results truncated", {
24798
+ cap: MAX_GREP_TEST_FILES
24799
+ });
24800
+ break outer;
24801
+ }
24802
+ }
24651
24803
  }
24652
- }
24653
- const matched = [];
24654
- for (const testFile of testFilePaths) {
24655
- let content;
24804
+ const results = await Promise.all(testFilePaths.map(async (testFile) => {
24656
24805
  try {
24657
- content = await _bunDeps.file(testFile).text();
24806
+ const content = await _bunDeps.file(testFile).text();
24807
+ return searchTerms.some((t) => content.includes(t)) ? testFile : null;
24658
24808
  } catch {
24659
- continue;
24660
- }
24661
- for (const term of searchTerms) {
24662
- if (content.includes(term)) {
24663
- matched.push(testFile);
24664
- break;
24665
- }
24809
+ return null;
24666
24810
  }
24667
- }
24668
- return matched;
24811
+ }));
24812
+ return results.filter((p) => p !== null);
24669
24813
  }
24670
24814
  async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePatterns = [...DEFAULT_TEST_FILE_PATTERNS]) {
24671
24815
  const testSuffixes = [...new Set(testFilePatterns.map(extractPatternSuffix).filter((s) => s !== null))];
@@ -24690,11 +24834,11 @@ async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePat
24690
24834
  }
24691
24835
  candidates.push(`${workdir}/${sourceWithoutExt}${suffix}`);
24692
24836
  }
24693
- for (const candidate of candidates) {
24694
- if (await _bunDeps.file(candidate).exists()) {
24695
- result.push(candidate);
24696
- }
24697
- }
24837
+ const existsFlags = await Promise.all(candidates.map((c) => _bunDeps.file(c).exists()));
24838
+ candidates.forEach((c, i) => {
24839
+ if (existsFlags[i])
24840
+ result.push(c);
24841
+ });
24698
24842
  }
24699
24843
  return result;
24700
24844
  }
@@ -24733,7 +24877,7 @@ async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileR
24733
24877
  const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(effectiveRepoRoot, packageDir);
24734
24878
  let effectivePrefix = packagePrefix;
24735
24879
  if (packagePrefix && repoRoot) {
24736
- const gitRoot = await _gitUtilDeps.getGitRoot(workdir);
24880
+ const gitRoot = await getGitRootMemo(workdir);
24737
24881
  const extraPrefix2 = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
24738
24882
  effectivePrefix = extraPrefix2 ? `${extraPrefix2}/${packagePrefix}` : packagePrefix;
24739
24883
  }
@@ -24760,7 +24904,7 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
24760
24904
  `).filter(Boolean);
24761
24905
  const packageDir = packagePrefix ? join8(repoRoot, packagePrefix) : undefined;
24762
24906
  const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(repoRoot, packageDir);
24763
- const gitRoot = await _gitUtilDeps.getGitRoot(workdir);
24907
+ const gitRoot = await getGitRootMemo(workdir);
24764
24908
  const extraPrefix = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
24765
24909
  const effectivePrefix = packagePrefix ? extraPrefix ? `${extraPrefix}/${packagePrefix}` : packagePrefix : undefined;
24766
24910
  const scopedRaw = effectivePrefix ? lines.filter((f) => f.startsWith(`${effectivePrefix}/`)) : lines;
@@ -24771,8 +24915,9 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
24771
24915
  return [];
24772
24916
  }
24773
24917
  }
24774
- var _bunDeps, _gitUtilDeps, _smartRunnerDeps;
24918
+ var _bunDeps, MAX_GREP_TEST_FILES = 200, _gitUtilDeps, _gitRootCache, _smartRunnerDeps;
24775
24919
  var init_smart_runner = __esm(() => {
24920
+ init_logger2();
24776
24921
  init_conventions();
24777
24922
  init_git();
24778
24923
  init_path_filters();
@@ -24783,6 +24928,7 @@ var init_smart_runner = __esm(() => {
24783
24928
  _gitUtilDeps = {
24784
24929
  getGitRoot
24785
24930
  };
24931
+ _gitRootCache = new Map;
24786
24932
  _smartRunnerDeps = {
24787
24933
  glob: _bunDeps.glob,
24788
24934
  file: _bunDeps.file,
@@ -25140,31 +25286,50 @@ async function resolveSourceGlob(override, packageDir) {
25140
25286
  const language = await _codeNeighborDeps.detectLanguage(packageDir);
25141
25287
  return (language && SOURCE_GLOB_BY_LANGUAGE[language]) ?? FALLBACK_SOURCE_GLOB;
25142
25288
  }
25143
- async function collectNeighbors(filePath, workdir, sourceGlob, maxGlobFiles, extraGlobWorkdirs, siblingTestContext, ignoreMatchers, globCtx) {
25289
+ function scanDirectory(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCtx) {
25290
+ const { files, truncated } = _codeNeighborDeps.glob(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCtx);
25291
+ return { workdir, files, truncated };
25292
+ }
25293
+ async function readCached(absolutePath, cache) {
25294
+ const cached2 = cache.get(absolutePath);
25295
+ if (cached2 !== undefined)
25296
+ return cached2;
25297
+ try {
25298
+ const content = await _codeNeighborDeps.readFile(absolutePath);
25299
+ cache.set(absolutePath, content);
25300
+ return content;
25301
+ } catch {
25302
+ cache.set(absolutePath, "");
25303
+ return null;
25304
+ }
25305
+ }
25306
+ async function collectNeighbors(filePath, workdir, scannedDirs, contentCache, siblingTestContext) {
25144
25307
  const neighbors = new Set;
25145
25308
  let anyTruncated = false;
25146
- if (await _codeNeighborDeps.fileExists(join10(workdir, filePath))) {
25147
- const content = await _codeNeighborDeps.readFile(join10(workdir, filePath));
25148
- for (const spec of parseImportSpecifiers(content)) {
25149
- const resolved = resolveImport(spec, filePath, workdir);
25150
- if (resolved && resolved !== filePath)
25151
- neighbors.add(resolved);
25309
+ const ownAbsPath = join10(workdir, filePath);
25310
+ if (await _codeNeighborDeps.fileExists(ownAbsPath)) {
25311
+ const ownContent = await readCached(ownAbsPath, contentCache);
25312
+ if (ownContent !== null && ownContent.length > 0) {
25313
+ for (const spec of parseImportSpecifiers(ownContent)) {
25314
+ const resolved = resolveImport(spec, filePath, workdir);
25315
+ if (resolved && resolved !== filePath)
25316
+ neighbors.add(resolved);
25317
+ }
25152
25318
  }
25153
25319
  }
25154
25320
  const fileBaseName = (filePath.split("/").pop() ?? filePath).replace(/\.[^.]+$/, "");
25155
25321
  const fileNoExt = filePath.replace(/\.[^.]+$/, "");
25156
- const scanForReverseDeps = async (scanWorkdir) => {
25157
- const { files: srcFiles, truncated } = _codeNeighborDeps.glob(sourceGlob, scanWorkdir, ignoreMatchers, maxGlobFiles, globCtx);
25158
- if (truncated)
25159
- anyTruncated = true;
25160
- for (const srcFile of srcFiles) {
25161
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25162
- break;
25163
- if (srcFile === filePath)
25164
- continue;
25165
- try {
25166
- const content = await _codeNeighborDeps.readFile(join10(scanWorkdir, srcFile));
25167
- if (content.includes(fileBaseName)) {
25322
+ outer:
25323
+ for (const { workdir: scanWorkdir, files: srcFiles, truncated } of scannedDirs) {
25324
+ if (truncated)
25325
+ anyTruncated = true;
25326
+ for (const srcFile of srcFiles) {
25327
+ if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25328
+ break outer;
25329
+ if (srcFile === filePath)
25330
+ continue;
25331
+ const content = await readCached(join10(scanWorkdir, srcFile), contentCache);
25332
+ if (content?.includes(fileBaseName)) {
25168
25333
  for (const spec of parseImportSpecifiers(content)) {
25169
25334
  const resolved = resolveImport(spec, srcFile, scanWorkdir);
25170
25335
  if (resolved === filePath || resolved === fileNoExt) {
@@ -25173,17 +25338,8 @@ async function collectNeighbors(filePath, workdir, sourceGlob, maxGlobFiles, ext
25173
25338
  }
25174
25339
  }
25175
25340
  }
25176
- } catch {}
25177
- }
25178
- };
25179
- await scanForReverseDeps(workdir);
25180
- if (extraGlobWorkdirs) {
25181
- for (const extraDir of extraGlobWorkdirs) {
25182
- if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
25183
- break;
25184
- await scanForReverseDeps(extraDir);
25341
+ }
25185
25342
  }
25186
- }
25187
25343
  if (siblingTestContext && !isTestFile2(filePath, siblingTestContext.regex)) {
25188
25344
  const candidates = deriveSiblingTestCandidates(filePath, siblingTestContext.globs);
25189
25345
  let chosen = null;
@@ -25246,10 +25402,17 @@ class CodeNeighborProvider {
25246
25402
  const ignoreMatchers = request.naxIgnoreIndex?.getMatchers(workdir);
25247
25403
  const sourceGlob = await resolveSourceGlob(this.sourceGlobOverride, request.packageDir);
25248
25404
  const globCtx = { storyId: request.storyId, packageDir: request.packageDir };
25405
+ const scannedDirs = [scanDirectory(sourceGlob, workdir, ignoreMatchers, this.maxGlobFiles, globCtx)];
25406
+ if (extraGlobWorkdirs) {
25407
+ for (const extraDir of extraGlobWorkdirs) {
25408
+ scannedDirs.push(scanDirectory(sourceGlob, extraDir, ignoreMatchers, this.maxGlobFiles, globCtx));
25409
+ }
25410
+ }
25411
+ const contentCache = new Map;
25249
25412
  const sections = [];
25250
25413
  let anyTruncated = false;
25251
25414
  for (const file3 of filesToProcess) {
25252
- const { neighbors, truncated } = await collectNeighbors(file3, workdir, sourceGlob, this.maxGlobFiles, extraGlobWorkdirs, siblingTestContext, ignoreMatchers, globCtx);
25415
+ const { neighbors, truncated } = await collectNeighbors(file3, workdir, scannedDirs, contentCache, siblingTestContext);
25253
25416
  if (truncated)
25254
25417
  anyTruncated = true;
25255
25418
  if (neighbors.length > 0) {
@@ -26167,13 +26330,25 @@ function buildPullToolDescriptors(stageToolNames, pullConfig) {
26167
26330
  const allowed = pullConfig.allowedTools;
26168
26331
  return stageToolNames.filter((name) => allowed.length === 0 || allowed.includes(name)).map((name) => PULL_TOOL_REGISTRY[name]).filter((d) => d !== undefined).map((d) => ({ ...d, maxCallsPerSession: pullConfig.maxCallsPerSession ?? d.maxCallsPerSession }));
26169
26332
  }
26170
- async function fetchWithTimeout(provider, request) {
26333
+ async function fetchWithTimeout(provider, request, timeoutMs = PROVIDER_FETCH_TIMEOUT_MS) {
26334
+ const controller = new AbortController;
26171
26335
  let handle;
26336
+ let timedOut = false;
26172
26337
  const timeout = new Promise((_, reject) => {
26173
- handle = setTimeout(() => reject(new Error(`Provider "${provider.id}" timed out`)), PROVIDER_FETCH_TIMEOUT_MS);
26338
+ handle = setTimeout(() => {
26339
+ timedOut = true;
26340
+ controller.abort();
26341
+ reject(new Error(`Provider "${provider.id}" timed out`));
26342
+ }, timeoutMs);
26343
+ });
26344
+ const fetchPromise = provider.fetch(request, controller.signal).then((result) => result, (err) => {
26345
+ if (timedOut) {
26346
+ return new Promise(() => {});
26347
+ }
26348
+ throw err;
26174
26349
  });
26175
26350
  try {
26176
- return await Promise.race([provider.fetch(request), timeout]);
26351
+ return await Promise.race([fetchPromise, timeout]);
26177
26352
  } finally {
26178
26353
  clearTimeout(handle);
26179
26354
  }
@@ -26812,12 +26987,7 @@ class GitHistoryProvider {
26812
26987
  return { chunks: [], pullTools: [] };
26813
26988
  }
26814
26989
  const filesToProcess = touchedFiles.filter(isRelativeAndSafe).slice(0, MAX_FILES2);
26815
- const sections = [];
26816
- for (const file3 of filesToProcess) {
26817
- const section = await fetchFileHistory(file3, workdir);
26818
- if (section)
26819
- sections.push(section);
26820
- }
26990
+ const sections = (await Promise.all(filesToProcess.map((file3) => fetchFileHistory(file3, workdir)))).filter((section) => section !== null);
26821
26991
  if (sections.length === 0) {
26822
26992
  return { chunks: [], pullTools: [] };
26823
26993
  }
@@ -27323,6 +27493,75 @@ function isStalled(prd) {
27323
27493
  return remaining.every((s) => s.status === "blocked" || s.status === "failed" || s.status === "paused" || s.status === "regression-failed" || s.dependencies.some((dep) => blockedIds.has(dep)));
27324
27494
  }
27325
27495
 
27496
+ // src/prd/verbatim-fidelity.ts
27497
+ function normalizeWs(text) {
27498
+ return text.replace(/\s+/g, " ").trim();
27499
+ }
27500
+ function stripBackticks(text) {
27501
+ return text.replace(/`/g, "");
27502
+ }
27503
+ function canonical(text) {
27504
+ return normalizeWs(stripBackticks(text));
27505
+ }
27506
+ function leadingTagGroup(line) {
27507
+ return line.match(LEADING_TAG_GROUP)?.[1] ?? null;
27508
+ }
27509
+ function isVerbatimBullet(line) {
27510
+ const tags = leadingTagGroup(line);
27511
+ return tags !== null && /\[verbatim\]/i.test(tags);
27512
+ }
27513
+ function isContinuation(line) {
27514
+ if (line.trim().length === 0)
27515
+ return false;
27516
+ if (HEADING.test(line))
27517
+ return false;
27518
+ if (LIST_ITEM_START.test(line))
27519
+ return false;
27520
+ return true;
27521
+ }
27522
+ function stripTagPrefix(block) {
27523
+ return block.replace(LEADING_TAG_GROUP, "");
27524
+ }
27525
+ function extractVerbatimAcs(specContent) {
27526
+ const lines = specContent.split(`
27527
+ `);
27528
+ const blocks = [];
27529
+ for (let i = 0;i < lines.length; i++) {
27530
+ if (!isVerbatimBullet(lines[i]))
27531
+ continue;
27532
+ const parts = [lines[i].trim()];
27533
+ let j = i + 1;
27534
+ while (j < lines.length && isContinuation(lines[j])) {
27535
+ parts.push(lines[j].trim());
27536
+ j += 1;
27537
+ }
27538
+ blocks.push(parts.join(" "));
27539
+ i = j - 1;
27540
+ }
27541
+ return blocks;
27542
+ }
27543
+ function prdAcPayloads(prd) {
27544
+ return (prd.userStories ?? []).flatMap((story) => (story.acceptanceCriteria ?? []).map(canonical));
27545
+ }
27546
+ function findMissingVerbatimAcs(specContent, prd) {
27547
+ const prdAcs = prdAcPayloads(prd);
27548
+ const missing = [];
27549
+ for (const block of extractVerbatimAcs(specContent)) {
27550
+ const payload = canonical(stripTagPrefix(block));
27551
+ if (payload.length === 0)
27552
+ continue;
27553
+ if (!prdAcs.some((ac) => ac.includes(payload)))
27554
+ missing.push(block);
27555
+ }
27556
+ return missing;
27557
+ }
27558
+ var LEADING_TAG_GROUP, LIST_ITEM_START, HEADING;
27559
+ var init_verbatim_fidelity = __esm(() => {
27560
+ LEADING_TAG_GROUP = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
27561
+ LIST_ITEM_START = /^\s*(?:[-*]|\d+\.)\s/;
27562
+ HEADING = /^\s*#/;
27563
+ });
27564
+
27326
27565
  // src/prd/validate.ts
27327
27566
  function validateStoryId(id) {
27328
27567
  if (!id || id.length === 0) {
@@ -27727,6 +27966,7 @@ function markStoryPaused(prd, storyId) {
27727
27966
  var PRD_MAX_FILE_SIZE;
27728
27967
  var init_prd = __esm(() => {
27729
27968
  init_json_file();
27969
+ init_verbatim_fidelity();
27730
27970
  init_schema2();
27731
27971
  PRD_MAX_FILE_SIZE = 5 * 1024 * 1024;
27732
27972
  });
@@ -30731,6 +30971,7 @@ Flag issues only when you have confirmed:
30731
30971
  Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.`, SEMANTIC_OUTPUT_SCHEMA = `Respond with JSON only \u2014 no explanation text before or after:
30732
30972
  {
30733
30973
  "passed": boolean,
30974
+ "inspectedFiles": ["relative/path/you/actually/opened.ts"],
30734
30975
  "findings": [
30735
30976
  {
30736
30977
  "severity": "error" | "warning" | "info" | "unverifiable",
@@ -30754,7 +30995,8 @@ Notes:
30754
30995
  - \`acIndex\` is required when severity is "error" (1-based, into the Acceptance Criteria list above).
30755
30996
  - \`acQuote\` is optional advisory metadata for human auditors \u2014 not validated.
30756
30997
  - Omit both for "warning", "info", "unverifiable".
30757
- If all ACs are correctly implemented, respond with { "passed": true, "findings": [] }.`, ReviewPromptBuilder;
30998
+ - \`inspectedFiles\` must list the relative paths you actually opened while reviewing. A \`passed:true\` verdict with an empty or absent \`inspectedFiles\` is invalid \u2014 walk each AC against the real files before passing.
30999
+ If all ACs are correctly implemented after inspecting the code, respond with { "passed": true, "inspectedFiles": ["..."], "findings": [] }.`, ReviewPromptBuilder;
30758
31000
  var init_review_builder = __esm(() => {
30759
31001
  SEMANTIC_ROLE = "You are a semantic code reviewer with access to the repository files. " + "Your job is to walk each acceptance criterion (AC) and judge whether the production code fulfills it \u2014 fully, partially, or not at all. " + "Test coverage gaps and convention/lint issues are out of scope \u2014 adversarial review and lint/typecheck handle those.";
30760
31002
  ReviewPromptBuilder = class ReviewPromptBuilder {
@@ -30807,6 +31049,16 @@ Respond with a condensed summary:
30807
31049
  - Keep \`verifiedBy\` for every finding. If \`verifiedBy.observed\` is long, abbreviate it to one line \u2014 never drop the field.
30808
31050
  Output ONLY a complete, valid JSON object. It must start with { and end with }.
30809
31051
  Schema: {"passed": boolean, "findings": [{"severity": string, "category": string, "file": string, "line": number, "issue": string, "suggestion": string, "verifiedBy": {"command": string, "file": string, "line": number, "observed": string}}]}`;
31052
+ }
31053
+ static demandInspection() {
31054
+ return `Your previous review returned \`passed:true\` with no findings and an empty (or absent) \`inspectedFiles\` list. That means you did not open any of the changed files \u2014 a verdict reached without reading the code is not valid.
31055
+
31056
+ Use your file-reading tools NOW to open the changed files and walk each acceptance criterion against the real implementation. Then re-issue your verdict as the same JSON object.
31057
+
31058
+ Rules:
31059
+ - Populate \`inspectedFiles\` with the relative paths you actually opened.
31060
+ - If, after genuinely inspecting the code, every AC is satisfied, you may still return \`passed:true\` \u2014 but \`inspectedFiles\` must list the files you read.
31061
+ - Return ONLY the JSON object \u2014 no markdown fences, no explanation.`;
30810
31062
  }
30811
31063
  static requoteVerbatim(opts) {
30812
31064
  const file3 = opts.finding.verifiedBy?.file ?? opts.finding.file;
@@ -30981,6 +31233,13 @@ What new exported units lack corresponding test files?
30981
31233
  - New public functions that only appear in implementation, not in tests
30982
31234
  - Acceptance criteria that touch a code path with no test coverage
30983
31235
 
31236
+ **Placeholder / tautological tests (blocking).** A test that exists but verifies nothing is worse than a missing test \u2014 it manufactures a false green. Treat the following as a **\`severity:"error"\`, \`category:"test-gap"\`** finding whenever the fake test is the only coverage for an acceptance criterion:
31237
+ - Bodies that always pass: \`expect(true).toBe(true)\`, \`expect(x).toBe(x)\`, \`expect(1).toBe(1)\`, an empty test body, or \`assert(true)\`.
31238
+ - Tests skipped/disabled (\`it.skip\`, \`test.todo\`, \`xit\`, commented-out assertions) that an AC depends on.
31239
+ - Assertions that never exercise the implementation (e.g. asserting on a literal, not on a value the production code produced).
31240
+
31241
+ For each such finding: set \`acIndex\` to the AC the fake test purports to cover, \`acQuote\` to a verbatim substring of that AC, and \`verifiedBy.observed\` to the placeholder line itself (e.g. \`expect(true).toBe(true)\`). Do **not** downgrade these to \`warning\` \u2014 a green suite built on placeholder assertions is a failing implementation with hidden evidence.
31242
+
30984
31243
  ### 5. Convention Breaks
30985
31244
  What pattern exists elsewhere that this code does not follow?
30986
31245
  - Logger calls missing \`storyId\` as first key in data object
@@ -31000,6 +31259,7 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
31000
31259
  \`\`\`json
31001
31260
  {
31002
31261
  "passed": true | false,
31262
+ "inspectedFiles": ["relative/path/you/actually/opened.ts"],
31003
31263
  "findings": [
31004
31264
  {
31005
31265
  "severity": "error" | "warning" | "info" | "unverifiable",
@@ -31021,6 +31281,8 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
31021
31281
  }
31022
31282
  \`\`\`
31023
31283
 
31284
+ **No rubber-stamping:** \`inspectedFiles\` must list the relative paths you actually opened with your tools while reviewing. A \`passed:true\` verdict with an empty or absent \`inspectedFiles\` is invalid \u2014 it means you never looked at the code. Fetch the diff and open the changed files before forming any verdict.
31285
+
31024
31286
  Severity guide:
31025
31287
  - \`"error"\`: confident this will cause real failure or regression
31026
31288
  - \`"warning"\`: fragile or incomplete but may ship without immediate breakage
@@ -31050,6 +31312,8 @@ Worked example:
31050
31312
 
31051
31313
  **Convention / coding-standard violations almost always belong as \`"info"\`** unless an AC specifically names the convention or the symbol it concerns.
31052
31314
 
31315
+ **Exception \u2014 the trap does NOT apply to \`category:"test-gap"\` findings.** A fake/placeholder/missing test is the *absence* of verification for an AC's behaviour; it cannot name a symbol that is present in the (worthless) test file, because the defect is precisely that the symbol-under-test is never exercised. A \`test-gap\` finding is grounded by the AC whose behaviour goes unverified \u2014 cite that AC's \`acIndex\` and a verbatim \`acQuote\` substring from it, and keep severity \`"error"\`. The symbol-naming requirement is waived for this category.
31316
+
31053
31317
  **Scope constraints are not Acceptance Criteria:**
31054
31318
  The story description may contain a "Scope" section with "In:" and "Out:" bullets. These are implementation guidelines, not ACs. A finding about code changed outside the stated scope (e.g., a file listed under "Out:") cannot cite a scope constraint as its \`acQuote\`/\`acIndex\` because scope text is not in the numbered AC list. Emit scope-violation findings as \`"warning"\` \u2014 never \`"error"\`. Never use \`acIndex: 0\`; \`acIndex\` is 1-based (first AC bullet = 1).
31055
31319
 
@@ -31138,6 +31402,16 @@ Rules:
31138
31402
  - observed must be a 1-3 line excerpt that proves the claim, taken from at or near line ${line}.
31139
31403
  - If after reading the file you cannot find anything that proves the claim, set observed to "".
31140
31404
  - Do not return a full review. Do not include markdown fences or explanation.`;
31405
+ }
31406
+ static demandInspection() {
31407
+ return `Your previous review returned \`passed:true\` with no findings and an empty (or absent) \`inspectedFiles\` list. That means you did not open any of the changed files \u2014 a verdict reached without inspecting the code is not valid.
31408
+
31409
+ Use your git and file-reading tools NOW to fetch the diff and open the changed files for this story. Then re-issue your verdict as the same JSON object.
31410
+
31411
+ Rules:
31412
+ - Populate \`inspectedFiles\` with the relative paths you actually opened.
31413
+ - If, after genuinely inspecting the code, there is truly nothing to flag, you may still return \`passed:true\` \u2014 but \`inspectedFiles\` must list the files you read.
31414
+ - Return ONLY the JSON object \u2014 no markdown fences, no explanation.`;
31141
31415
  }
31142
31416
  static DROP_CODE_MESSAGES_QUOTE = {
31143
31417
  missing_ac_quote: "no `acQuote` field was provided \u2014 every blocking finding must cite an AC",
@@ -31517,7 +31791,7 @@ var init_acceptance_builder = __esm(() => {
31517
31791
  });
31518
31792
 
31519
31793
  // src/review/ac-quote-validator.ts
31520
- function normalizeWs(s) {
31794
+ function normalizeWs2(s) {
31521
31795
  return s.replace(/\s+/g, " ").trim();
31522
31796
  }
31523
31797
  function stripMarkdownInline(s) {
@@ -31552,11 +31826,14 @@ function validateAcQuote(finding, acceptanceCriteria) {
31552
31826
  if (typeof acIndex !== "number" || acIndex < 1 || acIndex > acceptanceCriteria.length) {
31553
31827
  return { valid: false, code: "ac_index_out_of_range" };
31554
31828
  }
31555
- const acText = normalizeWs(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
31556
- const normalizedQuote = normalizeWs(stripMarkdownInline(acQuote));
31829
+ const acText = normalizeWs2(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
31830
+ const normalizedQuote = normalizeWs2(stripMarkdownInline(acQuote));
31557
31831
  if (!acText.toLowerCase().includes(normalizedQuote.toLowerCase())) {
31558
31832
  return { valid: false, code: "ac_quote_not_substring" };
31559
31833
  }
31834
+ if (finding.category === "test-gap") {
31835
+ return { valid: true };
31836
+ }
31560
31837
  const keywords = extractLocusKeywords(finding);
31561
31838
  if (keywords.length === 0) {
31562
31839
  return { valid: false, code: "ac_quote_does_not_constrain_locus" };
@@ -31875,6 +32152,10 @@ var init_semantic_evidence = __esm(() => {
31875
32152
  });
31876
32153
 
31877
32154
  // src/review/finding-filters.ts
32155
+ function hasInspectionTrail(raw) {
32156
+ const files = raw?.inspectedFiles;
32157
+ return Array.isArray(files) && files.some((f) => typeof f === "string" && f.trim().length > 0);
32158
+ }
31878
32159
  async function substantiateAdversarialFindings(opts) {
31879
32160
  const { findings, workdir, storyId, blockingThreshold } = opts;
31880
32161
  return Promise.all(findings.map(async (finding) => {
@@ -31906,9 +32187,9 @@ function parseRequoteResponse(output) {
31906
32187
  const parsed = tryParseLLMJson(output);
31907
32188
  if (!isRecord(parsed))
31908
32189
  return null;
31909
- const canonical = extractCanonical(parsed);
31910
- if (canonical)
31911
- return canonical;
32190
+ const canonical2 = extractCanonical(parsed);
32191
+ if (canonical2)
32192
+ return canonical2;
31912
32193
  const findings = parsed.findings;
31913
32194
  if (!Array.isArray(findings) || findings.length !== 1)
31914
32195
  return null;
@@ -32102,6 +32383,23 @@ async function performAdversarialReground(turn, firstParsed, drops, ctx) {
32102
32383
  output: withRepromptMarker(turn.output, { dropCount, outcome: "still-dropped", costUsd })
32103
32384
  };
32104
32385
  }
32386
+ async function maybeRepromptForInspection(turn, parsed, rawObject, ctx) {
32387
+ if (ctx.input.adversarialConfig.demandInspectionTrail === false)
32388
+ return null;
32389
+ if (!parsed.passed || parsed.findings.length !== 0)
32390
+ return null;
32391
+ if (hasInspectionTrail(rawObject))
32392
+ return null;
32393
+ const secondTurn = await ctx.send(AdversarialReviewPromptBuilder.demandInspection());
32394
+ const costUsd = (turn.estimatedCostUsd ?? 0) + (secondTurn.estimatedCostUsd ?? 0);
32395
+ const secondParsed = validateAdversarialShape(tryParseLLMJson(secondTurn.output));
32396
+ getSafeLogger()?.warn("review", "Adversarial reviewer returned empty pass with no inspection trail \u2014 re-prompted", {
32397
+ storyId: ctx.input.story.id,
32398
+ event: "review.adversarial.inspection_trail.reprompted",
32399
+ recovered: secondParsed !== null
32400
+ });
32401
+ return secondParsed ? { ...turn, output: secondTurn.output, estimatedCostUsd: costUsd } : { ...turn, estimatedCostUsd: costUsd };
32402
+ }
32105
32403
  var FAIL_OPEN, ADVERSARIAL_REQUOTE_RECOVERED_EVENT = "review.adversarial.finding.requote_recovered", ADVERSARIAL_REQUOTE_FAILED_EVENT = "review.adversarial.finding.requote_failed", DEFAULT_MAX_REQUOTES = 5, adversarialParseRetry = (input) => makeParseRetryStrategy({
32106
32404
  validate: (parsed) => validateAdversarialShape(parsed) !== null,
32107
32405
  reviewerKind: "adversarial",
@@ -32139,11 +32437,15 @@ var init_adversarial_review = __esm(() => {
32139
32437
  retry: (input) => adversarialParseRetry(input),
32140
32438
  async hopBody(initialPrompt, ctx) {
32141
32439
  const turn = await ctx.sendWithParseRetry(initialPrompt);
32142
- const parsed = validateAdversarialShape(tryParseLLMJson(turn.output));
32440
+ const rawObject = tryParseLLMJson(turn.output);
32441
+ const parsed = validateAdversarialShape(rawObject);
32143
32442
  if (!parsed)
32144
32443
  return turn;
32145
32444
  if (ctx.input.mode !== "ref")
32146
32445
  return turn;
32446
+ const inspectionGuard = await maybeRepromptForInspection(turn, parsed, rawObject, ctx);
32447
+ if (inspectionGuard)
32448
+ return inspectionGuard;
32147
32449
  const regroundEnabled = ctx.input.adversarialConfig.acRegroundOnDrop !== false;
32148
32450
  if (regroundEnabled) {
32149
32451
  const firstShape = { passed: parsed.passed, findings: parsed.findings };
@@ -33277,20 +33579,23 @@ function createDrainDeadline(deadlineMs) {
33277
33579
  };
33278
33580
  }
33279
33581
  async function runQualityCommand(opts) {
33280
- const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2 } = opts;
33582
+ const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2, stripEnvVars } = opts;
33281
33583
  const startTime = Date.now();
33282
33584
  const logger = getSafeLogger();
33283
33585
  logger?.info("quality", `Running ${commandName}`, { storyId, commandName, command, workdir });
33284
33586
  try {
33587
+ const baseEnv = {
33588
+ ...process.env
33589
+ };
33590
+ for (const key of stripEnvVars ?? []) {
33591
+ delete baseEnv[key];
33592
+ }
33285
33593
  const proc = _qualityRunnerDeps.spawn({
33286
33594
  cmd: ["/bin/sh", "-c", command],
33287
33595
  cwd: workdir,
33288
33596
  stdout: "pipe",
33289
33597
  stderr: "pipe",
33290
- env: {
33291
- ...process.env,
33292
- ...env2 ?? {}
33293
- }
33598
+ env: { ...baseEnv, ...env2 ?? {} }
33294
33599
  });
33295
33600
  let timedOut = false;
33296
33601
  let exitedBeforeSigkill = false;
@@ -33776,6 +34081,18 @@ var init_adapters = __esm(() => {
33776
34081
  init_typecheck();
33777
34082
  });
33778
34083
 
34084
+ // src/operations/verbatim-warn.ts
34085
+ function warnOnDroppedVerbatimAcs(prd, specContent, featureName) {
34086
+ const missing = findMissingVerbatimAcs(specContent, prd);
34087
+ if (missing.length > 0) {
34088
+ getSafeLogger()?.warn("plan", "[verbatim] spec acceptance criteria dropped from PRD \u2014 run spec-review --prd before executing", { featureName, missingCount: missing.length, missing });
34089
+ }
34090
+ }
34091
+ var init_verbatim_warn = __esm(() => {
34092
+ init_logger2();
34093
+ init_prd();
34094
+ });
34095
+
33779
34096
  // src/operations/plan.ts
33780
34097
  var planInteractiveOp;
33781
34098
  var init_plan = __esm(() => {
@@ -33783,6 +34100,7 @@ var init_plan = __esm(() => {
33783
34100
  init_config();
33784
34101
  init_schema2();
33785
34102
  init_prompts();
34103
+ init_verbatim_warn();
33786
34104
  planInteractiveOp = {
33787
34105
  kind: "run",
33788
34106
  name: "plan-interactive",
@@ -33822,9 +34140,10 @@ ${outputFormat}`, overridable: false }
33822
34140
  parse(output, input, _ctx) {
33823
34141
  return validatePlanOutput(output, input.featureName, input.branchName);
33824
34142
  },
33825
- verify: async (parsed, _input, _ctx) => {
34143
+ verify: async (parsed, input, _ctx) => {
33826
34144
  if (!parsed.userStories || parsed.userStories.length === 0)
33827
34145
  return null;
34146
+ warnOnDroppedVerbatimAcs(parsed, input.specContent, input.featureName);
33828
34147
  return parsed;
33829
34148
  },
33830
34149
  recover: async (input, ctx) => {
@@ -33875,13 +34194,39 @@ function validateRefinedPrd(prd) {
33875
34194
  validateRefinedStory(story);
33876
34195
  return prd;
33877
34196
  }
33878
- var NEGATIVE_PATH_TOKENS, planRefineOp;
34197
+ async function readMissingVerbatimAcs(input) {
34198
+ const content = await _planRefineDeps.readFile(input.outputPath);
34199
+ if (!content)
34200
+ return [];
34201
+ try {
34202
+ const prd = validatePlanOutput(content, input.featureName, input.branchName);
34203
+ return findMissingVerbatimAcs(input.specContent, prd);
34204
+ } catch {
34205
+ getSafeLogger()?.debug("plan", "Skipped [verbatim] self-heal \u2014 draft PRD not yet parseable", {
34206
+ featureName: input.featureName
34207
+ });
34208
+ return [];
34209
+ }
34210
+ }
34211
+ var _planRefineDeps, NEGATIVE_PATH_TOKENS, planRefineOp;
33879
34212
  var init_plan_refine = __esm(() => {
33880
34213
  init_retry();
33881
34214
  init_config();
33882
34215
  init_errors();
34216
+ init_logger2();
34217
+ init_prd();
33883
34218
  init_schema2();
33884
34219
  init_prompts();
34220
+ init_verbatim_warn();
34221
+ _planRefineDeps = {
34222
+ readFile: async (path3) => {
34223
+ try {
34224
+ return await Bun.file(path3).text();
34225
+ } catch {
34226
+ return null;
34227
+ }
34228
+ }
34229
+ };
33885
34230
  NEGATIVE_PATH_TOKENS = [
33886
34231
  "error",
33887
34232
  "fail",
@@ -33945,19 +34290,30 @@ ${outputFormat}`,
33945
34290
  };
33946
34291
  },
33947
34292
  async hopBody(initialPrompt, ctx) {
34293
+ const builder = new PlanPromptBuilder;
33948
34294
  const turn1 = await ctx.sendWithParseRetry(initialPrompt);
33949
- const refinePrompt = new PlanPromptBuilder().buildRefineContinuation(ctx.input.outputPath);
33950
- const turn2 = await ctx.send(refinePrompt);
33951
- return {
33952
- ...turn2,
33953
- estimatedCostUsd: (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0)
33954
- };
34295
+ const turn2 = await ctx.send(builder.buildRefineContinuation(ctx.input.outputPath));
34296
+ let totalCost = (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0);
34297
+ let last = turn2;
34298
+ const missing = await readMissingVerbatimAcs(ctx.input);
34299
+ if (missing.length > 0) {
34300
+ getSafeLogger()?.info("plan", "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn", {
34301
+ featureName: ctx.input.featureName,
34302
+ missingCount: missing.length
34303
+ });
34304
+ const turn3 = await ctx.send(builder.buildVerbatimRepair(missing, ctx.input.outputPath));
34305
+ totalCost += turn3.estimatedCostUsd ?? 0;
34306
+ last = turn3;
34307
+ }
34308
+ return { ...last, estimatedCostUsd: totalCost };
33955
34309
  },
33956
34310
  parse(output, input) {
33957
34311
  return validatePlanOutput(output, input.featureName, input.branchName);
33958
34312
  },
33959
- verify: async (parsed, _input, _ctx) => {
33960
- return validateRefinedPrd(parsed);
34313
+ verify: async (parsed, input, _ctx) => {
34314
+ const validated = validateRefinedPrd(parsed);
34315
+ warnOnDroppedVerbatimAcs(validated, input.specContent, input.featureName);
34316
+ return validated;
33961
34317
  },
33962
34318
  recover: async (input, ctx) => {
33963
34319
  const content = await ctx.readFile(input.outputPath);
@@ -35091,6 +35447,17 @@ function parseRefinementResponse(response, criteria) {
35091
35447
  return fallbackCriteria(criteria);
35092
35448
  }
35093
35449
  }
35450
+ function refinementWouldFallback(response) {
35451
+ if (!response || !response.trim())
35452
+ return true;
35453
+ try {
35454
+ const fromFence = extractJsonFromMarkdown(response);
35455
+ const cleaned = stripTrailingCommas(fromFence !== response ? fromFence : response);
35456
+ return !Array.isArray(JSON.parse(cleaned));
35457
+ } catch {
35458
+ return true;
35459
+ }
35460
+ }
35094
35461
  function fallbackCriteria(criteria, storyId = "") {
35095
35462
  return criteria.map((c) => ({
35096
35463
  original: c,
@@ -35106,6 +35473,7 @@ var acceptanceRefineOp;
35106
35473
  var init_acceptance_refine = __esm(() => {
35107
35474
  init_refinement();
35108
35475
  init_config();
35476
+ init_logger2();
35109
35477
  init_prompts();
35110
35478
  acceptanceRefineOp = {
35111
35479
  kind: "complete",
@@ -35128,6 +35496,9 @@ var init_acceptance_refine = __esm(() => {
35128
35496
  };
35129
35497
  },
35130
35498
  parse(output, input, _ctx) {
35499
+ if (refinementWouldFallback(output)) {
35500
+ getSafeLogger()?.warn("acceptance", "AC refinement returned no usable JSON \u2014 falling back to unrefined criteria", { storyId: input.storyId, criteriaCount: input.criteria.length, responseBytes: output?.length ?? 0 });
35501
+ }
35131
35502
  const items = parseRefinementResponse(output, input.criteria);
35132
35503
  return items.map((item) => ({ ...item, storyId: item.storyId || input.storyId }));
35133
35504
  }
@@ -35279,6 +35650,25 @@ function evaluateRepromptTrigger2(shape, input) {
35279
35650
  return { shouldReprompt: false };
35280
35651
  return { shouldReprompt: true, acDropped: dropped };
35281
35652
  }
35653
+ async function maybeRepromptForInspection2(turn, parsed, rawObject, ctx) {
35654
+ if (ctx.input.mode !== "ref")
35655
+ return null;
35656
+ if (ctx.input.semanticConfig.demandInspectionTrail === false)
35657
+ return null;
35658
+ if (!parsed.passed || parsed.findings.length !== 0)
35659
+ return null;
35660
+ if (hasInspectionTrail(rawObject))
35661
+ return null;
35662
+ const secondTurn = await ctx.send(ReviewPromptBuilder.demandInspection());
35663
+ const costUsd = (turn.estimatedCostUsd ?? 0) + (secondTurn.estimatedCostUsd ?? 0);
35664
+ const secondParsed = validateLLMShape(tryParseLLMJson(secondTurn.output));
35665
+ getSafeLogger()?.warn("review", "Semantic reviewer returned empty pass with no inspection trail \u2014 re-prompted", {
35666
+ storyId: ctx.input.story.id,
35667
+ event: "review.semantic.inspection_trail.reprompted",
35668
+ recovered: secondParsed !== null
35669
+ });
35670
+ return secondParsed ? { ...turn, output: secondTurn.output, estimatedCostUsd: costUsd } : { ...turn, estimatedCostUsd: costUsd };
35671
+ }
35282
35672
  async function performSemanticReground(turn, firstParsed, drops, ctx) {
35283
35673
  const threshold = ctx.input.blockingThreshold ?? "error";
35284
35674
  const acceptanceCriteria = ctx.input.story.acceptanceCriteria;
@@ -35399,9 +35789,13 @@ async function requoteBlockingFindings(findings, ctx) {
35399
35789
  }
35400
35790
  var FAIL_OPEN2, SEMANTIC_REQUOTE_RECOVERED_EVENT = "review.semantic.finding.requote_recovered", SEMANTIC_REQUOTE_FAILED_EVENT = "review.semantic.finding.requote_failed", DEFAULT_MAX_REQUOTES2 = 5, semanticReviewHopBody = async (initialPrompt, ctx) => {
35401
35791
  const turn = await ctx.sendWithParseRetry(initialPrompt);
35402
- const parsed = validateLLMShape(tryParseLLMJson(turn.output));
35792
+ const rawObject = tryParseLLMJson(turn.output);
35793
+ const parsed = validateLLMShape(rawObject);
35403
35794
  if (!parsed)
35404
35795
  return turn;
35796
+ const inspectionGuard = await maybeRepromptForInspection2(turn, parsed, rawObject, ctx);
35797
+ if (inspectionGuard)
35798
+ return inspectionGuard;
35405
35799
  const requoted = await requoteBlockingFindings(parsed.findings, ctx);
35406
35800
  if (requoted.changed) {
35407
35801
  const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
@@ -36870,7 +37264,7 @@ function buildVerifierFindings(verdict, categorization) {
36870
37264
  return [];
36871
37265
  }
36872
37266
  }
36873
- function parseVerdictFromStdout(output, _input, _ctx) {
37267
+ function parseVerdictFromStdout(output, input, _ctx) {
36874
37268
  if (!output || !output.trim()) {
36875
37269
  throw new ParseValidationError("verifier produced no stdout");
36876
37270
  }
@@ -36883,6 +37277,18 @@ function parseVerdictFromStdout(output, _input, _ctx) {
36883
37277
  throw new ParseValidationError("verifier stdout JSON missing required VerifierVerdict fields");
36884
37278
  }
36885
37279
  const categorization = categorizeVerdict(verdict, verdict.tests.allPassing === true);
37280
+ getSafeLogger()?.info("verifier", "Verdict categorized", {
37281
+ storyId: input.story.id,
37282
+ approved: verdict.approved,
37283
+ success: categorization.success,
37284
+ advisoryOverride: verdict.approved === false && categorization.success,
37285
+ testsPassing: verdict.tests.allPassing,
37286
+ passCount: verdict.tests.passCount,
37287
+ failCount: verdict.tests.failCount,
37288
+ testModsDetected: verdict.testModifications.detected,
37289
+ testModsLegitimate: verdict.testModifications.legitimate,
37290
+ ...categorization.failureCategory && { failureCategory: categorization.failureCategory }
37291
+ });
36886
37292
  return {
36887
37293
  success: categorization.success,
36888
37294
  filesChanged: [],
@@ -38235,7 +38641,8 @@ var init_mechanical_lintfix_strategy = __esm(() => {
38235
38641
  commandName: "lintFix",
38236
38642
  command,
38237
38643
  workdir: input.workdir,
38238
- storyId: input.storyId
38644
+ storyId: input.storyId,
38645
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38239
38646
  });
38240
38647
  return { applied: true, exitCode: result.exitCode };
38241
38648
  }
@@ -38293,7 +38700,8 @@ var init_mechanical_formatfix_strategy = __esm(() => {
38293
38700
  commandName: "formatFix",
38294
38701
  command,
38295
38702
  workdir: input.workdir,
38296
- storyId: input.storyId
38703
+ storyId: input.storyId,
38704
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38297
38705
  });
38298
38706
  return { applied: true, exitCode: result.exitCode };
38299
38707
  }
@@ -38326,7 +38734,8 @@ var init_lint_check = __esm(() => {
38326
38734
  commandName: "lint",
38327
38735
  command: command ?? "",
38328
38736
  workdir: input.workdir,
38329
- storyId: input.storyId
38737
+ storyId: input.storyId,
38738
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38330
38739
  });
38331
38740
  if (result.exitCode === 0) {
38332
38741
  return { success: true, findings: [], durationMs: Date.now() - start };
@@ -38575,7 +38984,8 @@ var init_typecheck_check = __esm(() => {
38575
38984
  commandName: "typecheck",
38576
38985
  command: command ?? "",
38577
38986
  workdir: input.workdir,
38578
- storyId: input.storyId
38987
+ storyId: input.storyId,
38988
+ stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
38579
38989
  });
38580
38990
  if (result.exitCode === 0) {
38581
38991
  return { success: true, findings: [], durationMs: Date.now() - start };
@@ -38740,6 +39150,7 @@ var init_operations = __esm(() => {
38740
39150
  init_call();
38741
39151
  init_plan();
38742
39152
  init_plan_refine();
39153
+ init_verbatim_warn();
38743
39154
  init_decompose2();
38744
39155
  init_build_hop_callback();
38745
39156
  init_classify_route();
@@ -39550,13 +39961,14 @@ async function resolveLintScope(args) {
39550
39961
  function resolveScopedTemplate(reviewCommands, qualityCommands) {
39551
39962
  return reviewCommands.lintScoped ?? qualityCommands?.lintScoped;
39552
39963
  }
39553
- async function runLintCommand(workdir, storyId, env2, command) {
39964
+ async function runLintCommand(workdir, storyId, env2, command, stripEnvVars) {
39554
39965
  return runQualityCommand({
39555
39966
  commandName: SCOPED_LINT_CHECK,
39556
39967
  command,
39557
39968
  workdir,
39558
39969
  storyId,
39559
- env: env2
39970
+ env: env2,
39971
+ stripEnvVars
39560
39972
  });
39561
39973
  }
39562
39974
  function toReviewCheck(result) {
@@ -39597,7 +40009,7 @@ async function runScopedLintCheck(args) {
39597
40009
  storyId: args.storyId,
39598
40010
  reason: scope.degradedReason
39599
40011
  });
39600
- const fullResult2 = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand);
40012
+ const fullResult2 = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand, args.stripEnvVars);
39601
40013
  return withLintScope(attachLintFindings(toReviewCheck(fullResult2), args.lintOutputFormat, args.workdir), scope, "degraded");
39602
40014
  }
39603
40015
  logger?.info("review", "lint_scope_empty", { storyId: args.storyId });
@@ -39622,19 +40034,19 @@ async function runScopedLintCheck(args) {
39622
40034
  }
39623
40035
  if (scopedTemplate) {
39624
40036
  const scopedCommand = scopedTemplate.replaceAll("{{files}}", scope.files.map(shellQuotePath4).join(" "));
39625
- const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand);
40037
+ const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand, args.stripEnvVars);
39626
40038
  return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
39627
40039
  }
39628
40040
  if (!scope.degradedReason && isSupportedDerivedScopedCommand(fullLintCommand)) {
39629
40041
  const scopedCommand = appendFilesToCommand(fullLintCommand, scope.files);
39630
- const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand);
40042
+ const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand, args.stripEnvVars);
39631
40043
  return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
39632
40044
  }
39633
40045
  logger?.warn("review", "lint_scope_degraded", {
39634
40046
  storyId: args.storyId,
39635
40047
  reason: scope.degradedReason ?? "unsupported_scoped_command_shape"
39636
40048
  });
39637
- const fullResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand);
40049
+ const fullResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand, args.stripEnvVars);
39638
40050
  if (fullResult.exitCode === 0)
39639
40051
  return withLintScope(toReviewCheck(fullResult), scope, "degraded");
39640
40052
  const parsed = parseLintOutput(fullResult.output, args.lintOutputFormat ?? "auto", {
@@ -40360,8 +40772,8 @@ async function resolveCommand(check2, config2, executionConfig, workdir, quality
40360
40772
  }
40361
40773
  return null;
40362
40774
  }
40363
- async function runCheck(check2, command, workdir, storyId, env2) {
40364
- const result = await runQualityCommand({ commandName: check2, command, workdir, storyId, env: env2 });
40775
+ async function runCheck(check2, command, workdir, storyId, env2, stripEnvVars) {
40776
+ const result = await runQualityCommand({ commandName: check2, command, workdir, storyId, env: env2, stripEnvVars });
40365
40777
  return {
40366
40778
  check: check2,
40367
40779
  command: result.command,
@@ -40567,8 +40979,9 @@ async function runReview(opts) {
40567
40979
  story,
40568
40980
  storyGitRef,
40569
40981
  env: env2,
40982
+ stripEnvVars: naxConfig?.quality?.stripEnvVars ?? [],
40570
40983
  naxIgnoreIndex
40571
- }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2), workdir);
40984
+ }) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2, naxConfig?.quality?.stripEnvVars ?? []), workdir);
40572
40985
  checks3.push(result);
40573
40986
  if (result.success) {
40574
40987
  logger?.info("review", `${checkName} passed`, {
@@ -41611,6 +42024,9 @@ Output ONLY the JSON object. Do not include markdown fences or explanation.`;
41611
42024
 
41612
42025
  Review the draft with a strict self-audit mindset. Re-read the codebase context and compare the PRD against it. Focus only on the issues below, then rewrite the PRD if needed.
41613
42026
 
42027
+ #### spec-ac-preservation
42028
+ Enumerate every acceptance criterion the spec states. Confirm each one appears in some story's acceptanceCriteria \u2014 never drop a spec AC during this audit. If an AC looks unsupported by the current codebase, keep it: the story may be adding that capability. Any AC the spec tags \`[verbatim]\` MUST appear character-for-character in an acceptanceCriteria entry \u2014 preserve every backtick-quoted command, file path, regex, and count exactly. If a \`[verbatim]\` AC is missing or altered, restore it verbatim.
42029
+
41614
42030
  #### ac-testable
41615
42031
  For each acceptance criterion, ask whether the assertion is observable through a return value, exception, log output, file content, or state change. If any AC is not directly testable, rewrite it so it is observable.
41616
42032
 
@@ -41623,7 +42039,7 @@ Check whether any sentence in any description contradicts an acceptance criterio
41623
42039
  #### codebase-fit
41624
42040
  For each story, verify:
41625
42041
  1. Proposed files, helpers, tests, dependencies, and implementation notes match the codebase context. Remove invented helpers, files, call sites, or dependencies unless the change clearly requires creating them.
41626
- 2. Each acceptance criterion's semantic meaning matches the spec's actual interface and data flow. Criteria that assert incorrect parameter semantics, wrong data flow, or behavior that contradicts the spec must be corrected or removed. Cross-check each AC against the spec's interface definitions, pseudocode, and design notes.
42042
+ 2. Each acceptance criterion's semantic meaning matches the spec's actual interface and data flow. Criteria that assert incorrect parameter semantics, wrong data flow, or behavior that contradicts the spec must be corrected. Never delete an AC that restates a spec AC \u2014 correct its wording to match the spec instead. Cross-check each AC against the spec's interface definitions, pseudocode, and design notes.
41627
42043
 
41628
42044
  #### contextfiles-spec-alignment
41629
42045
  For each story, compare contextFiles against files the spec explicitly lists as context (e.g., in "Context Files" sections). Ensure the most critical spec-recommended files are included, up to the 5-file limit. If a spec-recommended file is absent, add it (removing the least critical one if already at 5). Files the story will CREATE must not appear here.
@@ -41641,6 +42057,23 @@ If a story changes existing behavior, extracts a shared helper, extends an exist
41641
42057
  Check each story's title, description, scope, contextFiles, and acceptance criteria for internal consistency. If the story says a file or command is in scope anywhere else, do not list it as out of scope. If the title or acceptance criteria clearly include CLI, output, tests, or helper extraction work, the Scope section must reflect that accurately.
41642
42058
 
41643
42059
  Write the revised PRD to this file path: ${outputFilePath}
42060
+ Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
42061
+ }
42062
+ buildVerbatimRepair(missingAcs, outputFilePath) {
42063
+ const list = missingAcs.map((ac) => `- ${ac}`).join(`
42064
+ `);
42065
+ return `Your revised PRD dropped or altered acceptance criteria the spec marked \`[verbatim]\`. These are load-bearing executable checks (greps, file-existence checks, regex/count assertions, or architectural invariants) and MUST survive character-for-character \u2014 paraphrasing destroys the verification mechanism.
42066
+
42067
+ The following \`[verbatim]\` spec acceptance criteria are missing or altered in the PRD:
42068
+
42069
+ ${list}
42070
+
42071
+ For each one:
42072
+ - Add it to the \`acceptanceCriteria\` array of the single most relevant user story.
42073
+ - Preserve every backtick-quoted command, file path, regex, and count exactly as written in the spec. Do not paraphrase, retag, split, or move them into a description.
42074
+ - Do not remove or weaken any acceptance criteria that are already correct.
42075
+
42076
+ Write the corrected PRD to this file path: ${outputFilePath}
41644
42077
  Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
41645
42078
  }
41646
42079
  build(specContent, codebaseContext, outputFilePath, packages, packageDetails, projectProfile, proposers) {
@@ -43916,13 +44349,13 @@ var init_factory = __esm(() => {
43916
44349
 
43917
44350
  // src/execution/pid-registry.ts
43918
44351
  import { existsSync as existsSync7 } from "fs";
43919
- import { appendFile as appendFile2 } from "fs/promises";
43920
44352
 
43921
44353
  class PidRegistry {
43922
44354
  workdir;
43923
44355
  pidsFilePath;
43924
44356
  pids = new Set;
43925
44357
  frozen = false;
44358
+ writeQueueTail = Promise.resolve();
43926
44359
  constructor(workdir, _platform) {
43927
44360
  this.workdir = workdir;
43928
44361
  this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
@@ -43943,15 +44376,8 @@ class PidRegistry {
43943
44376
  return;
43944
44377
  }
43945
44378
  this.pids.add(pid);
43946
- const entry = {
43947
- pid,
43948
- spawnedAt: new Date().toISOString(),
43949
- workdir: this.workdir
43950
- };
43951
44379
  try {
43952
- const line = `${JSON.stringify(entry)}
43953
- `;
43954
- await appendFile2(this.pidsFilePath, line);
44380
+ await this.enqueueWrite();
43955
44381
  logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
43956
44382
  } catch (err) {
43957
44383
  logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
@@ -43963,7 +44389,7 @@ class PidRegistry {
43963
44389
  const logger = getSafeLogger();
43964
44390
  this.pids.delete(pid);
43965
44391
  try {
43966
- await this.writePidsFile();
44392
+ await this.enqueueWrite();
43967
44393
  logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
43968
44394
  } catch (err) {
43969
44395
  logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
@@ -43982,8 +44408,8 @@ class PidRegistry {
43982
44408
  const killPromises = pids.map((pid) => this.killPidTree(pid));
43983
44409
  await Promise.allSettled(killPromises);
43984
44410
  try {
43985
- await Bun.write(this.pidsFilePath, "");
43986
44411
  this.pids.clear();
44412
+ await this.enqueueWrite();
43987
44413
  logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
43988
44414
  } catch (err) {
43989
44415
  logger?.warn("pid-registry", "Failed to clear registry file", {
@@ -44202,6 +44628,37 @@ class PidRegistry {
44202
44628
  getPids() {
44203
44629
  return Array.from(this.pids);
44204
44630
  }
44631
+ snapshot() {
44632
+ return Array.from(this.pids);
44633
+ }
44634
+ async flush() {
44635
+ await this.writeQueueTail;
44636
+ }
44637
+ async readPidsFromDisk() {
44638
+ try {
44639
+ if (!existsSync7(this.pidsFilePath))
44640
+ return [];
44641
+ const content = await Bun.file(this.pidsFilePath).text();
44642
+ return content.split(`
44643
+ `).filter((line) => line.trim()).map((line) => {
44644
+ try {
44645
+ return JSON.parse(line).pid;
44646
+ } catch {
44647
+ return null;
44648
+ }
44649
+ }).filter((pid) => pid !== null);
44650
+ } catch {
44651
+ return [];
44652
+ }
44653
+ }
44654
+ enqueueWrite() {
44655
+ this.writeQueueTail = this.writeQueueTail.then(() => this.writePidsFile().catch((err) => {
44656
+ getSafeLogger()?.warn("pid-registry", "Failed to flush PID file \u2014 on-disk registry may be stale", {
44657
+ error: errorMessage(err)
44658
+ });
44659
+ }));
44660
+ return this.writeQueueTail;
44661
+ }
44205
44662
  }
44206
44663
  var PID_REGISTRY_FILE = ".nax-pids", PID_TREE_KILL_GRACE_MS = 250, _pidRegistryDeps;
44207
44664
  var init_pid_registry = __esm(() => {
@@ -52048,7 +52505,7 @@ var init_effectiveness = __esm(() => {
52048
52505
  });
52049
52506
 
52050
52507
  // src/execution/progress.ts
52051
- import { appendFile as appendFile3, mkdir as mkdir7 } from "fs/promises";
52508
+ import { appendFile as appendFile2, mkdir as mkdir7 } from "fs/promises";
52052
52509
  import { join as join44 } from "path";
52053
52510
  async function appendProgress(featureDir, storyId, status, message) {
52054
52511
  await mkdir7(featureDir, { recursive: true });
@@ -52056,7 +52513,7 @@ async function appendProgress(featureDir, storyId, status, message) {
52056
52513
  const timestamp = new Date().toISOString();
52057
52514
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
52058
52515
  `;
52059
- await appendFile3(progressPath, entry);
52516
+ await appendFile2(progressPath, entry);
52060
52517
  }
52061
52518
  var init_progress = () => {};
52062
52519
 
@@ -52163,6 +52620,7 @@ var init_completion = __esm(() => {
52163
52620
  const logger = getLogger();
52164
52621
  const isBatch = ctx.stories.length > 1;
52165
52622
  const sessionCost = ctx.agentResult?.estimatedCostUsd || 0;
52623
+ const persistPrd = ctx.skipPrdPersistence !== true;
52166
52624
  const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
52167
52625
  const storyStartTime = ctx.storyStartTime || new Date().toISOString();
52168
52626
  if (isBatch) {
@@ -52187,7 +52645,9 @@ var init_completion = __esm(() => {
52187
52645
  }
52188
52646
  }
52189
52647
  for (const completedStory of ctx.stories) {
52190
- markStoryPassed(ctx.prd, completedStory.id);
52648
+ if (persistPrd) {
52649
+ markStoryPassed(ctx.prd, completedStory.id);
52650
+ }
52191
52651
  const costPerStory = sessionCost / ctx.stories.length;
52192
52652
  logger.info("completion", "Story passed", {
52193
52653
  storyId: completedStory.id,
@@ -52219,7 +52679,9 @@ var init_completion = __esm(() => {
52219
52679
  }
52220
52680
  }
52221
52681
  }
52222
- await _completionDeps.savePRD(ctx.prd, prdPath);
52682
+ if (persistPrd) {
52683
+ await _completionDeps.savePRD(ctx.prd, prdPath);
52684
+ }
52223
52685
  const updatedCounts = countStories(ctx.prd);
52224
52686
  logger.info("completion", "Progress update", {
52225
52687
  storyId: ctx.story.id,
@@ -53022,7 +53484,7 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
53022
53484
  data.reviewReason = r.reviewReason;
53023
53485
  return { success: success2, data };
53024
53486
  }
53025
- function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage) {
53487
+ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage, progressData = {}) {
53026
53488
  if (isTddPhase)
53027
53489
  return;
53028
53490
  if (opName === "semantic-review" || opName === "adversarial-review")
@@ -53030,7 +53492,8 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
53030
53492
  const built = buildPhaseOutcomeLogData(storyId, opName, output, durationMs);
53031
53493
  if (!built)
53032
53494
  return;
53033
- const { success: success2, data } = built;
53495
+ const { success: success2 } = built;
53496
+ const data = { ...built.data, ...progressData };
53034
53497
  const logger = getSafeLogger();
53035
53498
  const message = formatPhaseResultMessage(opName, success2, stage);
53036
53499
  if (stage === "rectification") {
@@ -53093,17 +53556,21 @@ function logUnifiedReviewPhaseResult(storyId, opName, output) {
53093
53556
  truncated: findingsCount > findingsSummary.length
53094
53557
  });
53095
53558
  }
53096
- async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false) {
53559
+ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false, progress) {
53097
53560
  const logger = getSafeLogger();
53098
53561
  const opName = slot.op.name;
53562
+ const progressData = progress ? { phaseIndex: progress.index, totalPhases: progress.total } : {};
53099
53563
  const isTddPhase = isThreeSession && TDD_OP_NAMES.has(opName);
53100
53564
  const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
53101
53565
  let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
53102
53566
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
53103
53567
  if (isTddPhase) {
53104
- logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName });
53568
+ logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName, ...progressData });
53105
53569
  } else if (isThreeSession && opName === "full-suite-gate") {
53106
- logger?.info("tdd", "-> Running full test suite gate (before Verifier)", { storyId: ctx.storyId });
53570
+ logger?.info("tdd", "-> Running full test suite gate (before Verifier)", {
53571
+ storyId: ctx.storyId,
53572
+ ...progressData
53573
+ });
53107
53574
  }
53108
53575
  logUnifiedReviewPhaseStart(ctx.storyId, opName);
53109
53576
  const phaseStartedAt = Date.now();
@@ -53113,7 +53580,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
53113
53580
  phaseOutputs[opName] = output;
53114
53581
  emitReviewDecision(ctx, opName, output);
53115
53582
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
53116
- logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage);
53583
+ logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
53117
53584
  if (isTddPhase) {
53118
53585
  const durationMs = Date.now() - phaseStartedAt;
53119
53586
  logger?.info("tdd", `Session complete: ${opName}`, {
@@ -53282,9 +53749,13 @@ class ExecutionPlan {
53282
53749
  const phaseOutputs = {};
53283
53750
  const startedAt = Date.now();
53284
53751
  const logger = getSafeLogger();
53285
- for (const phase of collectOrderedPhases(this.state)) {
53752
+ const orderedPhases = collectOrderedPhases(this.state);
53753
+ for (const [phaseIndex, phase] of orderedPhases.entries()) {
53286
53754
  try {
53287
- await runPhase(this.ctx, phase.slot, phaseCosts, phaseOutputs, this.isThreeSession);
53755
+ await runPhase(this.ctx, phase.slot, phaseCosts, phaseOutputs, this.isThreeSession, {
53756
+ index: phaseIndex + 1,
53757
+ total: orderedPhases.length
53758
+ });
53288
53759
  } catch (error48) {
53289
53760
  logger?.error("story-orchestrator", "Phase threw unexpected error", {
53290
53761
  storyId: this.ctx.storyId,
@@ -56949,7 +57420,7 @@ function renderProposals(proposals, runId, observationCount) {
56949
57420
  }
56950
57421
 
56951
57422
  // src/plugins/builtin/curator/rollup.ts
56952
- import { appendFile as appendFile4, mkdir as mkdir10, writeFile } from "fs/promises";
57423
+ import { appendFile as appendFile3, mkdir as mkdir10, writeFile } from "fs/promises";
56953
57424
  import * as path14 from "path";
56954
57425
  async function appendToRollup(observations, rollupPath) {
56955
57426
  try {
@@ -56965,7 +57436,7 @@ async function appendToRollup(observations, rollupPath) {
56965
57436
  const newLines = `${observations.map((o) => JSON.stringify(o)).join(`
56966
57437
  `)}
56967
57438
  `;
56968
- await appendFile4(rollupPath, newLines);
57439
+ await appendFile3(rollupPath, newLines);
56969
57440
  } catch {}
56970
57441
  }
56971
57442
  var init_rollup = () => {};
@@ -57883,7 +58354,7 @@ var package_default;
57883
58354
  var init_package = __esm(() => {
57884
58355
  package_default = {
57885
58356
  name: "@nathapp/nax",
57886
- version: "0.68.1",
58357
+ version: "0.68.3",
57887
58358
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
57888
58359
  type: "module",
57889
58360
  bin: {
@@ -57978,8 +58449,8 @@ var init_version = __esm(() => {
57978
58449
  NAX_VERSION = package_default.version;
57979
58450
  NAX_COMMIT = (() => {
57980
58451
  try {
57981
- if (/^[0-9a-f]{6,10}$/.test("5d69e4e4"))
57982
- return "5d69e4e4";
58452
+ if (/^[0-9a-f]{6,10}$/.test("a1007103"))
58453
+ return "a1007103";
57983
58454
  } catch {}
57984
58455
  try {
57985
58456
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -59395,6 +59866,9 @@ async function handleRunCompletion(options) {
59395
59866
  if (options.pluginProviderCache) {
59396
59867
  await options.pluginProviderCache.disposeAll();
59397
59868
  }
59869
+ clearLanguageCache();
59870
+ clearWorkspaceCache();
59871
+ clearGitRootCache();
59398
59872
  const finalCounts = countStories(prd);
59399
59873
  const fallbackAggregate = deriveRunFallbackAggregates(allStoryMetrics);
59400
59874
  pipelineEventBus.emit({
@@ -59500,7 +59974,10 @@ var init_run_completion = __esm(() => {
59500
59974
  init_metrics();
59501
59975
  init_event_bus();
59502
59976
  init_prd();
59977
+ init_detector();
59503
59978
  init_scratch_purge();
59979
+ init_workspace();
59980
+ init_smart_runner();
59504
59981
  init_run_regression();
59505
59982
  _runCompletionDeps = {
59506
59983
  runDeferredRegression,
@@ -59598,7 +60075,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
59598
60075
  var DEFAULT_MAX_BATCH_SIZE = 4;
59599
60076
 
59600
60077
  // src/pipeline/subscribers/events-writer.ts
59601
- import { appendFile as appendFile5, mkdir as mkdir14 } from "fs/promises";
60078
+ import { appendFile as appendFile4, mkdir as mkdir14 } from "fs/promises";
59602
60079
  import { basename as basename14, join as join73 } from "path";
59603
60080
  function wireEventsWriter(bus, feature, runId, workdir) {
59604
60081
  const logger = getSafeLogger();
@@ -59613,7 +60090,7 @@ function wireEventsWriter(bus, feature, runId, workdir) {
59613
60090
  await mkdir14(eventsDir, { recursive: true });
59614
60091
  dirReady = true;
59615
60092
  }
59616
- await appendFile5(eventsFile, `${JSON.stringify(line)}
60093
+ await appendFile4(eventsFile, `${JSON.stringify(line)}
59617
60094
  `);
59618
60095
  } catch (err) {
59619
60096
  logger?.warn("events-writer", "Failed to write event line (non-fatal)", {
@@ -60623,7 +61100,7 @@ function extractQuoteTriples(reason) {
60623
61100
  }
60624
61101
  return triples;
60625
61102
  }
60626
- function normalizeWs2(s) {
61103
+ function normalizeWs3(s) {
60627
61104
  return s.replace(/\s+/g, " ").trim();
60628
61105
  }
60629
61106
  async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
@@ -60637,7 +61114,7 @@ async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
60637
61114
  const end = Math.min(lines.length, triple.line + CONTEXT_LINES);
60638
61115
  const window2 = lines.slice(start, end).join(`
60639
61116
  `);
60640
- return normalizeWs2(window2).toLowerCase().includes(normalizeWs2(triple.quote).toLowerCase());
61117
+ return normalizeWs3(window2).toLowerCase().includes(normalizeWs3(triple.quote).toLowerCase());
60641
61118
  }
60642
61119
  async function verifyEscalationQuotes(reason, workdir, storyId, deps = _quoteIntegrityDeps) {
60643
61120
  const triples = extractQuoteTriples(reason);
@@ -61510,9 +61987,13 @@ var exports_parallel_worker = {};
61510
61987
  __export(exports_parallel_worker, {
61511
61988
  executeStoryInWorktree: () => executeStoryInWorktree,
61512
61989
  executeParallelBatch: () => executeParallelBatch,
61990
+ buildWorktreePipelineContext: () => buildWorktreePipelineContext,
61513
61991
  _parallelWorkerDeps: () => _parallelWorkerDeps
61514
61992
  });
61515
61993
  import { join as join79 } from "path";
61994
+ function buildWorktreePipelineContext(base, _story) {
61995
+ return { ...base, prd: structuredClone(base.prd) };
61996
+ }
61516
61997
  async function executeStoryInWorktree(story, worktreePath, dependencyContext, context, routing, eventEmitter) {
61517
61998
  const logger = getSafeLogger();
61518
61999
  try {
@@ -61526,7 +62007,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
61526
62007
  }
61527
62008
  }
61528
62009
  const pipelineContext = {
61529
- ...context,
62010
+ ...buildWorktreePipelineContext(context, story),
61530
62011
  config: context.config,
61531
62012
  rootConfig: context.rootConfig,
61532
62013
  story,
@@ -61826,6 +62307,7 @@ var init_parallel_batch = __esm(() => {
61826
62307
  // src/execution/unified-executor.ts
61827
62308
  var exports_unified_executor = {};
61828
62309
  __export(exports_unified_executor, {
62310
+ reconcileBatchOutcome: () => reconcileBatchOutcome,
61829
62311
  executeUnified: () => executeUnified,
61830
62312
  _unifiedExecutorDeps: () => _unifiedExecutorDeps
61831
62313
  });
@@ -61949,7 +62431,10 @@ async function executeUnified(ctx, initialPrd) {
61949
62431
  const readyStories = getAllReadyStories(prd);
61950
62432
  const batch = _unifiedExecutorDeps.selectIndependentBatch(readyStories, ctx.parallelCount);
61951
62433
  if (batch.length > 1) {
61952
- for (const story of batch) {
62434
+ const batchAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62435
+ const batchCounts = countStories(prd);
62436
+ const batchBaseDone = batchCounts.total - batchCounts.pending;
62437
+ for (const [batchIndex, story] of batch.entries()) {
61953
62438
  const modelTier2 = story.routing?.modelTier ?? ctx.config.autoMode.complexityRouting?.[story.routing?.complexity ?? "medium"] ?? "balanced";
61954
62439
  pipelineEventBus.emit({
61955
62440
  type: "story:started",
@@ -61957,7 +62442,7 @@ async function executeUnified(ctx, initialPrd) {
61957
62442
  story: { id: story.id, title: story.title, status: story.status, attempts: story.attempts },
61958
62443
  workdir: ctx.workdir,
61959
62444
  modelTier: modelTier2,
61960
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62445
+ agent: batchAgent,
61961
62446
  iteration: iterations
61962
62447
  });
61963
62448
  logger?.info("story.start", `${story.title}`, {
@@ -61965,6 +62450,9 @@ async function executeUnified(ctx, initialPrd) {
61965
62450
  storyTitle: story.title,
61966
62451
  complexity: story.routing?.complexity ?? "unknown",
61967
62452
  modelTier: modelTier2,
62453
+ agent: batchAgent,
62454
+ storyNumber: batchBaseDone + batchIndex + 1,
62455
+ storyTotal: batchCounts.total,
61968
62456
  attempt: story.attempts + 1
61969
62457
  });
61970
62458
  }
@@ -61984,6 +62472,7 @@ async function executeUnified(ctx, initialPrd) {
61984
62472
  config: ctx.config,
61985
62473
  rootConfig: ctx.config,
61986
62474
  prd,
62475
+ skipPrdPersistence: true,
61987
62476
  projectDir: ctx.workdir,
61988
62477
  naxIgnoreIndex,
61989
62478
  hooks: ctx.hooks,
@@ -62031,6 +62520,8 @@ async function executeUnified(ctx, initialPrd) {
62031
62520
  abortSignal: ctx.abortSignal
62032
62521
  }, pipelineResult);
62033
62522
  }
62523
+ reconcileBatchOutcome(prd, batchResult);
62524
+ await savePRD(prd, ctx.prdPath);
62034
62525
  await pipelineEventBus.drain();
62035
62526
  totalCost += batchResult.totalCost;
62036
62527
  storiesCompleted += batchResult.completed.length;
@@ -62121,6 +62612,8 @@ async function executeUnified(ctx, initialPrd) {
62121
62612
  }
62122
62613
  }
62123
62614
  const modelTier2 = singleSelection.routing.modelTier;
62615
+ const singleAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62616
+ const singleCounts = countStories(prd);
62124
62617
  pipelineEventBus.emit({
62125
62618
  type: "story:started",
62126
62619
  storyId: singleStory.id,
@@ -62132,7 +62625,7 @@ async function executeUnified(ctx, initialPrd) {
62132
62625
  },
62133
62626
  workdir: ctx.workdir,
62134
62627
  modelTier: modelTier2,
62135
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62628
+ agent: singleAgent,
62136
62629
  iteration: iterations
62137
62630
  });
62138
62631
  logger?.info("story.start", `${singleStory.title}`, {
@@ -62140,6 +62633,9 @@ async function executeUnified(ctx, initialPrd) {
62140
62633
  storyTitle: singleStory.title,
62141
62634
  complexity: singleSelection.routing.complexity ?? "unknown",
62142
62635
  modelTier: modelTier2,
62636
+ agent: singleAgent,
62637
+ storyNumber: singleCounts.total - singleCounts.pending + 1,
62638
+ storyTotal: singleCounts.total,
62143
62639
  attempt: singleStory.attempts + 1
62144
62640
  });
62145
62641
  const singleIter = await _unifiedExecutorDeps.runIteration(ctx, prd, singleSelection, iterations, totalCost, allStoryMetrics);
@@ -62191,6 +62687,8 @@ async function executeUnified(ctx, initialPrd) {
62191
62687
  }
62192
62688
  }
62193
62689
  const modelTier = selection.routing.modelTier;
62690
+ const seqAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
62691
+ const seqCounts = countStories(prd);
62194
62692
  pipelineEventBus.emit({
62195
62693
  type: "story:started",
62196
62694
  storyId: selection.story.id,
@@ -62202,7 +62700,7 @@ async function executeUnified(ctx, initialPrd) {
62202
62700
  },
62203
62701
  workdir: ctx.workdir,
62204
62702
  modelTier,
62205
- agent: ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config),
62703
+ agent: seqAgent,
62206
62704
  iteration: iterations
62207
62705
  });
62208
62706
  logger?.info("story.start", `${selection.story.title}`, {
@@ -62210,6 +62708,9 @@ async function executeUnified(ctx, initialPrd) {
62210
62708
  storyTitle: selection.story.title,
62211
62709
  complexity: selection.routing.complexity ?? "unknown",
62212
62710
  modelTier,
62711
+ agent: seqAgent,
62712
+ storyNumber: seqCounts.total - seqCounts.pending + 1,
62713
+ storyTotal: seqCounts.total,
62213
62714
  attempt: selection.story.attempts + 1
62214
62715
  });
62215
62716
  const iter = await _unifiedExecutorDeps.runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
@@ -62268,6 +62769,18 @@ async function executeUnified(ctx, initialPrd) {
62268
62769
  return buildResult2("max-iterations");
62269
62770
  } finally {}
62270
62771
  }
62772
+ function reconcileBatchOutcome(prd, batchResult) {
62773
+ for (const story of batchResult.completed) {
62774
+ markStoryPassed(prd, story.id);
62775
+ }
62776
+ for (const conflict of batchResult.mergeConflicts) {
62777
+ if (conflict.rectified) {
62778
+ markStoryPassed(prd, conflict.story.id);
62779
+ } else {
62780
+ markStoryFailed(prd, conflict.story.id, undefined, "merge-conflict");
62781
+ }
62782
+ }
62783
+ }
62271
62784
  var TERMINAL_ACTIONS, _unifiedExecutorDeps;
62272
62785
  var init_unified_executor = __esm(() => {
62273
62786
  init_agents();
@@ -94922,6 +95435,7 @@ class DebatePlanStrategy {
94922
95435
  });
94923
95436
  if (debateResult.outcome !== "failed" && debateResult.output) {
94924
95437
  const prd2 = validatePlanOutput(debateResult.output, ctx.options.feature, ctx.branchName);
95438
+ warnOnDroppedVerbatimAcs(prd2, ctx.specContent, ctx.options.feature);
94925
95439
  const withProject2 = { ...prd2, project: ctx.projectName };
94926
95440
  return _debatePlanDeps.writeOrRecoverPrd(ctx, withProject2);
94927
95441
  }