@nathapp/nax 0.70.8 → 0.71.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.
Files changed (2) hide show
  1. package/dist/nax.js +958 -111
  2. package/package.json +3 -2
package/dist/nax.js CHANGED
@@ -16813,7 +16813,7 @@ var init_schemas_debate = __esm(() => {
16813
16813
  });
16814
16814
 
16815
16815
  // src/config/schemas-execution.ts
16816
- var AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, WorktreeDependenciesConfigSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema;
16816
+ var AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, WorktreeDependenciesConfigSchema, FlakeDetectionConfigSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema;
16817
16817
  var init_schemas_execution = __esm(() => {
16818
16818
  init_zod();
16819
16819
  init_schemas_model();
@@ -16879,6 +16879,12 @@ var init_schemas_execution = __esm(() => {
16879
16879
  });
16880
16880
  }
16881
16881
  });
16882
+ FlakeDetectionConfigSchema = exports_external.object({
16883
+ enabled: exports_external.boolean().default(true),
16884
+ probeRuns: exports_external.number().int().min(1).max(20).default(2),
16885
+ maxProbesPerGate: exports_external.number().int().min(1).max(100).default(5),
16886
+ probeTimeoutSeconds: exports_external.number().int().min(5).max(600).default(60)
16887
+ });
16882
16888
  ExecutionConfigSchema = exports_external.object({
16883
16889
  maxIterations: exports_external.number().int().positive({ message: "maxIterations must be > 0" }),
16884
16890
  iterationDelayMs: exports_external.number().int().nonnegative(),
@@ -16904,7 +16910,13 @@ var init_schemas_execution = __esm(() => {
16904
16910
  mode: "off",
16905
16911
  setupCommand: null
16906
16912
  }),
16907
- storyIsolation: exports_external.enum(["shared", "worktree"]).default("shared")
16913
+ storyIsolation: exports_external.enum(["shared", "worktree"]).default("shared"),
16914
+ flakeDetection: FlakeDetectionConfigSchema.default({
16915
+ enabled: true,
16916
+ probeRuns: 2,
16917
+ maxProbesPerGate: 5,
16918
+ probeTimeoutSeconds: 60
16919
+ })
16908
16920
  });
16909
16921
  QualityConfigSchema = exports_external.object({
16910
16922
  requireTypecheck: exports_external.boolean().default(true),
@@ -17407,7 +17419,13 @@ var init_schemas3 = __esm(() => {
17407
17419
  mode: "off",
17408
17420
  setupCommand: null
17409
17421
  },
17410
- storyIsolation: "shared"
17422
+ storyIsolation: "shared",
17423
+ flakeDetection: {
17424
+ enabled: true,
17425
+ probeRuns: 2,
17426
+ maxProbesPerGate: 5,
17427
+ probeTimeoutSeconds: 60
17428
+ }
17411
17429
  }),
17412
17430
  quality: QualityConfigSchema.default({
17413
17431
  requireTypecheck: true,
@@ -17738,6 +17756,22 @@ var init_schema = __esm(() => {
17738
17756
  init_defaults();
17739
17757
  });
17740
17758
 
17759
+ // src/review/severity.ts
17760
+ function isBlockingSeverity(sev, threshold = "error") {
17761
+ return (SEVERITY_RANK[sev] ?? 0) >= SEVERITY_RANK[threshold];
17762
+ }
17763
+ var SEVERITY_RANK;
17764
+ var init_severity = __esm(() => {
17765
+ SEVERITY_RANK = {
17766
+ info: 0,
17767
+ unverifiable: 0,
17768
+ low: 1,
17769
+ warning: 1,
17770
+ error: 2,
17771
+ critical: 3
17772
+ };
17773
+ });
17774
+
17741
17775
  // src/log-format/types.ts
17742
17776
  var EMOJI;
17743
17777
  var init_types2 = __esm(() => {
@@ -18037,6 +18071,31 @@ function formatRunSummary(summary, options) {
18037
18071
  return lines.join(`
18038
18072
  `);
18039
18073
  }
18074
+ function formatAdvisorySummary(findings, options) {
18075
+ if (findings.length === 0)
18076
+ return "";
18077
+ const { mode, useColor = true } = options;
18078
+ if (mode === "json") {
18079
+ return JSON.stringify(findings);
18080
+ }
18081
+ const c = useColor ? source_default : createNoopChalk();
18082
+ const sorted = [...findings].sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0));
18083
+ const lines = [];
18084
+ lines.push("");
18085
+ lines.push(c.yellow("\u2500".repeat(60)));
18086
+ lines.push(c.bold(c.yellow(` ${EMOJI.warning} NON-BLOCKING REVIEW FINDINGS (${findings.length})`)));
18087
+ lines.push(c.yellow("\u2500".repeat(60)));
18088
+ for (const f of sorted) {
18089
+ const location = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : undefined;
18090
+ const parts = [`[${f.severity}]`, f.storyId ?? "unknown", location, f.category].filter((v) => typeof v === "string" && v.length > 0);
18091
+ lines.push(` ${c.gray(parts.join(" \xB7 "))}`);
18092
+ lines.push(` ${f.issue}`);
18093
+ }
18094
+ lines.push(c.yellow("\u2500".repeat(60)));
18095
+ lines.push("");
18096
+ return lines.join(`
18097
+ `);
18098
+ }
18040
18099
  function createNoopChalk() {
18041
18100
  const noop = (s) => s;
18042
18101
  return {
@@ -18054,6 +18113,7 @@ function createNoopChalk() {
18054
18113
  var CONSUMED_META_KEYS;
18055
18114
  var init_formatter = __esm(() => {
18056
18115
  init_source();
18116
+ init_severity();
18057
18117
  init_types2();
18058
18118
  CONSUMED_META_KEYS = [
18059
18119
  "agentName",
@@ -18397,7 +18457,11 @@ function mergePackageConfig(root, packageOverride) {
18397
18457
  ...root.execution.regressionGate,
18398
18458
  ...packageOverride.execution?.regressionGate
18399
18459
  },
18400
- verificationTimeoutSeconds: packageOverride.execution?.verificationTimeoutSeconds ?? root.execution.verificationTimeoutSeconds
18460
+ verificationTimeoutSeconds: packageOverride.execution?.verificationTimeoutSeconds ?? root.execution.verificationTimeoutSeconds,
18461
+ flakeDetection: {
18462
+ ...root.execution.flakeDetection,
18463
+ ...packageOverride.execution?.flakeDetection
18464
+ }
18401
18465
  },
18402
18466
  review: {
18403
18467
  ...root.review,
@@ -28560,7 +28624,10 @@ function getNextStory(prd, currentStoryId, maxRetries) {
28560
28624
  return currentStory;
28561
28625
  }
28562
28626
  }
28563
- return prd.userStories.find((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed" && hasSatisfiedDependencies(s, storyIds, completedIds)) ?? null;
28627
+ const eligible = prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed" && hasSatisfiedDependencies(s, storyIds, completedIds));
28628
+ if (eligible.length === 0)
28629
+ return null;
28630
+ return eligible.reduce((best, s) => (s.priority ?? 0) > (best.priority ?? 0) ? s : best);
28564
28631
  }
28565
28632
  function isComplete(prd) {
28566
28633
  return prd.userStories.every((s) => s.passes || s.status === "passed" || s.status === "skipped");
@@ -28643,6 +28710,31 @@ function markStorySkipped(prd, storyId) {
28643
28710
  story.status = "skipped";
28644
28711
  }
28645
28712
  }
28713
+ function resetStoryToPending(prd, storyId) {
28714
+ const story = prd.userStories.find((s) => s.id === storyId);
28715
+ if (!story)
28716
+ return;
28717
+ story.status = "pending";
28718
+ story.attempts = 0;
28719
+ story.failureCategory = undefined;
28720
+ story.failureStage = undefined;
28721
+ if (story.routing) {
28722
+ story.routing = {
28723
+ ...story.routing,
28724
+ ...story.routing.initialModelTier !== undefined && {
28725
+ modelTier: story.routing.initialModelTier
28726
+ },
28727
+ agent: story.routing.initialAgent
28728
+ };
28729
+ }
28730
+ story.escalations = [];
28731
+ }
28732
+ function setStoryPriority(prd, storyId, priority) {
28733
+ const story = prd.userStories.find((s) => s.id === storyId);
28734
+ if (story) {
28735
+ story.priority = priority;
28736
+ }
28737
+ }
28646
28738
  function markStoryPaused(prd, storyId) {
28647
28739
  const story = prd.userStories.find((s) => s.id === storyId);
28648
28740
  if (story) {
@@ -32739,22 +32831,6 @@ var init_category_fix_target = __esm(() => {
32739
32831
  init_ac_structural_counterfactual();
32740
32832
  });
32741
32833
 
32742
- // src/review/severity.ts
32743
- function isBlockingSeverity(sev, threshold = "error") {
32744
- return (SEVERITY_RANK[sev] ?? 0) >= SEVERITY_RANK[threshold];
32745
- }
32746
- var SEVERITY_RANK;
32747
- var init_severity = __esm(() => {
32748
- SEVERITY_RANK = {
32749
- info: 0,
32750
- unverifiable: 0,
32751
- low: 1,
32752
- warning: 1,
32753
- error: 2,
32754
- critical: 3
32755
- };
32756
- });
32757
-
32758
32834
  // src/review/adversarial-helpers.ts
32759
32835
  function validateAdversarialShape(parsed) {
32760
32836
  if (typeof parsed !== "object" || parsed === null)
@@ -33849,15 +33925,36 @@ function createNoOpReviewAuditor() {
33849
33925
  return {
33850
33926
  recordDispatch() {},
33851
33927
  recordDecision() {},
33852
- async flush() {}
33928
+ async flush() {},
33929
+ getAdvisoryFindings() {
33930
+ return [];
33931
+ }
33853
33932
  };
33854
33933
  }
33934
+ function toAdvisorySummaryEntries(entry) {
33935
+ if (!entry.advisoryFindings || entry.advisoryFindings.length === 0)
33936
+ return [];
33937
+ return entry.advisoryFindings.map((raw) => {
33938
+ const f = raw;
33939
+ return {
33940
+ storyId: entry.storyId,
33941
+ featureName: entry.featureName,
33942
+ reviewer: entry.reviewer,
33943
+ severity: f.severity ?? "info",
33944
+ category: f.category,
33945
+ file: f.file,
33946
+ line: f.line,
33947
+ issue: f.issue ?? "(no description)"
33948
+ };
33949
+ });
33950
+ }
33855
33951
 
33856
33952
  class ReviewAuditor {
33857
33953
  _runId;
33858
33954
  _outputDir;
33859
33955
  _queue = Promise.resolve();
33860
33956
  _dispatches = new Map;
33957
+ _advisoryFindings = [];
33861
33958
  constructor(_runId, _outputDir) {
33862
33959
  this._runId = _runId;
33863
33960
  this._outputDir = _outputDir;
@@ -33866,6 +33963,7 @@ class ReviewAuditor {
33866
33963
  this._dispatches.set(auditKey(entry.reviewer, entry.storyId), entry);
33867
33964
  }
33868
33965
  recordDecision(entry) {
33966
+ this._advisoryFindings.push(...toAdvisorySummaryEntries(entry));
33869
33967
  const key = auditKey(entry.reviewer, entry.storyId);
33870
33968
  const dispatch = this._dispatches.get(key);
33871
33969
  this._dispatches.delete(key);
@@ -33894,6 +33992,9 @@ class ReviewAuditor {
33894
33992
  async flush() {
33895
33993
  await this._queue;
33896
33994
  }
33995
+ getAdvisoryFindings() {
33996
+ return [...this._advisoryFindings];
33997
+ }
33897
33998
  }
33898
33999
  async function writeReviewAudit(entry) {
33899
34000
  try {
@@ -34841,6 +34942,17 @@ var init_self_verification = __esm(() => {
34841
34942
  });
34842
34943
 
34843
34944
  // src/quality/index.ts
34945
+ var exports_quality = {};
34946
+ __export(exports_quality, {
34947
+ runQualityCommand: () => runQualityCommand,
34948
+ resolveSelfVerificationPromptInput: () => resolveSelfVerificationPromptInput,
34949
+ resolveQualityTestCommands: () => resolveQualityTestCommands,
34950
+ resolveDefaultQualityCommands: () => resolveDefaultQualityCommands,
34951
+ parseSelfVerificationMarker: () => parseSelfVerificationMarker,
34952
+ clearCommandDefaultsCache: () => clearCommandDefaultsCache,
34953
+ _commandResolverDeps: () => _commandResolverDeps,
34954
+ _commandDefaultsDeps: () => _commandDefaultsDeps
34955
+ });
34844
34956
  var init_quality = __esm(() => {
34845
34957
  init_runner();
34846
34958
  init_command_resolver();
@@ -39527,7 +39639,8 @@ var init_full_suite_gate = __esm(() => {
39527
39639
  status: "skipped",
39528
39640
  estimatedCostUsd: 0,
39529
39641
  attempts: 0,
39530
- findings: []
39642
+ findings: [],
39643
+ rawOutput: ""
39531
39644
  };
39532
39645
  }
39533
39646
  const gateCtx = await deps.resolveGateContext(input, ctx);
@@ -39546,7 +39659,15 @@ var init_full_suite_gate = __esm(() => {
39546
39659
  });
39547
39660
  const testResult = await deps.runTests(input, gateCtx);
39548
39661
  if (testResult.passed) {
39549
- return { success: true, passed: true, status: "passed", estimatedCostUsd: 0, attempts: 0, findings: [] };
39662
+ return {
39663
+ success: true,
39664
+ passed: true,
39665
+ status: "passed",
39666
+ estimatedCostUsd: 0,
39667
+ attempts: 0,
39668
+ findings: [],
39669
+ rawOutput: testResult.output
39670
+ };
39550
39671
  }
39551
39672
  if (testResult.timedOut) {
39552
39673
  const acceptOnTimeout = ctxConfig?.execution?.regressionGate?.acceptOnTimeout ?? true;
@@ -39560,7 +39681,8 @@ var init_full_suite_gate = __esm(() => {
39560
39681
  status: "passed-on-timeout",
39561
39682
  estimatedCostUsd: 0,
39562
39683
  attempts: 0,
39563
- findings: []
39684
+ findings: [],
39685
+ rawOutput: testResult.output
39564
39686
  };
39565
39687
  }
39566
39688
  logger.warn("verify[regression]", "Full-suite timed out (failing)", {
@@ -39572,7 +39694,8 @@ var init_full_suite_gate = __esm(() => {
39572
39694
  status: "timeout",
39573
39695
  estimatedCostUsd: 0,
39574
39696
  attempts: 0,
39575
- findings: []
39697
+ findings: [],
39698
+ rawOutput: testResult.output
39576
39699
  };
39577
39700
  }
39578
39701
  const findings = testSummaryToFindings(testResult.parsedSummary);
@@ -39597,10 +39720,19 @@ var init_full_suite_gate = __esm(() => {
39597
39720
  status: "execution-failed",
39598
39721
  estimatedCostUsd: 0,
39599
39722
  attempts: 0,
39600
- findings: [synth]
39723
+ findings: [synth],
39724
+ rawOutput: testResult.output
39601
39725
  };
39602
39726
  }
39603
- return { success: false, passed: false, status: "failed", estimatedCostUsd: 0, attempts: 0, findings };
39727
+ return {
39728
+ success: false,
39729
+ passed: false,
39730
+ status: "failed",
39731
+ estimatedCostUsd: 0,
39732
+ attempts: 0,
39733
+ findings,
39734
+ rawOutput: testResult.output
39735
+ };
39604
39736
  }
39605
39737
  };
39606
39738
  });
@@ -47347,6 +47479,190 @@ var init_session = __esm(() => {
47347
47479
  init_types7();
47348
47480
  });
47349
47481
 
47482
+ // src/verification/flake-probe.ts
47483
+ function escapeRegex2(input) {
47484
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47485
+ }
47486
+ function shellQuote(value) {
47487
+ return `'${value.replace(/'/g, "'\\''")}'`;
47488
+ }
47489
+ function buildIsolationCommand(baseCommand, failure, framework) {
47490
+ const file3 = failure.file;
47491
+ const name = failure.testName;
47492
+ switch (framework) {
47493
+ case "pytest":
47494
+ return `${baseCommand} ${shellQuote(`${file3}::${name}`)}`;
47495
+ case "go":
47496
+ return `${baseCommand} -run ${shellQuote(`^${escapeRegex2(name)}$`)}`;
47497
+ case "bun":
47498
+ case "jest":
47499
+ case "vitest":
47500
+ return `${baseCommand} ${file3} -t ${shellQuote(escapeRegex2(name))}`;
47501
+ default:
47502
+ throw new NaxError(`[flake-probe] unsupported framework: ${framework}`, "FLAKE_PROBE_UNSUPPORTED_FRAMEWORK", {
47503
+ stage: "verify",
47504
+ framework
47505
+ });
47506
+ }
47507
+ }
47508
+ async function runFlakeProbe(input) {
47509
+ const { framework, baseCommand, failure, cwd, probeRuns, probeTimeoutSeconds } = input;
47510
+ if (failure.file === "unknown" || framework === "unknown") {
47511
+ return {
47512
+ verdict: "unprobeable",
47513
+ reason: failure.file === "unknown" && framework === "unknown" ? "unknown file and framework" : failure.file === "unknown" ? "unknown file" : "unknown framework"
47514
+ };
47515
+ }
47516
+ const command = buildIsolationCommand(baseCommand, failure, framework);
47517
+ let probePasses = 0;
47518
+ for (let i = 0;i < probeRuns; i += 1) {
47519
+ let result;
47520
+ try {
47521
+ result = await _flakeProbeDeps.execute(command, probeTimeoutSeconds, undefined, { cwd });
47522
+ } catch {
47523
+ continue;
47524
+ }
47525
+ if (result.success && result.countsTowardEscalation) {
47526
+ probePasses += 1;
47527
+ }
47528
+ }
47529
+ if (probePasses > 0) {
47530
+ return { verdict: "flaky", probeRuns, probePasses };
47531
+ }
47532
+ return { verdict: "consistent-failure", probeRuns };
47533
+ }
47534
+ var _flakeProbeDeps;
47535
+ var init_flake_probe = __esm(() => {
47536
+ init_errors();
47537
+ init_executor();
47538
+ _flakeProbeDeps = {
47539
+ execute: executeWithTimeout
47540
+ };
47541
+ });
47542
+
47543
+ // src/verification/flake-triage.ts
47544
+ function flakeMemoKey(failure) {
47545
+ return `${failure.file ?? ""}::${failure.rule ?? ""}`;
47546
+ }
47547
+ function createQuarantineMemo() {
47548
+ const keys = new Set;
47549
+ return {
47550
+ has: (key) => keys.has(key),
47551
+ add: (key) => {
47552
+ keys.add(key);
47553
+ }
47554
+ };
47555
+ }
47556
+ async function triageFlakyFindings(input) {
47557
+ const { findings, diff, flakeDetection, baseCommand, cwd, framework, quarantineMemo } = input;
47558
+ const logger = getSafeLogger();
47559
+ const result = [];
47560
+ const keys = [];
47561
+ const reasons = [];
47562
+ if (findings.length === 0) {
47563
+ return { findings: result, quarantineReport: { keys, reasons } };
47564
+ }
47565
+ if (!flakeDetection.enabled) {
47566
+ for (const f of findings)
47567
+ result.push({ ...f });
47568
+ return { findings: result, quarantineReport: { keys, reasons } };
47569
+ }
47570
+ const changedTestSet = new Set(diff.changedTestFiles.map(basename5));
47571
+ const mappedTestSet = new Set(diff.mappedTestFiles.map(basename5));
47572
+ const candidates = findings.filter((f) => isProbeCandidate(f, changedTestSet, mappedTestSet));
47573
+ if (candidates.length > flakeDetection.maxProbesPerGate) {
47574
+ logger?.info("flake-triage", `Skipping flake triage \u2014 ${candidates.length} candidates exceed maxProbesPerGate=${flakeDetection.maxProbesPerGate}`);
47575
+ reasons.push(`skipped: ${candidates.length} candidates exceed maxProbesPerGate=${flakeDetection.maxProbesPerGate}`);
47576
+ for (const f of findings)
47577
+ result.push({ ...f });
47578
+ return { findings: result, quarantineReport: { keys, reasons } };
47579
+ }
47580
+ for (const finding of findings) {
47581
+ const copy = { ...finding };
47582
+ if (copy.category !== "failed-test") {
47583
+ result.push(copy);
47584
+ continue;
47585
+ }
47586
+ const key = flakeMemoKey(copy);
47587
+ if (quarantineMemo.has(key)) {
47588
+ copy.category = "flaky-test";
47589
+ keys.push(key);
47590
+ reasons.push(`quarantined (memo): ${key}`);
47591
+ result.push(copy);
47592
+ continue;
47593
+ }
47594
+ if (!isProbeCandidate(copy, changedTestSet, mappedTestSet)) {
47595
+ result.push(copy);
47596
+ continue;
47597
+ }
47598
+ const failure = {
47599
+ file: copy.file ?? "unknown",
47600
+ testName: copy.rule ?? "",
47601
+ error: copy.message,
47602
+ stackTrace: []
47603
+ };
47604
+ const probeInput = {
47605
+ framework,
47606
+ baseCommand,
47607
+ failure,
47608
+ cwd,
47609
+ probeRuns: flakeDetection.probeRuns,
47610
+ probeTimeoutSeconds: flakeDetection.probeTimeoutSeconds
47611
+ };
47612
+ let verdict;
47613
+ try {
47614
+ verdict = await _flakeTriageDeps.runFlakeProbe({ failure, config: flakeDetection, probeInput });
47615
+ } catch (err) {
47616
+ logger?.warn("flake-triage", `Probe dependency threw for ${key} \u2014 keeping finding blocking`, {
47617
+ file: copy.file,
47618
+ testName: copy.rule,
47619
+ error: err instanceof Error ? err.message : String(err)
47620
+ });
47621
+ result.push(copy);
47622
+ continue;
47623
+ }
47624
+ if (verdict.verdict === "flaky") {
47625
+ copy.category = "flaky-test";
47626
+ copy.meta = { ...copy.meta ?? {}, probeRuns: verdict.probeRuns, probePasses: verdict.probePasses };
47627
+ quarantineMemo.add(key);
47628
+ keys.push(key);
47629
+ reasons.push(`quarantined: ${key}`);
47630
+ }
47631
+ result.push(copy);
47632
+ }
47633
+ return { findings: result, quarantineReport: { keys, reasons } };
47634
+ }
47635
+ function basename5(path7) {
47636
+ const i = path7.lastIndexOf("/");
47637
+ return i === -1 ? path7 : path7.slice(i + 1);
47638
+ }
47639
+ function isProbeCandidate(finding, changedTestSet, mappedTestSet) {
47640
+ if (finding.category !== "failed-test")
47641
+ return false;
47642
+ if (!finding.file || finding.file === "unknown")
47643
+ return false;
47644
+ if (!finding.rule)
47645
+ return false;
47646
+ const base = basename5(finding.file);
47647
+ if (changedTestSet.has(base) || changedTestSet.has(finding.file))
47648
+ return false;
47649
+ if (mappedTestSet.has(base) || mappedTestSet.has(finding.file))
47650
+ return false;
47651
+ return true;
47652
+ }
47653
+ var NULL_QUARANTINE_MEMO, _flakeTriageDeps;
47654
+ var init_flake_triage = __esm(() => {
47655
+ init_logger2();
47656
+ init_flake_probe();
47657
+ NULL_QUARANTINE_MEMO = {
47658
+ has: () => false,
47659
+ add: () => {}
47660
+ };
47661
+ _flakeTriageDeps = {
47662
+ runFlakeProbe: async (call) => runFlakeProbe(call.probeInput)
47663
+ };
47664
+ });
47665
+
47350
47666
  // src/runtime/session-run-hop.ts
47351
47667
  function createSessionRunHop(sessionManager, getAgentManager) {
47352
47668
  return async (agentName, options) => {
@@ -47470,7 +47786,7 @@ __export(exports_runtime, {
47470
47786
  CostAggregator: () => CostAggregator,
47471
47787
  AgentStreamEventBus: () => AgentStreamEventBus
47472
47788
  });
47473
- import { basename as basename5, join as join30 } from "path";
47789
+ import { basename as basename6, join as join30 } from "path";
47474
47790
  function createRuntime(config2, workdir, opts) {
47475
47791
  const runId = crypto.randomUUID();
47476
47792
  const controller = new AbortController;
@@ -47482,7 +47798,7 @@ function createRuntime(config2, workdir, opts) {
47482
47798
  const configLoader = createConfigLoader(config2);
47483
47799
  const dispatchEvents = new DispatchEventBus;
47484
47800
  const agentStreamEvents = opts?.agentStreamEvents ?? new AgentStreamEventBus;
47485
- const projectKey = config2.name?.trim() || basename5(workdir);
47801
+ const projectKey = config2.name?.trim() || basename6(workdir);
47486
47802
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
47487
47803
  const globalDir = globalOutputDir();
47488
47804
  const curatorRollupPathValue = curatorRollupPath(globalDir, config2.curator?.rollupPath);
@@ -47544,6 +47860,7 @@ function createRuntime(config2, workdir, opts) {
47544
47860
  const offWatchdog = attachAgentIdleWatchdog(agentStreamEvents, watchdogControllerRegistry, config2);
47545
47861
  const packages = createPackageRegistry(configLoader, workdir);
47546
47862
  const logger = getLogger();
47863
+ const quarantineMemo = createQuarantineMemo();
47547
47864
  let closed = false;
47548
47865
  return {
47549
47866
  runId,
@@ -47564,6 +47881,7 @@ function createRuntime(config2, workdir, opts) {
47564
47881
  packages,
47565
47882
  pidRegistry,
47566
47883
  logger,
47884
+ quarantineMemo,
47567
47885
  get signal() {
47568
47886
  return controller.signal;
47569
47887
  },
@@ -47612,6 +47930,7 @@ var init_runtime = __esm(() => {
47612
47930
  init_logger2();
47613
47931
  init_review_audit();
47614
47932
  init_session();
47933
+ init_flake_triage();
47615
47934
  init_agent_stream_events();
47616
47935
  init_cost_aggregator();
47617
47936
  init_dispatch_events();
@@ -55463,6 +55782,12 @@ async function runNonBlockingFix(args, overrides = {}) {
55463
55782
  exhausted = true;
55464
55783
  }
55465
55784
  if (!exhausted) {
55785
+ if (args.keptTreeRegressed?.()) {
55786
+ logger?.info("non-blocking-fix", "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", {
55787
+ storyId: args.storyId
55788
+ });
55789
+ return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
55790
+ }
55466
55791
  const cap = args.cfg.sourceDiffCap;
55467
55792
  if (cap) {
55468
55793
  let metrics;
@@ -55638,6 +55963,8 @@ function gateFailureKeys(gateOutput) {
55638
55963
  for (const f of extractPhaseFindings(gateOutput)) {
55639
55964
  if (f.source !== "test-runner")
55640
55965
  continue;
55966
+ if (f.category === "flaky-test")
55967
+ continue;
55641
55968
  keys.add(`${f.file ?? ""}::${f.rule ?? ""}`);
55642
55969
  }
55643
55970
  return keys;
@@ -55775,6 +56102,85 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
55775
56102
  var init_story_orchestrator_logging = __esm(() => {
55776
56103
  init_logger2();
55777
56104
  });
56105
+ // src/verification/flake-baseline-diff.ts
56106
+ async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
56107
+ try {
56108
+ const resolved = await resolveTestFilePatterns(config2, workdir, storyWorkdir);
56109
+ const baseRef = await getMergeBase(workdir);
56110
+ const changedTestFiles = await getChangedTestFiles(workdir, workdir, baseRef, storyWorkdir, [...resolved.regex]);
56111
+ const changedNonTestFiles = await getChangedNonTestFiles(workdir, baseRef, storyWorkdir, [...resolved.regex], undefined, workdir);
56112
+ const mappedTestFiles = await mapSourceToTests(changedNonTestFiles, workdir, storyWorkdir, [...resolved.globs]);
56113
+ return { changedTestFiles, mappedTestFiles };
56114
+ } catch (err) {
56115
+ getSafeLogger()?.warn("flake-triage", "Baseline diff resolution failed \u2014 skipping triage this gate (fail closed)", {
56116
+ error: errorMessage(err)
56117
+ });
56118
+ return null;
56119
+ }
56120
+ }
56121
+ var init_flake_baseline_diff = __esm(() => {
56122
+ init_logger2();
56123
+ init_test_runners();
56124
+ init_git();
56125
+ init_smart_runner();
56126
+ });
56127
+
56128
+ // src/verification/index.ts
56129
+ var init_verification = __esm(() => {
56130
+ init_executor();
56131
+ init_runners();
56132
+ init_flake_probe();
56133
+ init_flake_triage();
56134
+ init_flake_baseline_diff();
56135
+ });
56136
+
56137
+ // src/execution/story-orchestrator/flake-triage-seam.ts
56138
+ var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
56139
+ const config2 = ctx.packageView.config;
56140
+ const flakeDetection = config2.execution?.flakeDetection;
56141
+ if (!flakeDetection?.enabled) {
56142
+ return [gateFindings, { quarantinedKeys: [] }];
56143
+ }
56144
+ const framework = detectFramework(rawOutput);
56145
+ if (framework === "unknown") {
56146
+ return [gateFindings, { quarantinedKeys: [] }];
56147
+ }
56148
+ const workdir = ctx.runtime.workdir;
56149
+ const storyWorkdir = ctx.story?.workdir;
56150
+ try {
56151
+ const { resolveQualityTestCommands: resolveQualityTestCommands2 } = await Promise.resolve().then(() => (init_quality(), exports_quality));
56152
+ const { testCommand } = await resolveQualityTestCommands2(config2, workdir, storyWorkdir);
56153
+ const baseCommand = testCommand ?? config2.quality?.commands?.test;
56154
+ if (!baseCommand) {
56155
+ return [gateFindings, { quarantinedKeys: [] }];
56156
+ }
56157
+ const diff = await resolveFlakeBaselineDiff(config2, workdir, storyWorkdir);
56158
+ if (diff === null) {
56159
+ return [gateFindings, { quarantinedKeys: [] }];
56160
+ }
56161
+ const result = await triageFlakyFindings({
56162
+ findings: gateFindings,
56163
+ diff,
56164
+ flakeDetection,
56165
+ baseCommand,
56166
+ cwd: ctx.packageDir,
56167
+ framework,
56168
+ quarantineMemo: ctx.runtime.quarantineMemo
56169
+ });
56170
+ return [result.findings, { quarantinedKeys: result.quarantineReport.keys }];
56171
+ } catch (err) {
56172
+ getSafeLogger()?.warn("story-orchestrator", "Flake triage seam failed resolving context \u2014 keeping findings blocking (no quarantine)", {
56173
+ storyId: ctx.storyId,
56174
+ error: errorMessage(err)
56175
+ });
56176
+ return [gateFindings, { quarantinedKeys: [] }];
56177
+ }
56178
+ };
56179
+ var init_flake_triage_seam = __esm(() => {
56180
+ init_logger2();
56181
+ init_test_runners();
56182
+ init_verification();
56183
+ });
55778
56184
 
55779
56185
  // src/execution/story-orchestrator/review-decision.ts
55780
56186
  function toReviewDecisionPayload(opName, output) {
@@ -56051,6 +56457,7 @@ var init_run_phase = __esm(() => {
56051
56457
  init_git();
56052
56458
  init_non_blocking_fix();
56053
56459
  init_story_orchestrator_logging();
56460
+ init_flake_triage_seam();
56054
56461
  init_review_decision();
56055
56462
  init_types9();
56056
56463
  _storyOrchestratorDeps = {
@@ -56059,7 +56466,8 @@ var init_run_phase = __esm(() => {
56059
56466
  captureGitRef,
56060
56467
  prepareSemanticReviewInput,
56061
56468
  prepareAdversarialReviewInput,
56062
- runNonBlockingFix
56469
+ runNonBlockingFix,
56470
+ triage: productionTriageSeam
56063
56471
  };
56064
56472
  });
56065
56473
 
@@ -56077,10 +56485,63 @@ function gatherRectificationFindings(phaseOutputs, phases, state) {
56077
56485
  for (const phase of phases) {
56078
56486
  if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
56079
56487
  continue;
56080
- findings.push(...extractPhaseFindings(phaseOutputs[phase.slot.op.name]));
56488
+ for (const f of extractPhaseFindings(phaseOutputs[phase.slot.op.name])) {
56489
+ if (f.category === "flaky-test")
56490
+ continue;
56491
+ findings.push(f);
56492
+ }
56081
56493
  }
56082
56494
  return findings;
56083
56495
  }
56496
+ async function triageGateFindings(phaseOutputs, gateName, ctx) {
56497
+ if (!gateName)
56498
+ return { triaged: false, quarantinedKeys: [], skipped: true };
56499
+ const triage = _storyOrchestratorDeps.triage;
56500
+ if (typeof triage !== "function")
56501
+ return { triaged: false, quarantinedKeys: [], skipped: true };
56502
+ const output = phaseOutputs[gateName];
56503
+ if (output === null || output === undefined || typeof output !== "object") {
56504
+ return { triaged: false, quarantinedKeys: [], skipped: true };
56505
+ }
56506
+ const record2 = output;
56507
+ const findings = extractPhaseFindings(output);
56508
+ if (findings.length === 0) {
56509
+ return { triaged: false, quarantinedKeys: [], skipped: true };
56510
+ }
56511
+ const rawOutput = typeof record2.rawOutput === "string" ? record2.rawOutput : "";
56512
+ let triagedFindings;
56513
+ let quarantinedKeys;
56514
+ try {
56515
+ const [triaged, report] = await triage(findings, { ctx, rawOutput });
56516
+ triagedFindings = triaged;
56517
+ quarantinedKeys = report.quarantinedKeys;
56518
+ } catch (err) {
56519
+ getSafeLogger()?.warn("story-orchestrator", "Flake triage threw \u2014 keeping findings blocking (no quarantine)", {
56520
+ storyId: ctx.storyId,
56521
+ gateName,
56522
+ error: err instanceof Error ? err.message : String(err)
56523
+ });
56524
+ return { triaged: false, quarantinedKeys: [], skipped: true };
56525
+ }
56526
+ const allTestRunnersQuarantined = triagedFindings.every((f) => f.source !== "test-runner" || f.category === "flaky-test");
56527
+ const nextRecord = { ...record2, findings: triagedFindings };
56528
+ if (allTestRunnersQuarantined) {
56529
+ nextRecord.success = true;
56530
+ nextRecord.passed = true;
56531
+ }
56532
+ phaseOutputs[gateName] = nextRecord;
56533
+ if (quarantinedKeys.length > 0) {
56534
+ const logger = getSafeLogger();
56535
+ for (const key of quarantinedKeys) {
56536
+ logger?.warn("story-orchestrator", `Flake quarantined: ${key}`, {
56537
+ storyId: ctx.storyId,
56538
+ key,
56539
+ previousFailureCount: findings.length
56540
+ });
56541
+ }
56542
+ }
56543
+ return { triaged: true, quarantinedKeys, skipped: false };
56544
+ }
56084
56545
  function collectRectificationPhases(state) {
56085
56546
  return [
56086
56547
  state.fullSuiteGate,
@@ -56093,16 +56554,32 @@ function collectRectificationPhases(state) {
56093
56554
  ].filter((phase) => phase !== undefined);
56094
56555
  }
56095
56556
  async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides) {
56096
- const rectification = state.rectification;
56557
+ const rectification2 = state.rectification;
56097
56558
  const baseValidationPhases = collectRectificationPhases(state);
56098
56559
  const validationPhases = overrides?.excludePhaseKinds ? baseValidationPhases.filter((p) => !overrides.excludePhaseKinds?.includes(p.kind)) : baseValidationPhases;
56099
- if (!rectification || validationPhases.length === 0) {
56560
+ if (!rectification2 || validationPhases.length === 0) {
56100
56561
  return {};
56101
56562
  }
56102
56563
  if (ctx.runtime.signal?.aborted) {
56103
56564
  return {};
56104
56565
  }
56105
- const initialFindings = overrides?.initialFindings ? [...overrides.initialFindings] : gatherRectificationFindings(phaseOutputs, validationPhases, state);
56566
+ let initialFindings;
56567
+ if (overrides?.initialFindings) {
56568
+ initialFindings = [...overrides.initialFindings];
56569
+ } else {
56570
+ const gateName = state.fullSuiteGate?.slot.op.name;
56571
+ if (overrides?.skipGateTriage !== true) {
56572
+ const triageReport = await triageGateFindings(phaseOutputs, gateName, ctx);
56573
+ if (triageReport.skipped && gateName !== undefined) {
56574
+ getSafeLogger()?.debug("story-orchestrator", "Gate triage skipped \u2014 passthrough to fix cycle", {
56575
+ storyId: ctx.storyId,
56576
+ gateName,
56577
+ triaged: triageReport.triaged
56578
+ });
56579
+ }
56580
+ }
56581
+ initialFindings = gatherRectificationFindings(phaseOutputs, validationPhases, state);
56582
+ }
56106
56583
  if (initialFindings.length === 0) {
56107
56584
  return {};
56108
56585
  }
@@ -56117,8 +56594,8 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
56117
56594
  const cycle = {
56118
56595
  findings: [...initialFindings],
56119
56596
  iterations: [],
56120
- strategies: withIncreasingFailuresBail(overrides?.strategies ?? rectification.strategies, rectification.abortOnIncreasingFailures, rectification.consecutiveIncreasesToBail ?? 1),
56121
- config: { maxAttemptsTotal: overrides?.maxAttempts ?? rectification.maxAttempts, validatorRetries: 1 },
56597
+ strategies: withIncreasingFailuresBail(overrides?.strategies ?? rectification2.strategies, rectification2.abortOnIncreasingFailures, rectification2.consecutiveIncreasesToBail ?? 1),
56598
+ config: { maxAttemptsTotal: overrides?.maxAttempts ?? rectification2.maxAttempts, validatorRetries: 1 },
56122
56599
  validate: async (_validateCtx, opts) => {
56123
56600
  if (ctx.runtime.signal?.aborted)
56124
56601
  return { findings: [], shortCircuited: false };
@@ -56150,7 +56627,7 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
56150
56627
  break;
56151
56628
  }
56152
56629
  }
56153
- const postValidateFn = overrides?.postValidate ?? rectification.postValidate;
56630
+ const postValidateFn = overrides?.postValidate ?? rectification2.postValidate;
56154
56631
  const validated = postValidateFn ? await postValidateFn(findings, _validateCtx) : findings;
56155
56632
  return { findings: validated, shortCircuited };
56156
56633
  }
@@ -56270,7 +56747,9 @@ class ExecutionPlan {
56270
56747
  if (!resumeRectifyUsed) {
56271
56748
  resumeRectifyUsed = true;
56272
56749
  logger?.info("story-orchestrator", "Phase failed in post-rectification resume \u2014 invoking second rectification pass", { storyId: this.ctx.storyId, phase: name, source: "post-rectification-resume" });
56273
- const secondRect = await runRectification(this.ctx, this.state, phaseCosts, phaseOutputs);
56750
+ const secondRect = await runRectification(this.ctx, this.state, phaseCosts, phaseOutputs, {
56751
+ skipGateTriage: true
56752
+ });
56274
56753
  if (secondRect.rectificationExhausted) {
56275
56754
  logger?.warn("story-orchestrator", "Second rectification pass exhausted \u2014 terminal failure", {
56276
56755
  storyId: this.ctx.storyId,
@@ -56339,7 +56818,8 @@ class ExecutionPlan {
56339
56818
  extraRevalidationKinds: nonBlockingExtraPhases(advCfg),
56340
56819
  maxAttempts,
56341
56820
  postValidate: this.state.nonBlockingFixPostValidate
56342
- })
56821
+ }),
56822
+ keptTreeRegressed: () => gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId)
56343
56823
  }, {
56344
56824
  measureSourceDiff: createMeasureSourceDiff({
56345
56825
  config: this.ctx.runtime.configLoader.current(),
@@ -57874,6 +58354,27 @@ function parseQueueFile(content) {
57874
58354
  }
57875
58355
  } else if (upper === "SKIP") {
57876
58356
  guidance.push(trimmed);
58357
+ } else if (upper.startsWith("RETRY ")) {
58358
+ const storyId = trimmed.substring(6).trim();
58359
+ if (storyId) {
58360
+ commands.push({ type: "RETRY", storyId });
58361
+ } else {
58362
+ guidance.push(trimmed);
58363
+ }
58364
+ } else if (upper === "RETRY") {
58365
+ guidance.push(trimmed);
58366
+ } else if (upper.startsWith("PRIORITY ")) {
58367
+ const rest = trimmed.substring(9).trim();
58368
+ const parts = rest.split(/\s+/);
58369
+ const storyId = parts[0];
58370
+ const value = parts[1] !== undefined ? Number(parts[1]) : Number.NaN;
58371
+ if (storyId && parts.length === 2 && Number.isFinite(value)) {
58372
+ commands.push({ type: "PRIORITY", storyId, value });
58373
+ } else {
58374
+ guidance.push(trimmed);
58375
+ }
58376
+ } else if (upper === "PRIORITY") {
58377
+ guidance.push(trimmed);
57877
58378
  } else {
57878
58379
  if (inPendingSection) {
57879
58380
  guidance.push(trimmed);
@@ -57974,6 +58475,23 @@ var init_queue_check = __esm(() => {
57974
58475
  await clearQueueFile(ctx.workdir);
57975
58476
  return { action: "pause", reason: "User requested abort" };
57976
58477
  }
58478
+ if (cmd.type === "RETRY") {
58479
+ logger.warn("queue", "Retrying story by user request", { storyId: cmd.storyId });
58480
+ resetStoryToPending(ctx.prd, cmd.storyId);
58481
+ const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
58482
+ await savePRD(ctx.prd, prdPath);
58483
+ continue;
58484
+ }
58485
+ if (cmd.type === "PRIORITY") {
58486
+ logger.warn("queue", "Setting story priority by user request", {
58487
+ storyId: cmd.storyId,
58488
+ priority: cmd.value
58489
+ });
58490
+ setStoryPriority(ctx.prd, cmd.storyId, cmd.value);
58491
+ const prdPath = ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`;
58492
+ await savePRD(ctx.prd, prdPath);
58493
+ continue;
58494
+ }
57977
58495
  if (cmd.type === "SKIP") {
57978
58496
  const isTargeted = ctx.stories.some((s) => s.id === cmd.storyId);
57979
58497
  if (isTargeted) {
@@ -58163,6 +58681,7 @@ var init_pipeline = __esm(() => {
58163
58681
  init_runner4();
58164
58682
  init_events();
58165
58683
  init_stages();
58684
+ init_queue_check();
58166
58685
  init_execution_helpers();
58167
58686
  init_event_bus();
58168
58687
  });
@@ -58568,7 +59087,7 @@ __export(exports_init_context, {
58568
59087
  generatePackageContextTemplate: () => generatePackageContextTemplate,
58569
59088
  generateContextTemplate: () => generateContextTemplate
58570
59089
  });
58571
- import { basename as basename9, join as join53 } from "path";
59090
+ import { basename as basename10, join as join53 } from "path";
58572
59091
  async function bunFileExists(path15) {
58573
59092
  return Bun.file(path15).exists();
58574
59093
  }
@@ -58663,7 +59182,7 @@ async function scanProject(projectRoot) {
58663
59182
  const readmeSnippet = await readReadmeSnippet(projectRoot);
58664
59183
  const entryPoints = await detectEntryPoints(projectRoot);
58665
59184
  const configFiles = await detectConfigFiles(projectRoot);
58666
- const projectName = packageManifest?.name || basename9(projectRoot);
59185
+ const projectName = packageManifest?.name || basename10(projectRoot);
58667
59186
  return {
58668
59187
  projectName,
58669
59188
  fileTree,
@@ -60800,11 +61319,11 @@ function getSafeLogger6() {
60800
61319
  return getSafeLogger();
60801
61320
  }
60802
61321
  function extractPluginName(pluginPath) {
60803
- const basename11 = path18.basename(pluginPath);
60804
- if (basename11 === "index.ts" || basename11 === "index.js" || basename11 === "index.mjs") {
61322
+ const basename12 = path18.basename(pluginPath);
61323
+ if (basename12 === "index.ts" || basename12 === "index.js" || basename12 === "index.mjs") {
60805
61324
  return path18.basename(path18.dirname(pluginPath));
60806
61325
  }
60807
- return basename11.replace(/\.(ts|js|mjs)$/, "");
61326
+ return basename12.replace(/\.(ts|js|mjs)$/, "");
60808
61327
  }
60809
61328
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins, isTestFileFn) {
60810
61329
  const loadedPlugins = [];
@@ -61190,7 +61709,7 @@ var package_default;
61190
61709
  var init_package = __esm(() => {
61191
61710
  package_default = {
61192
61711
  name: "@nathapp/nax",
61193
- version: "0.70.8",
61712
+ version: "0.71.1",
61194
61713
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
61195
61714
  type: "module",
61196
61715
  bin: {
@@ -61201,7 +61720,7 @@ var init_package = __esm(() => {
61201
61720
  dev: "bun run bin/nax.ts",
61202
61721
  build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
61203
61722
  typecheck: "bun x tsc --noEmit && bun x tsc --noEmit -p tsconfig.contracts.json",
61204
- lint: "bun x biome check src/ bin/ && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:file-sizes",
61723
+ lint: "bun x biome check src/ bin/ && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
61205
61724
  "lint:json": "bun x biome check src/ bin/ --reporter json && bun run check:nax-error 1>&2 && bun run check:logger-storyid 1>&2",
61206
61725
  "lint:fix": "bun x biome check --write src/ bin/",
61207
61726
  "check:no-real-global-nax": "bun run scripts/check-no-real-global-nax.ts",
@@ -61212,6 +61731,7 @@ var init_package = __esm(() => {
61212
61731
  "check:nax-error:update": "bun run scripts/check-nax-error.ts --update-baseline",
61213
61732
  "check:logger-storyid": "bun run scripts/check-logger-storyid.ts",
61214
61733
  "check:logger-storyid:update": "bun run scripts/check-logger-storyid.ts --update-baseline",
61734
+ "check:log-format-layering": "bun run scripts/check-log-format-layering.ts",
61215
61735
  "check:file-sizes": "bun run scripts/check-file-sizes.ts",
61216
61736
  "check:file-sizes:update": "bun run scripts/check-file-sizes.ts --update-baseline",
61217
61737
  release: "bun scripts/release.ts",
@@ -61290,8 +61810,8 @@ var init_version = __esm(() => {
61290
61810
  NAX_VERSION = package_default.version;
61291
61811
  NAX_COMMIT = (() => {
61292
61812
  try {
61293
- if (/^[0-9a-f]{6,10}$/.test("54e59fa6"))
61294
- return "54e59fa6";
61813
+ if (/^[0-9a-f]{6,10}$/.test("62eedccd"))
61814
+ return "62eedccd";
61295
61815
  } catch {}
61296
61816
  try {
61297
61817
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -62247,10 +62767,134 @@ var init_scratch_purge = __esm(() => {
62247
62767
  now: () => Date.now()
62248
62768
  };
62249
62769
  });
62250
- // src/verification/index.ts
62251
- var init_verification = __esm(() => {
62252
- init_executor();
62253
- init_runners();
62770
+
62771
+ // src/execution/lifecycle/backfill-story-metrics.ts
62772
+ function isExecutionFailure(story) {
62773
+ return story != null && (story.status === "failed" || story.status === "regression-failed") && (story.attempts ?? 0) > 0;
62774
+ }
62775
+ function synthesizeBackfillMetric(args) {
62776
+ const { storyId, story, totalCostUsd, config: config2, defaultAgent, timestamp } = args;
62777
+ if (story != null && isExecutionFailure(story)) {
62778
+ const tier = story.routing?.modelTier ?? "balanced";
62779
+ const agent = story.routing?.agent ?? defaultAgent;
62780
+ let modelUsed = agent;
62781
+ try {
62782
+ modelUsed = resolveModelForAgent(config2.models, agent, tier, defaultAgent).model;
62783
+ } catch {}
62784
+ const escalations = story.escalations ?? [];
62785
+ const finalTier = escalations.length > 0 ? escalations[escalations.length - 1].toTier : tier;
62786
+ const attempts = (story.priorFailures?.length ?? 0) + Math.max(1, story.attempts ?? 1);
62787
+ return {
62788
+ storyId,
62789
+ complexity: story.routing?.complexity ?? "medium",
62790
+ initialComplexity: story.routing?.initialComplexity ?? story.routing?.complexity,
62791
+ modelTier: tier,
62792
+ modelUsed,
62793
+ agentUsed: agent,
62794
+ attempts,
62795
+ finalTier,
62796
+ success: false,
62797
+ cost: totalCostUsd,
62798
+ durationMs: 0,
62799
+ firstPassSuccess: false,
62800
+ startedAt: timestamp,
62801
+ completedAt: timestamp,
62802
+ source: "execution-failed",
62803
+ runtimeCrashes: 0
62804
+ };
62805
+ }
62806
+ return {
62807
+ storyId,
62808
+ complexity: story?.routing?.complexity ?? "medium",
62809
+ modelTier: "balanced",
62810
+ modelUsed: defaultAgent,
62811
+ attempts: 0,
62812
+ finalTier: "balanced",
62813
+ success: story?.passes ?? true,
62814
+ cost: totalCostUsd,
62815
+ durationMs: 0,
62816
+ firstPassSuccess: story?.passes ?? true,
62817
+ startedAt: timestamp,
62818
+ completedAt: timestamp,
62819
+ source: "completion-phase",
62820
+ runtimeCrashes: 0
62821
+ };
62822
+ }
62823
+ var init_backfill_story_metrics = __esm(() => {
62824
+ init_config();
62825
+ });
62826
+
62827
+ // src/execution/lifecycle/run-regression-triage.ts
62828
+ async function runRegressionFlakeTriage(params) {
62829
+ const {
62830
+ regressionFindings,
62831
+ testSummary,
62832
+ rawOutput,
62833
+ config: config2,
62834
+ workdir,
62835
+ testCommand,
62836
+ quarantineMemo,
62837
+ triageFn,
62838
+ flakeDetection
62839
+ } = params;
62840
+ const logger = getSafeLogger();
62841
+ const baselineDiff = await resolveFlakeBaselineDiff(config2, workdir);
62842
+ if (baselineDiff === null) {
62843
+ const untriaged = regressionFindings.filter((f) => f.category === "failed-test");
62844
+ const testFilesInFailures2 = new Set;
62845
+ for (const finding of untriaged) {
62846
+ if (finding.file)
62847
+ testFilesInFailures2.add(finding.file);
62848
+ }
62849
+ return { shortCircuit: false, triagedFailedFindings: untriaged, testFilesInFailures: testFilesInFailures2, quarantineReport: undefined };
62850
+ }
62851
+ const triageResult = await triageFn({
62852
+ findings: regressionFindings,
62853
+ diff: baselineDiff,
62854
+ flakeDetection,
62855
+ baseCommand: testCommand,
62856
+ cwd: workdir,
62857
+ framework: detectFramework(rawOutput),
62858
+ quarantineMemo
62859
+ });
62860
+ const quarantineReport = triageResult.quarantineReport.keys.length > 0 ? triageResult.quarantineReport : undefined;
62861
+ const triagedFailedFindings = triageResult.findings.filter((f) => f.category === "failed-test");
62862
+ logger?.warn("regression", "Regression detected", {
62863
+ failedTests: testSummary.failed,
62864
+ passedTests: testSummary.passed,
62865
+ quarantined: triageResult.quarantineReport.keys.length
62866
+ });
62867
+ const testFilesInFailures = new Set;
62868
+ for (const finding of triagedFailedFindings) {
62869
+ if (finding.file)
62870
+ testFilesInFailures.add(finding.file);
62871
+ }
62872
+ if (testFilesInFailures.size === 0 && triagedFailedFindings.length === 0 && quarantineReport) {
62873
+ logger?.info("regression", "All regression failures quarantined as flaky \u2014 accepting as pass", {
62874
+ quarantined: quarantineReport.keys.length
62875
+ });
62876
+ return {
62877
+ shortCircuit: true,
62878
+ result: {
62879
+ success: true,
62880
+ failedTests: 0,
62881
+ failedTestFiles: [],
62882
+ passedTests: testSummary.passed,
62883
+ rectificationAttempts: 0,
62884
+ affectedStories: [],
62885
+ storyCosts: {},
62886
+ storyDurations: {},
62887
+ storyOutcomes: {},
62888
+ quarantineReport
62889
+ }
62890
+ };
62891
+ }
62892
+ return { shortCircuit: false, triagedFailedFindings, testFilesInFailures, quarantineReport };
62893
+ }
62894
+ var init_run_regression_triage = __esm(() => {
62895
+ init_logger2();
62896
+ init_test_runners();
62897
+ init_verification();
62254
62898
  });
62255
62899
 
62256
62900
  // src/execution/lifecycle/run-regression.ts
@@ -62405,18 +63049,25 @@ async function runDeferredRegression(options) {
62405
63049
  storyOutcomes: {}
62406
63050
  };
62407
63051
  }
62408
- const affectedStories = new Set;
62409
- const affectedStoriesObjs = new Map;
62410
- logger?.warn("regression", "Regression detected", {
62411
- failedTests: testSummary.failed,
62412
- passedTests: testSummary.passed
63052
+ const regressionFindings = buildRegressionFindings(testSummary, fullSuiteResult.output);
63053
+ const quarantineMemo = options.quarantineMemo ?? NULL_QUARANTINE_MEMO;
63054
+ const triageOutcome = await runRegressionFlakeTriage({
63055
+ regressionFindings,
63056
+ testSummary,
63057
+ rawOutput: fullSuiteResult.output,
63058
+ config: config2,
63059
+ workdir,
63060
+ testCommand,
63061
+ quarantineMemo,
63062
+ triageFn: _regressionDeps.triageFlakyFindings,
63063
+ flakeDetection: config2.execution.flakeDetection
62413
63064
  });
62414
- const testFilesInFailures = new Set;
62415
- for (const failure of testSummary.failures) {
62416
- if (failure.file) {
62417
- testFilesInFailures.add(failure.file);
62418
- }
63065
+ if (triageOutcome.shortCircuit) {
63066
+ return triageOutcome.result;
62419
63067
  }
63068
+ const { testFilesInFailures, quarantineReport } = triageOutcome;
63069
+ const affectedStories = new Set;
63070
+ const affectedStoriesObjs = new Map;
62420
63071
  if (testFilesInFailures.size === 0) {
62421
63072
  logger?.warn("regression", "No test files found in failures (unmapped)");
62422
63073
  for (const story of passedStories) {
@@ -62459,7 +63110,8 @@ async function runDeferredRegression(options) {
62459
63110
  affectedStories: Array.from(affectedStories),
62460
63111
  storyCosts: {},
62461
63112
  storyDurations: {},
62462
- storyOutcomes: {}
63113
+ storyOutcomes: {},
63114
+ ...quarantineReport ? { quarantineReport } : {}
62463
63115
  };
62464
63116
  }
62465
63117
  for (const storyId of affectedStories) {
@@ -62543,7 +63195,8 @@ async function runDeferredRegression(options) {
62543
63195
  affectedStories: Array.from(affectedStories),
62544
63196
  storyCosts: storyCostAccum,
62545
63197
  storyDurations: storyDurationAccum,
62546
- storyOutcomes: storyOutcomeAccum
63198
+ storyOutcomes: storyOutcomeAccum,
63199
+ ...quarantineReport ? { quarantineReport } : {}
62547
63200
  };
62548
63201
  }
62549
63202
  logger?.warn("regression", "Full suite still failing after story rectification \u2014 continuing", {
@@ -62574,7 +63227,8 @@ async function runDeferredRegression(options) {
62574
63227
  affectedStories: Array.from(affectedStories),
62575
63228
  storyCosts: storyCostAccum,
62576
63229
  storyDurations: storyDurationAccum,
62577
- storyOutcomes: storyOutcomeAccum
63230
+ storyOutcomes: storyOutcomeAccum,
63231
+ ...quarantineReport ? { quarantineReport } : {}
62578
63232
  };
62579
63233
  }
62580
63234
  var SYNTHETIC_FINDING_OUTPUT_LIMIT = 2000, _regressionDeps;
@@ -62587,10 +63241,12 @@ var init_run_regression = __esm(() => {
62587
63241
  init_test_runners();
62588
63242
  init_git();
62589
63243
  init_verification();
63244
+ init_run_regression_triage();
62590
63245
  _regressionDeps = {
62591
63246
  runVerification: fullSuite,
62592
63247
  runFixCycle: (cycle, ctx, name) => runFixCycle(cycle, ctx, name),
62593
- parseTestOutput
63248
+ parseTestOutput,
63249
+ triageFlakyFindings
62594
63250
  };
62595
63251
  });
62596
63252
 
@@ -62614,8 +63270,10 @@ async function handleRunCompletion(options) {
62614
63270
  workdir,
62615
63271
  statusWriter,
62616
63272
  config: config2,
62617
- hooksConfig
63273
+ hooksConfig,
63274
+ exitReason
62618
63275
  } = options;
63276
+ let regressionGateFailed = false;
62619
63277
  const regressionMode = config2.execution.regressionGate?.mode;
62620
63278
  if (options.skipRegression) {} else if ((regressionMode === "deferred" || regressionMode === "per-story") && config2.quality.commands.test) {
62621
63279
  statusWriter.setPostRunPhase("regression", { status: "running" });
@@ -62625,6 +63283,7 @@ async function handleRunCompletion(options) {
62625
63283
  prd,
62626
63284
  workdir,
62627
63285
  runtime: options.runtime,
63286
+ quarantineMemo: options.runtime.quarantineMemo,
62628
63287
  storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
62629
63288
  storyId: m.storyId,
62630
63289
  completedAt: m.completedAt,
@@ -62655,6 +63314,7 @@ async function handleRunCompletion(options) {
62655
63314
  }
62656
63315
  }
62657
63316
  statusWriter.setRunStatus("failed");
63317
+ regressionGateFailed = true;
62658
63318
  if (hooksConfig) {
62659
63319
  await _runCompletionDeps.fireHook(hooksConfig, "on-final-regression-fail", {
62660
63320
  event: "on-final-regression-fail",
@@ -62739,22 +63399,14 @@ async function handleRunCompletion(options) {
62739
63399
  const existingIdx = existingIndex.get(storyId);
62740
63400
  if (existingIdx === undefined) {
62741
63401
  const story = prd.userStories.find((s) => s.id === storyId);
62742
- allStoryMetrics.push({
63402
+ allStoryMetrics.push(synthesizeBackfillMetric({
62743
63403
  storyId,
62744
- complexity: story?.routing?.complexity ?? "medium",
62745
- modelTier: "balanced",
62746
- modelUsed: defaultAgent,
62747
- attempts: 0,
62748
- finalTier: "balanced",
62749
- success: story?.passes ?? true,
62750
- cost: snap.totalCostUsd,
62751
- durationMs: 0,
62752
- firstPassSuccess: story?.passes ?? true,
62753
- startedAt: completionCompletedAt,
62754
- completedAt: completionCompletedAt,
62755
- source: "completion-phase",
62756
- runtimeCrashes: 0
62757
- });
63404
+ story,
63405
+ totalCostUsd: snap.totalCostUsd,
63406
+ config: config2,
63407
+ defaultAgent,
63408
+ timestamp: completionCompletedAt
63409
+ }));
62758
63410
  } else {
62759
63411
  const existing = allStoryMetrics[existingIdx];
62760
63412
  if (snap.totalCostUsd > (existing.cost ?? 0)) {
@@ -62856,7 +63508,7 @@ async function handleRunCompletion(options) {
62856
63508
  });
62857
63509
  statusWriter.setPrd(prd);
62858
63510
  statusWriter.setCurrentStory(null);
62859
- statusWriter.setRunStatus(isComplete(prd) ? "completed" : isStalled(prd) ? "stalled" : "running");
63511
+ statusWriter.setRunStatus(regressionGateFailed ? "failed" : exitReason === "cost-limit" ? "cost-limit" : isComplete(prd) ? "completed" : isStalled(prd) ? "stalled" : "running");
62860
63512
  await statusWriter.update(reportedTotal, iterations);
62861
63513
  return {
62862
63514
  durationMs,
@@ -62884,6 +63536,7 @@ var init_run_completion = __esm(() => {
62884
63536
  init_scratch_purge();
62885
63537
  init_workspace();
62886
63538
  init_smart_runner();
63539
+ init_backfill_story_metrics();
62887
63540
  init_run_regression();
62888
63541
  _runCompletionDeps = {
62889
63542
  runDeferredRegression,
@@ -62896,7 +63549,8 @@ var init_run_completion = __esm(() => {
62896
63549
  var exports_headless_formatter = {};
62897
63550
  __export(exports_headless_formatter, {
62898
63551
  outputRunHeader: () => outputRunHeader,
62899
- outputRunFooter: () => outputRunFooter
63552
+ outputRunFooter: () => outputRunFooter,
63553
+ outputAdvisoryFindingsSummary: () => outputAdvisoryFindingsSummary
62900
63554
  });
62901
63555
  async function outputRunHeader(options) {
62902
63556
  const { feature, totalStories, pendingStories, workdir, formatterMode } = options;
@@ -62934,10 +63588,19 @@ function outputRunFooter(options) {
62934
63588
  });
62935
63589
  console.log(summaryOutput);
62936
63590
  }
63591
+ function outputAdvisoryFindingsSummary(findings, formatterMode) {
63592
+ if (findings.length === 0 || formatterMode === "json") {
63593
+ return;
63594
+ }
63595
+ const output = formatAdvisorySummary(findings, { mode: formatterMode, useColor: true });
63596
+ if (output) {
63597
+ console.log(output);
63598
+ }
63599
+ }
62937
63600
  var init_headless_formatter = __esm(() => {
62938
- init_source();
62939
63601
  init_log_format();
62940
63602
  init_version();
63603
+ init_source();
62941
63604
  });
62942
63605
 
62943
63606
  // src/execution/batching.ts
@@ -62982,10 +63645,10 @@ var DEFAULT_MAX_BATCH_SIZE = 4;
62982
63645
 
62983
63646
  // src/pipeline/subscribers/events-writer.ts
62984
63647
  import { appendFile as appendFile4, mkdir as mkdir13 } from "fs/promises";
62985
- import { basename as basename13, join as join80 } from "path";
63648
+ import { basename as basename14, join as join80 } from "path";
62986
63649
  function wireEventsWriter(bus, feature, runId, workdir) {
62987
63650
  const logger = getSafeLogger();
62988
- const project = basename13(workdir);
63651
+ const project = basename14(workdir);
62989
63652
  const eventsDir = join80(getEventsRootDir(), project);
62990
63653
  const eventsFile = join80(eventsDir, "events.jsonl");
62991
63654
  let dirReady = false;
@@ -63168,10 +63831,10 @@ var init_interaction2 = __esm(() => {
63168
63831
 
63169
63832
  // src/pipeline/subscribers/registry.ts
63170
63833
  import { mkdir as mkdir14, writeFile as writeFile2 } from "fs/promises";
63171
- import { basename as basename14, join as join81 } from "path";
63834
+ import { basename as basename15, join as join81 } from "path";
63172
63835
  function wireRegistry(bus, feature, runId, workdir, outputDir) {
63173
63836
  const logger = getSafeLogger();
63174
- const project = basename14(workdir);
63837
+ const project = basename15(workdir);
63175
63838
  const runDir = join81(getRunsDir(), `${project}-${feature}-${runId}`);
63176
63839
  const metaFile = join81(runDir, "meta.json");
63177
63840
  const unsub = bus.on("run:started", (_ev) => {
@@ -96308,9 +96971,9 @@ __export(exports_curator, {
96308
96971
  });
96309
96972
  import { readdirSync as readdirSync9 } from "fs";
96310
96973
  import { unlink as unlink4 } from "fs/promises";
96311
- import { basename as basename15, join as join90 } from "path";
96974
+ import { basename as basename16, join as join90 } from "path";
96312
96975
  function getProjectKey(config2, projectDir) {
96313
- return config2.name?.trim() || basename15(projectDir);
96976
+ return config2.name?.trim() || basename16(projectDir);
96314
96977
  }
96315
96978
  function listRunIds(runsDir) {
96316
96979
  try {
@@ -96632,7 +97295,7 @@ var init_curator2 = __esm(() => {
96632
97295
  init_source();
96633
97296
  import { existsSync as existsSync37, mkdirSync as mkdirSync7 } from "fs";
96634
97297
  import { homedir as homedir3 } from "os";
96635
- import { basename as basename16, join as join91 } from "path";
97298
+ import { basename as basename17, join as join91 } from "path";
96636
97299
 
96637
97300
  // node_modules/commander/esm.mjs
96638
97301
  var import__ = __toESM(require_commander(), 1);
@@ -97203,10 +97866,10 @@ init_config();
97203
97866
  init_logger2();
97204
97867
  init_metrics();
97205
97868
  init_runtime();
97206
- import { basename as basename6 } from "path";
97869
+ import { basename as basename7 } from "path";
97207
97870
  async function resolveOutputDir(workdir) {
97208
97871
  const config2 = await loadConfig(workdir).catch(() => null);
97209
- const projectKey = config2?.name?.trim() || basename6(workdir);
97872
+ const projectKey = config2?.name?.trim() || basename7(workdir);
97210
97873
  return projectOutputDir(projectKey, config2?.outputDir);
97211
97874
  }
97212
97875
  async function displayCostMetrics(workdir) {
@@ -97321,7 +97984,7 @@ init_interaction();
97321
97984
  init_prd();
97322
97985
  init_runtime();
97323
97986
  import { existsSync as existsSync17, readdirSync as readdirSync3 } from "fs";
97324
- import { basename as basename7, join as join44, resolve as resolve14 } from "path";
97987
+ import { basename as basename8, join as join44, resolve as resolve14 } from "path";
97325
97988
  var _statusFeaturesDeps = {
97326
97989
  projectOutputDir,
97327
97990
  loadConfig
@@ -97348,7 +98011,7 @@ async function loadStatusFile(featureDir) {
97348
98011
  }
97349
98012
  async function loadProjectStatusFile(projectDir) {
97350
98013
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
97351
- const projectKey = config2?.name?.trim() || basename7(projectDir);
98014
+ const projectKey = config2?.name?.trim() || basename8(projectDir);
97352
98015
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
97353
98016
  const statusPath = join44(outputDir, "status.json");
97354
98017
  if (!existsSync17(statusPath)) {
@@ -97403,6 +98066,8 @@ async function getFeatureSummary(featureName, featureDir) {
97403
98066
  pid: status.run.pid,
97404
98067
  crashedAt: status.run.crashedAt
97405
98068
  };
98069
+ } else if (status.run.status === "cost-limit") {
98070
+ summary.costLimitStopped = true;
97406
98071
  }
97407
98072
  }
97408
98073
  const runsDir = join44(featureDir, "runs");
@@ -97417,7 +98082,7 @@ async function getFeatureSummary(featureName, featureDir) {
97417
98082
  }
97418
98083
  async function displayAllFeatures(projectDir) {
97419
98084
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
97420
- const projectKey = config2?.name?.trim() || basename7(projectDir);
98085
+ const projectKey = config2?.name?.trim() || basename8(projectDir);
97421
98086
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
97422
98087
  const featuresDir = join44(outputDir, "features");
97423
98088
  if (!existsSync17(featuresDir)) {
@@ -97458,6 +98123,14 @@ async function displayAllFeatures(projectDir) {
97458
98123
  console.log(source_default.dim(` Signal: ${projectStatus.run.crashSignal}`));
97459
98124
  }
97460
98125
  console.log();
98126
+ } else if (projectStatus.run.status === "cost-limit") {
98127
+ console.log(source_default.magenta(`\uD83D\uDCB0 Cost Limit Reached:
98128
+ `));
98129
+ console.log(source_default.dim(` Feature: ${projectStatus.run.feature}`));
98130
+ console.log(source_default.dim(` Run ID: ${projectStatus.run.id}`));
98131
+ console.log(source_default.dim(` Spent: $${projectStatus.cost.spent.toFixed(4)}`));
98132
+ console.log(source_default.dim(` Limit: $${(projectStatus.cost.limit ?? 0).toFixed(4)}`));
98133
+ console.log();
97461
98134
  }
97462
98135
  }
97463
98136
  const summaries = await Promise.all(features.map((name) => getFeatureSummary(name, join44(featuresDir, name))));
@@ -97478,6 +98151,8 @@ async function displayAllFeatures(projectDir) {
97478
98151
  status = source_default.yellow("\u26A1 Running");
97479
98152
  } else if (summary.crashedRun) {
97480
98153
  status = source_default.red("\uD83D\uDCA5 Crashed");
98154
+ } else if (summary.costLimitStopped) {
98155
+ status = source_default.magenta("\uD83D\uDCB0 Cost limit");
97481
98156
  } else {
97482
98157
  status = source_default.dim("\u2014");
97483
98158
  }
@@ -97630,7 +98305,7 @@ async function displayFeatureStatus(options = {}) {
97630
98305
  if (options.dir) {
97631
98306
  const projectDir = resolve14(options.dir);
97632
98307
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
97633
- const projectKey = config2?.name?.trim() || basename7(projectDir);
98308
+ const projectKey = config2?.name?.trim() || basename8(projectDir);
97634
98309
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
97635
98310
  featureDir = join44(outputDir, "features", options.feature);
97636
98311
  } else {
@@ -97652,12 +98327,12 @@ init_errors();
97652
98327
  init_logger2();
97653
98328
  init_runtime();
97654
98329
  import { existsSync as existsSync18, readdirSync as readdirSync4 } from "fs";
97655
- import { basename as basename8, join as join45 } from "path";
98330
+ import { basename as basename9, join as join45 } from "path";
97656
98331
  async function resolveOutputDir2(workdir, override) {
97657
98332
  if (override)
97658
98333
  return override;
97659
98334
  const config2 = await loadConfig(workdir).catch(() => null);
97660
- const projectKey = config2?.name?.trim() || basename8(workdir);
98335
+ const projectKey = config2?.name?.trim() || basename9(workdir);
97661
98336
  return projectOutputDir(projectKey, config2?.outputDir);
97662
98337
  }
97663
98338
  async function parseRunLog(logPath) {
@@ -98044,7 +98719,7 @@ var FIELD_DESCRIPTIONS = {
98044
98719
  execution: "Execution limits and timeouts",
98045
98720
  "execution.maxIterations": "Max iterations per feature run (auto-calculated if not set)",
98046
98721
  "execution.iterationDelayMs": "Delay between iterations in milliseconds",
98047
- "execution.costLimit": "Max cost in USD before pausing execution",
98722
+ "execution.costLimit": "Max cost in USD before pausing execution (override per run with `nax run --max-cost`)",
98048
98723
  "execution.sessionTimeoutSeconds": "Timeout per agent coding session in seconds",
98049
98724
  "execution.verificationTimeoutSeconds": "Verification subprocess timeout in seconds",
98050
98725
  "execution.maxStoriesPerFeature": "Max stories per feature (prevents memory exhaustion)",
@@ -98092,7 +98767,7 @@ var FIELD_DESCRIPTIONS = {
98092
98767
  "tdd.autoApproveVerifier": "Auto-approve legitimate fixes in verifier session",
98093
98768
  "tdd.sessionTiers": "Per-session model tier overrides",
98094
98769
  "tdd.sessionTiers.testWriter": "Model tier for test-writer session",
98095
- "tdd.sessionTiers.implementer": "Model tier for implementer session",
98770
+ "tdd.sessionTiers.implementer": "Ignored by design \u2014 implementer follows story.routing.modelTier + escalation, not this field. Kept optional so legacy configs still parse.",
98096
98771
  "tdd.sessionTiers.verifier": "Model tier for verifier session",
98097
98772
  "tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
98098
98773
  "tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
@@ -98695,7 +99370,7 @@ async function contextInspectCommand(options) {
98695
99370
  init_canonical_loader();
98696
99371
  init_errors();
98697
99372
  import { mkdir as mkdir11 } from "fs/promises";
98698
- import { basename as basename12, join as join68 } from "path";
99373
+ import { basename as basename13, join as join68 } from "path";
98699
99374
  var _rulesCLIDeps = {
98700
99375
  readFile: async (path20) => Bun.file(path20).text(),
98701
99376
  writeFile: async (path20, content) => {
@@ -98795,7 +99470,7 @@ async function collectMigrationSources(workdir) {
98795
99470
  try {
98796
99471
  const content = await _rulesCLIDeps.readFile(filePath);
98797
99472
  if (content.trim()) {
98798
- sources.push({ sourcePath: filePath, targetFileName: basename12(filePath), content });
99473
+ sources.push({ sourcePath: filePath, targetFileName: basename13(filePath), content });
98799
99474
  }
98800
99475
  } catch {}
98801
99476
  }
@@ -99680,6 +100355,8 @@ function colorStatus(status) {
99680
100355
  return source_default.red(status);
99681
100356
  case "running":
99682
100357
  return source_default.yellow(status);
100358
+ case "cost-limit":
100359
+ return source_default.magenta(status);
99683
100360
  case "[unavailable]":
99684
100361
  return source_default.dim(status);
99685
100362
  default:
@@ -99970,17 +100647,27 @@ async function runCompletionPhase(options) {
99970
100647
  sessionManager: options.sessionManager,
99971
100648
  pluginProviderCache: options.pluginProviderCache,
99972
100649
  deferredReview: options.deferredReview,
100650
+ exitReason: options.exitReason,
99973
100651
  runtime: options.runtime,
99974
100652
  abortSignal: options.abortSignal
99975
100653
  });
99976
100654
  const { durationMs, runCompletedAt, finalCounts, reportedTotal, pluginGateFailed } = completionResult;
99977
100655
  if (options.featureDir) {
99978
- const finalStatus = isComplete(options.prd) && !pluginGateFailed ? "completed" : "failed";
100656
+ const finalStatus = pluginGateFailed ? "failed" : options.exitReason === "cost-limit" ? "cost-limit" : isComplete(options.prd) ? "completed" : "failed";
99979
100657
  options.statusWriter.setRunStatus(finalStatus);
99980
100658
  await options.statusWriter.writeFeatureStatus(options.featureDir, reportedTotal, options.iterations);
99981
100659
  }
100660
+ const advisoryFindings = options.runtime?.reviewAuditor?.getAdvisoryFindings() ?? [];
100661
+ if (advisoryFindings.length > 0) {
100662
+ logger?.warn("review", `${advisoryFindings.length} non-blocking review finding(s) surfaced at run end`, {
100663
+ storyId: "_run",
100664
+ count: advisoryFindings.length,
100665
+ findings: advisoryFindings
100666
+ });
100667
+ }
99982
100668
  if (options.headless && options.formatterMode !== "json") {
99983
- const { outputRunFooter: outputRunFooter2 } = await Promise.resolve().then(() => (init_headless_formatter(), exports_headless_formatter));
100669
+ const { outputAdvisoryFindingsSummary: outputAdvisoryFindingsSummary2, outputRunFooter: outputRunFooter2 } = await Promise.resolve().then(() => (init_headless_formatter(), exports_headless_formatter));
100670
+ outputAdvisoryFindingsSummary2(advisoryFindings, options.formatterMode);
99984
100671
  outputRunFooter2({
99985
100672
  finalCounts: {
99986
100673
  total: finalCounts.total,
@@ -100175,7 +100862,8 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
100175
100862
  storiesCompleted,
100176
100863
  totalCost,
100177
100864
  allStoryMetrics,
100178
- deferredReview: unifiedResult.deferredReview
100865
+ deferredReview: unifiedResult.deferredReview,
100866
+ exitReason: unifiedResult.exitReason
100179
100867
  };
100180
100868
  }
100181
100869
 
@@ -100344,6 +101032,7 @@ async function run(options) {
100344
101032
  agentManager,
100345
101033
  pluginProviderCache,
100346
101034
  deferredReview: executionResult.deferredReview,
101035
+ exitReason: executionResult.exitReason,
100347
101036
  runtime,
100348
101037
  abortSignal: shutdownController.signal
100349
101038
  });
@@ -100411,6 +101100,7 @@ init_pid_registry();
100411
101100
  init_acceptance_loop();
100412
101101
  init_headless_formatter();
100413
101102
  init_run_completion();
101103
+ init_backfill_story_metrics();
100414
101104
  init_run_cleanup();
100415
101105
  init_run_setup();
100416
101106
  init_run_regression();
@@ -100428,6 +101118,128 @@ init_logger2();
100428
101118
  init_prd();
100429
101119
  init_runtime();
100430
101120
 
101121
+ // src/schedule/parse.ts
101122
+ var ACCEPTED = "Accepted: relative (30m, 2h, 90s, 1h30m, 1d), time-of-day (17:00), or ISO datetime (2026-07-02T02:00).";
101123
+ var UNIT_MS = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
101124
+ var DURATION_RE = /^(\d+[smhd])+$/;
101125
+ var SEGMENT_RE = /(\d+)([smhd])/g;
101126
+ var TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
101127
+ var NAIVE_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
101128
+ var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
101129
+ var OFFSET_RE = /T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
101130
+ function parseTimeOfDay(input, now2) {
101131
+ const m = TIME_RE.exec(input);
101132
+ if (!m)
101133
+ return null;
101134
+ const hours = Number(m[1]);
101135
+ const minutes = Number(m[2]);
101136
+ const target = new Date(now2.getFullYear(), now2.getMonth(), now2.getDate(), hours, minutes, 0, 0);
101137
+ if (target.getTime() <= now2.getTime()) {
101138
+ target.setDate(target.getDate() + 1);
101139
+ }
101140
+ return { ok: true, target };
101141
+ }
101142
+ function parseIso(input, now2) {
101143
+ if (DATE_ONLY_RE.test(input)) {
101144
+ return {
101145
+ ok: false,
101146
+ error: `Date-only value "${input}" has no time \u2014 specify a time, e.g. ${input}T02:00.`
101147
+ };
101148
+ }
101149
+ let target = null;
101150
+ const naive = NAIVE_RE.exec(input);
101151
+ if (naive) {
101152
+ const [, y, mo, d, h, mi, s] = naive;
101153
+ target = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s ?? "0"), 0);
101154
+ } else if (OFFSET_RE.test(input)) {
101155
+ const parsed = new Date(input);
101156
+ if (!Number.isNaN(parsed.getTime()))
101157
+ target = parsed;
101158
+ }
101159
+ if (!target || Number.isNaN(target.getTime())) {
101160
+ return { ok: false, error: `Unrecognized schedule "${input}". ${ACCEPTED}` };
101161
+ }
101162
+ if (target.getTime() <= now2.getTime()) {
101163
+ return { ok: false, error: `Scheduled time ${input} is in the past.` };
101164
+ }
101165
+ return { ok: true, target };
101166
+ }
101167
+ function parseRelative(input, now2) {
101168
+ if (!DURATION_RE.test(input))
101169
+ return null;
101170
+ let totalMs = 0;
101171
+ for (const [, n, unit] of input.matchAll(SEGMENT_RE)) {
101172
+ totalMs += Number(n) * UNIT_MS[unit];
101173
+ }
101174
+ if (totalMs <= 0)
101175
+ return { ok: false, error: `Duration must be positive. ${ACCEPTED}` };
101176
+ return { ok: true, target: new Date(now2.getTime() + totalMs) };
101177
+ }
101178
+ function resolveScheduleGate(input, now2) {
101179
+ if (input === undefined)
101180
+ return { ok: true, target: null };
101181
+ const parsed = parseSchedule(input, now2);
101182
+ if (!parsed.ok)
101183
+ return { ok: false, error: parsed.error };
101184
+ return { ok: true, target: parsed.target };
101185
+ }
101186
+ function parseSchedule(input, now2) {
101187
+ const trimmed = input.trim();
101188
+ if (trimmed === "")
101189
+ return { ok: false, error: `Empty schedule value. ${ACCEPTED}` };
101190
+ const relative16 = parseRelative(trimmed, now2);
101191
+ if (relative16)
101192
+ return relative16;
101193
+ const timeOfDay = parseTimeOfDay(trimmed, now2);
101194
+ if (timeOfDay)
101195
+ return timeOfDay;
101196
+ return parseIso(trimmed, now2);
101197
+ }
101198
+ // src/schedule/wait.ts
101199
+ init_logger2();
101200
+ init_bun_deps();
101201
+ function formatRemaining(ms) {
101202
+ const total = Math.max(0, Math.floor(ms / 1000));
101203
+ const h = Math.floor(total / 3600);
101204
+ const m = Math.floor(total % 3600 / 60);
101205
+ const s = total % 60;
101206
+ const pad4 = (n) => String(n).padStart(2, "0");
101207
+ return `${pad4(h)}:${pad4(m)}:${pad4(s)}`;
101208
+ }
101209
+ var TICK_MS = 1000;
101210
+ var DEFAULT_DEPS2 = {
101211
+ now: () => Date.now(),
101212
+ delay: (ms, signal) => cancellableDelay(ms, signal),
101213
+ render: (line) => {
101214
+ process.stdout.write(`\r${line}`);
101215
+ },
101216
+ log: (message, data) => getSafeLogger()?.info("schedule", message, data)
101217
+ };
101218
+ async function waitForSchedule(target, opts) {
101219
+ const deps = { ...DEFAULT_DEPS2, ...opts._deps };
101220
+ const targetMs = target.getTime();
101221
+ if (opts.headless) {
101222
+ deps.log("Scheduled run waiting", { label: opts.label, targetIso: target.toISOString() });
101223
+ }
101224
+ while (deps.now() < targetMs) {
101225
+ if (opts.signal.aborted)
101226
+ return "cancelled";
101227
+ const remaining = targetMs - deps.now();
101228
+ if (!opts.headless) {
101229
+ deps.render(`[WAIT] Scheduled run of "${opts.label}" \u2014 starting in ${formatRemaining(remaining)} (Ctrl-C to cancel)`);
101230
+ }
101231
+ const wait = Math.min(TICK_MS, remaining);
101232
+ try {
101233
+ await deps.delay(wait, opts.signal);
101234
+ } catch {
101235
+ return "cancelled";
101236
+ }
101237
+ }
101238
+ if (!opts.headless)
101239
+ deps.render(`
101240
+ `);
101241
+ return "fired";
101242
+ }
100431
101243
  // node_modules/ink/build/render.js
100432
101244
  import { Stream } from "stream";
100433
101245
  import process14 from "process";
@@ -106591,6 +107403,12 @@ async function writeQueueCommand(queueFilePath, command) {
106591
107403
  case "SKIP":
106592
107404
  commandLine2 = `SKIP ${command.storyId}`;
106593
107405
  break;
107406
+ case "RETRY":
107407
+ commandLine2 = `RETRY ${command.storyId}`;
107408
+ break;
107409
+ case "PRIORITY":
107410
+ commandLine2 = `PRIORITY ${command.storyId} ${command.value}`;
107411
+ break;
106594
107412
  default: {
106595
107413
  const _exhaustive = command;
106596
107414
  throw new Error(`Unhandled queue command: ${_exhaustive}`);
@@ -108456,7 +109274,7 @@ program2.command("setup").description("Analyze repo and generate .nax/config.jso
108456
109274
  });
108457
109275
  process.exit(exitCode);
108458
109276
  });
108459
- program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).action(async (options) => {
109277
+ program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--max-cost <usd>", "Override cost limit (USD) for this run \u2014 aborts execution when exceeded").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).option("--profile <name>", "Profile(s) to overlay (comma-separated or repeated; later overrides earlier)", collectProfile, []).option("--schedule <when>", "Defer run start until <when> (e.g. 30m, 1h30m, 17:00, 2026-07-02T02:00)").action(async (options) => {
108460
109278
  let workdir;
108461
109279
  try {
108462
109280
  workdir = validateDirectory(options.dir);
@@ -108464,6 +109282,11 @@ program2.command("run").description("Run the orchestration loop for a feature").
108464
109282
  console.error(source_default.red(`Invalid directory: ${err.message}`));
108465
109283
  process.exit(1);
108466
109284
  }
109285
+ const scheduleGate = resolveScheduleGate(options.schedule, new Date);
109286
+ if (!scheduleGate.ok) {
109287
+ console.error(source_default.red(`Invalid --schedule: ${scheduleGate.error}`));
109288
+ process.exit(1);
109289
+ }
108467
109290
  if (options.plan && !options.from) {
108468
109291
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
108469
109292
  process.exit(1);
@@ -108578,7 +109401,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
108578
109401
  process.exit(1);
108579
109402
  }
108580
109403
  resetLogger();
108581
- const projectKey = config2.name?.trim() || basename16(workdir);
109404
+ const projectKey = config2.name?.trim() || basename17(workdir);
108582
109405
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
108583
109406
  const runsDir = join91(outputDir, "features", options.feature, "runs");
108584
109407
  mkdirSync7(runsDir, { recursive: true });
@@ -108601,6 +109424,14 @@ program2.command("run").description("Run the orchestration loop for a feature").
108601
109424
  config2.agent.default = options.agent;
108602
109425
  }
108603
109426
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
109427
+ if (options.maxCost !== undefined) {
109428
+ const maxCost = Number(options.maxCost);
109429
+ if (!Number.isFinite(maxCost) || maxCost <= 0) {
109430
+ console.error(source_default.red("--max-cost must be a positive number"));
109431
+ process.exit(1);
109432
+ }
109433
+ config2.execution.costLimit = maxCost;
109434
+ }
108604
109435
  const globalNaxDir = join91(homedir3(), ".nax");
108605
109436
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
108606
109437
  const eventEmitter = new PipelineEventEmitter;
@@ -108635,6 +109466,22 @@ program2.command("run").description("Run the orchestration loop for a feature").
108635
109466
  process.exit(1);
108636
109467
  }
108637
109468
  }
109469
+ if (scheduleGate.target) {
109470
+ const scheduleController = new AbortController;
109471
+ const onSigint = () => scheduleController.abort();
109472
+ process.once("SIGINT", onSigint);
109473
+ const outcome = await waitForSchedule(scheduleGate.target, {
109474
+ label: options.feature,
109475
+ headless: useHeadless || formatterMode === "json",
109476
+ signal: scheduleController.signal
109477
+ });
109478
+ process.removeListener("SIGINT", onSigint);
109479
+ if (outcome === "cancelled") {
109480
+ console.log(source_default.dim(`
109481
+ Scheduled run cancelled.`));
109482
+ process.exit(0);
109483
+ }
109484
+ }
108638
109485
  const result2 = await run({
108639
109486
  prdPath,
108640
109487
  workdir,
@@ -109105,7 +109952,7 @@ program2.command("unlock").description("Release stale lock from crashed nax proc
109105
109952
  process.exit(1);
109106
109953
  }
109107
109954
  });
109108
- var runs = program2.command("runs").description("Show all registered runs from the central registry (~/.nax/runs/)").option("--project <name>", "Filter by project name").option("--last <N>", "Limit to N most recent runs (default: 20)").option("--status <status>", "Filter by status (running|completed|failed|crashed)").action(async (options) => {
109955
+ var runs = program2.command("runs").description("Show all registered runs from the central registry (~/.nax/runs/)").option("--project <name>", "Filter by project name").option("--last <N>", "Limit to N most recent runs (default: 20)").option("--status <status>", "Filter by status (running|completed|failed|crashed|cost-limit)").action(async (options) => {
109109
109956
  try {
109110
109957
  await runsCommand({
109111
109958
  project: options.project,