@nathapp/nax 0.75.3 → 0.75.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nax.js CHANGED
@@ -17317,7 +17317,11 @@ var init_schemas_reporters = __esm(() => {
17317
17317
  maxBatchSize: exports_external.number().int().positive().default(64),
17318
17318
  flushIntervalMs: exports_external.number().int().positive().default(5000),
17319
17319
  maxQueueSize: exports_external.number().int().positive().default(2048),
17320
- phases: exports_external.array(exports_external.string()).optional()
17320
+ phases: exports_external.array(exports_external.string()).optional(),
17321
+ logs: exports_external.object({
17322
+ enabled: exports_external.boolean().default(false),
17323
+ level: exports_external.enum(["silent", "error", "warn", "info", "debug"]).default("info")
17324
+ }).default({ enabled: false, level: "info" })
17321
17325
  }).default({
17322
17326
  enabled: false,
17323
17327
  headers: {},
@@ -17327,7 +17331,8 @@ var init_schemas_reporters = __esm(() => {
17327
17331
  heartbeatIntervalMs: 1e4,
17328
17332
  maxBatchSize: 64,
17329
17333
  flushIntervalMs: 5000,
17330
- maxQueueSize: 2048
17334
+ maxQueueSize: 2048,
17335
+ logs: { enabled: false, level: "info" }
17331
17336
  });
17332
17337
  ReportersConfigSchema = exports_external.object({
17333
17338
  webhook: WebhookReporterConfigSchema,
@@ -17347,7 +17352,8 @@ var init_schemas_reporters = __esm(() => {
17347
17352
  heartbeatIntervalMs: 1e4,
17348
17353
  maxBatchSize: 64,
17349
17354
  flushIntervalMs: 5000,
17350
- maxQueueSize: 2048
17355
+ maxQueueSize: 2048,
17356
+ logs: { enabled: false, level: "info" }
17351
17357
  }
17352
17358
  });
17353
17359
  });
@@ -17817,6 +17823,7 @@ var init_schemas3 = __esm(() => {
17817
17823
  quality: exports_external.string().nullable().default(null)
17818
17824
  }).default({ spec: null, quality: null }),
17819
17825
  escalate: exports_external.object({ telegram: exports_external.boolean().default(true) }).default({ telegram: true }),
17826
+ notify: exports_external.object({ mode: exports_external.enum(["escalation", "always", "off"]).default("escalation") }).default({ mode: "escalation" }),
17820
17827
  timeouts: exports_external.object({
17821
17828
  acceptanceMs: exports_external.number().int().positive().default(600000),
17822
17829
  gateMs: exports_external.number().int().positive().default(900000),
@@ -17829,6 +17836,7 @@ var init_schemas3 = __esm(() => {
17829
17836
  defaultAgent: null,
17830
17837
  reviewers: { spec: null, quality: null },
17831
17838
  escalate: { telegram: true },
17839
+ notify: { mode: "escalation" },
17832
17840
  timeouts: { acceptanceMs: 600000, gateMs: 900000, flowMs: 5400000, stepMs: null }
17833
17841
  })
17834
17842
  }).default({
@@ -17838,6 +17846,7 @@ var init_schemas3 = __esm(() => {
17838
17846
  defaultAgent: null,
17839
17847
  reviewers: { spec: null, quality: null },
17840
17848
  escalate: { telegram: true },
17849
+ notify: { mode: "escalation" },
17841
17850
  timeouts: { acceptanceMs: 600000, gateMs: 900000, flowMs: 5400000, stepMs: null }
17842
17851
  }
17843
17852
  }),
@@ -18388,6 +18397,30 @@ var init_redact = __esm(() => {
18388
18397
  ];
18389
18398
  });
18390
18399
 
18400
+ // src/logger/sink-registry.ts
18401
+ class SinkRegistry {
18402
+ sinks = [];
18403
+ add(sink) {
18404
+ this.sinks.push(sink);
18405
+ return () => {
18406
+ const idx = this.sinks.indexOf(sink);
18407
+ if (idx !== -1) {
18408
+ this.sinks.splice(idx, 1);
18409
+ }
18410
+ };
18411
+ }
18412
+ dispatch(entry) {
18413
+ for (const sink of this.sinks) {
18414
+ try {
18415
+ sink({ ...entry });
18416
+ } catch (error48) {
18417
+ process.stderr.write(`[logger] Sink threw: ${error48}
18418
+ `);
18419
+ }
18420
+ }
18421
+ }
18422
+ }
18423
+
18391
18424
  // src/logger/logger.ts
18392
18425
  import { mkdirSync } from "fs";
18393
18426
  import { appendFile } from "fs/promises";
@@ -18400,6 +18433,7 @@ class Logger {
18400
18433
  suppressConsole;
18401
18434
  writeQueueTail = Promise.resolve();
18402
18435
  pendingLines = [];
18436
+ sinkRegistry = new SinkRegistry;
18403
18437
  constructor(options) {
18404
18438
  this.level = options.level;
18405
18439
  this.filePath = options.filePath;
@@ -18443,10 +18477,11 @@ class Logger {
18443
18477
  ...sessionRole && { sessionRole },
18444
18478
  ...strippedData && { data: strippedData }
18445
18479
  };
18480
+ const entry = redactEntry(rawEntry);
18481
+ this.sinkRegistry.dispatch(entry);
18446
18482
  const consoleEnabled = this.shouldLog(level) && !this.suppressConsole;
18447
18483
  if (!consoleEnabled && !this.filePath)
18448
18484
  return;
18449
- const entry = redactEntry(rawEntry);
18450
18485
  if (consoleEnabled) {
18451
18486
  let consoleOutput = null;
18452
18487
  if (this.formatterMode) {
@@ -18530,8 +18565,19 @@ ${JSON.stringify(entry.data, null, 2)}`;
18530
18565
  debug: (stage, message, data) => this.log("debug", stage, message, data, storyId)
18531
18566
  };
18532
18567
  }
18568
+ addSink(sink) {
18569
+ return this.sinkRegistry.add(sink);
18570
+ }
18533
18571
  close() {}
18534
18572
  }
18573
+ function addSink(sink) {
18574
+ if (!instance) {
18575
+ throw new NaxError("Logger not initialized. Call initLogger() before addSink().", "LOGGER_NOT_INITIALIZED", {
18576
+ stage: "logger"
18577
+ });
18578
+ }
18579
+ return instance.addSink(sink);
18580
+ }
18535
18581
  function initLogger(options = { level: "silent" }) {
18536
18582
  if (instance) {
18537
18583
  throw new Error("Logger already initialized. Call getLogger() to access existing instance.");
@@ -18560,6 +18606,7 @@ function resetLogger() {
18560
18606
  }
18561
18607
  var LOG_LEVEL_PRIORITY, MAX_BATCH_BYTES, instance = null, noopLogger;
18562
18608
  var init_logger = __esm(() => {
18609
+ init_errors();
18563
18610
  init_log_format();
18564
18611
  init_formatters();
18565
18612
  init_redact();
@@ -42579,7 +42626,7 @@ var package_default;
42579
42626
  var init_package = __esm(() => {
42580
42627
  package_default = {
42581
42628
  name: "@nathapp/nax",
42582
- version: "0.75.3",
42629
+ version: "0.75.5",
42583
42630
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42584
42631
  type: "module",
42585
42632
  bin: {
@@ -42683,8 +42730,8 @@ var init_version = __esm(() => {
42683
42730
  NAX_VERSION = package_default.version;
42684
42731
  NAX_COMMIT = (() => {
42685
42732
  try {
42686
- if (/^[0-9a-f]{6,10}$/.test("ae716fd4"))
42687
- return "ae716fd4";
42733
+ if (/^[0-9a-f]{6,10}$/.test("c8f74c5f"))
42734
+ return "c8f74c5f";
42688
42735
  } catch {}
42689
42736
  try {
42690
42737
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -57527,10 +57574,13 @@ ${stderr}` };
57527
57574
 
57528
57575
  // src/context/engine/effectiveness.ts
57529
57576
  function tokenize2(text) {
57530
- if (!text)
57531
- return new Set;
57532
- const raw = text.toLowerCase().split(/[\s_\-./:,;()\[\]{}'"!?]+/).filter((t) => t.length >= MIN_TOKEN_LEN2 && !STOPWORDS2.has(t));
57533
- return new Set(raw);
57577
+ const terms = new Set;
57578
+ for (const match of text.matchAll(TOKEN_PATTERN)) {
57579
+ const term = match[0].toLowerCase();
57580
+ if (term.length >= MIN_TOKEN_LEN2 && !STOPWORDS2.has(term))
57581
+ terms.add(term);
57582
+ }
57583
+ return terms;
57534
57584
  }
57535
57585
  function sharedTermCount2(a, b) {
57536
57586
  let count = 0;
@@ -57540,34 +57590,35 @@ function sharedTermCount2(a, b) {
57540
57590
  }
57541
57591
  return count;
57542
57592
  }
57543
- function classifyEffectiveness(chunkSummary, agentOutput, diffText, findingMessages) {
57544
- const summaryTerms = tokenize2(chunkSummary);
57545
- if (summaryTerms.size < MIN_SIGNIFICANT_TERMS) {
57546
- return { signal: "unknown" };
57593
+ function buildEvidenceTerms(agentOutput, diffText, findingMessages) {
57594
+ const diffTerms = diffText ? _effectivenessDeps.tokenize(diffText) : undefined;
57595
+ const outputTerms = agentOutput ? _effectivenessDeps.tokenize(agentOutput) : undefined;
57596
+ let combined;
57597
+ if (diffTerms || outputTerms) {
57598
+ combined = new Set(diffTerms);
57599
+ for (const term of outputTerms ?? [])
57600
+ combined.add(term);
57547
57601
  }
57548
- for (const finding of findingMessages) {
57549
- const findingTerms = tokenize2(finding);
57550
- if (sharedTermCount2(summaryTerms, findingTerms) >= MIN_SIGNIFICANT_TERMS) {
57551
- return {
57552
- signal: "contradicted",
57553
- evidence: finding.slice(0, 200)
57554
- };
57602
+ return {
57603
+ findings: findingMessages.map((message) => ({ message, terms: _effectivenessDeps.tokenize(message) })),
57604
+ diff: diffTerms,
57605
+ combined
57606
+ };
57607
+ }
57608
+ function classifyWithTerms(chunkSummary, evidence) {
57609
+ const summaryTerms = _effectivenessDeps.tokenize(chunkSummary);
57610
+ if (summaryTerms.size < MIN_SIGNIFICANT_TERMS)
57611
+ return { signal: "unknown" };
57612
+ for (const finding of evidence.findings) {
57613
+ if (sharedTermCount2(summaryTerms, finding.terms) >= MIN_SIGNIFICANT_TERMS) {
57614
+ return { signal: "contradicted", evidence: finding.message.slice(0, 200) };
57555
57615
  }
57556
57616
  }
57557
- if (diffText) {
57558
- const diffTerms = tokenize2(diffText);
57559
- if (sharedTermCount2(summaryTerms, diffTerms) >= MIN_SIGNIFICANT_TERMS) {
57560
- return {
57561
- signal: "followed",
57562
- evidence: "terms found in diff"
57563
- };
57564
- }
57617
+ if (evidence.diff && sharedTermCount2(summaryTerms, evidence.diff) >= MIN_SIGNIFICANT_TERMS) {
57618
+ return { signal: "followed", evidence: "terms found in diff" };
57565
57619
  }
57566
- if (diffText || agentOutput) {
57567
- const combinedTerms = tokenize2(`${diffText} ${agentOutput}`);
57568
- if (sharedTermCount2(summaryTerms, combinedTerms) < MIN_SIGNIFICANT_TERMS) {
57569
- return { signal: "ignored" };
57570
- }
57620
+ if (evidence.combined && sharedTermCount2(summaryTerms, evidence.combined) < MIN_SIGNIFICANT_TERMS) {
57621
+ return { signal: "ignored" };
57571
57622
  }
57572
57623
  return { signal: "unknown" };
57573
57624
  }
@@ -57577,16 +57628,18 @@ async function annotateManifestEffectiveness(projectDir, featureId, storyId, {
57577
57628
  findingMessages
57578
57629
  }) {
57579
57630
  const stored = await loadContextManifests(projectDir, storyId, featureId);
57631
+ let evidenceTerms;
57580
57632
  for (const item of stored) {
57581
57633
  const { manifest } = item;
57582
57634
  if (!manifest.chunkSummaries || manifest.includedChunks.length === 0)
57583
57635
  continue;
57636
+ evidenceTerms ??= buildEvidenceTerms(agentOutput, diffText, findingMessages);
57584
57637
  const effectiveness = {};
57585
57638
  for (const id of manifest.includedChunks) {
57586
57639
  const summary = manifest.chunkSummaries[id];
57587
57640
  if (!summary)
57588
57641
  continue;
57589
- effectiveness[id] = classifyEffectiveness(summary, agentOutput, diffText, findingMessages);
57642
+ effectiveness[id] = classifyWithTerms(summary, evidenceTerms);
57590
57643
  }
57591
57644
  if (Object.keys(effectiveness).length === 0)
57592
57645
  continue;
@@ -57604,12 +57657,13 @@ async function annotateManifestEffectiveness(projectDir, featureId, storyId, {
57604
57657
  }
57605
57658
  }
57606
57659
  }
57607
- var _effectivenessDeps, MIN_SIGNIFICANT_TERMS = 3, STOPWORDS2, MIN_TOKEN_LEN2 = 4;
57660
+ var _effectivenessDeps, MIN_SIGNIFICANT_TERMS = 3, STOPWORDS2, MIN_TOKEN_LEN2 = 4, TOKEN_PATTERN;
57608
57661
  var init_effectiveness = __esm(() => {
57609
57662
  init_logger2();
57610
57663
  init_manifest_store();
57611
57664
  _effectivenessDeps = {
57612
- getLogger
57665
+ getLogger,
57666
+ tokenize: tokenize2
57613
57667
  };
57614
57668
  STOPWORDS2 = new Set([
57615
57669
  "the",
@@ -57651,6 +57705,7 @@ var init_effectiveness = __esm(() => {
57651
57705
  "you",
57652
57706
  "your"
57653
57707
  ]);
57708
+ TOKEN_PATTERN = /[^\s_\-./:,;()\[\]{}'"!?]+/g;
57654
57709
  });
57655
57710
 
57656
57711
  // src/execution/progress.ts
@@ -57667,19 +57722,61 @@ async function appendProgress(featureDir, storyId, status, message) {
57667
57722
  var init_progress = () => {};
57668
57723
 
57669
57724
  // src/pipeline/stages/completion.ts
57725
+ function logHighMemoryCheckpoint(logger, ctx) {
57726
+ const usage = process.memoryUsage();
57727
+ if (usage.heapUsed < HIGH_MEMORY_TELEMETRY_BYTES && usage.rss < HIGH_MEMORY_TELEMETRY_BYTES)
57728
+ return;
57729
+ logger.debug("completion.memory", "High memory at completion boundary", {
57730
+ storyId: ctx.story.id,
57731
+ heapUsedBytes: usage.heapUsed,
57732
+ rssBytes: usage.rss,
57733
+ externalBytes: usage.external,
57734
+ arrayBuffersBytes: usage.arrayBuffers,
57735
+ agentOutputChars: ctx.agentResult?.output.length ?? 0
57736
+ });
57737
+ }
57738
+ async function readTextStreamPrefix(stream, maxChars) {
57739
+ const reader = stream.getReader();
57740
+ const decoder = new TextDecoder;
57741
+ let output = "";
57742
+ try {
57743
+ while (true) {
57744
+ const { done, value } = await reader.read();
57745
+ if (done)
57746
+ break;
57747
+ if (output.length >= maxChars)
57748
+ continue;
57749
+ const decoded = decoder.decode(value, { stream: true });
57750
+ output += decoded.slice(0, maxChars - output.length);
57751
+ }
57752
+ if (output.length < maxChars) {
57753
+ output += decoder.decode().slice(0, maxChars - output.length);
57754
+ }
57755
+ return output;
57756
+ } finally {
57757
+ reader.releaseLock();
57758
+ }
57759
+ }
57670
57760
  async function getDiffText(workdir, baseRef) {
57671
57761
  if (!baseRef)
57672
57762
  return "";
57673
57763
  try {
57674
- const proc = Bun.spawn(["git", "diff", `${baseRef}..HEAD`], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
57675
- const output = await new Response(proc.stdout).text();
57676
- await proc.exited;
57677
- return output.slice(0, 8000);
57764
+ const proc = _completionDeps.spawn(["git", "diff", `${baseRef}..HEAD`], {
57765
+ cwd: workdir,
57766
+ stdout: "pipe",
57767
+ stderr: "pipe"
57768
+ });
57769
+ const [output] = await Promise.all([
57770
+ readTextStreamPrefix(proc.stdout, MAX_EFFECTIVENESS_DIFF_CHARS),
57771
+ readTextStreamPrefix(proc.stderr, 0),
57772
+ proc.exited
57773
+ ]);
57774
+ return output;
57678
57775
  } catch {
57679
57776
  return "";
57680
57777
  }
57681
57778
  }
57682
- var completionStage, _completionDeps;
57779
+ var MAX_EFFECTIVENESS_DIFF_CHARS = 8000, HIGH_MEMORY_TELEMETRY_BYTES, completionStage, _completionDeps;
57683
57780
  var init_completion = __esm(() => {
57684
57781
  init_semantic_verdict();
57685
57782
  init_effectiveness();
@@ -57689,6 +57786,7 @@ var init_completion = __esm(() => {
57689
57786
  init_metrics();
57690
57787
  init_prd();
57691
57788
  init_event_bus();
57789
+ HIGH_MEMORY_TELEMETRY_BYTES = 512 * 1024 * 1024;
57692
57790
  completionStage = {
57693
57791
  name: "completion",
57694
57792
  enabled: () => true,
@@ -57758,6 +57856,7 @@ var init_completion = __esm(() => {
57758
57856
  if (persistPrd) {
57759
57857
  await _completionDeps.savePRD(ctx.prd, prdPath);
57760
57858
  }
57859
+ logHighMemoryCheckpoint(logger, ctx);
57761
57860
  const updatedCounts = countStories(ctx.prd);
57762
57861
  logger.info("completion", "Progress update", {
57763
57862
  storyId: ctx.story.id,
@@ -57773,7 +57872,9 @@ var init_completion = __esm(() => {
57773
57872
  checkReviewGate,
57774
57873
  persistSemanticVerdict,
57775
57874
  savePRD,
57776
- getDiffText
57875
+ getDiffText,
57876
+ readTextStreamPrefix,
57877
+ spawn: Bun.spawn
57777
57878
  };
57778
57879
  });
57779
57880
 
@@ -58476,6 +58577,253 @@ var init_paths3 = __esm(() => {
58476
58577
  init_paths();
58477
58578
  });
58478
58579
 
58580
+ // src/execution/story-orchestrator/types.ts
58581
+ var EXHAUSTED_EXIT_REASONS, TDD_OP_NAMES, CANONICAL_ORDER, PHASE_KIND_TO_STATE_KEY, STRATEGY_TO_REVALIDATION_PHASES, STRICT_VERDICT_PHASE_NAMES;
58582
+ var init_types9 = __esm(() => {
58583
+ EXHAUSTED_EXIT_REASONS = new Set([
58584
+ "max-attempts-total",
58585
+ "max-attempts-per-strategy",
58586
+ "bail-when",
58587
+ "no-strategy",
58588
+ "agent-gave-up",
58589
+ "validate-short-circuit"
58590
+ ]);
58591
+ TDD_OP_NAMES = new Set(["test-writer", "implementer", "verifier"]);
58592
+ CANONICAL_ORDER = [
58593
+ "test-writer",
58594
+ "greenfield-gate",
58595
+ "implementer",
58596
+ "test-presence-gate",
58597
+ "full-suite-gate",
58598
+ "mutation-check",
58599
+ "verifier",
58600
+ "verify-scoped",
58601
+ "lint-check",
58602
+ "typecheck-check",
58603
+ "semantic-review",
58604
+ "adversarial-review"
58605
+ ];
58606
+ PHASE_KIND_TO_STATE_KEY = {
58607
+ "test-writer": "testWriter",
58608
+ "greenfield-gate": "greenfieldGate",
58609
+ implementer: "implementer",
58610
+ "test-presence-gate": "testPresenceGate",
58611
+ "full-suite-gate": "fullSuiteGate",
58612
+ "mutation-check": "mutationCheck",
58613
+ verifier: "verifier",
58614
+ "verify-scoped": "verifyScoped",
58615
+ "lint-check": "lintCheck",
58616
+ "typecheck-check": "typecheckCheck",
58617
+ "semantic-review": "semanticReview",
58618
+ "adversarial-review": "adversarialReview"
58619
+ };
58620
+ STRATEGY_TO_REVALIDATION_PHASES = {
58621
+ "mechanical-lintfix": ["lint-check"],
58622
+ "mechanical-formatfix": ["lint-check"],
58623
+ "autofix-implementer": ["lint-check", "typecheck-check", "full-suite-gate", "semantic-review", "adversarial-review"],
58624
+ "autofix-test-writer": ["lint-check", "typecheck-check", "full-suite-gate", "adversarial-review"],
58625
+ "full-suite-rectify": [
58626
+ "lint-check",
58627
+ "typecheck-check",
58628
+ "full-suite-gate",
58629
+ "verifier",
58630
+ "verify-scoped",
58631
+ "semantic-review",
58632
+ "adversarial-review"
58633
+ ]
58634
+ };
58635
+ STRICT_VERDICT_PHASE_NAMES = new Set([
58636
+ "full-suite-gate",
58637
+ "verify-scoped",
58638
+ "lint-check",
58639
+ "typecheck-check",
58640
+ "verifier"
58641
+ ]);
58642
+ });
58643
+
58644
+ // src/execution/story-orchestrator/phase-eval.ts
58645
+ function phaseExplicitlyPassed(output) {
58646
+ if (output === null || output === undefined || typeof output !== "object")
58647
+ return false;
58648
+ const r = output;
58649
+ return r.success === true || r.passed === true;
58650
+ }
58651
+ function phasePassed(opName, output, storyId) {
58652
+ const strictVerdictPhase = STRICT_VERDICT_PHASE_NAMES.has(opName);
58653
+ if (output === null || output === undefined) {
58654
+ getSafeLogger()?.warn("story-orchestrator", strictVerdictPhase ? "Strict phase produced no output \u2014 treating as fail" : "Phase produced no output \u2014 treating as pass", {
58655
+ storyId,
58656
+ phase: opName
58657
+ });
58658
+ return !strictVerdictPhase;
58659
+ }
58660
+ if (typeof output !== "object") {
58661
+ if (!strictVerdictPhase)
58662
+ return true;
58663
+ getSafeLogger()?.warn("story-orchestrator", "Strict phase produced non-object output \u2014 treating as fail", {
58664
+ storyId,
58665
+ phase: opName
58666
+ });
58667
+ return false;
58668
+ }
58669
+ const r = output;
58670
+ if ("success" in r)
58671
+ return r.success !== false;
58672
+ if ("passed" in r)
58673
+ return r.passed !== false;
58674
+ getSafeLogger()?.warn("story-orchestrator", strictVerdictPhase ? "Strict phase output has neither 'success' nor 'passed' \u2014 treating as fail" : "Phase output has neither 'success' nor 'passed' \u2014 treating as pass", {
58675
+ storyId,
58676
+ phase: opName
58677
+ });
58678
+ return !strictVerdictPhase;
58679
+ }
58680
+ function isFinding(value) {
58681
+ return typeof value === "object" && value !== null && typeof value.source === "string" && value.source.length > 0;
58682
+ }
58683
+ function extractPhaseFindings(output) {
58684
+ if (output === null || output === undefined || typeof output !== "object") {
58685
+ return [];
58686
+ }
58687
+ const record2 = output;
58688
+ const rawArray = Array.isArray(record2.normalizedFindings) && record2.normalizedFindings.length > 0 ? record2.normalizedFindings : Array.isArray(record2.findings) ? record2.findings : [];
58689
+ const findings = rawArray.filter(isFinding);
58690
+ const success2 = "success" in record2 ? record2.success === true : ("passed" in record2) ? record2.passed === true : findings.length === 0;
58691
+ return success2 ? [] : findings;
58692
+ }
58693
+ function gateFailureKeys(gateOutput) {
58694
+ const keys = new Set;
58695
+ for (const f of extractPhaseFindings(gateOutput)) {
58696
+ if (f.source !== "test-runner")
58697
+ continue;
58698
+ if (f.category === "flaky-test")
58699
+ continue;
58700
+ keys.add(gateFindingKey(f));
58701
+ }
58702
+ return keys;
58703
+ }
58704
+ function gateFindingKey(finding) {
58705
+ return `${finding.file ?? ""}::${finding.rule ?? ""}`;
58706
+ }
58707
+ function isQuarantinedFlake(finding, quarantineMemo) {
58708
+ if (finding.source !== "test-runner")
58709
+ return false;
58710
+ const key = gateFindingKey(finding);
58711
+ return key !== KEYLESS_GATE_FAILURE_KEY && quarantineMemo?.has(key) === true;
58712
+ }
58713
+ function describeGateRegression(input) {
58714
+ const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
58715
+ const notRegressed = {
58716
+ regressed: false,
58717
+ regressedKeys: [],
58718
+ memoExcludedKeys: [],
58719
+ baselineKeySize: baselineKeys.size,
58720
+ keyless: false
58721
+ };
58722
+ if (gateName === undefined || phasePassed(gateName, gateOutput, storyId))
58723
+ return notRegressed;
58724
+ const allKeys = gateFailureKeys(gateOutput);
58725
+ const memoExcludedKeys = quarantineMemo ? [...allKeys].filter((k) => quarantineMemo.has(k)) : [];
58726
+ const excluded = new Set(memoExcludedKeys);
58727
+ const regressedKeys = [...allKeys].filter((k) => !baselineKeys.has(k) && !excluded.has(k));
58728
+ const keyless = allKeys.size === 0 || allKeys.has(KEYLESS_GATE_FAILURE_KEY);
58729
+ return {
58730
+ regressed: regressedKeys.length > 0 || keyless,
58731
+ regressedKeys,
58732
+ memoExcludedKeys,
58733
+ baselineKeySize: baselineKeys.size,
58734
+ keyless
58735
+ };
58736
+ }
58737
+ function phasesToRevalidate(strategiesRun, allPhases) {
58738
+ if (!strategiesRun || strategiesRun.length === 0)
58739
+ return allPhases;
58740
+ const unknown2 = strategiesRun.some((name) => STRATEGY_TO_REVALIDATION_PHASES[name] === undefined);
58741
+ if (unknown2)
58742
+ return allPhases;
58743
+ const needed = new Set;
58744
+ for (const name of strategiesRun) {
58745
+ for (const kind of STRATEGY_TO_REVALIDATION_PHASES[name] ?? []) {
58746
+ needed.add(kind);
58747
+ }
58748
+ }
58749
+ return allPhases.filter((p) => needed.has(p.kind));
58750
+ }
58751
+ function orderGateLast(phases) {
58752
+ const rest = phases.filter((p) => p.kind !== "full-suite-gate");
58753
+ const gates = phases.filter((p) => p.kind === "full-suite-gate");
58754
+ return [...rest, ...gates];
58755
+ }
58756
+ var KEYLESS_GATE_FAILURE_KEY = "::";
58757
+ var init_phase_eval = __esm(() => {
58758
+ init_logger2();
58759
+ init_types9();
58760
+ });
58761
+
58762
+ // src/execution/story-orchestrator/nbf-flake-triage.ts
58763
+ function isCandidate(input) {
58764
+ if (input.finding.source !== "test-runner" || input.finding.category !== "failed-test")
58765
+ return false;
58766
+ const key = gateFindingKey(input.finding);
58767
+ return !input.transactionInput.baselineKeys.has(key) && !input.memo.has(key) && !input.attemptedKeys.has(key);
58768
+ }
58769
+ function createNbfFlakeTriageTransaction(input) {
58770
+ const pendingKeys = new Set;
58771
+ const attemptedKeys = new Set;
58772
+ let flakeTriageRan = false;
58773
+ const memo2 = {
58774
+ has: (key) => pendingKeys.has(key) || input.baseMemo?.has(key) === true,
58775
+ add: (key) => pendingKeys.add(key)
58776
+ };
58777
+ return {
58778
+ memo: memo2,
58779
+ get flakeTriageRan() {
58780
+ return flakeTriageRan;
58781
+ },
58782
+ candidates: (findings) => findings.filter((finding) => isCandidate({ finding, transactionInput: input, memo: memo2, attemptedKeys })),
58783
+ recordAttempt: (findings, ran) => {
58784
+ if (!ran)
58785
+ return;
58786
+ flakeTriageRan = true;
58787
+ for (const finding of findings)
58788
+ attemptedKeys.add(gateFindingKey(finding));
58789
+ },
58790
+ commit: () => {
58791
+ for (const key of pendingKeys)
58792
+ input.baseMemo?.add(key);
58793
+ }
58794
+ };
58795
+ }
58796
+ async function triageNbfGate(input) {
58797
+ const candidates = input.transaction.candidates(extractPhaseFindings(input.output));
58798
+ if (candidates.length === 0)
58799
+ return;
58800
+ const record2 = input.output;
58801
+ const rawOutput = typeof record2.rawOutput === "string" ? record2.rawOutput : "";
58802
+ try {
58803
+ const [, report] = await input.triage(candidates, {
58804
+ ctx: input.ctx,
58805
+ rawOutput,
58806
+ quarantineMemo: input.transaction.memo
58807
+ });
58808
+ const ran = report.flakeTriageRan ?? true;
58809
+ if (ran) {
58810
+ for (const key of report.quarantinedKeys)
58811
+ input.transaction.memo.add(key);
58812
+ }
58813
+ input.transaction.recordAttempt(candidates, ran);
58814
+ } catch (err) {
58815
+ getSafeLogger()?.warn("story-orchestrator", "NBF flake triage threw \u2014 keeping findings blocking", {
58816
+ storyId: input.ctx.storyId,
58817
+ gateName: input.gateName,
58818
+ error: err instanceof Error ? err.message : String(err)
58819
+ });
58820
+ }
58821
+ }
58822
+ var init_nbf_flake_triage = __esm(() => {
58823
+ init_logger2();
58824
+ init_phase_eval();
58825
+ });
58826
+
58479
58827
  // src/execution/non-blocking-fix.ts
58480
58828
  function actionableAdvisoryFindings(findings) {
58481
58829
  return findings.filter((f) => f.actionRequired !== false);
@@ -58540,9 +58888,13 @@ async function runNonBlockingFix(args, overrides = {}) {
58540
58888
  return { ran: false, kept: false, restored: false };
58541
58889
  }
58542
58890
  const maxAttempts = 1 + args.cfg.regressionAttempts;
58891
+ const flakeTriage = createNbfFlakeTriageTransaction({
58892
+ baseMemo: args.quarantineMemo,
58893
+ baselineKeys: args.gateBaselineKeys ?? new Set
58894
+ });
58543
58895
  let exhausted = false;
58544
58896
  try {
58545
- const result = await args.runRectify(maxAttempts);
58897
+ const result = await args.runRectify(maxAttempts, flakeTriage);
58546
58898
  exhausted = result.rectificationExhausted === true;
58547
58899
  } catch (err) {
58548
58900
  logger?.warn("non-blocking-fix", "best-effort pass threw \u2014 restoring", {
@@ -58552,16 +58904,14 @@ async function runNonBlockingFix(args, overrides = {}) {
58552
58904
  exhausted = true;
58553
58905
  }
58554
58906
  if (!exhausted) {
58555
- const gateVerdict = args.keptTreeRegressed?.();
58907
+ const gateVerdict = args.keptTreeRegressed?.(flakeTriage.memo);
58556
58908
  if (gateVerdict?.regressed) {
58557
- logger?.info("non-blocking-fix", "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", {
58909
+ logGateRegression({
58910
+ logger,
58558
58911
  storyId: args.storyId,
58559
- regressedKeys: gateVerdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
58560
- regressedKeyCount: gateVerdict.regressedKeys.length,
58561
- baselineKeySize: gateVerdict.baselineKeySize,
58562
- keyless: gateVerdict.keyless,
58563
- memoExcludedKeyCount: gateVerdict.memoExcludedKeys.length,
58564
- flakeTriageRan: false
58912
+ message: "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)",
58913
+ verdict: gateVerdict,
58914
+ flakeTriageRan: flakeTriage.flakeTriageRan
58565
58915
  });
58566
58916
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58567
58917
  }
@@ -58587,11 +58937,34 @@ async function runNonBlockingFix(args, overrides = {}) {
58587
58937
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58588
58938
  }
58589
58939
  }
58940
+ flakeTriage.commit();
58590
58941
  logger?.info("non-blocking-fix", "best-effort fix kept", { storyId: args.storyId });
58591
58942
  return { ran: true, kept: true, restored: false };
58592
58943
  }
58944
+ const exhaustedGateVerdict = args.keptTreeRegressed?.(flakeTriage.memo);
58945
+ if (exhaustedGateVerdict?.regressed) {
58946
+ logGateRegression({
58947
+ logger,
58948
+ storyId: args.storyId,
58949
+ message: "best-effort fix exhausted with the full-suite gate red",
58950
+ verdict: exhaustedGateVerdict,
58951
+ flakeTriageRan: flakeTriage.flakeTriageRan
58952
+ });
58953
+ }
58593
58954
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58594
58955
  }
58956
+ function logGateRegression(input) {
58957
+ const { logger, storyId, message, verdict, flakeTriageRan } = input;
58958
+ logger?.info("non-blocking-fix", message, {
58959
+ storyId,
58960
+ regressedKeys: verdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
58961
+ regressedKeyCount: verdict.regressedKeys.length,
58962
+ baselineKeySize: verdict.baselineKeySize,
58963
+ keyless: verdict.keyless,
58964
+ memoExcludedKeyCount: verdict.memoExcludedKeys.length,
58965
+ flakeTriageRan
58966
+ });
58967
+ }
58595
58968
  async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger) {
58596
58969
  await _deps.rollbackToRef(args.workdir, restoreRef);
58597
58970
  for (const key of Object.keys(args.phaseOutputs))
@@ -58612,6 +58985,7 @@ var init_non_blocking_fix = __esm(() => {
58612
58985
  init_test_runners();
58613
58986
  init_bun_deps();
58614
58987
  init_paths3();
58988
+ init_nbf_flake_triage();
58615
58989
  REVIEW_PHASE_KINDS = ["semantic-review", "adversarial-review"];
58616
58990
  _nonBlockingFixDeps = {
58617
58991
  spawn: typedSpawn,
@@ -58624,179 +58998,6 @@ var init_non_blocking_fix = __esm(() => {
58624
58998
  };
58625
58999
  });
58626
59000
 
58627
- // src/execution/story-orchestrator/types.ts
58628
- var EXHAUSTED_EXIT_REASONS, TDD_OP_NAMES, CANONICAL_ORDER, PHASE_KIND_TO_STATE_KEY, STRATEGY_TO_REVALIDATION_PHASES, STRICT_VERDICT_PHASE_NAMES;
58629
- var init_types9 = __esm(() => {
58630
- EXHAUSTED_EXIT_REASONS = new Set([
58631
- "max-attempts-total",
58632
- "max-attempts-per-strategy",
58633
- "bail-when",
58634
- "no-strategy",
58635
- "agent-gave-up",
58636
- "validate-short-circuit"
58637
- ]);
58638
- TDD_OP_NAMES = new Set(["test-writer", "implementer", "verifier"]);
58639
- CANONICAL_ORDER = [
58640
- "test-writer",
58641
- "greenfield-gate",
58642
- "implementer",
58643
- "test-presence-gate",
58644
- "full-suite-gate",
58645
- "mutation-check",
58646
- "verifier",
58647
- "verify-scoped",
58648
- "lint-check",
58649
- "typecheck-check",
58650
- "semantic-review",
58651
- "adversarial-review"
58652
- ];
58653
- PHASE_KIND_TO_STATE_KEY = {
58654
- "test-writer": "testWriter",
58655
- "greenfield-gate": "greenfieldGate",
58656
- implementer: "implementer",
58657
- "test-presence-gate": "testPresenceGate",
58658
- "full-suite-gate": "fullSuiteGate",
58659
- "mutation-check": "mutationCheck",
58660
- verifier: "verifier",
58661
- "verify-scoped": "verifyScoped",
58662
- "lint-check": "lintCheck",
58663
- "typecheck-check": "typecheckCheck",
58664
- "semantic-review": "semanticReview",
58665
- "adversarial-review": "adversarialReview"
58666
- };
58667
- STRATEGY_TO_REVALIDATION_PHASES = {
58668
- "mechanical-lintfix": ["lint-check"],
58669
- "mechanical-formatfix": ["lint-check"],
58670
- "autofix-implementer": ["lint-check", "typecheck-check", "full-suite-gate", "semantic-review", "adversarial-review"],
58671
- "autofix-test-writer": ["lint-check", "typecheck-check", "full-suite-gate", "adversarial-review"],
58672
- "full-suite-rectify": [
58673
- "lint-check",
58674
- "typecheck-check",
58675
- "full-suite-gate",
58676
- "verifier",
58677
- "verify-scoped",
58678
- "semantic-review",
58679
- "adversarial-review"
58680
- ]
58681
- };
58682
- STRICT_VERDICT_PHASE_NAMES = new Set([
58683
- "full-suite-gate",
58684
- "verify-scoped",
58685
- "lint-check",
58686
- "typecheck-check",
58687
- "verifier"
58688
- ]);
58689
- });
58690
-
58691
- // src/execution/story-orchestrator/phase-eval.ts
58692
- function phaseExplicitlyPassed(output) {
58693
- if (output === null || output === undefined || typeof output !== "object")
58694
- return false;
58695
- const r = output;
58696
- return r.success === true || r.passed === true;
58697
- }
58698
- function phasePassed(opName, output, storyId) {
58699
- const strictVerdictPhase = STRICT_VERDICT_PHASE_NAMES.has(opName);
58700
- if (output === null || output === undefined) {
58701
- getSafeLogger()?.warn("story-orchestrator", strictVerdictPhase ? "Strict phase produced no output \u2014 treating as fail" : "Phase produced no output \u2014 treating as pass", {
58702
- storyId,
58703
- phase: opName
58704
- });
58705
- return !strictVerdictPhase;
58706
- }
58707
- if (typeof output !== "object") {
58708
- if (!strictVerdictPhase)
58709
- return true;
58710
- getSafeLogger()?.warn("story-orchestrator", "Strict phase produced non-object output \u2014 treating as fail", {
58711
- storyId,
58712
- phase: opName
58713
- });
58714
- return false;
58715
- }
58716
- const r = output;
58717
- if ("success" in r)
58718
- return r.success !== false;
58719
- if ("passed" in r)
58720
- return r.passed !== false;
58721
- getSafeLogger()?.warn("story-orchestrator", strictVerdictPhase ? "Strict phase output has neither 'success' nor 'passed' \u2014 treating as fail" : "Phase output has neither 'success' nor 'passed' \u2014 treating as pass", {
58722
- storyId,
58723
- phase: opName
58724
- });
58725
- return !strictVerdictPhase;
58726
- }
58727
- function isFinding(value) {
58728
- return typeof value === "object" && value !== null && typeof value.source === "string" && value.source.length > 0;
58729
- }
58730
- function extractPhaseFindings(output) {
58731
- if (output === null || output === undefined || typeof output !== "object") {
58732
- return [];
58733
- }
58734
- const record2 = output;
58735
- const rawArray = Array.isArray(record2.normalizedFindings) && record2.normalizedFindings.length > 0 ? record2.normalizedFindings : Array.isArray(record2.findings) ? record2.findings : [];
58736
- const findings = rawArray.filter(isFinding);
58737
- const success2 = "success" in record2 ? record2.success === true : ("passed" in record2) ? record2.passed === true : findings.length === 0;
58738
- return success2 ? [] : findings;
58739
- }
58740
- function gateFailureKeys(gateOutput) {
58741
- const keys = new Set;
58742
- for (const f of extractPhaseFindings(gateOutput)) {
58743
- if (f.source !== "test-runner")
58744
- continue;
58745
- if (f.category === "flaky-test")
58746
- continue;
58747
- keys.add(`${f.file ?? ""}::${f.rule ?? ""}`);
58748
- }
58749
- return keys;
58750
- }
58751
- function describeGateRegression(input) {
58752
- const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
58753
- const notRegressed = {
58754
- regressed: false,
58755
- regressedKeys: [],
58756
- memoExcludedKeys: [],
58757
- baselineKeySize: baselineKeys.size,
58758
- keyless: false
58759
- };
58760
- if (gateName === undefined || phasePassed(gateName, gateOutput, storyId))
58761
- return notRegressed;
58762
- const allKeys = gateFailureKeys(gateOutput);
58763
- const memoExcludedKeys = quarantineMemo ? [...allKeys].filter((k) => quarantineMemo.has(k)) : [];
58764
- const excluded = new Set(memoExcludedKeys);
58765
- const regressedKeys = [...allKeys].filter((k) => !baselineKeys.has(k) && !excluded.has(k));
58766
- const keyless = allKeys.size === 0 || allKeys.has(KEYLESS_GATE_FAILURE_KEY);
58767
- return {
58768
- regressed: regressedKeys.length > 0 || keyless,
58769
- regressedKeys,
58770
- memoExcludedKeys,
58771
- baselineKeySize: baselineKeys.size,
58772
- keyless
58773
- };
58774
- }
58775
- function phasesToRevalidate(strategiesRun, allPhases) {
58776
- if (!strategiesRun || strategiesRun.length === 0)
58777
- return allPhases;
58778
- const unknown2 = strategiesRun.some((name) => STRATEGY_TO_REVALIDATION_PHASES[name] === undefined);
58779
- if (unknown2)
58780
- return allPhases;
58781
- const needed = new Set;
58782
- for (const name of strategiesRun) {
58783
- for (const kind of STRATEGY_TO_REVALIDATION_PHASES[name] ?? []) {
58784
- needed.add(kind);
58785
- }
58786
- }
58787
- return allPhases.filter((p) => needed.has(p.kind));
58788
- }
58789
- function orderGateLast(phases) {
58790
- const rest = phases.filter((p) => p.kind !== "full-suite-gate");
58791
- const gates = phases.filter((p) => p.kind === "full-suite-gate");
58792
- return [...rest, ...gates];
58793
- }
58794
- var KEYLESS_GATE_FAILURE_KEY = "::";
58795
- var init_phase_eval = __esm(() => {
58796
- init_logger2();
58797
- init_types9();
58798
- });
58799
-
58800
59001
  // src/execution/story-orchestrator/phase-state.ts
58801
59002
  function isSlot(value) {
58802
59003
  return value !== null && typeof value === "object" && "op" in value && "input" in value && typeof value.op?.kind === "string";
@@ -58990,15 +59191,15 @@ var init_verification = __esm(() => {
58990
59191
  });
58991
59192
 
58992
59193
  // src/execution/story-orchestrator/flake-triage-seam.ts
58993
- var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
59194
+ var productionTriageSeam = async (gateFindings, { ctx, rawOutput, quarantineMemo }) => {
58994
59195
  const config2 = ctx.packageView.config;
58995
59196
  const flakeDetection = config2.execution?.flakeDetection;
58996
59197
  if (!flakeDetection?.enabled) {
58997
- return [gateFindings, { quarantinedKeys: [] }];
59198
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
58998
59199
  }
58999
59200
  const framework = detectFramework(rawOutput);
59000
59201
  if (framework === "unknown") {
59001
- return [gateFindings, { quarantinedKeys: [] }];
59202
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59002
59203
  }
59003
59204
  const workdir = ctx.runtime.workdir;
59004
59205
  const storyWorkdir = ctx.story?.workdir;
@@ -59007,11 +59208,11 @@ var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
59007
59208
  const { testCommand } = await resolveQualityTestCommands2(config2, workdir, storyWorkdir);
59008
59209
  const baseCommand = testCommand ?? config2.quality?.commands?.test;
59009
59210
  if (!baseCommand) {
59010
- return [gateFindings, { quarantinedKeys: [] }];
59211
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59011
59212
  }
59012
59213
  const diff = await resolveFlakeBaselineDiff(config2, workdir, storyWorkdir);
59013
59214
  if (diff === null) {
59014
- return [gateFindings, { quarantinedKeys: [] }];
59215
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59015
59216
  }
59016
59217
  const result = await triageFlakyFindings({
59017
59218
  findings: gateFindings,
@@ -59020,15 +59221,15 @@ var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
59020
59221
  baseCommand,
59021
59222
  cwd: ctx.packageDir,
59022
59223
  framework,
59023
- quarantineMemo: ctx.runtime.quarantineMemo
59224
+ quarantineMemo: quarantineMemo ?? ctx.runtime.quarantineMemo
59024
59225
  });
59025
- return [result.findings, { quarantinedKeys: result.quarantineReport.keys }];
59226
+ return [result.findings, { quarantinedKeys: result.quarantineReport.keys, flakeTriageRan: true }];
59026
59227
  } catch (err) {
59027
59228
  getSafeLogger()?.warn("story-orchestrator", "Flake triage seam failed resolving context \u2014 keeping findings blocking (no quarantine)", {
59028
59229
  storyId: ctx.storyId,
59029
59230
  error: errorMessage(err)
59030
59231
  });
59031
- return [gateFindings, { quarantinedKeys: [] }];
59232
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59032
59233
  }
59033
59234
  };
59034
59235
  var init_flake_triage_seam = __esm(() => {
@@ -59447,9 +59648,12 @@ var init_run_phase = __esm(() => {
59447
59648
  });
59448
59649
 
59449
59650
  // src/execution/story-orchestrator/rectification.ts
59450
- function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
59651
+ function shouldSkipPhaseForRectification(input) {
59652
+ const { phase, state, phaseOutputs, nbfPath } = input;
59451
59653
  if (phase.kind !== "full-suite-gate")
59452
59654
  return false;
59655
+ if (nbfPath)
59656
+ return false;
59453
59657
  const verifierName = state.verifier?.slot.op.name;
59454
59658
  if (!verifierName)
59455
59659
  return false;
@@ -59458,7 +59662,7 @@ function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
59458
59662
  function gatherRectificationFindings(phaseOutputs, phases, state) {
59459
59663
  const findings = [];
59460
59664
  for (const phase of phases) {
59461
- if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
59665
+ if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs }))
59462
59666
  continue;
59463
59667
  for (const f of extractPhaseFindings(phaseOutputs[phase.slot.op.name])) {
59464
59668
  if (f.category === "flaky-test")
@@ -59528,6 +59732,9 @@ function collectRectificationPhases(state) {
59528
59732
  state.adversarialReview
59529
59733
  ].filter((phase) => phase !== undefined);
59530
59734
  }
59735
+ function isQuarantinedOnlyGateFailure(phase, rawFindings, blockingFindings) {
59736
+ return phase.kind === "full-suite-gate" && rawFindings.length > 0 && blockingFindings.length === 0;
59737
+ }
59531
59738
  async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides) {
59532
59739
  const rectification2 = state.rectification;
59533
59740
  const baseValidationPhases = collectRectificationPhases(state);
@@ -59539,7 +59746,9 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59539
59746
  return {};
59540
59747
  }
59541
59748
  let initialFindings;
59749
+ let nbfPath = false;
59542
59750
  if (overrides?.initialFindings) {
59751
+ nbfPath = true;
59543
59752
  initialFindings = [...overrides.initialFindings];
59544
59753
  } else {
59545
59754
  const gateName = state.fullSuiteGate?.slot.op.name;
@@ -59589,11 +59798,24 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59589
59798
  let shortCircuited = false;
59590
59799
  for (const phase of phases) {
59591
59800
  await runPhase(ctx, phase.slot, phaseCosts, phaseOutputs);
59592
- if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
59801
+ if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs, nbfPath }))
59593
59802
  continue;
59594
59803
  const output = phaseOutputs[phase.slot.op.name];
59595
- findings.push(...extractPhaseFindings(output));
59596
- if (!phasePassed(phase.slot.op.name, output, ctx.storyId)) {
59804
+ const nbfFlakeTriage = overrides?.nbfFlakeTriage;
59805
+ if (nbfPath && phase.kind === "full-suite-gate" && nbfFlakeTriage) {
59806
+ await triageNbfGate({
59807
+ output,
59808
+ gateName: phase.slot.op.name,
59809
+ ctx,
59810
+ transaction: nbfFlakeTriage,
59811
+ triage: _storyOrchestratorDeps.triage
59812
+ });
59813
+ }
59814
+ const phaseFindings = extractPhaseFindings(output);
59815
+ const blockingFindings = nbfPath ? phaseFindings.filter((finding) => !isQuarantinedFlake(finding, nbfFlakeTriage?.memo ?? ctx.runtime.quarantineMemo)) : phaseFindings;
59816
+ findings.push(...blockingFindings);
59817
+ const quarantinedOnly = nbfPath && isQuarantinedOnlyGateFailure(phase, phaseFindings, blockingFindings);
59818
+ if (!phasePassed(phase.slot.op.name, output, ctx.storyId) && !quarantinedOnly) {
59597
59819
  getSafeLogger()?.warn("story-orchestrator", "Short-circuiting revalidation on phase failure", {
59598
59820
  storyId: ctx.storyId,
59599
59821
  phase: phase.slot.op.name
@@ -59651,6 +59873,7 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59651
59873
  }
59652
59874
  var init_rectification = __esm(() => {
59653
59875
  init_logger2();
59876
+ init_nbf_flake_triage();
59654
59877
  init_phase_eval();
59655
59878
  init_phase_eval();
59656
59879
  init_run_phase();
@@ -59667,13 +59890,13 @@ class ExecutionPlan {
59667
59890
  this.state = state;
59668
59891
  this.isThreeSession = isThreeSession;
59669
59892
  }
59670
- describeGateRegressionNow(phaseOutputs, gateName, baselineKeys) {
59893
+ describeGateRegressionNow(phaseOutputs, gateName, options) {
59671
59894
  return describeGateRegression({
59672
59895
  gateOutput: gateName === undefined ? undefined : phaseOutputs[gateName],
59673
- baselineKeys,
59896
+ baselineKeys: options.baselineKeys,
59674
59897
  gateName,
59675
59898
  storyId: this.ctx.storyId,
59676
- quarantineMemo: this.ctx.runtime.quarantineMemo
59899
+ quarantineMemo: options.quarantineMemo ?? this.ctx.runtime.quarantineMemo
59677
59900
  });
59678
59901
  }
59679
59902
  phaseNames() {
@@ -59821,15 +60044,21 @@ class ExecutionPlan {
59821
60044
  cfg: advCfg,
59822
60045
  phaseOutputs,
59823
60046
  phaseCosts,
59824
- runRectify: (maxAttempts) => runRectification(this.ctx, this.state, phaseCosts, phaseOutputs, {
60047
+ quarantineMemo: this.ctx.runtime.quarantineMemo,
60048
+ gateBaselineKeys: preRectGateFailureKeys,
60049
+ runRectify: (maxAttempts, nbfFlakeTriage) => runRectification(this.ctx, this.state, phaseCosts, phaseOutputs, {
59825
60050
  initialFindings: advisoryFindings,
60051
+ nbfFlakeTriage,
59826
60052
  strategies: this.state.nonBlockingFixStrategies ?? [],
59827
60053
  excludePhaseKinds: nonBlockingExcludePhases(),
59828
60054
  extraRevalidationKinds: nonBlockingExtraPhases(advCfg),
59829
60055
  maxAttempts,
59830
60056
  postValidate: this.state.nonBlockingFixPostValidate
59831
60057
  }),
59832
- keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
60058
+ keptTreeRegressed: (quarantineMemo) => this.describeGateRegressionNow(phaseOutputs, gateName, {
60059
+ baselineKeys: preRectGateFailureKeys,
60060
+ quarantineMemo
60061
+ })
59833
60062
  }, {
59834
60063
  measureSourceDiff: createMeasureSourceDiff({
59835
60064
  config: this.ctx.runtime.configLoader.current(),
@@ -59840,7 +60069,9 @@ class ExecutionPlan {
59840
60069
  }
59841
60070
  const verifierName = this.state.verifier?.slot.op.name;
59842
60071
  const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
59843
- const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
60072
+ const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, {
60073
+ baselineKeys: preRectGateFailureKeys
60074
+ }).regressed;
59844
60075
  const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
59845
60076
  if (verifierExplicitlyPassed && gateRegressedDuringRect) {
59846
60077
  logger?.warn("story-orchestrator", "Gate regressed during rectification after verifier passed \u2014 verifier verdict is stale, failing story", { storyId: this.ctx.storyId, packageDir: this.ctx.packageDir });
@@ -59991,6 +60222,7 @@ var init_story_orchestrator = __esm(() => {
59991
60222
  init_execution_plan();
59992
60223
  init_phase_eval();
59993
60224
  init_rectification();
60225
+ init_nbf_flake_triage();
59994
60226
  init_run_phase();
59995
60227
  init_types9();
59996
60228
  });
@@ -64778,6 +65010,7 @@ function getFinishAutoFlowConfig(ctx) {
64778
65010
  quality: autoFlow.reviewers?.quality ?? null
64779
65011
  },
64780
65012
  escalate: { telegram: autoFlow.escalate?.telegram !== false },
65013
+ notify: { mode: autoFlow.notify?.mode ?? defaults.notify.mode },
64781
65014
  timeouts: {
64782
65015
  acceptanceMs: autoFlow.timeouts?.acceptanceMs ?? defaults.timeouts.acceptanceMs,
64783
65016
  gateMs: autoFlow.timeouts?.gateMs ?? defaults.timeouts.gateMs,
@@ -64802,39 +65035,74 @@ var init_config2 = __esm(() => {
64802
65035
  defaultAgent: null,
64803
65036
  reviewers: { spec: null, quality: null },
64804
65037
  escalate: { telegram: true },
65038
+ notify: { mode: "escalation" },
64805
65039
  timeouts: { acceptanceMs: 600000, gateMs: 900000, flowMs: 5400000, stepMs: null }
64806
65040
  };
64807
65041
  });
64808
65042
 
65043
+ // src/plugins/builtin/nax-finish/output.ts
65044
+ function logTail(stream) {
65045
+ if (stream.length <= LOG_TAIL_CHARS)
65046
+ return stream;
65047
+ return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
65048
+ ${stream.slice(-LOG_TAIL_CHARS)}`;
65049
+ }
65050
+ function stderrTail(stderr) {
65051
+ const trimmed = stderr.trim();
65052
+ if (!trimmed)
65053
+ return "";
65054
+ const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
65055
+ return tail.replace(/\s+/g, " ");
65056
+ }
65057
+ var STDERR_TAIL_CHARS = 400, LOG_TAIL_CHARS = 20000;
65058
+
64809
65059
  // src/plugins/builtin/nax-finish/telegram.ts
65060
+ function buildTerminalMessage(options) {
65061
+ const lines = [`nax-finish ${options.status} ${options.feature}`];
65062
+ if (options.detail)
65063
+ lines.push(options.detail);
65064
+ if (options.url)
65065
+ lines.push(options.url);
65066
+ const message = lines.join(`
65067
+ `);
65068
+ return message.length <= TELEGRAM_MAX_MESSAGE_CHARS ? message : `${message.slice(0, TELEGRAM_MAX_MESSAGE_CHARS - 1)}\u2026`;
65069
+ }
65070
+ function buildEscalationMessage(feature, reason, findings) {
65071
+ const head = `nax-finish escalated ${feature}: ${reason}`;
65072
+ if (findings.length === 0)
65073
+ return head;
65074
+ const footerReserve = `
65075
+ \u2026and ${findings.length} more`.length;
65076
+ const lines = [];
65077
+ let used = head.length;
65078
+ for (const f of findings) {
65079
+ const title = f.title.length > MAX_FINDING_TITLE_CHARS ? `${f.title.slice(0, MAX_FINDING_TITLE_CHARS)}\u2026` : f.title;
65080
+ const line = `
65081
+ - [${f.severity}] ${title}`;
65082
+ if (used + line.length + footerReserve > TELEGRAM_MAX_MESSAGE_CHARS)
65083
+ break;
65084
+ lines.push(line);
65085
+ used += line.length;
65086
+ }
65087
+ const omitted = findings.length - lines.length;
65088
+ return `${head}${lines.join("")}${omitted > 0 ? `
65089
+ \u2026and ${omitted} more` : ""}`;
65090
+ }
64810
65091
  async function sendTelegramNotify(cfg, text) {
64811
65092
  const res = await _telegramDeps.fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
64812
65093
  method: "POST",
64813
65094
  headers: { "content-type": "application/json" },
64814
- body: JSON.stringify({ chat_id: cfg.chatId, text, parse_mode: "Markdown" })
65095
+ body: JSON.stringify({ chat_id: cfg.chatId, text })
64815
65096
  });
64816
65097
  return res.ok;
64817
65098
  }
64818
- var _telegramDeps;
65099
+ var TELEGRAM_MAX_MESSAGE_CHARS = 4096, MAX_FINDING_TITLE_CHARS = 120, _telegramDeps;
64819
65100
  var init_telegram2 = __esm(() => {
64820
65101
  _telegramDeps = { fetch: (...a) => fetch(...a) };
64821
65102
  });
64822
65103
 
64823
65104
  // src/plugins/builtin/nax-finish/index.ts
64824
65105
  import * as path21 from "path";
64825
- function logTail(stream) {
64826
- if (stream.length <= LOG_TAIL_CHARS)
64827
- return stream;
64828
- return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
64829
- ${stream.slice(-LOG_TAIL_CHARS)}`;
64830
- }
64831
- function stderrTail(stderr) {
64832
- const trimmed = stderr.trim();
64833
- if (!trimmed)
64834
- return "";
64835
- const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
64836
- return tail.replace(/\s+/g, " ");
64837
- }
64838
65106
  async function defaultRun2(cmd, opts) {
64839
65107
  const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
64840
65108
  let timedOut = false;
@@ -64865,6 +65133,11 @@ async function defaultReadResult(workdir) {
64865
65133
  return null;
64866
65134
  return JSON.parse(await f.text());
64867
65135
  }
65136
+ async function defaultClearResult(workdir) {
65137
+ const file3 = Bun.file(path21.join(workdir, ".nax", "nax-finish-result.json"));
65138
+ if (await file3.exists())
65139
+ await file3.delete();
65140
+ }
64868
65141
  function isFeatureBranch(b) {
64869
65142
  return b !== "main" && b !== "master" && b.length > 0;
64870
65143
  }
@@ -64908,13 +65181,133 @@ function buildFlowEnv(cfg) {
64908
65181
  env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
64909
65182
  return env2;
64910
65183
  }
64911
- var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, STDERR_TAIL_CHARS = 400, LOG_TAIL_CHARS = 20000, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
65184
+ function missingResultOutcome(ctx, res, escalateTelegram) {
65185
+ ctx.logger.warn("nax-finish flow produced no result file", {
65186
+ exitCode: res.exitCode,
65187
+ stdout: logTail(res.stdout),
65188
+ stderr: logTail(res.stderr)
65189
+ });
65190
+ const tail = stderrTail(res.stderr);
65191
+ return {
65192
+ actionResult: {
65193
+ success: false,
65194
+ message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
65195
+ },
65196
+ escalateTelegram
65197
+ };
65198
+ }
65199
+ async function executeFinishFlow(options) {
65200
+ const { ctx, cfg, escalateTelegram } = options;
65201
+ const flowPath = await resolveFlowPath(ctx.workdir, cfg.flowPath);
65202
+ if (!flowPath) {
65203
+ return {
65204
+ actionResult: {
65205
+ success: false,
65206
+ message: `nax-finish: flow module "${cfg.flowPath}" not found in the nax install or ${ctx.workdir}`
65207
+ },
65208
+ escalateTelegram
65209
+ };
65210
+ }
65211
+ await _naxFinishDeps.clearResult(ctx.workdir);
65212
+ const input = {
65213
+ feature: ctx.feature,
65214
+ workdir: ctx.workdir,
65215
+ branch: ctx.branch,
65216
+ prdPath: ctx.prdPath,
65217
+ escalateTelegram,
65218
+ timeouts: { acceptanceMs: cfg.timeouts.acceptanceMs, gateMs: cfg.timeouts.gateMs }
65219
+ };
65220
+ const cmd = buildFlowArgv(flowPath, JSON.stringify(input), cfg.defaultAgent, cfg.timeouts.stepMs);
65221
+ const res = await _naxFinishDeps.run(cmd, {
65222
+ cwd: ctx.workdir,
65223
+ env: buildFlowEnv(cfg),
65224
+ timeoutMs: cfg.timeouts.flowMs
65225
+ });
65226
+ const result = await _naxFinishDeps.readResult(ctx.workdir);
65227
+ if (!result)
65228
+ return missingResultOutcome(ctx, res, escalateTelegram);
65229
+ return {
65230
+ actionResult: { success: true, message: `nax-finish: ${result.status}`, url: result.url },
65231
+ result,
65232
+ escalateTelegram
65233
+ };
65234
+ }
65235
+ async function settleFinishFlow(options) {
65236
+ const escalateTelegram = options.cfg.notify.mode !== "off" && options.cfg.escalate.telegram && options.creds !== null;
65237
+ try {
65238
+ return await executeFinishFlow({ ...options, escalateTelegram });
65239
+ } catch (error48) {
65240
+ options.ctx.logger.warn("nax-finish execute failed", { error: errorMessage(error48) });
65241
+ return {
65242
+ actionResult: { success: false, message: `nax-finish failed: ${errorMessage(error48)}` },
65243
+ escalateTelegram
65244
+ };
65245
+ }
65246
+ }
65247
+ async function notifyBestEffort(ctx, creds, message) {
65248
+ if (!creds) {
65249
+ ctx.logger.warn("nax-finish terminal notification skipped: Telegram credentials are unavailable");
65250
+ return;
65251
+ }
65252
+ try {
65253
+ if (!await _naxFinishDeps.notify(creds, message)) {
65254
+ ctx.logger.warn("nax-finish terminal notification was rejected", { feature: ctx.feature });
65255
+ }
65256
+ } catch (error48) {
65257
+ ctx.logger.warn("nax-finish terminal notification failed", { feature: ctx.feature, error: errorMessage(error48) });
65258
+ }
65259
+ }
65260
+ async function finalizeEscalation(ctx, outcome, creds) {
65261
+ const result = outcome.result;
65262
+ if (!result)
65263
+ return outcome.actionResult;
65264
+ const problems = [];
65265
+ let delivered = !outcome.escalateTelegram && !result.deliveryError;
65266
+ if (outcome.escalateTelegram && creds) {
65267
+ try {
65268
+ delivered = await _naxFinishDeps.notify(creds, buildEscalationMessage(result.feature, result.escalationReason ?? "", result.findings ?? []));
65269
+ if (!delivered)
65270
+ problems.push("Telegram rejected the message");
65271
+ } catch (error48) {
65272
+ problems.push(`Telegram failed: ${errorMessage(error48)}`);
65273
+ }
65274
+ }
65275
+ if (delivered)
65276
+ return outcome.actionResult;
65277
+ if (result.deliveryError)
65278
+ problems.push(`the flow could not post it: ${result.deliveryError}`);
65279
+ if (problems.length === 0)
65280
+ problems.push("no escalation channel was reachable");
65281
+ ctx.logger.warn("nax-finish escalation was not delivered", {
65282
+ feature: result.feature,
65283
+ reasons: problems,
65284
+ escalationReason: result.escalationReason
65285
+ });
65286
+ return {
65287
+ success: false,
65288
+ message: `nax-finish: escalated but undelivered \u2014 ${problems.join("; ")}`,
65289
+ url: result.url
65290
+ };
65291
+ }
65292
+ async function finalizeFinishOutcome(options) {
65293
+ const { ctx, cfg, creds, outcome } = options;
65294
+ if (outcome.result?.status === "escalated")
65295
+ return finalizeEscalation(ctx, outcome, creds);
65296
+ if (cfg.notify.mode === "always") {
65297
+ const status = outcome.result?.status ?? "failed";
65298
+ const detail = outcome.result ? undefined : outcome.actionResult.message;
65299
+ await notifyBestEffort(ctx, creds, buildTerminalMessage({ feature: ctx.feature, status, detail, url: outcome.actionResult.url }));
65300
+ }
65301
+ return outcome.actionResult;
65302
+ }
65303
+ var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
64912
65304
  var init_nax_finish = __esm(() => {
64913
65305
  init_config2();
64914
65306
  init_telegram2();
64915
65307
  _naxFinishDeps = {
64916
65308
  run: defaultRun2,
64917
65309
  readResult: defaultReadResult,
65310
+ clearResult: defaultClearResult,
64918
65311
  exists: (p) => Bun.file(p).exists(),
64919
65312
  moduleDir: import.meta.dir,
64920
65313
  notify: sendTelegramNotify
@@ -64932,52 +65325,10 @@ var init_nax_finish = __esm(() => {
64932
65325
  return isFeatureBranch(ctx.branch);
64933
65326
  },
64934
65327
  async execute(ctx) {
64935
- try {
64936
- const cfg = getFinishAutoFlowConfig(ctx);
64937
- const flowPath = await resolveFlowPath(ctx.workdir, cfg.flowPath);
64938
- if (!flowPath) {
64939
- return {
64940
- success: false,
64941
- message: `nax-finish: flow module "${cfg.flowPath}" not found in the nax install or ${ctx.workdir}`
64942
- };
64943
- }
64944
- const creds = telegramCreds(ctx.config);
64945
- const escalateTelegram = cfg.escalate.telegram && creds !== null;
64946
- const input = {
64947
- feature: ctx.feature,
64948
- workdir: ctx.workdir,
64949
- branch: ctx.branch,
64950
- prdPath: ctx.prdPath,
64951
- escalateTelegram,
64952
- timeouts: { acceptanceMs: cfg.timeouts.acceptanceMs, gateMs: cfg.timeouts.gateMs }
64953
- };
64954
- const cmd = buildFlowArgv(flowPath, JSON.stringify(input), cfg.defaultAgent, cfg.timeouts.stepMs);
64955
- const res = await _naxFinishDeps.run(cmd, {
64956
- cwd: ctx.workdir,
64957
- env: buildFlowEnv(cfg),
64958
- timeoutMs: cfg.timeouts.flowMs
64959
- });
64960
- const result = await _naxFinishDeps.readResult(ctx.workdir);
64961
- if (!result) {
64962
- ctx.logger.warn("nax-finish flow produced no result file", {
64963
- exitCode: res.exitCode,
64964
- stdout: logTail(res.stdout),
64965
- stderr: logTail(res.stderr)
64966
- });
64967
- const tail = stderrTail(res.stderr);
64968
- return {
64969
- success: false,
64970
- message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
64971
- };
64972
- }
64973
- if (result.status === "escalated" && escalateTelegram && creds) {
64974
- await _naxFinishDeps.notify(creds, `nax-finish escalated *${result.feature}*: ${result.escalationReason ?? ""}`);
64975
- }
64976
- return { success: true, message: `nax-finish: ${result.status}`, url: result.url };
64977
- } catch (err) {
64978
- ctx.logger.warn("nax-finish execute failed", { error: String(err) });
64979
- return { success: false, message: `nax-finish failed: ${String(err)}` };
64980
- }
65328
+ const cfg = getFinishAutoFlowConfig(ctx);
65329
+ const creds = telegramCreds(ctx.config);
65330
+ const outcome = await settleFinishFlow({ ctx, cfg, creds });
65331
+ return finalizeFinishOutcome({ ctx, cfg, creds, outcome });
64981
65332
  }
64982
65333
  };
64983
65334
  naxFinishPlugin = {
@@ -65114,6 +65465,7 @@ var init_batch_queue = __esm(() => {
65114
65465
  });
65115
65466
 
65116
65467
  // src/plugins/builtin/otel-reporter/otlp.ts
65468
+ import { hostname as hostname3 } from "os";
65117
65469
  function attr(key, value) {
65118
65470
  return typeof value === "number" ? { key, value: { doubleValue: value } } : { key, value: { stringValue: value } };
65119
65471
  }
@@ -65133,8 +65485,25 @@ function buildHistogramPoint(values, bounds, attributes, timeUnixNano) {
65133
65485
  function buildCounterPoint(count, attributes, timeUnixNano) {
65134
65486
  return { attributes, timeUnixNano, asInt: String(count) };
65135
65487
  }
65136
- function buildResourceAttributes(serviceName, runId) {
65137
- return [attr("service.name", serviceName), attr("nax.run_id", runId)];
65488
+ function buildResourceAttributes(input) {
65489
+ const attrs = [
65490
+ attr("service.name", input.serviceName),
65491
+ attr("nax.run_id", input.runId),
65492
+ attr("nax.version", NAX_VERSION),
65493
+ attr("process.pid", process.pid)
65494
+ ];
65495
+ try {
65496
+ attrs.push(attr("host.name", hostname3()));
65497
+ } catch {}
65498
+ if (input.feature !== undefined)
65499
+ attrs.push(attr("nax.feature", input.feature));
65500
+ if (input.project !== undefined)
65501
+ attrs.push(attr("nax.project", input.project));
65502
+ if (input.git?.branch !== undefined)
65503
+ attrs.push(attr("nax.git.branch", input.git.branch));
65504
+ if (input.git?.sha !== undefined)
65505
+ attrs.push(attr("nax.git.sha", input.git.sha));
65506
+ return attrs;
65138
65507
  }
65139
65508
  function buildTracesPayload(p) {
65140
65509
  const span = {
@@ -65160,7 +65529,18 @@ function buildTracesPayload(p) {
65160
65529
  return {
65161
65530
  resourceSpans: [
65162
65531
  {
65163
- resource: { attributes: [attr("service.name", p.serviceName)] },
65532
+ resource: {
65533
+ attributes: buildResourceAttributes({
65534
+ serviceName: p.serviceName,
65535
+ runId: p.runId,
65536
+ feature: p.feature,
65537
+ project: p.project,
65538
+ git: {
65539
+ branch: p.gitBranch,
65540
+ sha: p.gitSha
65541
+ }
65542
+ })
65543
+ },
65164
65544
  scopeSpans: [{ scope: { name: "nax" }, spans: [span, ...p.extraSpans ?? []] }]
65165
65545
  }
65166
65546
  ]
@@ -65187,7 +65567,18 @@ function buildMetricsPayload(p) {
65187
65567
  return {
65188
65568
  resourceMetrics: [
65189
65569
  {
65190
- resource: { attributes: [attr("service.name", p.serviceName)] },
65570
+ resource: {
65571
+ attributes: buildResourceAttributes({
65572
+ serviceName: p.serviceName,
65573
+ runId: p.runId,
65574
+ feature: p.feature,
65575
+ project: p.project,
65576
+ git: {
65577
+ branch: p.gitBranch,
65578
+ sha: p.gitSha
65579
+ }
65580
+ })
65581
+ },
65191
65582
  scopeMetrics: [
65192
65583
  {
65193
65584
  scope: { name: "nax" },
@@ -65198,6 +65589,9 @@ function buildMetricsPayload(p) {
65198
65589
  ]
65199
65590
  };
65200
65591
  }
65592
+ var init_otlp = __esm(() => {
65593
+ init_version();
65594
+ });
65201
65595
 
65202
65596
  // src/plugins/builtin/otel-reporter/heartbeat.ts
65203
65597
  function startHeartbeat(opts) {
@@ -65250,7 +65644,12 @@ function buildHeartbeatMetricsPayload(p) {
65250
65644
  return {
65251
65645
  resourceMetrics: [
65252
65646
  {
65253
- resource: { attributes: [attr("service.name", p.serviceName)] },
65647
+ resource: {
65648
+ attributes: buildResourceAttributes({
65649
+ serviceName: p.serviceName,
65650
+ runId: p.snapshot.attributes.runId
65651
+ })
65652
+ },
65254
65653
  scopeMetrics: [
65255
65654
  {
65256
65655
  scope: { name: "nax" },
@@ -65268,6 +65667,7 @@ function buildHeartbeatMetricsPayload(p) {
65268
65667
  var STAGE2 = "otel-reporter-heartbeat";
65269
65668
  var init_heartbeat = __esm(() => {
65270
65669
  init_logger2();
65670
+ init_otlp();
65271
65671
  });
65272
65672
 
65273
65673
  // src/plugins/builtin/otel-reporter/ids.ts
@@ -65281,6 +65681,78 @@ function randomHex(bytes) {
65281
65681
  }
65282
65682
  var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
65283
65683
 
65684
+ // src/plugins/builtin/otel-reporter/logs.ts
65685
+ function entryTimestampMs(entry) {
65686
+ return new Date(entry.timestamp).getTime();
65687
+ }
65688
+ function toLogRecord(entry) {
65689
+ const timeUnixNano = msToUnixNano(entryTimestampMs(entry));
65690
+ const { number: severityNumber, text: severityText } = SEVERITY[entry.level];
65691
+ const attributes = [attr("nax.stage", entry.stage)];
65692
+ if (entry.storyId !== undefined)
65693
+ attributes.push(attr("nax.story_id", entry.storyId));
65694
+ if (entry.sessionRole !== undefined)
65695
+ attributes.push(attr("nax.session_role", entry.sessionRole));
65696
+ const data = entry.data ?? {};
65697
+ const nonScalars = {};
65698
+ for (const [key, value] of Object.entries(data)) {
65699
+ if (typeof value === "string") {
65700
+ attributes.push(attr(`nax.data.${key}`, truncate3(value)));
65701
+ } else if (typeof value === "number") {
65702
+ if (Number.isFinite(value)) {
65703
+ attributes.push(attr(`nax.data.${key}`, value));
65704
+ } else {
65705
+ nonScalars[key] = value;
65706
+ }
65707
+ } else if (typeof value === "boolean") {
65708
+ attributes.push(attr(`nax.data.${key}`, String(value)));
65709
+ } else {
65710
+ nonScalars[key] = value;
65711
+ }
65712
+ }
65713
+ if (Object.keys(nonScalars).length > 0) {
65714
+ attributes.push(attr("nax.data_json", truncate3(JSON.stringify(nonScalars))));
65715
+ }
65716
+ return {
65717
+ body: { stringValue: entry.message },
65718
+ timeUnixNano,
65719
+ severityNumber,
65720
+ severityText,
65721
+ attributes
65722
+ };
65723
+ }
65724
+ function buildLogsPayload(entries, resource) {
65725
+ const logRecords = entries.map(toLogRecord);
65726
+ return {
65727
+ resourceLogs: [
65728
+ {
65729
+ resource: {
65730
+ attributes: buildResourceAttributes(resource)
65731
+ },
65732
+ scopeLogs: [{ scope: { name: "nax" }, logRecords }]
65733
+ }
65734
+ ]
65735
+ };
65736
+ }
65737
+ function truncate3(value) {
65738
+ if (value.length <= DATA_JSON_MAX)
65739
+ return value;
65740
+ const marker = TRUNCATION_MARKER;
65741
+ const keep = DATA_JSON_MAX - marker.length;
65742
+ return `${value.slice(0, keep)}${marker}`;
65743
+ }
65744
+ var SEVERITY, DATA_JSON_MAX = 2048, TRUNCATION_MARKER = "...[truncated]";
65745
+ var init_logs = __esm(() => {
65746
+ init_otlp();
65747
+ SEVERITY = {
65748
+ silent: { number: 0, text: "SILENT" },
65749
+ error: { number: 17, text: "ERROR" },
65750
+ warn: { number: 13, text: "WARN" },
65751
+ info: { number: 9, text: "INFO" },
65752
+ debug: { number: 5, text: "DEBUG" }
65753
+ };
65754
+ });
65755
+
65284
65756
  // src/plugins/builtin/otel-reporter/span-tree.ts
65285
65757
  function createSpanTree(traceId, runSpanId) {
65286
65758
  const storySpanIds = new Map;
@@ -65364,7 +65836,8 @@ function createPhaseMetricsAggregator() {
65364
65836
  function recordEscalation(toTier, count) {
65365
65837
  bumpCounter(escalations, [attr("to_tier", toTier)], count);
65366
65838
  }
65367
- function buildMetricsPayload2(serviceName, runId, timeUnixNano) {
65839
+ function buildMetricsPayload2(input) {
65840
+ const { serviceName, runId, timeUnixNano, feature, project, gitBranch, gitSha } = input;
65368
65841
  const groups = [...phaseGroups.values()];
65369
65842
  const counterMetric = (name, source) => ({
65370
65843
  name,
@@ -65400,7 +65873,15 @@ function createPhaseMetricsAggregator() {
65400
65873
  return {
65401
65874
  resourceMetrics: [
65402
65875
  {
65403
- resource: { attributes: buildResourceAttributes(serviceName, runId) },
65876
+ resource: {
65877
+ attributes: buildResourceAttributes({
65878
+ serviceName,
65879
+ runId,
65880
+ feature,
65881
+ project,
65882
+ git: { branch: gitBranch, sha: gitSha }
65883
+ })
65884
+ },
65404
65885
  scopeMetrics: [{ scope: { name: "nax" }, metrics }]
65405
65886
  }
65406
65887
  ]
@@ -65410,6 +65891,7 @@ function createPhaseMetricsAggregator() {
65410
65891
  }
65411
65892
  var PHASE_DURATION_BOUNDS, PHASE_COST_BOUNDS;
65412
65893
  var init_span_tree = __esm(() => {
65894
+ init_otlp();
65413
65895
  PHASE_DURATION_BOUNDS = [100, 500, 1000, 5000, 15000, 60000, 300000, 900000];
65414
65896
  PHASE_COST_BOUNDS = [0.001, 0.01, 0.05, 0.1, 0.5, 1, 5];
65415
65897
  });
@@ -65484,11 +65966,11 @@ function reviewSpanEvents(details, timeUnixNano, verbose) {
65484
65966
  return { timeUnixNano, name: "review.finding", attributes };
65485
65967
  });
65486
65968
  }
65487
- function createOtelReporterPlugin(cfg, deps) {
65969
+ function createOtelReporterPlugin(cfg, deps, workdir) {
65488
65970
  const states = new Map;
65489
65971
  const base = cfg.endpoint?.replace(/\/$/, "");
65490
65972
  let tornDown = false;
65491
- const sendSpanBatch = async (batch) => {
65973
+ const makeSendSpanBatch = (resourceAttrs) => async (batch) => {
65492
65974
  if (!base || batch.length === 0)
65493
65975
  return true;
65494
65976
  const { resolved, missing } = interpolateHeaders(cfg.headers);
@@ -65499,21 +65981,55 @@ function createOtelReporterPlugin(cfg, deps) {
65499
65981
  const payload = {
65500
65982
  resourceSpans: [
65501
65983
  {
65502
- resource: { attributes: [attr("service.name", cfg.serviceName)] },
65984
+ resource: { attributes: resourceAttrs },
65503
65985
  scopeSpans: [{ scope: { name: "nax" }, spans: batch }]
65504
65986
  }
65505
65987
  ]
65506
65988
  };
65507
- return postJson(`${base}/v1/traces`, payload, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
65989
+ return postJson(`${base}/v1/traces`, payload, {
65990
+ headers: resolved,
65991
+ timeoutMs: cfg.timeoutMs,
65992
+ stage: STAGE3,
65993
+ deps
65994
+ });
65508
65995
  };
65509
- const makeSpanQueue = () => createBatchQueue({
65996
+ const makeSpanQueue = (resourceAttrs) => createBatchQueue({
65510
65997
  maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
65511
65998
  flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
65512
65999
  maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
65513
- send: sendSpanBatch
66000
+ send: makeSendSpanBatch(resourceAttrs)
66001
+ });
66002
+ const makeSendLogsBatch = (resource) => async (batch) => {
66003
+ if (!base || batch.length === 0)
66004
+ return true;
66005
+ const { resolved, missing } = interpolateHeaders(cfg.headers);
66006
+ if (missing.length > 0) {
66007
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
66008
+ return true;
66009
+ }
66010
+ const payload = buildLogsPayload(batch, {
66011
+ serviceName: resource.serviceName,
66012
+ runId: resource.runId,
66013
+ feature: resource.feature,
66014
+ project: resource.project,
66015
+ git: { branch: resource.gitBranch, sha: resource.gitSha }
66016
+ });
66017
+ return postJson(`${base}/v1/logs`, payload, {
66018
+ headers: resolved,
66019
+ timeoutMs: cfg.timeoutMs,
66020
+ stage: STAGE3,
66021
+ deps
66022
+ });
66023
+ };
66024
+ const makeLogsQueue = (resource) => createBatchQueue({
66025
+ maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
66026
+ flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
66027
+ maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
66028
+ send: makeSendLogsBatch(resource)
65514
66029
  });
65515
66030
  const buildOrphanState = (startMs) => {
65516
66031
  const identity = rootSpanIdentity();
66032
+ const orphanAttrs = buildResourceAttributes({ serviceName: cfg.serviceName, runId: "orphan" });
65517
66033
  return {
65518
66034
  ...identity,
65519
66035
  startMs,
@@ -65521,7 +66037,7 @@ function createOtelReporterPlugin(cfg, deps) {
65521
66037
  project: "",
65522
66038
  events: [],
65523
66039
  spanTree: createSpanTree(identity.traceId, identity.spanId),
65524
- spanQueue: makeSpanQueue(),
66040
+ spanQueue: makeSpanQueue(orphanAttrs),
65525
66041
  metrics: createPhaseMetricsAggregator(),
65526
66042
  storyBounds: new Map,
65527
66043
  costUsd: 0,
@@ -65561,6 +66077,9 @@ function createOtelReporterPlugin(cfg, deps) {
65561
66077
  startUnixNano,
65562
66078
  endUnixNano,
65563
66079
  feature: st.feature,
66080
+ project: st.project,
66081
+ gitBranch: st.gitBranch,
66082
+ gitSha: st.gitSha,
65564
66083
  runId: e.runId,
65565
66084
  storySummary: e.storySummary,
65566
66085
  totalCost: e.totalCost,
@@ -65570,11 +66089,23 @@ function createOtelReporterPlugin(cfg, deps) {
65570
66089
  serviceName: cfg.serviceName,
65571
66090
  runId: e.runId,
65572
66091
  timeUnixNano: endUnixNano,
66092
+ feature: st.feature,
66093
+ project: st.project,
66094
+ gitBranch: st.gitBranch,
66095
+ gitSha: st.gitSha,
65573
66096
  storySummary: e.storySummary,
65574
66097
  totalCost: e.totalCost,
65575
66098
  totalDurationMs: e.totalDurationMs
65576
66099
  });
65577
- const aggMetrics = st.metrics.buildMetricsPayload(cfg.serviceName, e.runId, endUnixNano);
66100
+ const aggMetrics = st.metrics.buildMetricsPayload({
66101
+ serviceName: cfg.serviceName,
66102
+ runId: e.runId,
66103
+ timeUnixNano: endUnixNano,
66104
+ feature: st.feature,
66105
+ project: st.project,
66106
+ gitBranch: st.gitBranch,
66107
+ gitSha: st.gitSha
66108
+ });
65578
66109
  metrics.resourceMetrics[0].scopeMetrics[0].metrics.push(...aggMetrics.resourceMetrics[0].scopeMetrics[0].metrics);
65579
66110
  const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps };
65580
66111
  await postJson(`${base}/v1/traces`, traces, opts);
@@ -65585,14 +66116,41 @@ function createOtelReporterPlugin(cfg, deps) {
65585
66116
  async onRunStart(event) {
65586
66117
  const identity = rootSpanIdentity();
65587
66118
  const runId = event.runId;
66119
+ let gitBranch;
66120
+ let gitSha;
66121
+ if (base && workdir) {
66122
+ const [branchResult, shaResult] = await Promise.all([
66123
+ gitWithTimeout(["rev-parse", "--abbrev-ref", "HEAD"], workdir).catch(() => null),
66124
+ gitWithTimeout(["rev-parse", "HEAD"], workdir).catch(() => null)
66125
+ ]);
66126
+ if (branchResult?.exitCode === 0) {
66127
+ const branch = branchResult.stdout.trim();
66128
+ if (branch && branch !== "HEAD")
66129
+ gitBranch = branch;
66130
+ }
66131
+ if (shaResult?.exitCode === 0) {
66132
+ const sha = shaResult.stdout.trim();
66133
+ if (sha)
66134
+ gitSha = sha;
66135
+ }
66136
+ }
66137
+ const resourceAttrs = buildResourceAttributes({
66138
+ serviceName: cfg.serviceName,
66139
+ runId,
66140
+ feature: event.feature,
66141
+ project: event.project,
66142
+ git: { branch: gitBranch, sha: gitSha }
66143
+ });
65588
66144
  const state = {
65589
66145
  ...identity,
65590
66146
  startMs: Date.parse(event.startTime),
65591
66147
  feature: event.feature,
65592
66148
  project: event.project ?? "",
66149
+ gitBranch,
66150
+ gitSha,
65593
66151
  events: [],
65594
66152
  spanTree: createSpanTree(identity.traceId, identity.spanId),
65595
- spanQueue: makeSpanQueue(),
66153
+ spanQueue: makeSpanQueue(resourceAttrs),
65596
66154
  metrics: createPhaseMetricsAggregator(),
65597
66155
  storyBounds: new Map,
65598
66156
  costUsd: 0,
@@ -65603,6 +66161,27 @@ function createOtelReporterPlugin(cfg, deps) {
65603
66161
  })
65604
66162
  };
65605
66163
  states.set(runId, state);
66164
+ if (cfg.logs?.enabled) {
66165
+ const logsQueue = makeLogsQueue({
66166
+ serviceName: cfg.serviceName,
66167
+ runId,
66168
+ feature: event.feature,
66169
+ project: event.project ?? "",
66170
+ gitBranch,
66171
+ gitSha
66172
+ });
66173
+ const floorKey = cfg.logs.level;
66174
+ const sank = (entry) => {
66175
+ if (REENTRY_STAGES.has(entry.stage))
66176
+ return;
66177
+ if (LOG_PRIORITY[entry.level] > LOG_PRIORITY[floorKey])
66178
+ return;
66179
+ logsQueue.enqueue(entry);
66180
+ };
66181
+ const addSinkFn = deps?.addSink ?? addSink;
66182
+ state.logsQueue = logsQueue;
66183
+ state.logUnsubscribe = addSinkFn(sank);
66184
+ }
65606
66185
  },
65607
66186
  async onStoryComplete(event) {
65608
66187
  const st = states.get(event.runId);
@@ -65674,6 +66253,11 @@ function createOtelReporterPlugin(cfg, deps) {
65674
66253
  states.delete(event.runId);
65675
66254
  await st.spanQueue.flushNow();
65676
66255
  st.spanQueue.teardown();
66256
+ if (st.logsQueue) {
66257
+ await st.logsQueue.flushNow();
66258
+ st.logsQueue.teardown();
66259
+ st.logUnsubscribe?.();
66260
+ }
65677
66261
  await flush(st, startMs + event.totalDurationMs, event);
65678
66262
  }
65679
66263
  };
@@ -65691,6 +66275,11 @@ function createOtelReporterPlugin(cfg, deps) {
65691
66275
  st.heartbeat.stop();
65692
66276
  await st.spanQueue.flushNow();
65693
66277
  st.spanQueue.teardown();
66278
+ if (st.logsQueue) {
66279
+ await st.logsQueue.flushNow();
66280
+ st.logsQueue.teardown();
66281
+ st.logUnsubscribe?.();
66282
+ }
65694
66283
  const endMs = Date.now();
65695
66284
  await flush(st, endMs, {
65696
66285
  runId,
@@ -65703,14 +66292,25 @@ function createOtelReporterPlugin(cfg, deps) {
65703
66292
  extensions: { reporter }
65704
66293
  };
65705
66294
  }
65706
- var STAGE3 = "otel-reporter", DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048;
66295
+ var STAGE3 = "otel-reporter", REENTRY_STAGE = "otel-batch-queue", REENTRY_STAGES, DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048, LOG_PRIORITY;
65707
66296
  var init_otel_reporter = __esm(() => {
65708
66297
  init_logger2();
66298
+ init_git();
65709
66299
  init_reporter_shared();
65710
66300
  init_batch_queue();
65711
66301
  init_heartbeat();
66302
+ init_logs();
66303
+ init_otlp();
65712
66304
  init_span_tree();
65713
66305
  init_traceparent();
66306
+ REENTRY_STAGES = new Set([STAGE3, REENTRY_STAGE]);
66307
+ LOG_PRIORITY = {
66308
+ silent: -1,
66309
+ error: 0,
66310
+ warn: 1,
66311
+ info: 2,
66312
+ debug: 3
66313
+ };
65714
66314
  });
65715
66315
 
65716
66316
  // src/plugins/builtin/webhook-reporter/index.ts
@@ -65775,7 +66375,7 @@ class PluginRegistry {
65775
66375
  sources;
65776
66376
  builtinPostRunActions;
65777
66377
  constructor(loadedPlugins, builtinPostRunActions = []) {
65778
- this.builtinPostRunActions = builtinPostRunActions;
66378
+ this.builtinPostRunActions = builtinPostRunActions.map((registration) => ("action" in registration) ? registration : { pluginName: registration.name, action: registration });
65779
66379
  if (loadedPlugins.length > 0 && "plugin" in loadedPlugins[0]) {
65780
66380
  const typed = loadedPlugins;
65781
66381
  this.plugins = typed.map((lp) => lp.plugin);
@@ -65814,7 +66414,13 @@ class PluginRegistry {
65814
66414
  return this.plugins.filter((p) => p.provides.includes("reporter")).map((p) => p.extensions.reporter).filter((reporter) => reporter !== undefined);
65815
66415
  }
65816
66416
  getPostRunActions() {
65817
- const pluginActions = this.plugins.filter((p) => p.provides.includes("post-run-action")).map((p) => p.extensions.postRunAction).filter((action) => action !== undefined);
66417
+ return this.getPostRunActionRegistrations().map(({ action }) => action);
66418
+ }
66419
+ getPostRunActionRegistrations() {
66420
+ const pluginActions = this.plugins.flatMap((plugin) => {
66421
+ const action = plugin.extensions.postRunAction;
66422
+ return plugin.provides.includes("post-run-action") && action ? [{ pluginName: plugin.name, action }] : [];
66423
+ });
65818
66424
  return [...pluginActions, ...this.builtinPostRunActions];
65819
66425
  }
65820
66426
  async teardownAll() {
@@ -66129,7 +66735,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66129
66735
  }
66130
66736
  const autoPrAction2 = autoPrPlugin.extensions.postRunAction;
66131
66737
  if (autoPrAction2) {
66132
- builtinPostRunActions.push(autoPrAction2);
66738
+ builtinPostRunActions.push({ pluginName: autoPrPlugin.name, action: autoPrAction2 });
66133
66739
  }
66134
66740
  } else {
66135
66741
  logger?.info("plugins", `Skipping disabled plugin: '${autoPrPlugin.name}' (built-in)`);
@@ -66141,7 +66747,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66141
66747
  }
66142
66748
  const action = naxFinishPlugin.extensions.postRunAction;
66143
66749
  if (action)
66144
- builtinPostRunActions.push(action);
66750
+ builtinPostRunActions.push({ pluginName: naxFinishPlugin.name, action });
66145
66751
  } else {
66146
66752
  logger?.info("plugins", `Skipping disabled plugin: '${naxFinishPlugin.name}' (built-in)`);
66147
66753
  }
@@ -66152,7 +66758,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66152
66758
  }
66153
66759
  const autoRouteAction2 = autoRoutePlugin.extensions.postRunAction;
66154
66760
  if (autoRouteAction2) {
66155
- builtinPostRunActions.push(autoRouteAction2);
66761
+ builtinPostRunActions.push({ pluginName: autoRoutePlugin.name, action: autoRouteAction2 });
66156
66762
  }
66157
66763
  } else {
66158
66764
  logger?.info("plugins", `Skipping disabled plugin: '${autoRoutePlugin.name}' (built-in)`);
@@ -66166,7 +66772,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66166
66772
  {
66167
66773
  name: "otel-reporter",
66168
66774
  enabled: reporters.otel.enabled,
66169
- make: () => createOtelReporterPlugin(reporters.otel)
66775
+ make: () => createOtelReporterPlugin(reporters.otel, undefined, effectiveProjectRoot)
66170
66776
  }
66171
66777
  ] : [];
66172
66778
  for (const { name, enabled: reporterEnabled, make } of reporterFactories) {
@@ -66546,6 +67152,25 @@ var init_checkpoint = __esm(() => {
66546
67152
  init_resume_cli();
66547
67153
  });
66548
67154
 
67155
+ // src/hooks/types.ts
67156
+ var HOOK_EVENTS;
67157
+ var init_types10 = __esm(() => {
67158
+ HOOK_EVENTS = [
67159
+ "on-start",
67160
+ "on-story-start",
67161
+ "on-story-complete",
67162
+ "on-story-fail",
67163
+ "on-pause",
67164
+ "on-resume",
67165
+ "on-session-end",
67166
+ "on-all-stories-complete",
67167
+ "on-complete",
67168
+ "on-error",
67169
+ "on-final-regression-fail",
67170
+ "on-post-run-action"
67171
+ ];
67172
+ });
67173
+
66549
67174
  // src/hooks/runner.ts
66550
67175
  import { join as join84 } from "path";
66551
67176
  function createDrainDeadline2(deadlineMs) {
@@ -66607,6 +67232,12 @@ function buildEnv(ctx) {
66607
67232
  env2.NAX_AGENT = escapeEnvValue(ctx.agent);
66608
67233
  if (ctx.iteration !== undefined)
66609
67234
  env2.NAX_ITERATION = String(ctx.iteration);
67235
+ if (ctx.pluginName)
67236
+ env2.NAX_PLUGIN_NAME = escapeEnvValue(ctx.pluginName);
67237
+ if (ctx.actionName)
67238
+ env2.NAX_ACTION_NAME = escapeEnvValue(ctx.actionName);
67239
+ if (ctx.url)
67240
+ env2.NAX_RESULT_URL = escapeEnvValue(ctx.url);
66610
67241
  return env2;
66611
67242
  }
66612
67243
  function hasShellOperators(command) {
@@ -66737,9 +67368,11 @@ var init_runner5 = __esm(() => {
66737
67368
  var exports_hooks = {};
66738
67369
  __export(exports_hooks, {
66739
67370
  loadHooksConfig: () => loadHooksConfig,
66740
- fireHook: () => fireHook
67371
+ fireHook: () => fireHook,
67372
+ HOOK_EVENTS: () => HOOK_EVENTS
66741
67373
  });
66742
67374
  var init_hooks = __esm(() => {
67375
+ init_types10();
66743
67376
  init_runner5();
66744
67377
  });
66745
67378
 
@@ -69387,7 +70020,7 @@ function buildPreviewRouting(story, config2) {
69387
70020
 
69388
70021
  // src/worktree/types.ts
69389
70022
  var WorktreeDependencyPreparationError;
69390
- var init_types10 = __esm(() => {
70023
+ var init_types11 = __esm(() => {
69391
70024
  WorktreeDependencyPreparationError = class WorktreeDependencyPreparationError extends Error {
69392
70025
  mode;
69393
70026
  failureCategory = "dependency-prep";
@@ -69457,7 +70090,7 @@ var PHASE_ONE_INHERIT_UNSUPPORTED_FILES, _worktreeDependencyDeps;
69457
70090
  var init_dependencies = __esm(() => {
69458
70091
  init_bun_deps();
69459
70092
  init_command_argv();
69460
- init_types10();
70093
+ init_types11();
69461
70094
  PHASE_ONE_INHERIT_UNSUPPORTED_FILES = [
69462
70095
  "package.json",
69463
70096
  "bun.lock",
@@ -70674,6 +71307,22 @@ var init_pipeline_result_handler = __esm(() => {
70674
71307
  // src/execution/iteration-runner.ts
70675
71308
  import { existsSync as existsSync35 } from "fs";
70676
71309
  import { join as join91 } from "path";
71310
+ function releaseHeavyPipelineContext(ctx) {
71311
+ ctx.agentResult = undefined;
71312
+ ctx.prompt = undefined;
71313
+ ctx.contextMarkdown = undefined;
71314
+ ctx.featureContextMarkdown = undefined;
71315
+ ctx.builtContext = undefined;
71316
+ ctx.contextBundle = undefined;
71317
+ ctx.constitution = undefined;
71318
+ ctx.acceptanceFailures = undefined;
71319
+ ctx.autofixPriorIterations = undefined;
71320
+ ctx.priorSemanticIterations = undefined;
71321
+ ctx.priorAdversarialIterations = undefined;
71322
+ ctx.reviewFindings = undefined;
71323
+ ctx.selfVerification = undefined;
71324
+ ctx.tddIsolations = undefined;
71325
+ }
70677
71326
  async function runIteration(ctx, prd, selection, iterations, totalCost2, allStoryMetrics) {
70678
71327
  const { story, storiesToExecute, routing, isBatchExecution } = selection;
70679
71328
  if (ctx.dryRun) {
@@ -70854,11 +71503,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost2, allStor
70854
71503
  subStoryCount: pipelineResult.subStoryCount
70855
71504
  };
70856
71505
  }
70857
- pipelineContext.agentResult = undefined;
70858
- pipelineContext.prompt = undefined;
70859
- pipelineContext.contextMarkdown = undefined;
70860
- pipelineContext.builtContext = undefined;
70861
- pipelineContext.constitution = undefined;
71506
+ releaseHeavyPipelineContext(pipelineContext);
70862
71507
  return iterResult;
70863
71508
  }
70864
71509
  var _iterationRunnerDeps;
@@ -73104,8 +73749,63 @@ async function runSetupPhase(options) {
73104
73749
  var exports_run_cleanup = {};
73105
73750
  __export(exports_run_cleanup, {
73106
73751
  cleanupRun: () => cleanupRun,
73107
- buildPostRunContext: () => buildPostRunContext
73752
+ buildPostRunContext: () => buildPostRunContext,
73753
+ _runCleanupDeps: () => _runCleanupDeps
73108
73754
  });
73755
+ async function settlePostRunAction(action, ctx) {
73756
+ try {
73757
+ if (!await action.shouldRun(ctx))
73758
+ return { status: "skipped", reason: "shouldRun=false" };
73759
+ return outcomeFromResult(await action.execute(ctx));
73760
+ } catch (error48) {
73761
+ return { status: "error", reason: errorMessage(error48) };
73762
+ }
73763
+ }
73764
+ function outcomeFromResult(result) {
73765
+ if (result.skipped)
73766
+ return { status: "skipped", reason: result.reason ?? result.message };
73767
+ if (!result.success)
73768
+ return { status: "failed", message: result.message, url: result.url };
73769
+ return { status: "succeeded", message: result.message, url: result.url };
73770
+ }
73771
+ function logPostRunOutcome(actionName, outcome) {
73772
+ const logger = getSafeLogger();
73773
+ if (outcome.status === "skipped") {
73774
+ const level = outcome.reason === "shouldRun=false" ? "debug" : "info";
73775
+ logger?.[level]("post-run", `[post-run] ${actionName}: skipped \u2014 ${outcome.reason}`);
73776
+ } else if (outcome.status === "failed") {
73777
+ logger?.warn("post-run", `[post-run] ${actionName}: failed \u2014 ${outcome.message}`);
73778
+ } else if (outcome.status === "error") {
73779
+ logger?.warn("post-run", `[post-run] ${actionName}: error \u2014 ${outcome.reason}`);
73780
+ } else {
73781
+ const suffix = outcome.url ? `${outcome.message} (${outcome.url})` : outcome.message;
73782
+ logger?.info("post-run", `[post-run] ${actionName}: ${suffix}`);
73783
+ }
73784
+ }
73785
+ function postRunHookContext(feature, registration, outcome) {
73786
+ const reason = outcome.status === "succeeded" || outcome.status === "failed" ? outcome.message : outcome.reason;
73787
+ return {
73788
+ event: "on-post-run-action",
73789
+ feature,
73790
+ pluginName: registration.pluginName,
73791
+ actionName: registration.action.name,
73792
+ status: outcome.status,
73793
+ reason,
73794
+ url: "url" in outcome ? outcome.url : undefined
73795
+ };
73796
+ }
73797
+ async function runPostRunActions(options, ctx) {
73798
+ const registrations = options.pluginRegistry.getPostRunActionRegistrations();
73799
+ for (const registration of registrations) {
73800
+ const outcome = await settlePostRunAction(registration.action, ctx);
73801
+ logPostRunOutcome(registration.action.name, outcome);
73802
+ try {
73803
+ await _runCleanupDeps.fireHook(options.hooks, "on-post-run-action", postRunHookContext(options.feature, registration, outcome), options.workdir);
73804
+ } catch (error48) {
73805
+ getSafeLogger()?.warn("hooks", `on-post-run-action hook failed for '${registration.pluginName}'`, { error: error48 });
73806
+ }
73807
+ }
73808
+ }
73109
73809
  function buildPostRunContext(opts, durationMs, logger) {
73110
73810
  const {
73111
73811
  runId,
@@ -73178,7 +73878,6 @@ async function cleanupRun(options) {
73178
73878
  }
73179
73879
  }
73180
73880
  }
73181
- const actions = pluginRegistry.getPostRunActions();
73182
73881
  const pluginLogger = {
73183
73882
  debug: (msg, data) => logger?.debug("post-run", msg, data),
73184
73883
  info: (msg, data) => logger?.info("post-run", msg, data),
@@ -73186,26 +73885,7 @@ async function cleanupRun(options) {
73186
73885
  error: (msg, data) => logger?.error("post-run", msg, data)
73187
73886
  };
73188
73887
  const ctx = buildPostRunContext(options, durationMs, pluginLogger);
73189
- for (const action of actions) {
73190
- try {
73191
- const shouldRun = await action.shouldRun(ctx);
73192
- if (!shouldRun) {
73193
- logger?.debug("post-run", `[post-run] ${action.name}: shouldRun=false, skipping`);
73194
- continue;
73195
- }
73196
- const result = await action.execute(ctx);
73197
- if (result.skipped) {
73198
- logger?.info("post-run", `[post-run] ${action.name}: skipped \u2014 ${result.reason}`);
73199
- } else if (!result.success) {
73200
- logger?.warn("post-run", `[post-run] ${action.name}: failed \u2014 ${result.message}`);
73201
- } else {
73202
- const msg = result.url ? `[post-run] ${action.name}: ${result.message} (${result.url})` : `[post-run] ${action.name}: ${result.message}`;
73203
- logger?.info("post-run", msg);
73204
- }
73205
- } catch (error48) {
73206
- logger?.warn("post-run", `[post-run] ${action.name}: error \u2014 ${error48}`);
73207
- }
73208
- }
73888
+ await runPostRunActions(options, ctx);
73209
73889
  try {
73210
73890
  await pluginRegistry.teardownAll();
73211
73891
  } catch (error48) {
@@ -73222,11 +73902,14 @@ async function cleanupRun(options) {
73222
73902
  disposeFeatureResolver(workdir);
73223
73903
  await releaseLock(workdir);
73224
73904
  }
73905
+ var _runCleanupDeps;
73225
73906
  var init_run_cleanup = __esm(() => {
73226
73907
  init_context();
73908
+ init_hooks();
73227
73909
  init_logger2();
73228
73910
  init_prd();
73229
73911
  init_helpers();
73912
+ _runCleanupDeps = { fireHook };
73230
73913
  });
73231
73914
 
73232
73915
  // src/execution/runner.ts
@@ -73423,6 +74106,7 @@ async function run(options) {
73423
74106
  prdPath,
73424
74107
  branch,
73425
74108
  version: NAX_VERSION,
74109
+ hooks,
73426
74110
  runCompleted,
73427
74111
  outputDir: runtime.outputDir,
73428
74112
  globalDir: runtime.globalDir,
@@ -73481,12 +74165,14 @@ __export(exports_execution, {
73481
74165
  startHeartbeat: () => startHeartbeat2,
73482
74166
  runRectification: () => runRectification,
73483
74167
  runPhase: () => runPhase,
74168
+ runNonBlockingFix: () => runNonBlockingFix,
73484
74169
  runDeferredRegression: () => runDeferredRegression,
73485
74170
  runCompletionPhase: () => runCompletionPhase,
73486
74171
  run: () => run,
73487
74172
  resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
73488
74173
  resetCrashHandlers: () => resetCrashHandlers,
73489
74174
  releaseLock: () => releaseLock,
74175
+ releaseHeavyPipelineContext: () => releaseHeavyPipelineContext,
73490
74176
  refreshReviewInputForDispatch: () => refreshReviewInputForDispatch,
73491
74177
  recordOscillations: () => recordOscillations,
73492
74178
  readQueueFile: () => readQueueFile,
@@ -73519,6 +74205,7 @@ __export(exports_execution, {
73519
74205
  describeGateRegression: () => describeGateRegression,
73520
74206
  deriveTddFailureCategory: () => deriveTddFailureCategory,
73521
74207
  decideStageAction: () => decideStageAction,
74208
+ createNbfFlakeTriageTransaction: () => createNbfFlakeTriageTransaction,
73522
74209
  createCheckpointWriter: () => createCheckpointWriter,
73523
74210
  countOscillationOutcomes: () => countOscillationOutcomes,
73524
74211
  clearQueueFile: () => clearQueueFile,
@@ -73541,6 +74228,7 @@ __export(exports_execution, {
73541
74228
  _runnerDeps: () => _runnerDeps,
73542
74229
  _runnerCompletionDeps: () => _runnerCompletionDeps,
73543
74230
  _runCompletionDeps: () => _runCompletionDeps,
74231
+ _runCleanupDeps: () => _runCleanupDeps,
73544
74232
  _regressionDeps: () => _regressionDeps,
73545
74233
  _postRunDeps: () => _postRunDeps,
73546
74234
  _pidRegistryDeps: () => _pidRegistryDeps,
@@ -73558,6 +74246,7 @@ var init_execution2 = __esm(() => {
73558
74246
  init_oscillation_breaker();
73559
74247
  init_runner6();
73560
74248
  init_progress();
74249
+ init_iteration_runner();
73561
74250
  init_escalation();
73562
74251
  init_queue_handler();
73563
74252
  init_ensure_package_dirs();
@@ -73569,6 +74258,7 @@ var init_execution2 = __esm(() => {
73569
74258
  init_story_orchestrator();
73570
74259
  init_story_orchestrator_logging();
73571
74260
  init_plan_inputs();
74261
+ init_non_blocking_fix();
73572
74262
  init_build_plan_for_strategy();
73573
74263
  init_checkpoint();
73574
74264
  init_runner_completion();