@nathapp/nax 0.75.4 → 0.75.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +858 -427
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17377,7 +17377,11 @@ var init_schemas_review = __esm(() => {
17377
17377
  maxRequotes: 5
17378
17378
  }),
17379
17379
  excludePatterns: exports_external.array(exports_external.string()).optional(),
17380
- demandInspectionTrail: exports_external.boolean().default(true)
17380
+ demandInspectionTrail: exports_external.boolean().default(true),
17381
+ recurrenceDemotion: exports_external.object({
17382
+ enabled: exports_external.boolean().default(false),
17383
+ maxBlockingRounds: exports_external.number().int().min(1).default(2)
17384
+ }).default({ enabled: false, maxBlockingRounds: 2 })
17381
17385
  });
17382
17386
  AdversarialReviewConfigSchema = exports_external.object({
17383
17387
  model: ConfiguredModelSchema.default("balanced"),
@@ -17641,6 +17645,7 @@ var init_schemas3 = __esm(() => {
17641
17645
  rules: [],
17642
17646
  timeoutMs: 600000,
17643
17647
  demandInspectionTrail: true,
17648
+ recurrenceDemotion: { enabled: false, maxBlockingRounds: 2 },
17644
17649
  substantiation: {
17645
17650
  requote: true,
17646
17651
  maxRequotes: 5
@@ -17823,6 +17828,7 @@ var init_schemas3 = __esm(() => {
17823
17828
  quality: exports_external.string().nullable().default(null)
17824
17829
  }).default({ spec: null, quality: null }),
17825
17830
  escalate: exports_external.object({ telegram: exports_external.boolean().default(true) }).default({ telegram: true }),
17831
+ notify: exports_external.object({ mode: exports_external.enum(["escalation", "always", "off"]).default("escalation") }).default({ mode: "escalation" }),
17826
17832
  timeouts: exports_external.object({
17827
17833
  acceptanceMs: exports_external.number().int().positive().default(600000),
17828
17834
  gateMs: exports_external.number().int().positive().default(900000),
@@ -17835,6 +17841,7 @@ var init_schemas3 = __esm(() => {
17835
17841
  defaultAgent: null,
17836
17842
  reviewers: { spec: null, quality: null },
17837
17843
  escalate: { telegram: true },
17844
+ notify: { mode: "escalation" },
17838
17845
  timeouts: { acceptanceMs: 600000, gateMs: 900000, flowMs: 5400000, stepMs: null }
17839
17846
  })
17840
17847
  }).default({
@@ -17844,6 +17851,7 @@ var init_schemas3 = __esm(() => {
17844
17851
  defaultAgent: null,
17845
17852
  reviewers: { spec: null, quality: null },
17846
17853
  escalate: { telegram: true },
17854
+ notify: { mode: "escalation" },
17847
17855
  timeouts: { acceptanceMs: 600000, gateMs: 900000, flowMs: 5400000, stepMs: null }
17848
17856
  }
17849
17857
  }),
@@ -21654,7 +21662,7 @@ class AcpAgentAdapter {
21654
21662
  }
21655
21663
  }
21656
21664
  }
21657
- var MAX_AGENT_OUTPUT_CHARS = 5000, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, ACP_ADAPTER_NAMES;
21665
+ var INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, ACP_ADAPTER_NAMES;
21658
21666
  var init_adapter = __esm(() => {
21659
21667
  init_errors();
21660
21668
  init_logger2();
@@ -22944,11 +22952,38 @@ var init_compose = __esm(() => {
22944
22952
 
22945
22953
  // src/review/truncation.ts
22946
22954
  function looksLikeTruncatedJson(raw) {
22947
- return raw.trimEnd().length >= MAX_AGENT_OUTPUT_CHARS - 100;
22955
+ const text = raw.trimEnd();
22956
+ if (text.length === 0)
22957
+ return false;
22958
+ let depth = 0;
22959
+ let inString = false;
22960
+ let escaped = false;
22961
+ let opened = false;
22962
+ for (const ch of text) {
22963
+ if (escaped) {
22964
+ escaped = false;
22965
+ continue;
22966
+ }
22967
+ if (inString) {
22968
+ if (ch === "\\")
22969
+ escaped = true;
22970
+ else if (ch === '"')
22971
+ inString = false;
22972
+ continue;
22973
+ }
22974
+ if (ch === '"') {
22975
+ inString = true;
22976
+ continue;
22977
+ }
22978
+ if (ch === "{" || ch === "[") {
22979
+ depth++;
22980
+ opened = true;
22981
+ } else if (ch === "}" || ch === "]") {
22982
+ depth--;
22983
+ }
22984
+ }
22985
+ return opened && (inString || depth > 0);
22948
22986
  }
22949
- var init_truncation = __esm(() => {
22950
- init_adapter();
22951
- });
22952
22987
 
22953
22988
  // src/utils/llm-json.ts
22954
22989
  function extractJsonFromMarkdown(text) {
@@ -23091,9 +23126,9 @@ function makeParseRetryStrategy(opts) {
23091
23126
  }
23092
23127
  };
23093
23128
  }
23129
+ var UNPARSED_PREVIEW_BYTES = 600;
23094
23130
  var init_parse_retry = __esm(() => {
23095
23131
  init_logger2();
23096
- init_truncation();
23097
23132
  init_types4();
23098
23133
  });
23099
23134
 
@@ -23111,7 +23146,7 @@ function makeTieredParseRetryStrategy(opts) {
23111
23146
  if (attempt >= opts.maxAttempts - 1) {
23112
23147
  return { retry: false, fallback: opts.exhaustedFallback(inspection, ctx.lastOutput) };
23113
23148
  }
23114
- const isTruncated = ctx.lastOutput.trimEnd().length >= MAX_AGENT_OUTPUT_CHARS - 100;
23149
+ const isTruncated = looksLikeTruncatedJson(ctx.lastOutput);
23115
23150
  const logger = opts._logger ?? getSafeLogger();
23116
23151
  logger?.warn(opts.reviewerKind, `Parse retry \u2014 ${inspection.kind ?? "unknown"}`, {
23117
23152
  storyId: ctx.storyId,
@@ -23125,7 +23160,6 @@ function makeTieredParseRetryStrategy(opts) {
23125
23160
  }
23126
23161
  var init_tiered_parse_retry = __esm(() => {
23127
23162
  init_logger2();
23128
- init_adapter();
23129
23163
  init_types4();
23130
23164
  });
23131
23165
 
@@ -36186,6 +36220,89 @@ var init_finding_filters = __esm(() => {
36186
36220
  init_ac_quote_validator();
36187
36221
  });
36188
36222
 
36223
+ // src/review/recurrence-demotion.ts
36224
+ function normalizeIssueText(s) {
36225
+ return s.replace(/`/g, "").replace(/\s+/g, " ").trim().toLowerCase().slice(0, MAX_ISSUE_PREFIX);
36226
+ }
36227
+ function normalizeFingerprintPath(file3) {
36228
+ return (file3 ?? "").replace(/\\/g, "/").replace(/^(?:\.{1,2}\/)+/, "");
36229
+ }
36230
+ function fingerprintFor(file3, category, text, acIndex) {
36231
+ const normFile = normalizeFingerprintPath(file3);
36232
+ if (typeof acIndex === "number" && Number.isInteger(acIndex) && acIndex >= 1) {
36233
+ return `${normFile}|ac${acIndex}`;
36234
+ }
36235
+ return `${normFile}|${category ?? ""}|${normalizeIssueText(text).slice(0, FP_ISSUE_PREFIX)}`;
36236
+ }
36237
+ function lookupPriorAppearance(priorCounts, finding) {
36238
+ const acKey = finding.acIndex === undefined ? undefined : priorCounts.get(fingerprintFor(finding.file, finding.category, finding.issue, finding.acIndex));
36239
+ const proseKey = priorCounts.get(fingerprintFor(finding.file, finding.category, finding.issue));
36240
+ if (!acKey)
36241
+ return proseKey;
36242
+ if (!proseKey)
36243
+ return acKey;
36244
+ return acKey.count >= proseKey.count ? acKey : proseKey;
36245
+ }
36246
+ function countPriorAppearances(priorIterations, source = "adversarial-review") {
36247
+ const counts = new Map;
36248
+ for (const it of priorIterations) {
36249
+ const seenThisIter = new Map;
36250
+ for (const f of it.findingsAfter ?? []) {
36251
+ if (f.source !== source)
36252
+ continue;
36253
+ const acIndex = typeof f.meta?.acIndex === "number" ? f.meta.acIndex : undefined;
36254
+ seenThisIter.set(fingerprintFor(f.file, f.category, f.message), f.severity);
36255
+ if (acIndex !== undefined) {
36256
+ seenThisIter.set(fingerprintFor(f.file, f.category, f.message, acIndex), f.severity);
36257
+ }
36258
+ }
36259
+ for (const [fp, sev] of seenThisIter) {
36260
+ const cur = counts.get(fp);
36261
+ counts.set(fp, { count: (cur?.count ?? 0) + 1, lastSeverity: sev });
36262
+ }
36263
+ }
36264
+ return counts;
36265
+ }
36266
+ function tagCoverageGap(findings) {
36267
+ return findings.map((f) => ({ ...f, meta: { ...f.meta ?? {}, coverageGap: true } }));
36268
+ }
36269
+ function classifyRecurrence(accepted, priorIterations, cfg, testFileMatch, threshold, source = "adversarial-review") {
36270
+ const blocking = [];
36271
+ const advisory = [];
36272
+ const demoted = [];
36273
+ if (!cfg.enabled) {
36274
+ for (const f of accepted)
36275
+ (isBlockingSeverity(f.severity, threshold) ? blocking : advisory).push(f);
36276
+ return { blocking, advisory, demoted };
36277
+ }
36278
+ const priorCounts = countPriorAppearances(priorIterations, source);
36279
+ for (const f of accepted) {
36280
+ if (f.category === "test-gap" && testFileMatch(f.file) && isBlockingSeverity(f.severity, threshold)) {
36281
+ blocking.push(f);
36282
+ continue;
36283
+ }
36284
+ if (!isBlockingSeverity(f.severity, threshold)) {
36285
+ advisory.push(f);
36286
+ continue;
36287
+ }
36288
+ const prior = lookupPriorAppearance(priorCounts, f);
36289
+ const n = (prior?.count ?? 0) + 1;
36290
+ const prevWasBlocking = prior !== undefined && isBlockingSeverity(prior.lastSeverity, threshold);
36291
+ if (n >= cfg.maxBlockingRounds + 1) {
36292
+ demoted.push(f);
36293
+ } else if (n === 1 || prevWasBlocking) {
36294
+ blocking.push(f);
36295
+ } else {
36296
+ advisory.push(f);
36297
+ }
36298
+ }
36299
+ return { blocking, advisory, demoted };
36300
+ }
36301
+ var MAX_ISSUE_PREFIX = 160, FP_ISSUE_PREFIX = 48;
36302
+ var init_recurrence_demotion = __esm(() => {
36303
+ init_adversarial_helpers();
36304
+ });
36305
+
36189
36306
  // src/review/requote-response.ts
36190
36307
  function parseRequoteResponse(output) {
36191
36308
  const parsed = tryParseLLMJson(output);
@@ -36233,6 +36350,17 @@ function isRecord(value) {
36233
36350
  }
36234
36351
  var init_requote_response = () => {};
36235
36352
 
36353
+ // src/operations/_review-fallback.ts
36354
+ function reviewExhaustedFallback(lastOutput, failOpen) {
36355
+ const unparsedPreview = previewOutput(lastOutput, UNPARSED_PREVIEW_BYTES);
36356
+ if (!/"passed"\s*:\s*false/.test(lastOutput))
36357
+ return { ...failOpen, unparsedPreview };
36358
+ return { ...failOpen, passed: false, failOpen: false, looksLikeFail: true, unparsedPreview };
36359
+ }
36360
+ var init__review_fallback = __esm(() => {
36361
+ init_retry();
36362
+ });
36363
+
36236
36364
  // src/operations/semantic-review.ts
36237
36365
  function withRepromptMarker(output, info) {
36238
36366
  const parsed = tryParseLLMJson(output);
@@ -36451,7 +36579,9 @@ var init_semantic_review = __esm(() => {
36451
36579
  init_logger2();
36452
36580
  init_prompts();
36453
36581
  init_finding_filters();
36582
+ init_recurrence_demotion();
36454
36583
  init_requote_response();
36584
+ init__review_fallback();
36455
36585
  FAIL_OPEN = {
36456
36586
  passed: true,
36457
36587
  findings: [],
@@ -36475,7 +36605,8 @@ var init_semantic_review = __esm(() => {
36475
36605
  invalid: () => ReviewPromptBuilder.jsonRetry(),
36476
36606
  truncated: () => ReviewPromptBuilder.jsonRetryCondensed({ blockingThreshold: input.blockingThreshold })
36477
36607
  },
36478
- exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], normalizedFindings: [], acDropped: [], looksLikeFail: true } : FAIL_OPEN,
36608
+ exhaustedFallback: (lastOutput) => reviewExhaustedFallback(lastOutput, FAIL_OPEN),
36609
+ outputPreviewBytes: UNPARSED_PREVIEW_BYTES,
36479
36610
  logContext: { blockingThreshold: input.blockingThreshold ?? "error" }
36480
36611
  }),
36481
36612
  hopBody: semanticReviewHopBody,
@@ -36507,6 +36638,7 @@ var init_semantic_review = __esm(() => {
36507
36638
  repromptEvent
36508
36639
  };
36509
36640
  }
36641
+ const unparsedPreview = previewOutput(output, UNPARSED_PREVIEW_BYTES);
36510
36642
  if (/"passed"\s*:\s*false/.test(output)) {
36511
36643
  return {
36512
36644
  passed: false,
@@ -36514,10 +36646,11 @@ var init_semantic_review = __esm(() => {
36514
36646
  normalizedFindings: [],
36515
36647
  acDropped: [],
36516
36648
  looksLikeFail: true,
36649
+ unparsedPreview,
36517
36650
  repromptEvent
36518
36651
  };
36519
36652
  }
36520
- return FAIL_OPEN;
36653
+ return { ...FAIL_OPEN, unparsedPreview };
36521
36654
  },
36522
36655
  async verify(parsed, input, _verifyCtx) {
36523
36656
  if (parsed.failOpen || parsed.looksLikeFail)
@@ -36529,84 +36662,30 @@ var init_semantic_review = __esm(() => {
36529
36662
  const sanitized = sanitizeRefModeFindings(findings, input.mode, threshold);
36530
36663
  const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold, input.repoRoot);
36531
36664
  const { accepted, dropped } = filterByAcGroundingMinimal(substantiated, input.story.acceptanceCriteria);
36532
- const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
36665
+ const isTestFile3 = semanticTestFileMatch(input);
36666
+ const recurrenceCfg = input.semanticConfig.recurrenceDemotion ?? { enabled: false, maxBlockingRounds: 2 };
36667
+ const {
36668
+ blocking,
36669
+ advisory: subThreshold,
36670
+ demoted
36671
+ } = classifyRecurrence(accepted, input.priorSemanticIterations ?? [], recurrenceCfg, isTestFile3, threshold, "semantic-review");
36672
+ const advisoryFindings = [
36673
+ ...toReviewFindings(subThreshold.filter((f) => isBlockingSeverity(f.severity, threshold)), { isTestFile: isTestFile3 }),
36674
+ ...tagCoverageGap(toReviewFindings(demoted, { isTestFile: isTestFile3 }))
36675
+ ];
36533
36676
  const passed = blocking.length === 0 && (parsed.passed || accepted.length > 0);
36534
36677
  return {
36535
36678
  ...parsed,
36536
36679
  passed,
36537
36680
  findings: accepted,
36538
- normalizedFindings: toReviewFindings(blocking, { isTestFile: semanticTestFileMatch(input) }),
36681
+ normalizedFindings: toReviewFindings(blocking, { isTestFile: isTestFile3 }),
36682
+ advisoryFindings,
36539
36683
  acDropped: dropped
36540
36684
  };
36541
36685
  }
36542
36686
  };
36543
36687
  });
36544
36688
 
36545
- // src/review/recurrence-demotion.ts
36546
- function normalizeIssueText(s) {
36547
- return s.replace(/`/g, "").replace(/\s+/g, " ").trim().toLowerCase().slice(0, MAX_ISSUE_PREFIX);
36548
- }
36549
- function fingerprintFor(file3, category, text) {
36550
- const normFile = (file3 ?? "").replace(/\\/g, "/");
36551
- return `${normFile}|${category ?? ""}|${normalizeIssueText(text).slice(0, FP_ISSUE_PREFIX)}`;
36552
- }
36553
- function countPriorAppearances(priorIterations) {
36554
- const counts = new Map;
36555
- for (const it of priorIterations) {
36556
- const seenThisIter = new Map;
36557
- for (const f of it.findingsAfter ?? []) {
36558
- if (f.source !== "adversarial-review")
36559
- continue;
36560
- const fp = fingerprintFor(f.file, f.category, f.message);
36561
- seenThisIter.set(fp, f.severity);
36562
- }
36563
- for (const [fp, sev] of seenThisIter) {
36564
- const cur = counts.get(fp);
36565
- counts.set(fp, { count: (cur?.count ?? 0) + 1, lastSeverity: sev });
36566
- }
36567
- }
36568
- return counts;
36569
- }
36570
- function tagCoverageGap(findings) {
36571
- return findings.map((f) => ({ ...f, meta: { ...f.meta ?? {}, coverageGap: true } }));
36572
- }
36573
- function classifyRecurrence(accepted, priorIterations, cfg, testFileMatch, threshold) {
36574
- const blocking = [];
36575
- const advisory = [];
36576
- const demoted = [];
36577
- if (!cfg.enabled) {
36578
- for (const f of accepted)
36579
- (isBlockingSeverity(f.severity, threshold) ? blocking : advisory).push(f);
36580
- return { blocking, advisory, demoted };
36581
- }
36582
- const priorCounts = countPriorAppearances(priorIterations);
36583
- for (const f of accepted) {
36584
- if (f.category === "test-gap" && testFileMatch(f.file) && isBlockingSeverity(f.severity, threshold)) {
36585
- blocking.push(f);
36586
- continue;
36587
- }
36588
- if (!isBlockingSeverity(f.severity, threshold)) {
36589
- advisory.push(f);
36590
- continue;
36591
- }
36592
- const prior = priorCounts.get(fingerprintFor(f.file, f.category, f.issue));
36593
- const n = (prior?.count ?? 0) + 1;
36594
- const prevWasBlocking = prior !== undefined && isBlockingSeverity(prior.lastSeverity, threshold);
36595
- if (n >= cfg.maxBlockingRounds + 1) {
36596
- demoted.push(f);
36597
- } else if (n === 1 || prevWasBlocking) {
36598
- blocking.push(f);
36599
- } else {
36600
- advisory.push(f);
36601
- }
36602
- }
36603
- return { blocking, advisory, demoted };
36604
- }
36605
- var MAX_ISSUE_PREFIX = 160, FP_ISSUE_PREFIX = 48;
36606
- var init_recurrence_demotion = __esm(() => {
36607
- init_adversarial_helpers();
36608
- });
36609
-
36610
36689
  // src/operations/adversarial-review.ts
36611
36690
  function withRepromptMarker2(output, info) {
36612
36691
  const parsed = tryParseLLMJson(output);
@@ -36791,7 +36870,8 @@ var FAIL_OPEN2, ADVERSARIAL_REQUOTE_RECOVERED_EVENT = "review.adversarial.findin
36791
36870
  invalid: () => ReviewPromptBuilder.jsonRetry(),
36792
36871
  truncated: () => ReviewPromptBuilder.jsonRetryCondensed({ blockingThreshold: input.blockingThreshold })
36793
36872
  },
36794
- exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], normalizedFindings: [], acDropped: [], looksLikeFail: true } : FAIL_OPEN2,
36873
+ exhaustedFallback: (lastOutput) => reviewExhaustedFallback(lastOutput, FAIL_OPEN2),
36874
+ outputPreviewBytes: UNPARSED_PREVIEW_BYTES,
36795
36875
  logContext: { blockingThreshold: input.blockingThreshold ?? "error" }
36796
36876
  }), adversarialReviewOp;
36797
36877
  var init_adversarial_review = __esm(() => {
@@ -36803,6 +36883,7 @@ var init_adversarial_review = __esm(() => {
36803
36883
  init_finding_filters();
36804
36884
  init_recurrence_demotion();
36805
36885
  init_requote_response();
36886
+ init__review_fallback();
36806
36887
  FAIL_OPEN2 = {
36807
36888
  passed: true,
36808
36889
  findings: [],
@@ -38723,7 +38804,6 @@ var init_ground = __esm(() => {
38723
38804
  init_errors();
38724
38805
  init_logger2();
38725
38806
  init_prompts();
38726
- init_truncation();
38727
38807
  groundOp = {
38728
38808
  kind: "run",
38729
38809
  name: "ground",
@@ -42160,11 +42240,11 @@ var init_findings = __esm(() => {
42160
42240
  init_cycle();
42161
42241
  });
42162
42242
 
42163
- // src/review/adversarial-iteration-store.ts
42164
- function getAdversarialIterations(store, storyId) {
42243
+ // src/review/review-iteration-store.ts
42244
+ function getReviewIterations(store, storyId) {
42165
42245
  return store.get(storyId) ?? [];
42166
42246
  }
42167
- function recordAdversarialIteration(store, storyId, roundFindings) {
42247
+ function recordReviewIteration(store, storyId, roundFindings) {
42168
42248
  const prior = store.get(storyId) ?? [];
42169
42249
  const findingsBefore = prior.length > 0 ? prior[prior.length - 1].findingsAfter : [];
42170
42250
  const findingsAfter = [...roundFindings];
@@ -42180,7 +42260,7 @@ function recordAdversarialIteration(store, storyId, roundFindings) {
42180
42260
  };
42181
42261
  store.set(storyId, [...prior, iteration]);
42182
42262
  }
42183
- var init_adversarial_iteration_store = __esm(() => {
42263
+ var init_review_iteration_store = __esm(() => {
42184
42264
  init_findings();
42185
42265
  });
42186
42266
 
@@ -42623,7 +42703,7 @@ var package_default;
42623
42703
  var init_package = __esm(() => {
42624
42704
  package_default = {
42625
42705
  name: "@nathapp/nax",
42626
- version: "0.75.4",
42706
+ version: "0.75.6",
42627
42707
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42628
42708
  type: "module",
42629
42709
  bin: {
@@ -42727,8 +42807,8 @@ var init_version = __esm(() => {
42727
42807
  NAX_VERSION = package_default.version;
42728
42808
  NAX_COMMIT = (() => {
42729
42809
  try {
42730
- if (/^[0-9a-f]{6,10}$/.test("5aee16bf"))
42731
- return "5aee16bf";
42810
+ if (/^[0-9a-f]{6,10}$/.test("064f9083"))
42811
+ return "064f9083";
42732
42812
  } catch {}
42733
42813
  try {
42734
42814
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42775,6 +42855,8 @@ function toPersistedEntry(entry, epochMs) {
42775
42855
  blockingThreshold: entry.blockingThreshold ?? "error",
42776
42856
  result: entry.result,
42777
42857
  advisoryFindings: entry.advisoryFindings ?? null,
42858
+ acDropped: entry.acDropped ?? null,
42859
+ ...entry.parsed ? {} : { unparsedPreview: entry.unparsedPreview ?? null },
42778
42860
  diffAvailable: entry.diffAvailable ?? null,
42779
42861
  adversarialDropAnalysis: entry.adversarialDropAnalysis ?? null,
42780
42862
  adversarialAcceptAnalysis: entry.adversarialAcceptAnalysis ?? null
@@ -44746,7 +44828,7 @@ var init_review = __esm(() => {
44746
44828
  init_category_fix_target();
44747
44829
  init_finding_filters();
44748
44830
  init_ac_quote_validator();
44749
- init_adversarial_iteration_store();
44831
+ init_review_iteration_store();
44750
44832
  init_ac_structural_counterfactual();
44751
44833
  init_adversarial();
44752
44834
  init_semantic_evidence();
@@ -48169,6 +48251,8 @@ function attachReviewAuditSubscriber(bus, auditor, runId) {
48169
48251
  blockingThreshold: event.blockingThreshold,
48170
48252
  result: event.result,
48171
48253
  advisoryFindings: event.advisoryFindings,
48254
+ acDropped: event.acDropped ? [...event.acDropped] : undefined,
48255
+ unparsedPreview: event.unparsedPreview,
48172
48256
  diffAvailable: event.diffAvailable,
48173
48257
  adversarialDropAnalysis: event.adversarialDropAnalysis,
48174
48258
  adversarialAcceptAnalysis: event.adversarialAcceptAnalysis
@@ -49860,6 +49944,7 @@ function createRuntime(config2, workdir, opts) {
49860
49944
  const logger = getLogger();
49861
49945
  const quarantineMemo = createQuarantineMemo();
49862
49946
  const adversarialIterations = new Map;
49947
+ const semanticIterations = new Map;
49863
49948
  const rectificationOscillations = new Map;
49864
49949
  let closed = false;
49865
49950
  return {
@@ -49883,6 +49968,7 @@ function createRuntime(config2, workdir, opts) {
49883
49968
  logger,
49884
49969
  quarantineMemo,
49885
49970
  adversarialIterations,
49971
+ semanticIterations,
49886
49972
  rectificationOscillations,
49887
49973
  get signal() {
49888
49974
  return controller.signal;
@@ -57571,10 +57657,13 @@ ${stderr}` };
57571
57657
 
57572
57658
  // src/context/engine/effectiveness.ts
57573
57659
  function tokenize2(text) {
57574
- if (!text)
57575
- return new Set;
57576
- const raw = text.toLowerCase().split(/[\s_\-./:,;()\[\]{}'"!?]+/).filter((t) => t.length >= MIN_TOKEN_LEN2 && !STOPWORDS2.has(t));
57577
- return new Set(raw);
57660
+ const terms = new Set;
57661
+ for (const match of text.matchAll(TOKEN_PATTERN)) {
57662
+ const term = match[0].toLowerCase();
57663
+ if (term.length >= MIN_TOKEN_LEN2 && !STOPWORDS2.has(term))
57664
+ terms.add(term);
57665
+ }
57666
+ return terms;
57578
57667
  }
57579
57668
  function sharedTermCount2(a, b) {
57580
57669
  let count = 0;
@@ -57584,34 +57673,35 @@ function sharedTermCount2(a, b) {
57584
57673
  }
57585
57674
  return count;
57586
57675
  }
57587
- function classifyEffectiveness(chunkSummary, agentOutput, diffText, findingMessages) {
57588
- const summaryTerms = tokenize2(chunkSummary);
57589
- if (summaryTerms.size < MIN_SIGNIFICANT_TERMS) {
57590
- return { signal: "unknown" };
57676
+ function buildEvidenceTerms(agentOutput, diffText, findingMessages) {
57677
+ const diffTerms = diffText ? _effectivenessDeps.tokenize(diffText) : undefined;
57678
+ const outputTerms = agentOutput ? _effectivenessDeps.tokenize(agentOutput) : undefined;
57679
+ let combined;
57680
+ if (diffTerms || outputTerms) {
57681
+ combined = new Set(diffTerms);
57682
+ for (const term of outputTerms ?? [])
57683
+ combined.add(term);
57591
57684
  }
57592
- for (const finding of findingMessages) {
57593
- const findingTerms = tokenize2(finding);
57594
- if (sharedTermCount2(summaryTerms, findingTerms) >= MIN_SIGNIFICANT_TERMS) {
57595
- return {
57596
- signal: "contradicted",
57597
- evidence: finding.slice(0, 200)
57598
- };
57685
+ return {
57686
+ findings: findingMessages.map((message) => ({ message, terms: _effectivenessDeps.tokenize(message) })),
57687
+ diff: diffTerms,
57688
+ combined
57689
+ };
57690
+ }
57691
+ function classifyWithTerms(chunkSummary, evidence) {
57692
+ const summaryTerms = _effectivenessDeps.tokenize(chunkSummary);
57693
+ if (summaryTerms.size < MIN_SIGNIFICANT_TERMS)
57694
+ return { signal: "unknown" };
57695
+ for (const finding of evidence.findings) {
57696
+ if (sharedTermCount2(summaryTerms, finding.terms) >= MIN_SIGNIFICANT_TERMS) {
57697
+ return { signal: "contradicted", evidence: finding.message.slice(0, 200) };
57599
57698
  }
57600
57699
  }
57601
- if (diffText) {
57602
- const diffTerms = tokenize2(diffText);
57603
- if (sharedTermCount2(summaryTerms, diffTerms) >= MIN_SIGNIFICANT_TERMS) {
57604
- return {
57605
- signal: "followed",
57606
- evidence: "terms found in diff"
57607
- };
57608
- }
57700
+ if (evidence.diff && sharedTermCount2(summaryTerms, evidence.diff) >= MIN_SIGNIFICANT_TERMS) {
57701
+ return { signal: "followed", evidence: "terms found in diff" };
57609
57702
  }
57610
- if (diffText || agentOutput) {
57611
- const combinedTerms = tokenize2(`${diffText} ${agentOutput}`);
57612
- if (sharedTermCount2(summaryTerms, combinedTerms) < MIN_SIGNIFICANT_TERMS) {
57613
- return { signal: "ignored" };
57614
- }
57703
+ if (evidence.combined && sharedTermCount2(summaryTerms, evidence.combined) < MIN_SIGNIFICANT_TERMS) {
57704
+ return { signal: "ignored" };
57615
57705
  }
57616
57706
  return { signal: "unknown" };
57617
57707
  }
@@ -57621,16 +57711,18 @@ async function annotateManifestEffectiveness(projectDir, featureId, storyId, {
57621
57711
  findingMessages
57622
57712
  }) {
57623
57713
  const stored = await loadContextManifests(projectDir, storyId, featureId);
57714
+ let evidenceTerms;
57624
57715
  for (const item of stored) {
57625
57716
  const { manifest } = item;
57626
57717
  if (!manifest.chunkSummaries || manifest.includedChunks.length === 0)
57627
57718
  continue;
57719
+ evidenceTerms ??= buildEvidenceTerms(agentOutput, diffText, findingMessages);
57628
57720
  const effectiveness = {};
57629
57721
  for (const id of manifest.includedChunks) {
57630
57722
  const summary = manifest.chunkSummaries[id];
57631
57723
  if (!summary)
57632
57724
  continue;
57633
- effectiveness[id] = classifyEffectiveness(summary, agentOutput, diffText, findingMessages);
57725
+ effectiveness[id] = classifyWithTerms(summary, evidenceTerms);
57634
57726
  }
57635
57727
  if (Object.keys(effectiveness).length === 0)
57636
57728
  continue;
@@ -57648,12 +57740,13 @@ async function annotateManifestEffectiveness(projectDir, featureId, storyId, {
57648
57740
  }
57649
57741
  }
57650
57742
  }
57651
- var _effectivenessDeps, MIN_SIGNIFICANT_TERMS = 3, STOPWORDS2, MIN_TOKEN_LEN2 = 4;
57743
+ var _effectivenessDeps, MIN_SIGNIFICANT_TERMS = 3, STOPWORDS2, MIN_TOKEN_LEN2 = 4, TOKEN_PATTERN;
57652
57744
  var init_effectiveness = __esm(() => {
57653
57745
  init_logger2();
57654
57746
  init_manifest_store();
57655
57747
  _effectivenessDeps = {
57656
- getLogger
57748
+ getLogger,
57749
+ tokenize: tokenize2
57657
57750
  };
57658
57751
  STOPWORDS2 = new Set([
57659
57752
  "the",
@@ -57695,6 +57788,7 @@ var init_effectiveness = __esm(() => {
57695
57788
  "you",
57696
57789
  "your"
57697
57790
  ]);
57791
+ TOKEN_PATTERN = /[^\s_\-./:,;()\[\]{}'"!?]+/g;
57698
57792
  });
57699
57793
 
57700
57794
  // src/execution/progress.ts
@@ -57711,19 +57805,61 @@ async function appendProgress(featureDir, storyId, status, message) {
57711
57805
  var init_progress = () => {};
57712
57806
 
57713
57807
  // src/pipeline/stages/completion.ts
57808
+ function logHighMemoryCheckpoint(logger, ctx) {
57809
+ const usage = process.memoryUsage();
57810
+ if (usage.heapUsed < HIGH_MEMORY_TELEMETRY_BYTES && usage.rss < HIGH_MEMORY_TELEMETRY_BYTES)
57811
+ return;
57812
+ logger.debug("completion.memory", "High memory at completion boundary", {
57813
+ storyId: ctx.story.id,
57814
+ heapUsedBytes: usage.heapUsed,
57815
+ rssBytes: usage.rss,
57816
+ externalBytes: usage.external,
57817
+ arrayBuffersBytes: usage.arrayBuffers,
57818
+ agentOutputChars: ctx.agentResult?.output.length ?? 0
57819
+ });
57820
+ }
57821
+ async function readTextStreamPrefix(stream, maxChars) {
57822
+ const reader = stream.getReader();
57823
+ const decoder = new TextDecoder;
57824
+ let output = "";
57825
+ try {
57826
+ while (true) {
57827
+ const { done, value } = await reader.read();
57828
+ if (done)
57829
+ break;
57830
+ if (output.length >= maxChars)
57831
+ continue;
57832
+ const decoded = decoder.decode(value, { stream: true });
57833
+ output += decoded.slice(0, maxChars - output.length);
57834
+ }
57835
+ if (output.length < maxChars) {
57836
+ output += decoder.decode().slice(0, maxChars - output.length);
57837
+ }
57838
+ return output;
57839
+ } finally {
57840
+ reader.releaseLock();
57841
+ }
57842
+ }
57714
57843
  async function getDiffText(workdir, baseRef) {
57715
57844
  if (!baseRef)
57716
57845
  return "";
57717
57846
  try {
57718
- const proc = Bun.spawn(["git", "diff", `${baseRef}..HEAD`], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
57719
- const output = await new Response(proc.stdout).text();
57720
- await proc.exited;
57721
- return output.slice(0, 8000);
57847
+ const proc = _completionDeps.spawn(["git", "diff", `${baseRef}..HEAD`], {
57848
+ cwd: workdir,
57849
+ stdout: "pipe",
57850
+ stderr: "pipe"
57851
+ });
57852
+ const [output] = await Promise.all([
57853
+ readTextStreamPrefix(proc.stdout, MAX_EFFECTIVENESS_DIFF_CHARS),
57854
+ readTextStreamPrefix(proc.stderr, 0),
57855
+ proc.exited
57856
+ ]);
57857
+ return output;
57722
57858
  } catch {
57723
57859
  return "";
57724
57860
  }
57725
57861
  }
57726
- var completionStage, _completionDeps;
57862
+ var MAX_EFFECTIVENESS_DIFF_CHARS = 8000, HIGH_MEMORY_TELEMETRY_BYTES, completionStage, _completionDeps;
57727
57863
  var init_completion = __esm(() => {
57728
57864
  init_semantic_verdict();
57729
57865
  init_effectiveness();
@@ -57733,6 +57869,7 @@ var init_completion = __esm(() => {
57733
57869
  init_metrics();
57734
57870
  init_prd();
57735
57871
  init_event_bus();
57872
+ HIGH_MEMORY_TELEMETRY_BYTES = 512 * 1024 * 1024;
57736
57873
  completionStage = {
57737
57874
  name: "completion",
57738
57875
  enabled: () => true,
@@ -57802,6 +57939,7 @@ var init_completion = __esm(() => {
57802
57939
  if (persistPrd) {
57803
57940
  await _completionDeps.savePRD(ctx.prd, prdPath);
57804
57941
  }
57942
+ logHighMemoryCheckpoint(logger, ctx);
57805
57943
  const updatedCounts = countStories(ctx.prd);
57806
57944
  logger.info("completion", "Progress update", {
57807
57945
  storyId: ctx.story.id,
@@ -57817,7 +57955,9 @@ var init_completion = __esm(() => {
57817
57955
  checkReviewGate,
57818
57956
  persistSemanticVerdict,
57819
57957
  savePRD,
57820
- getDiffText
57958
+ getDiffText,
57959
+ readTextStreamPrefix,
57960
+ spawn: Bun.spawn
57821
57961
  };
57822
57962
  });
57823
57963
 
@@ -58520,161 +58660,6 @@ var init_paths3 = __esm(() => {
58520
58660
  init_paths();
58521
58661
  });
58522
58662
 
58523
- // src/execution/non-blocking-fix.ts
58524
- function actionableAdvisoryFindings(findings) {
58525
- return findings.filter((f) => f.actionRequired !== false);
58526
- }
58527
- function shouldRunNonBlockingFix(cfg, advisoryCount) {
58528
- return cfg?.enabled === true && advisoryCount > 0;
58529
- }
58530
- function nonBlockingExcludePhases() {
58531
- return REVIEW_PHASE_KINDS;
58532
- }
58533
- function nonBlockingExtraPhases(cfg) {
58534
- return (cfg.scope === "both" || cfg.scope === "triage") && cfg.verifierGuard ? ["verifier"] : [];
58535
- }
58536
- function createMeasureSourceDiff(args) {
58537
- const packageDirRel = packageDirRelative(args.projectDir, args.packageDir);
58538
- return async (workdir, fromRef) => {
58539
- const resolved = await _nonBlockingFixDeps.resolveTestFilePatterns(args.config, args.projectDir, packageDirRel);
58540
- const isTestFile3 = createTestFileClassifier(resolved);
58541
- const proc = _nonBlockingFixDeps.spawn(["git", "diff", "--numstat", fromRef], {
58542
- cwd: workdir,
58543
- stdout: "pipe",
58544
- stderr: "pipe"
58545
- });
58546
- const stdout = await Bun.readableStreamToText(proc.stdout);
58547
- const stderr = await Bun.readableStreamToText(proc.stderr);
58548
- const exitCode = await proc.exited;
58549
- if (exitCode !== 0) {
58550
- const detail = stderr.trim() || `exit ${exitCode}`;
58551
- throw new Error(`[non-blocking-fix] git diff --numstat failed: ${detail}`);
58552
- }
58553
- let fileCount = 0;
58554
- let sourceLineCount = 0;
58555
- for (const line of stdout.trim().split(`
58556
- `).filter(Boolean)) {
58557
- const [addedStr, _deletedStr, filePath] = line.split("\t");
58558
- if (!filePath || isTestFile3(filePath))
58559
- continue;
58560
- fileCount += 1;
58561
- const added = Number.parseInt(addedStr ?? "", 10);
58562
- if (Number.isFinite(added))
58563
- sourceLineCount += added;
58564
- }
58565
- return { fileCount, sourceLineCount };
58566
- };
58567
- }
58568
- async function runNonBlockingFix(args, overrides = {}) {
58569
- const _deps = { ...DEFAULT_DEPS, ...overrides };
58570
- const logger = getSafeLogger();
58571
- if (!shouldRunNonBlockingFix(args.cfg, args.advisoryFindings.length)) {
58572
- return { ran: false, kept: false, restored: false };
58573
- }
58574
- const phaseOutputsSnapshot = { ...args.phaseOutputs };
58575
- const phaseCostsSnapshot = { ...args.phaseCosts };
58576
- let restoreRef;
58577
- try {
58578
- restoreRef = await _deps.captureSnapshotRef(args.workdir, args.storyId);
58579
- } catch (err) {
58580
- logger?.warn("non-blocking-fix", "snapshot capture failed \u2014 skipping best-effort pass (no rollback point)", {
58581
- storyId: args.storyId,
58582
- error: err instanceof Error ? err.message : String(err)
58583
- });
58584
- return { ran: false, kept: false, restored: false };
58585
- }
58586
- const maxAttempts = 1 + args.cfg.regressionAttempts;
58587
- let exhausted = false;
58588
- try {
58589
- const result = await args.runRectify(maxAttempts);
58590
- exhausted = result.rectificationExhausted === true;
58591
- } catch (err) {
58592
- logger?.warn("non-blocking-fix", "best-effort pass threw \u2014 restoring", {
58593
- storyId: args.storyId,
58594
- error: err instanceof Error ? err.message : String(err)
58595
- });
58596
- exhausted = true;
58597
- }
58598
- if (!exhausted) {
58599
- const gateVerdict = args.keptTreeRegressed?.();
58600
- if (gateVerdict?.regressed) {
58601
- logGateRegression(logger, args.storyId, "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", gateVerdict);
58602
- return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58603
- }
58604
- const cap = args.cfg.sourceDiffCap;
58605
- if (cap) {
58606
- let metrics;
58607
- try {
58608
- metrics = await _deps.measureSourceDiff(args.workdir, restoreRef);
58609
- } catch (err) {
58610
- logger?.warn("non-blocking-fix", "source-diff measurement threw \u2014 restoring", {
58611
- storyId: args.storyId,
58612
- error: err instanceof Error ? err.message : String(err)
58613
- });
58614
- return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58615
- }
58616
- if (metrics.fileCount > cap.maxFiles || metrics.sourceLineCount > cap.maxLines) {
58617
- logger?.info("non-blocking-fix", "source diff exceeded cap \u2014 restoring", {
58618
- storyId: args.storyId,
58619
- fileCount: metrics.fileCount,
58620
- sourceLineCount: metrics.sourceLineCount,
58621
- cap
58622
- });
58623
- return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58624
- }
58625
- }
58626
- logger?.info("non-blocking-fix", "best-effort fix kept", { storyId: args.storyId });
58627
- return { ran: true, kept: true, restored: false };
58628
- }
58629
- const exhaustedGateVerdict = args.keptTreeRegressed?.();
58630
- if (exhaustedGateVerdict?.regressed) {
58631
- logGateRegression(logger, args.storyId, "best-effort fix exhausted with the full-suite gate red", exhaustedGateVerdict);
58632
- }
58633
- return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58634
- }
58635
- function logGateRegression(logger, storyId, message, verdict) {
58636
- logger?.info("non-blocking-fix", message, {
58637
- storyId,
58638
- regressedKeys: verdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
58639
- regressedKeyCount: verdict.regressedKeys.length,
58640
- baselineKeySize: verdict.baselineKeySize,
58641
- keyless: verdict.keyless,
58642
- memoExcludedKeyCount: verdict.memoExcludedKeys.length,
58643
- flakeTriageRan: false
58644
- });
58645
- }
58646
- async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger) {
58647
- await _deps.rollbackToRef(args.workdir, restoreRef);
58648
- for (const key of Object.keys(args.phaseOutputs))
58649
- delete args.phaseOutputs[key];
58650
- Object.assign(args.phaseOutputs, phaseOutputsSnapshot);
58651
- for (const key of Object.keys(args.phaseCosts))
58652
- delete args.phaseCosts[key];
58653
- Object.assign(args.phaseCosts, phaseCostsSnapshot);
58654
- logger?.info("non-blocking-fix", "best-effort fix exhausted \u2014 restored to adversarial-passed", {
58655
- storyId: args.storyId
58656
- });
58657
- return { ran: true, kept: false, restored: true };
58658
- }
58659
- var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
58660
- var init_non_blocking_fix = __esm(() => {
58661
- init_logger2();
58662
- init_rollback();
58663
- init_test_runners();
58664
- init_bun_deps();
58665
- init_paths3();
58666
- REVIEW_PHASE_KINDS = ["semantic-review", "adversarial-review"];
58667
- _nonBlockingFixDeps = {
58668
- spawn: typedSpawn,
58669
- resolveTestFilePatterns
58670
- };
58671
- DEFAULT_DEPS = {
58672
- captureSnapshotRef,
58673
- rollbackToRef,
58674
- measureSourceDiff: async () => ({ fileCount: 0, sourceLineCount: 0 })
58675
- };
58676
- });
58677
-
58678
58663
  // src/execution/story-orchestrator/types.ts
58679
58664
  var EXHAUSTED_EXIT_REASONS, TDD_OP_NAMES, CANONICAL_ORDER, PHASE_KIND_TO_STATE_KEY, STRATEGY_TO_REVALIDATION_PHASES, STRICT_VERDICT_PHASE_NAMES;
58680
58665
  var init_types9 = __esm(() => {
@@ -58805,7 +58790,8 @@ function gateFindingKey(finding) {
58805
58790
  function isQuarantinedFlake(finding, quarantineMemo) {
58806
58791
  if (finding.source !== "test-runner")
58807
58792
  return false;
58808
- return quarantineMemo?.has(gateFindingKey(finding)) === true;
58793
+ const key = gateFindingKey(finding);
58794
+ return key !== KEYLESS_GATE_FAILURE_KEY && quarantineMemo?.has(key) === true;
58809
58795
  }
58810
58796
  function describeGateRegression(input) {
58811
58797
  const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
@@ -58856,6 +58842,245 @@ var init_phase_eval = __esm(() => {
58856
58842
  init_types9();
58857
58843
  });
58858
58844
 
58845
+ // src/execution/story-orchestrator/nbf-flake-triage.ts
58846
+ function isCandidate(input) {
58847
+ if (input.finding.source !== "test-runner" || input.finding.category !== "failed-test")
58848
+ return false;
58849
+ const key = gateFindingKey(input.finding);
58850
+ return !input.transactionInput.baselineKeys.has(key) && !input.memo.has(key) && !input.attemptedKeys.has(key);
58851
+ }
58852
+ function createNbfFlakeTriageTransaction(input) {
58853
+ const pendingKeys = new Set;
58854
+ const attemptedKeys = new Set;
58855
+ let flakeTriageRan = false;
58856
+ const memo2 = {
58857
+ has: (key) => pendingKeys.has(key) || input.baseMemo?.has(key) === true,
58858
+ add: (key) => pendingKeys.add(key)
58859
+ };
58860
+ return {
58861
+ memo: memo2,
58862
+ get flakeTriageRan() {
58863
+ return flakeTriageRan;
58864
+ },
58865
+ candidates: (findings) => findings.filter((finding) => isCandidate({ finding, transactionInput: input, memo: memo2, attemptedKeys })),
58866
+ recordAttempt: (findings, ran) => {
58867
+ if (!ran)
58868
+ return;
58869
+ flakeTriageRan = true;
58870
+ for (const finding of findings)
58871
+ attemptedKeys.add(gateFindingKey(finding));
58872
+ },
58873
+ commit: () => {
58874
+ for (const key of pendingKeys)
58875
+ input.baseMemo?.add(key);
58876
+ }
58877
+ };
58878
+ }
58879
+ async function triageNbfGate(input) {
58880
+ const candidates = input.transaction.candidates(extractPhaseFindings(input.output));
58881
+ if (candidates.length === 0)
58882
+ return;
58883
+ const record2 = input.output;
58884
+ const rawOutput = typeof record2.rawOutput === "string" ? record2.rawOutput : "";
58885
+ try {
58886
+ const [, report] = await input.triage(candidates, {
58887
+ ctx: input.ctx,
58888
+ rawOutput,
58889
+ quarantineMemo: input.transaction.memo
58890
+ });
58891
+ const ran = report.flakeTriageRan ?? true;
58892
+ if (ran) {
58893
+ for (const key of report.quarantinedKeys)
58894
+ input.transaction.memo.add(key);
58895
+ }
58896
+ input.transaction.recordAttempt(candidates, ran);
58897
+ } catch (err) {
58898
+ getSafeLogger()?.warn("story-orchestrator", "NBF flake triage threw \u2014 keeping findings blocking", {
58899
+ storyId: input.ctx.storyId,
58900
+ gateName: input.gateName,
58901
+ error: err instanceof Error ? err.message : String(err)
58902
+ });
58903
+ }
58904
+ }
58905
+ var init_nbf_flake_triage = __esm(() => {
58906
+ init_logger2();
58907
+ init_phase_eval();
58908
+ });
58909
+
58910
+ // src/execution/non-blocking-fix.ts
58911
+ function actionableAdvisoryFindings(findings) {
58912
+ return findings.filter((f) => f.actionRequired !== false);
58913
+ }
58914
+ function shouldRunNonBlockingFix(cfg, advisoryCount) {
58915
+ return cfg?.enabled === true && advisoryCount > 0;
58916
+ }
58917
+ function nonBlockingExcludePhases() {
58918
+ return REVIEW_PHASE_KINDS;
58919
+ }
58920
+ function nonBlockingExtraPhases(cfg) {
58921
+ return (cfg.scope === "both" || cfg.scope === "triage") && cfg.verifierGuard ? ["verifier"] : [];
58922
+ }
58923
+ function createMeasureSourceDiff(args) {
58924
+ const packageDirRel = packageDirRelative(args.projectDir, args.packageDir);
58925
+ return async (workdir, fromRef) => {
58926
+ const resolved = await _nonBlockingFixDeps.resolveTestFilePatterns(args.config, args.projectDir, packageDirRel);
58927
+ const isTestFile3 = createTestFileClassifier(resolved);
58928
+ const proc = _nonBlockingFixDeps.spawn(["git", "diff", "--numstat", fromRef], {
58929
+ cwd: workdir,
58930
+ stdout: "pipe",
58931
+ stderr: "pipe"
58932
+ });
58933
+ const stdout = await Bun.readableStreamToText(proc.stdout);
58934
+ const stderr = await Bun.readableStreamToText(proc.stderr);
58935
+ const exitCode = await proc.exited;
58936
+ if (exitCode !== 0) {
58937
+ const detail = stderr.trim() || `exit ${exitCode}`;
58938
+ throw new Error(`[non-blocking-fix] git diff --numstat failed: ${detail}`);
58939
+ }
58940
+ let fileCount = 0;
58941
+ let sourceLineCount = 0;
58942
+ for (const line of stdout.trim().split(`
58943
+ `).filter(Boolean)) {
58944
+ const [addedStr, _deletedStr, filePath] = line.split("\t");
58945
+ if (!filePath || isTestFile3(filePath))
58946
+ continue;
58947
+ fileCount += 1;
58948
+ const added = Number.parseInt(addedStr ?? "", 10);
58949
+ if (Number.isFinite(added))
58950
+ sourceLineCount += added;
58951
+ }
58952
+ return { fileCount, sourceLineCount };
58953
+ };
58954
+ }
58955
+ async function runNonBlockingFix(args, overrides = {}) {
58956
+ const _deps = { ...DEFAULT_DEPS, ...overrides };
58957
+ const logger = getSafeLogger();
58958
+ if (!shouldRunNonBlockingFix(args.cfg, args.advisoryFindings.length)) {
58959
+ return { ran: false, kept: false, restored: false };
58960
+ }
58961
+ const phaseOutputsSnapshot = { ...args.phaseOutputs };
58962
+ const phaseCostsSnapshot = { ...args.phaseCosts };
58963
+ let restoreRef;
58964
+ try {
58965
+ restoreRef = await _deps.captureSnapshotRef(args.workdir, args.storyId);
58966
+ } catch (err) {
58967
+ logger?.warn("non-blocking-fix", "snapshot capture failed \u2014 skipping best-effort pass (no rollback point)", {
58968
+ storyId: args.storyId,
58969
+ error: err instanceof Error ? err.message : String(err)
58970
+ });
58971
+ return { ran: false, kept: false, restored: false };
58972
+ }
58973
+ const maxAttempts = 1 + args.cfg.regressionAttempts;
58974
+ const flakeTriage = createNbfFlakeTriageTransaction({
58975
+ baseMemo: args.quarantineMemo,
58976
+ baselineKeys: args.gateBaselineKeys ?? new Set
58977
+ });
58978
+ let exhausted = false;
58979
+ try {
58980
+ const result = await args.runRectify(maxAttempts, flakeTriage);
58981
+ exhausted = result.rectificationExhausted === true;
58982
+ } catch (err) {
58983
+ logger?.warn("non-blocking-fix", "best-effort pass threw \u2014 restoring", {
58984
+ storyId: args.storyId,
58985
+ error: err instanceof Error ? err.message : String(err)
58986
+ });
58987
+ exhausted = true;
58988
+ }
58989
+ if (!exhausted) {
58990
+ const gateVerdict = args.keptTreeRegressed?.(flakeTriage.memo);
58991
+ if (gateVerdict?.regressed) {
58992
+ logGateRegression({
58993
+ logger,
58994
+ storyId: args.storyId,
58995
+ message: "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)",
58996
+ verdict: gateVerdict,
58997
+ flakeTriageRan: flakeTriage.flakeTriageRan
58998
+ });
58999
+ return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
59000
+ }
59001
+ const cap = args.cfg.sourceDiffCap;
59002
+ if (cap) {
59003
+ let metrics;
59004
+ try {
59005
+ metrics = await _deps.measureSourceDiff(args.workdir, restoreRef);
59006
+ } catch (err) {
59007
+ logger?.warn("non-blocking-fix", "source-diff measurement threw \u2014 restoring", {
59008
+ storyId: args.storyId,
59009
+ error: err instanceof Error ? err.message : String(err)
59010
+ });
59011
+ return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
59012
+ }
59013
+ if (metrics.fileCount > cap.maxFiles || metrics.sourceLineCount > cap.maxLines) {
59014
+ logger?.info("non-blocking-fix", "source diff exceeded cap \u2014 restoring", {
59015
+ storyId: args.storyId,
59016
+ fileCount: metrics.fileCount,
59017
+ sourceLineCount: metrics.sourceLineCount,
59018
+ cap
59019
+ });
59020
+ return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
59021
+ }
59022
+ }
59023
+ flakeTriage.commit();
59024
+ logger?.info("non-blocking-fix", "best-effort fix kept", { storyId: args.storyId });
59025
+ return { ran: true, kept: true, restored: false };
59026
+ }
59027
+ const exhaustedGateVerdict = args.keptTreeRegressed?.(flakeTriage.memo);
59028
+ if (exhaustedGateVerdict?.regressed) {
59029
+ logGateRegression({
59030
+ logger,
59031
+ storyId: args.storyId,
59032
+ message: "best-effort fix exhausted with the full-suite gate red",
59033
+ verdict: exhaustedGateVerdict,
59034
+ flakeTriageRan: flakeTriage.flakeTriageRan
59035
+ });
59036
+ }
59037
+ return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
59038
+ }
59039
+ function logGateRegression(input) {
59040
+ const { logger, storyId, message, verdict, flakeTriageRan } = input;
59041
+ logger?.info("non-blocking-fix", message, {
59042
+ storyId,
59043
+ regressedKeys: verdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
59044
+ regressedKeyCount: verdict.regressedKeys.length,
59045
+ baselineKeySize: verdict.baselineKeySize,
59046
+ keyless: verdict.keyless,
59047
+ memoExcludedKeyCount: verdict.memoExcludedKeys.length,
59048
+ flakeTriageRan
59049
+ });
59050
+ }
59051
+ async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger) {
59052
+ await _deps.rollbackToRef(args.workdir, restoreRef);
59053
+ for (const key of Object.keys(args.phaseOutputs))
59054
+ delete args.phaseOutputs[key];
59055
+ Object.assign(args.phaseOutputs, phaseOutputsSnapshot);
59056
+ for (const key of Object.keys(args.phaseCosts))
59057
+ delete args.phaseCosts[key];
59058
+ Object.assign(args.phaseCosts, phaseCostsSnapshot);
59059
+ logger?.info("non-blocking-fix", "best-effort fix exhausted \u2014 restored to adversarial-passed", {
59060
+ storyId: args.storyId
59061
+ });
59062
+ return { ran: true, kept: false, restored: true };
59063
+ }
59064
+ var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
59065
+ var init_non_blocking_fix = __esm(() => {
59066
+ init_logger2();
59067
+ init_rollback();
59068
+ init_test_runners();
59069
+ init_bun_deps();
59070
+ init_paths3();
59071
+ init_nbf_flake_triage();
59072
+ REVIEW_PHASE_KINDS = ["semantic-review", "adversarial-review"];
59073
+ _nonBlockingFixDeps = {
59074
+ spawn: typedSpawn,
59075
+ resolveTestFilePatterns
59076
+ };
59077
+ DEFAULT_DEPS = {
59078
+ captureSnapshotRef,
59079
+ rollbackToRef,
59080
+ measureSourceDiff: async () => ({ fileCount: 0, sourceLineCount: 0 })
59081
+ };
59082
+ });
59083
+
58859
59084
  // src/execution/story-orchestrator/phase-state.ts
58860
59085
  function isSlot(value) {
58861
59086
  return value !== null && typeof value === "object" && "op" in value && "input" in value && typeof value.op?.kind === "string";
@@ -59049,15 +59274,15 @@ var init_verification = __esm(() => {
59049
59274
  });
59050
59275
 
59051
59276
  // src/execution/story-orchestrator/flake-triage-seam.ts
59052
- var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
59277
+ var productionTriageSeam = async (gateFindings, { ctx, rawOutput, quarantineMemo }) => {
59053
59278
  const config2 = ctx.packageView.config;
59054
59279
  const flakeDetection = config2.execution?.flakeDetection;
59055
59280
  if (!flakeDetection?.enabled) {
59056
- return [gateFindings, { quarantinedKeys: [] }];
59281
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59057
59282
  }
59058
59283
  const framework = detectFramework(rawOutput);
59059
59284
  if (framework === "unknown") {
59060
- return [gateFindings, { quarantinedKeys: [] }];
59285
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59061
59286
  }
59062
59287
  const workdir = ctx.runtime.workdir;
59063
59288
  const storyWorkdir = ctx.story?.workdir;
@@ -59066,11 +59291,11 @@ var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
59066
59291
  const { testCommand } = await resolveQualityTestCommands2(config2, workdir, storyWorkdir);
59067
59292
  const baseCommand = testCommand ?? config2.quality?.commands?.test;
59068
59293
  if (!baseCommand) {
59069
- return [gateFindings, { quarantinedKeys: [] }];
59294
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59070
59295
  }
59071
59296
  const diff = await resolveFlakeBaselineDiff(config2, workdir, storyWorkdir);
59072
59297
  if (diff === null) {
59073
- return [gateFindings, { quarantinedKeys: [] }];
59298
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59074
59299
  }
59075
59300
  const result = await triageFlakyFindings({
59076
59301
  findings: gateFindings,
@@ -59079,15 +59304,15 @@ var productionTriageSeam = async (gateFindings, { ctx, rawOutput }) => {
59079
59304
  baseCommand,
59080
59305
  cwd: ctx.packageDir,
59081
59306
  framework,
59082
- quarantineMemo: ctx.runtime.quarantineMemo
59307
+ quarantineMemo: quarantineMemo ?? ctx.runtime.quarantineMemo
59083
59308
  });
59084
- return [result.findings, { quarantinedKeys: result.quarantineReport.keys }];
59309
+ return [result.findings, { quarantinedKeys: result.quarantineReport.keys, flakeTriageRan: true }];
59085
59310
  } catch (err) {
59086
59311
  getSafeLogger()?.warn("story-orchestrator", "Flake triage seam failed resolving context \u2014 keeping findings blocking (no quarantine)", {
59087
59312
  storyId: ctx.storyId,
59088
59313
  error: errorMessage(err)
59089
59314
  });
59090
- return [gateFindings, { quarantinedKeys: [] }];
59315
+ return [gateFindings, { quarantinedKeys: [], flakeTriageRan: false }];
59091
59316
  }
59092
59317
  };
59093
59318
  var init_flake_triage_seam = __esm(() => {
@@ -59104,11 +59329,12 @@ function toReviewDecisionPayload(opName, output) {
59104
59329
  const reviewer = opName === "semantic-review" ? "semantic" : opName === "adversarial-review" ? "adversarial" : null;
59105
59330
  if (!reviewer)
59106
59331
  return null;
59332
+ const unparsedPreview = typeof record2.unparsedPreview === "string" ? record2.unparsedPreview : undefined;
59107
59333
  if (record2.failOpen === true) {
59108
- return { reviewer, parsed: false, passed: true, failOpen: true, result: null };
59334
+ return { reviewer, parsed: false, passed: true, failOpen: true, result: null, unparsedPreview };
59109
59335
  }
59110
59336
  if (record2.looksLikeFail === true) {
59111
- return { reviewer, parsed: false, passed: false, looksLikeFail: true, result: null };
59337
+ return { reviewer, parsed: false, passed: false, looksLikeFail: true, result: null, unparsedPreview };
59112
59338
  }
59113
59339
  if (typeof record2.passed !== "boolean" || !Array.isArray(record2.findings)) {
59114
59340
  return null;
@@ -59130,7 +59356,8 @@ function toReviewDecisionPayload(opName, output) {
59130
59356
  parsed: true,
59131
59357
  passed: record2.passed,
59132
59358
  result: { passed: record2.passed, findings: record2.findings },
59133
- acDropped
59359
+ acDropped,
59360
+ ...Array.isArray(record2.advisoryFindings) ? { advisoryFindings: record2.advisoryFindings } : {}
59134
59361
  };
59135
59362
  }
59136
59363
  function emitReviewDecision(ctx, opName, output) {
@@ -59151,7 +59378,10 @@ function emitReviewDecision(ctx, opName, output) {
59151
59378
  looksLikeFail: payload.parsed ? undefined : payload.looksLikeFail,
59152
59379
  failOpen: payload.parsed ? false : payload.failOpen,
59153
59380
  passed: payload.passed,
59154
- result: payload.result
59381
+ result: payload.result,
59382
+ advisoryFindings: payload.parsed ? payload.advisoryFindings : undefined,
59383
+ acDropped: payload.parsed ? payload.acDropped : undefined,
59384
+ unparsedPreview: payload.parsed ? undefined : payload.unparsedPreview
59155
59385
  });
59156
59386
  }
59157
59387
  function logUnifiedReviewPhaseStart(storyId, opName) {
@@ -59288,13 +59518,19 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59288
59518
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
59289
59519
  let advIterationBefore = 0;
59290
59520
  if (opName === "adversarial-review" && ctx.storyId) {
59291
- const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59521
+ const priorIterations = getReviewIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59292
59522
  advIterationBefore = priorIterations.length;
59293
59523
  dispatchInput = {
59294
59524
  ...dispatchInput,
59295
59525
  priorAdversarialIterations: priorIterations
59296
59526
  };
59297
59527
  }
59528
+ if (opName === "semantic-review" && ctx.storyId) {
59529
+ dispatchInput = {
59530
+ ...dispatchInput,
59531
+ priorSemanticIterations: getReviewIterations(ctx.runtime.semanticIterations, ctx.storyId)
59532
+ };
59533
+ }
59298
59534
  if (isTddPhase) {
59299
59535
  logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName, ...progressData });
59300
59536
  } else if (isThreeSession && opName === "full-suite-gate") {
@@ -59316,11 +59552,18 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59316
59552
  emitReviewDecision(ctx, opName, output);
59317
59553
  if (opName === "adversarial-review" && ctx.storyId) {
59318
59554
  const advOut = output;
59319
- recordAdversarialIteration(ctx.runtime.adversarialIterations, ctx.storyId, [
59555
+ recordReviewIteration(ctx.runtime.adversarialIterations, ctx.storyId, [
59320
59556
  ...advOut.normalizedFindings ?? [],
59321
59557
  ...advOut.advisoryFindings ?? []
59322
59558
  ]);
59323
59559
  }
59560
+ if (opName === "semantic-review" && ctx.storyId) {
59561
+ const semOut = output;
59562
+ recordReviewIteration(ctx.runtime.semanticIterations, ctx.storyId, [
59563
+ ...semOut.normalizedFindings ?? [],
59564
+ ...semOut.advisoryFindings ?? []
59565
+ ]);
59566
+ }
59324
59567
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
59325
59568
  logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
59326
59569
  outcome = derivePhaseOutcome(output);
@@ -59590,6 +59833,9 @@ function collectRectificationPhases(state) {
59590
59833
  state.adversarialReview
59591
59834
  ].filter((phase) => phase !== undefined);
59592
59835
  }
59836
+ function isQuarantinedOnlyGateFailure(phase, rawFindings, blockingFindings) {
59837
+ return phase.kind === "full-suite-gate" && rawFindings.length > 0 && blockingFindings.length === 0;
59838
+ }
59593
59839
  async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides) {
59594
59840
  const rectification2 = state.rectification;
59595
59841
  const baseValidationPhases = collectRectificationPhases(state);
@@ -59656,9 +59902,21 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59656
59902
  if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs, nbfPath }))
59657
59903
  continue;
59658
59904
  const output = phaseOutputs[phase.slot.op.name];
59905
+ const nbfFlakeTriage = overrides?.nbfFlakeTriage;
59906
+ if (nbfPath && phase.kind === "full-suite-gate" && nbfFlakeTriage) {
59907
+ await triageNbfGate({
59908
+ output,
59909
+ gateName: phase.slot.op.name,
59910
+ ctx,
59911
+ transaction: nbfFlakeTriage,
59912
+ triage: _storyOrchestratorDeps.triage
59913
+ });
59914
+ }
59659
59915
  const phaseFindings = extractPhaseFindings(output);
59660
- findings.push(...nbfPath ? phaseFindings.filter((f) => !isQuarantinedFlake(f, ctx.runtime.quarantineMemo)) : phaseFindings);
59661
- if (!phasePassed(phase.slot.op.name, output, ctx.storyId)) {
59916
+ const blockingFindings = nbfPath ? phaseFindings.filter((finding) => !isQuarantinedFlake(finding, nbfFlakeTriage?.memo ?? ctx.runtime.quarantineMemo)) : phaseFindings;
59917
+ findings.push(...blockingFindings);
59918
+ const quarantinedOnly = nbfPath && isQuarantinedOnlyGateFailure(phase, phaseFindings, blockingFindings);
59919
+ if (!phasePassed(phase.slot.op.name, output, ctx.storyId) && !quarantinedOnly) {
59662
59920
  getSafeLogger()?.warn("story-orchestrator", "Short-circuiting revalidation on phase failure", {
59663
59921
  storyId: ctx.storyId,
59664
59922
  phase: phase.slot.op.name
@@ -59716,6 +59974,7 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59716
59974
  }
59717
59975
  var init_rectification = __esm(() => {
59718
59976
  init_logger2();
59977
+ init_nbf_flake_triage();
59719
59978
  init_phase_eval();
59720
59979
  init_phase_eval();
59721
59980
  init_run_phase();
@@ -59732,13 +59991,13 @@ class ExecutionPlan {
59732
59991
  this.state = state;
59733
59992
  this.isThreeSession = isThreeSession;
59734
59993
  }
59735
- describeGateRegressionNow(phaseOutputs, gateName, baselineKeys) {
59994
+ describeGateRegressionNow(phaseOutputs, gateName, options) {
59736
59995
  return describeGateRegression({
59737
59996
  gateOutput: gateName === undefined ? undefined : phaseOutputs[gateName],
59738
- baselineKeys,
59997
+ baselineKeys: options.baselineKeys,
59739
59998
  gateName,
59740
59999
  storyId: this.ctx.storyId,
59741
- quarantineMemo: this.ctx.runtime.quarantineMemo
60000
+ quarantineMemo: options.quarantineMemo ?? this.ctx.runtime.quarantineMemo
59742
60001
  });
59743
60002
  }
59744
60003
  phaseNames() {
@@ -59886,15 +60145,21 @@ class ExecutionPlan {
59886
60145
  cfg: advCfg,
59887
60146
  phaseOutputs,
59888
60147
  phaseCosts,
59889
- runRectify: (maxAttempts) => runRectification(this.ctx, this.state, phaseCosts, phaseOutputs, {
60148
+ quarantineMemo: this.ctx.runtime.quarantineMemo,
60149
+ gateBaselineKeys: preRectGateFailureKeys,
60150
+ runRectify: (maxAttempts, nbfFlakeTriage) => runRectification(this.ctx, this.state, phaseCosts, phaseOutputs, {
59890
60151
  initialFindings: advisoryFindings,
60152
+ nbfFlakeTriage,
59891
60153
  strategies: this.state.nonBlockingFixStrategies ?? [],
59892
60154
  excludePhaseKinds: nonBlockingExcludePhases(),
59893
60155
  extraRevalidationKinds: nonBlockingExtraPhases(advCfg),
59894
60156
  maxAttempts,
59895
60157
  postValidate: this.state.nonBlockingFixPostValidate
59896
60158
  }),
59897
- keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
60159
+ keptTreeRegressed: (quarantineMemo) => this.describeGateRegressionNow(phaseOutputs, gateName, {
60160
+ baselineKeys: preRectGateFailureKeys,
60161
+ quarantineMemo
60162
+ })
59898
60163
  }, {
59899
60164
  measureSourceDiff: createMeasureSourceDiff({
59900
60165
  config: this.ctx.runtime.configLoader.current(),
@@ -59905,7 +60170,9 @@ class ExecutionPlan {
59905
60170
  }
59906
60171
  const verifierName = this.state.verifier?.slot.op.name;
59907
60172
  const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
59908
- const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
60173
+ const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, {
60174
+ baselineKeys: preRectGateFailureKeys
60175
+ }).regressed;
59909
60176
  const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
59910
60177
  if (verifierExplicitlyPassed && gateRegressedDuringRect) {
59911
60178
  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 });
@@ -60056,7 +60323,9 @@ var init_story_orchestrator = __esm(() => {
60056
60323
  init_execution_plan();
60057
60324
  init_phase_eval();
60058
60325
  init_rectification();
60326
+ init_nbf_flake_triage();
60059
60327
  init_run_phase();
60328
+ init_review_decision();
60060
60329
  init_types9();
60061
60330
  });
60062
60331
 
@@ -60406,7 +60675,6 @@ async function assemblePlanInputsFromCtx(ctx) {
60406
60675
  diff: prepared.diff,
60407
60676
  excludePatterns: prepared.excludePatterns,
60408
60677
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-semantic"),
60409
- priorSemanticIterations: ctx.priorSemanticIterations,
60410
60678
  resolvedTestPatterns,
60411
60679
  blockingThreshold: ctx.config.review.blockingThreshold,
60412
60680
  _refresh: {
@@ -60444,7 +60712,6 @@ async function assemblePlanInputsFromCtx(ctx) {
60444
60712
  testGlobs: prepared.testGlobs,
60445
60713
  refExcludePatterns: prepared.refExcludePatterns,
60446
60714
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-adversarial"),
60447
- priorAdversarialIterations: ctx.priorAdversarialIterations,
60448
60715
  resolvedTestPatterns,
60449
60716
  blockingThreshold: ctx.config.review.blockingThreshold,
60450
60717
  _refresh: {
@@ -61946,7 +62213,7 @@ async function fanOutReporters(reporters, hook, invoke) {
61946
62213
  }
61947
62214
  }
61948
62215
  }
61949
- function wireReporters(bus, pluginRegistry, runId, startTime) {
62216
+ function wireReporters(bus, pluginRegistry, runId, startTime, projectKey) {
61950
62217
  const logger = getSafeLogger();
61951
62218
  const safe = (name, fn) => {
61952
62219
  return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
@@ -61996,7 +62263,8 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
61996
62263
  runId,
61997
62264
  feature: ev.feature,
61998
62265
  totalStories: ev.totalStories,
61999
- startTime: new Date(startTime).toISOString()
62266
+ startTime: new Date(startTime).toISOString(),
62267
+ project: projectKey
62000
62268
  });
62001
62269
  } catch (err) {
62002
62270
  logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
@@ -64843,6 +65111,7 @@ function getFinishAutoFlowConfig(ctx) {
64843
65111
  quality: autoFlow.reviewers?.quality ?? null
64844
65112
  },
64845
65113
  escalate: { telegram: autoFlow.escalate?.telegram !== false },
65114
+ notify: { mode: autoFlow.notify?.mode ?? defaults.notify.mode },
64846
65115
  timeouts: {
64847
65116
  acceptanceMs: autoFlow.timeouts?.acceptanceMs ?? defaults.timeouts.acceptanceMs,
64848
65117
  gateMs: autoFlow.timeouts?.gateMs ?? defaults.timeouts.gateMs,
@@ -64867,11 +65136,38 @@ var init_config2 = __esm(() => {
64867
65136
  defaultAgent: null,
64868
65137
  reviewers: { spec: null, quality: null },
64869
65138
  escalate: { telegram: true },
65139
+ notify: { mode: "escalation" },
64870
65140
  timeouts: { acceptanceMs: 600000, gateMs: 900000, flowMs: 5400000, stepMs: null }
64871
65141
  };
64872
65142
  });
64873
65143
 
65144
+ // src/plugins/builtin/nax-finish/output.ts
65145
+ function logTail(stream) {
65146
+ if (stream.length <= LOG_TAIL_CHARS)
65147
+ return stream;
65148
+ return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
65149
+ ${stream.slice(-LOG_TAIL_CHARS)}`;
65150
+ }
65151
+ function stderrTail(stderr) {
65152
+ const trimmed = stderr.trim();
65153
+ if (!trimmed)
65154
+ return "";
65155
+ const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
65156
+ return tail.replace(/\s+/g, " ");
65157
+ }
65158
+ var STDERR_TAIL_CHARS = 400, LOG_TAIL_CHARS = 20000;
65159
+
64874
65160
  // src/plugins/builtin/nax-finish/telegram.ts
65161
+ function buildTerminalMessage(options) {
65162
+ const lines = [`nax-finish ${options.status} ${options.feature}`];
65163
+ if (options.detail)
65164
+ lines.push(options.detail);
65165
+ if (options.url)
65166
+ lines.push(options.url);
65167
+ const message = lines.join(`
65168
+ `);
65169
+ return message.length <= TELEGRAM_MAX_MESSAGE_CHARS ? message : `${message.slice(0, TELEGRAM_MAX_MESSAGE_CHARS - 1)}\u2026`;
65170
+ }
64875
65171
  function buildEscalationMessage(feature, reason, findings) {
64876
65172
  const head = `nax-finish escalated ${feature}: ${reason}`;
64877
65173
  if (findings.length === 0)
@@ -64908,19 +65204,6 @@ var init_telegram2 = __esm(() => {
64908
65204
 
64909
65205
  // src/plugins/builtin/nax-finish/index.ts
64910
65206
  import * as path21 from "path";
64911
- function logTail(stream) {
64912
- if (stream.length <= LOG_TAIL_CHARS)
64913
- return stream;
64914
- return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
64915
- ${stream.slice(-LOG_TAIL_CHARS)}`;
64916
- }
64917
- function stderrTail(stderr) {
64918
- const trimmed = stderr.trim();
64919
- if (!trimmed)
64920
- return "";
64921
- const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
64922
- return tail.replace(/\s+/g, " ");
64923
- }
64924
65207
  async function defaultRun2(cmd, opts) {
64925
65208
  const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
64926
65209
  let timedOut = false;
@@ -64951,6 +65234,11 @@ async function defaultReadResult(workdir) {
64951
65234
  return null;
64952
65235
  return JSON.parse(await f.text());
64953
65236
  }
65237
+ async function defaultClearResult(workdir) {
65238
+ const file3 = Bun.file(path21.join(workdir, ".nax", "nax-finish-result.json"));
65239
+ if (await file3.exists())
65240
+ await file3.delete();
65241
+ }
64954
65242
  function isFeatureBranch(b) {
64955
65243
  return b !== "main" && b !== "master" && b.length > 0;
64956
65244
  }
@@ -64994,13 +65282,133 @@ function buildFlowEnv(cfg) {
64994
65282
  env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
64995
65283
  return env2;
64996
65284
  }
64997
- 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;
65285
+ function missingResultOutcome(ctx, res, escalateTelegram) {
65286
+ ctx.logger.warn("nax-finish flow produced no result file", {
65287
+ exitCode: res.exitCode,
65288
+ stdout: logTail(res.stdout),
65289
+ stderr: logTail(res.stderr)
65290
+ });
65291
+ const tail = stderrTail(res.stderr);
65292
+ return {
65293
+ actionResult: {
65294
+ success: false,
65295
+ message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
65296
+ },
65297
+ escalateTelegram
65298
+ };
65299
+ }
65300
+ async function executeFinishFlow(options) {
65301
+ const { ctx, cfg, escalateTelegram } = options;
65302
+ const flowPath = await resolveFlowPath(ctx.workdir, cfg.flowPath);
65303
+ if (!flowPath) {
65304
+ return {
65305
+ actionResult: {
65306
+ success: false,
65307
+ message: `nax-finish: flow module "${cfg.flowPath}" not found in the nax install or ${ctx.workdir}`
65308
+ },
65309
+ escalateTelegram
65310
+ };
65311
+ }
65312
+ await _naxFinishDeps.clearResult(ctx.workdir);
65313
+ const input = {
65314
+ feature: ctx.feature,
65315
+ workdir: ctx.workdir,
65316
+ branch: ctx.branch,
65317
+ prdPath: ctx.prdPath,
65318
+ escalateTelegram,
65319
+ timeouts: { acceptanceMs: cfg.timeouts.acceptanceMs, gateMs: cfg.timeouts.gateMs }
65320
+ };
65321
+ const cmd = buildFlowArgv(flowPath, JSON.stringify(input), cfg.defaultAgent, cfg.timeouts.stepMs);
65322
+ const res = await _naxFinishDeps.run(cmd, {
65323
+ cwd: ctx.workdir,
65324
+ env: buildFlowEnv(cfg),
65325
+ timeoutMs: cfg.timeouts.flowMs
65326
+ });
65327
+ const result = await _naxFinishDeps.readResult(ctx.workdir);
65328
+ if (!result)
65329
+ return missingResultOutcome(ctx, res, escalateTelegram);
65330
+ return {
65331
+ actionResult: { success: true, message: `nax-finish: ${result.status}`, url: result.url },
65332
+ result,
65333
+ escalateTelegram
65334
+ };
65335
+ }
65336
+ async function settleFinishFlow(options) {
65337
+ const escalateTelegram = options.cfg.notify.mode !== "off" && options.cfg.escalate.telegram && options.creds !== null;
65338
+ try {
65339
+ return await executeFinishFlow({ ...options, escalateTelegram });
65340
+ } catch (error48) {
65341
+ options.ctx.logger.warn("nax-finish execute failed", { error: errorMessage(error48) });
65342
+ return {
65343
+ actionResult: { success: false, message: `nax-finish failed: ${errorMessage(error48)}` },
65344
+ escalateTelegram
65345
+ };
65346
+ }
65347
+ }
65348
+ async function notifyBestEffort(ctx, creds, message) {
65349
+ if (!creds) {
65350
+ ctx.logger.warn("nax-finish terminal notification skipped: Telegram credentials are unavailable");
65351
+ return;
65352
+ }
65353
+ try {
65354
+ if (!await _naxFinishDeps.notify(creds, message)) {
65355
+ ctx.logger.warn("nax-finish terminal notification was rejected", { feature: ctx.feature });
65356
+ }
65357
+ } catch (error48) {
65358
+ ctx.logger.warn("nax-finish terminal notification failed", { feature: ctx.feature, error: errorMessage(error48) });
65359
+ }
65360
+ }
65361
+ async function finalizeEscalation(ctx, outcome, creds) {
65362
+ const result = outcome.result;
65363
+ if (!result)
65364
+ return outcome.actionResult;
65365
+ const problems = [];
65366
+ let delivered = !outcome.escalateTelegram && !result.deliveryError;
65367
+ if (outcome.escalateTelegram && creds) {
65368
+ try {
65369
+ delivered = await _naxFinishDeps.notify(creds, buildEscalationMessage(result.feature, result.escalationReason ?? "", result.findings ?? []));
65370
+ if (!delivered)
65371
+ problems.push("Telegram rejected the message");
65372
+ } catch (error48) {
65373
+ problems.push(`Telegram failed: ${errorMessage(error48)}`);
65374
+ }
65375
+ }
65376
+ if (delivered)
65377
+ return outcome.actionResult;
65378
+ if (result.deliveryError)
65379
+ problems.push(`the flow could not post it: ${result.deliveryError}`);
65380
+ if (problems.length === 0)
65381
+ problems.push("no escalation channel was reachable");
65382
+ ctx.logger.warn("nax-finish escalation was not delivered", {
65383
+ feature: result.feature,
65384
+ reasons: problems,
65385
+ escalationReason: result.escalationReason
65386
+ });
65387
+ return {
65388
+ success: false,
65389
+ message: `nax-finish: escalated but undelivered \u2014 ${problems.join("; ")}`,
65390
+ url: result.url
65391
+ };
65392
+ }
65393
+ async function finalizeFinishOutcome(options) {
65394
+ const { ctx, cfg, creds, outcome } = options;
65395
+ if (outcome.result?.status === "escalated")
65396
+ return finalizeEscalation(ctx, outcome, creds);
65397
+ if (cfg.notify.mode === "always") {
65398
+ const status = outcome.result?.status ?? "failed";
65399
+ const detail = outcome.result ? undefined : outcome.actionResult.message;
65400
+ await notifyBestEffort(ctx, creds, buildTerminalMessage({ feature: ctx.feature, status, detail, url: outcome.actionResult.url }));
65401
+ }
65402
+ return outcome.actionResult;
65403
+ }
65404
+ var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
64998
65405
  var init_nax_finish = __esm(() => {
64999
65406
  init_config2();
65000
65407
  init_telegram2();
65001
65408
  _naxFinishDeps = {
65002
65409
  run: defaultRun2,
65003
65410
  readResult: defaultReadResult,
65411
+ clearResult: defaultClearResult,
65004
65412
  exists: (p) => Bun.file(p).exists(),
65005
65413
  moduleDir: import.meta.dir,
65006
65414
  notify: sendTelegramNotify
@@ -65018,76 +65426,10 @@ var init_nax_finish = __esm(() => {
65018
65426
  return isFeatureBranch(ctx.branch);
65019
65427
  },
65020
65428
  async execute(ctx) {
65021
- try {
65022
- const cfg = getFinishAutoFlowConfig(ctx);
65023
- const flowPath = await resolveFlowPath(ctx.workdir, cfg.flowPath);
65024
- if (!flowPath) {
65025
- return {
65026
- success: false,
65027
- message: `nax-finish: flow module "${cfg.flowPath}" not found in the nax install or ${ctx.workdir}`
65028
- };
65029
- }
65030
- const creds = telegramCreds(ctx.config);
65031
- const escalateTelegram = cfg.escalate.telegram && creds !== null;
65032
- const input = {
65033
- feature: ctx.feature,
65034
- workdir: ctx.workdir,
65035
- branch: ctx.branch,
65036
- prdPath: ctx.prdPath,
65037
- escalateTelegram,
65038
- timeouts: { acceptanceMs: cfg.timeouts.acceptanceMs, gateMs: cfg.timeouts.gateMs }
65039
- };
65040
- const cmd = buildFlowArgv(flowPath, JSON.stringify(input), cfg.defaultAgent, cfg.timeouts.stepMs);
65041
- const res = await _naxFinishDeps.run(cmd, {
65042
- cwd: ctx.workdir,
65043
- env: buildFlowEnv(cfg),
65044
- timeoutMs: cfg.timeouts.flowMs
65045
- });
65046
- const result = await _naxFinishDeps.readResult(ctx.workdir);
65047
- if (!result) {
65048
- ctx.logger.warn("nax-finish flow produced no result file", {
65049
- exitCode: res.exitCode,
65050
- stdout: logTail(res.stdout),
65051
- stderr: logTail(res.stderr)
65052
- });
65053
- const tail = stderrTail(res.stderr);
65054
- return {
65055
- success: false,
65056
- message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
65057
- };
65058
- }
65059
- if (result.status === "escalated") {
65060
- const problems = [];
65061
- let delivered = !escalateTelegram && !result.deliveryError;
65062
- if (escalateTelegram && creds) {
65063
- const sent = await _naxFinishDeps.notify(creds, buildEscalationMessage(result.feature, result.escalationReason ?? "", result.findings ?? []));
65064
- if (sent)
65065
- delivered = true;
65066
- else
65067
- problems.push("Telegram rejected the message");
65068
- }
65069
- if (!delivered) {
65070
- if (result.deliveryError)
65071
- problems.push(`the flow could not post it: ${result.deliveryError}`);
65072
- if (problems.length === 0)
65073
- problems.push("no escalation channel was reachable");
65074
- ctx.logger.warn("nax-finish escalation was not delivered", {
65075
- feature: result.feature,
65076
- reasons: problems,
65077
- escalationReason: result.escalationReason
65078
- });
65079
- return {
65080
- success: false,
65081
- message: `nax-finish: escalated but undelivered \u2014 ${problems.join("; ")}`,
65082
- url: result.url
65083
- };
65084
- }
65085
- }
65086
- return { success: true, message: `nax-finish: ${result.status}`, url: result.url };
65087
- } catch (err) {
65088
- ctx.logger.warn("nax-finish execute failed", { error: String(err) });
65089
- return { success: false, message: `nax-finish failed: ${String(err)}` };
65090
- }
65429
+ const cfg = getFinishAutoFlowConfig(ctx);
65430
+ const creds = telegramCreds(ctx.config);
65431
+ const outcome = await settleFinishFlow({ ctx, cfg, creds });
65432
+ return finalizeFinishOutcome({ ctx, cfg, creds, outcome });
65091
65433
  }
65092
65434
  };
65093
65435
  naxFinishPlugin = {
@@ -66134,7 +66476,7 @@ class PluginRegistry {
66134
66476
  sources;
66135
66477
  builtinPostRunActions;
66136
66478
  constructor(loadedPlugins, builtinPostRunActions = []) {
66137
- this.builtinPostRunActions = builtinPostRunActions;
66479
+ this.builtinPostRunActions = builtinPostRunActions.map((registration) => ("action" in registration) ? registration : { pluginName: registration.name, action: registration });
66138
66480
  if (loadedPlugins.length > 0 && "plugin" in loadedPlugins[0]) {
66139
66481
  const typed = loadedPlugins;
66140
66482
  this.plugins = typed.map((lp) => lp.plugin);
@@ -66173,7 +66515,13 @@ class PluginRegistry {
66173
66515
  return this.plugins.filter((p) => p.provides.includes("reporter")).map((p) => p.extensions.reporter).filter((reporter) => reporter !== undefined);
66174
66516
  }
66175
66517
  getPostRunActions() {
66176
- const pluginActions = this.plugins.filter((p) => p.provides.includes("post-run-action")).map((p) => p.extensions.postRunAction).filter((action) => action !== undefined);
66518
+ return this.getPostRunActionRegistrations().map(({ action }) => action);
66519
+ }
66520
+ getPostRunActionRegistrations() {
66521
+ const pluginActions = this.plugins.flatMap((plugin) => {
66522
+ const action = plugin.extensions.postRunAction;
66523
+ return plugin.provides.includes("post-run-action") && action ? [{ pluginName: plugin.name, action }] : [];
66524
+ });
66177
66525
  return [...pluginActions, ...this.builtinPostRunActions];
66178
66526
  }
66179
66527
  async teardownAll() {
@@ -66488,7 +66836,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66488
66836
  }
66489
66837
  const autoPrAction2 = autoPrPlugin.extensions.postRunAction;
66490
66838
  if (autoPrAction2) {
66491
- builtinPostRunActions.push(autoPrAction2);
66839
+ builtinPostRunActions.push({ pluginName: autoPrPlugin.name, action: autoPrAction2 });
66492
66840
  }
66493
66841
  } else {
66494
66842
  logger?.info("plugins", `Skipping disabled plugin: '${autoPrPlugin.name}' (built-in)`);
@@ -66500,7 +66848,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66500
66848
  }
66501
66849
  const action = naxFinishPlugin.extensions.postRunAction;
66502
66850
  if (action)
66503
- builtinPostRunActions.push(action);
66851
+ builtinPostRunActions.push({ pluginName: naxFinishPlugin.name, action });
66504
66852
  } else {
66505
66853
  logger?.info("plugins", `Skipping disabled plugin: '${naxFinishPlugin.name}' (built-in)`);
66506
66854
  }
@@ -66511,7 +66859,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
66511
66859
  }
66512
66860
  const autoRouteAction2 = autoRoutePlugin.extensions.postRunAction;
66513
66861
  if (autoRouteAction2) {
66514
- builtinPostRunActions.push(autoRouteAction2);
66862
+ builtinPostRunActions.push({ pluginName: autoRoutePlugin.name, action: autoRouteAction2 });
66515
66863
  }
66516
66864
  } else {
66517
66865
  logger?.info("plugins", `Skipping disabled plugin: '${autoRoutePlugin.name}' (built-in)`);
@@ -66905,6 +67253,25 @@ var init_checkpoint = __esm(() => {
66905
67253
  init_resume_cli();
66906
67254
  });
66907
67255
 
67256
+ // src/hooks/types.ts
67257
+ var HOOK_EVENTS;
67258
+ var init_types10 = __esm(() => {
67259
+ HOOK_EVENTS = [
67260
+ "on-start",
67261
+ "on-story-start",
67262
+ "on-story-complete",
67263
+ "on-story-fail",
67264
+ "on-pause",
67265
+ "on-resume",
67266
+ "on-session-end",
67267
+ "on-all-stories-complete",
67268
+ "on-complete",
67269
+ "on-error",
67270
+ "on-final-regression-fail",
67271
+ "on-post-run-action"
67272
+ ];
67273
+ });
67274
+
66908
67275
  // src/hooks/runner.ts
66909
67276
  import { join as join84 } from "path";
66910
67277
  function createDrainDeadline2(deadlineMs) {
@@ -66966,6 +67333,12 @@ function buildEnv(ctx) {
66966
67333
  env2.NAX_AGENT = escapeEnvValue(ctx.agent);
66967
67334
  if (ctx.iteration !== undefined)
66968
67335
  env2.NAX_ITERATION = String(ctx.iteration);
67336
+ if (ctx.pluginName)
67337
+ env2.NAX_PLUGIN_NAME = escapeEnvValue(ctx.pluginName);
67338
+ if (ctx.actionName)
67339
+ env2.NAX_ACTION_NAME = escapeEnvValue(ctx.actionName);
67340
+ if (ctx.url)
67341
+ env2.NAX_RESULT_URL = escapeEnvValue(ctx.url);
66969
67342
  return env2;
66970
67343
  }
66971
67344
  function hasShellOperators(command) {
@@ -67096,9 +67469,11 @@ var init_runner5 = __esm(() => {
67096
67469
  var exports_hooks = {};
67097
67470
  __export(exports_hooks, {
67098
67471
  loadHooksConfig: () => loadHooksConfig,
67099
- fireHook: () => fireHook
67472
+ fireHook: () => fireHook,
67473
+ HOOK_EVENTS: () => HOOK_EVENTS
67100
67474
  });
67101
67475
  var init_hooks = __esm(() => {
67476
+ init_types10();
67102
67477
  init_runner5();
67103
67478
  });
67104
67479
 
@@ -69746,7 +70121,7 @@ function buildPreviewRouting(story, config2) {
69746
70121
 
69747
70122
  // src/worktree/types.ts
69748
70123
  var WorktreeDependencyPreparationError;
69749
- var init_types10 = __esm(() => {
70124
+ var init_types11 = __esm(() => {
69750
70125
  WorktreeDependencyPreparationError = class WorktreeDependencyPreparationError extends Error {
69751
70126
  mode;
69752
70127
  failureCategory = "dependency-prep";
@@ -69816,7 +70191,7 @@ var PHASE_ONE_INHERIT_UNSUPPORTED_FILES, _worktreeDependencyDeps;
69816
70191
  var init_dependencies = __esm(() => {
69817
70192
  init_bun_deps();
69818
70193
  init_command_argv();
69819
- init_types10();
70194
+ init_types11();
69820
70195
  PHASE_ONE_INHERIT_UNSUPPORTED_FILES = [
69821
70196
  "package.json",
69822
70197
  "bun.lock",
@@ -71033,6 +71408,20 @@ var init_pipeline_result_handler = __esm(() => {
71033
71408
  // src/execution/iteration-runner.ts
71034
71409
  import { existsSync as existsSync35 } from "fs";
71035
71410
  import { join as join91 } from "path";
71411
+ function releaseHeavyPipelineContext(ctx) {
71412
+ ctx.agentResult = undefined;
71413
+ ctx.prompt = undefined;
71414
+ ctx.contextMarkdown = undefined;
71415
+ ctx.featureContextMarkdown = undefined;
71416
+ ctx.builtContext = undefined;
71417
+ ctx.contextBundle = undefined;
71418
+ ctx.constitution = undefined;
71419
+ ctx.acceptanceFailures = undefined;
71420
+ ctx.autofixPriorIterations = undefined;
71421
+ ctx.reviewFindings = undefined;
71422
+ ctx.selfVerification = undefined;
71423
+ ctx.tddIsolations = undefined;
71424
+ }
71036
71425
  async function runIteration(ctx, prd, selection, iterations, totalCost2, allStoryMetrics) {
71037
71426
  const { story, storiesToExecute, routing, isBatchExecution } = selection;
71038
71427
  if (ctx.dryRun) {
@@ -71213,11 +71602,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost2, allStor
71213
71602
  subStoryCount: pipelineResult.subStoryCount
71214
71603
  };
71215
71604
  }
71216
- pipelineContext.agentResult = undefined;
71217
- pipelineContext.prompt = undefined;
71218
- pipelineContext.contextMarkdown = undefined;
71219
- pipelineContext.builtContext = undefined;
71220
- pipelineContext.constitution = undefined;
71605
+ releaseHeavyPipelineContext(pipelineContext);
71221
71606
  return iterResult;
71222
71607
  }
71223
71608
  var _iterationRunnerDeps;
@@ -71691,7 +72076,7 @@ async function executeUnified(ctx, initialPrd) {
71691
72076
  _prevRunUnsubscribers = [];
71692
72077
  const thisRunUnsubscribers = [
71693
72078
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature),
71694
- wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime),
72079
+ wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime, ctx.runtime.projectKey),
71695
72080
  wireInteraction(pipelineEventBus, ctx.interactionChain, ctx.config),
71696
72081
  wireEventsWriter(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir),
71697
72082
  wireRegistry(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir, ctx.runtime.outputDir)
@@ -73463,8 +73848,63 @@ async function runSetupPhase(options) {
73463
73848
  var exports_run_cleanup = {};
73464
73849
  __export(exports_run_cleanup, {
73465
73850
  cleanupRun: () => cleanupRun,
73466
- buildPostRunContext: () => buildPostRunContext
73851
+ buildPostRunContext: () => buildPostRunContext,
73852
+ _runCleanupDeps: () => _runCleanupDeps
73467
73853
  });
73854
+ async function settlePostRunAction(action, ctx) {
73855
+ try {
73856
+ if (!await action.shouldRun(ctx))
73857
+ return { status: "skipped", reason: "shouldRun=false" };
73858
+ return outcomeFromResult(await action.execute(ctx));
73859
+ } catch (error48) {
73860
+ return { status: "error", reason: errorMessage(error48) };
73861
+ }
73862
+ }
73863
+ function outcomeFromResult(result) {
73864
+ if (result.skipped)
73865
+ return { status: "skipped", reason: result.reason ?? result.message };
73866
+ if (!result.success)
73867
+ return { status: "failed", message: result.message, url: result.url };
73868
+ return { status: "succeeded", message: result.message, url: result.url };
73869
+ }
73870
+ function logPostRunOutcome(actionName, outcome) {
73871
+ const logger = getSafeLogger();
73872
+ if (outcome.status === "skipped") {
73873
+ const level = outcome.reason === "shouldRun=false" ? "debug" : "info";
73874
+ logger?.[level]("post-run", `[post-run] ${actionName}: skipped \u2014 ${outcome.reason}`);
73875
+ } else if (outcome.status === "failed") {
73876
+ logger?.warn("post-run", `[post-run] ${actionName}: failed \u2014 ${outcome.message}`);
73877
+ } else if (outcome.status === "error") {
73878
+ logger?.warn("post-run", `[post-run] ${actionName}: error \u2014 ${outcome.reason}`);
73879
+ } else {
73880
+ const suffix = outcome.url ? `${outcome.message} (${outcome.url})` : outcome.message;
73881
+ logger?.info("post-run", `[post-run] ${actionName}: ${suffix}`);
73882
+ }
73883
+ }
73884
+ function postRunHookContext(feature, registration, outcome) {
73885
+ const reason = outcome.status === "succeeded" || outcome.status === "failed" ? outcome.message : outcome.reason;
73886
+ return {
73887
+ event: "on-post-run-action",
73888
+ feature,
73889
+ pluginName: registration.pluginName,
73890
+ actionName: registration.action.name,
73891
+ status: outcome.status,
73892
+ reason,
73893
+ url: "url" in outcome ? outcome.url : undefined
73894
+ };
73895
+ }
73896
+ async function runPostRunActions(options, ctx) {
73897
+ const registrations = options.pluginRegistry.getPostRunActionRegistrations();
73898
+ for (const registration of registrations) {
73899
+ const outcome = await settlePostRunAction(registration.action, ctx);
73900
+ logPostRunOutcome(registration.action.name, outcome);
73901
+ try {
73902
+ await _runCleanupDeps.fireHook(options.hooks, "on-post-run-action", postRunHookContext(options.feature, registration, outcome), options.workdir);
73903
+ } catch (error48) {
73904
+ getSafeLogger()?.warn("hooks", `on-post-run-action hook failed for '${registration.pluginName}'`, { error: error48 });
73905
+ }
73906
+ }
73907
+ }
73468
73908
  function buildPostRunContext(opts, durationMs, logger) {
73469
73909
  const {
73470
73910
  runId,
@@ -73537,7 +73977,6 @@ async function cleanupRun(options) {
73537
73977
  }
73538
73978
  }
73539
73979
  }
73540
- const actions = pluginRegistry.getPostRunActions();
73541
73980
  const pluginLogger = {
73542
73981
  debug: (msg, data) => logger?.debug("post-run", msg, data),
73543
73982
  info: (msg, data) => logger?.info("post-run", msg, data),
@@ -73545,26 +73984,7 @@ async function cleanupRun(options) {
73545
73984
  error: (msg, data) => logger?.error("post-run", msg, data)
73546
73985
  };
73547
73986
  const ctx = buildPostRunContext(options, durationMs, pluginLogger);
73548
- for (const action of actions) {
73549
- try {
73550
- const shouldRun = await action.shouldRun(ctx);
73551
- if (!shouldRun) {
73552
- logger?.debug("post-run", `[post-run] ${action.name}: shouldRun=false, skipping`);
73553
- continue;
73554
- }
73555
- const result = await action.execute(ctx);
73556
- if (result.skipped) {
73557
- logger?.info("post-run", `[post-run] ${action.name}: skipped \u2014 ${result.reason}`);
73558
- } else if (!result.success) {
73559
- logger?.warn("post-run", `[post-run] ${action.name}: failed \u2014 ${result.message}`);
73560
- } else {
73561
- const msg = result.url ? `[post-run] ${action.name}: ${result.message} (${result.url})` : `[post-run] ${action.name}: ${result.message}`;
73562
- logger?.info("post-run", msg);
73563
- }
73564
- } catch (error48) {
73565
- logger?.warn("post-run", `[post-run] ${action.name}: error \u2014 ${error48}`);
73566
- }
73567
- }
73987
+ await runPostRunActions(options, ctx);
73568
73988
  try {
73569
73989
  await pluginRegistry.teardownAll();
73570
73990
  } catch (error48) {
@@ -73581,11 +74001,14 @@ async function cleanupRun(options) {
73581
74001
  disposeFeatureResolver(workdir);
73582
74002
  await releaseLock(workdir);
73583
74003
  }
74004
+ var _runCleanupDeps;
73584
74005
  var init_run_cleanup = __esm(() => {
73585
74006
  init_context();
74007
+ init_hooks();
73586
74008
  init_logger2();
73587
74009
  init_prd();
73588
74010
  init_helpers();
74011
+ _runCleanupDeps = { fireHook };
73589
74012
  });
73590
74013
 
73591
74014
  // src/execution/runner.ts
@@ -73782,6 +74205,7 @@ async function run(options) {
73782
74205
  prdPath,
73783
74206
  branch,
73784
74207
  version: NAX_VERSION,
74208
+ hooks,
73785
74209
  runCompleted,
73786
74210
  outputDir: runtime.outputDir,
73787
74211
  globalDir: runtime.globalDir,
@@ -73835,17 +74259,20 @@ var exports_execution = {};
73835
74259
  __export(exports_execution, {
73836
74260
  writeExitSummary: () => writeExitSummary,
73837
74261
  withIncreasingFailuresBail: () => withIncreasingFailuresBail,
74262
+ toReviewDecisionPayload: () => toReviewDecisionPayload,
73838
74263
  synthesizeBackfillMetric: () => synthesizeBackfillMetric,
73839
74264
  stopHeartbeat: () => stopHeartbeat,
73840
74265
  startHeartbeat: () => startHeartbeat2,
73841
74266
  runRectification: () => runRectification,
73842
74267
  runPhase: () => runPhase,
74268
+ runNonBlockingFix: () => runNonBlockingFix,
73843
74269
  runDeferredRegression: () => runDeferredRegression,
73844
74270
  runCompletionPhase: () => runCompletionPhase,
73845
74271
  run: () => run,
73846
74272
  resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
73847
74273
  resetCrashHandlers: () => resetCrashHandlers,
73848
74274
  releaseLock: () => releaseLock,
74275
+ releaseHeavyPipelineContext: () => releaseHeavyPipelineContext,
73849
74276
  refreshReviewInputForDispatch: () => refreshReviewInputForDispatch,
73850
74277
  recordOscillations: () => recordOscillations,
73851
74278
  readQueueFile: () => readQueueFile,
@@ -73878,6 +74305,7 @@ __export(exports_execution, {
73878
74305
  describeGateRegression: () => describeGateRegression,
73879
74306
  deriveTddFailureCategory: () => deriveTddFailureCategory,
73880
74307
  decideStageAction: () => decideStageAction,
74308
+ createNbfFlakeTriageTransaction: () => createNbfFlakeTriageTransaction,
73881
74309
  createCheckpointWriter: () => createCheckpointWriter,
73882
74310
  countOscillationOutcomes: () => countOscillationOutcomes,
73883
74311
  clearQueueFile: () => clearQueueFile,
@@ -73900,6 +74328,7 @@ __export(exports_execution, {
73900
74328
  _runnerDeps: () => _runnerDeps,
73901
74329
  _runnerCompletionDeps: () => _runnerCompletionDeps,
73902
74330
  _runCompletionDeps: () => _runCompletionDeps,
74331
+ _runCleanupDeps: () => _runCleanupDeps,
73903
74332
  _regressionDeps: () => _regressionDeps,
73904
74333
  _postRunDeps: () => _postRunDeps,
73905
74334
  _pidRegistryDeps: () => _pidRegistryDeps,
@@ -73917,6 +74346,7 @@ var init_execution2 = __esm(() => {
73917
74346
  init_oscillation_breaker();
73918
74347
  init_runner6();
73919
74348
  init_progress();
74349
+ init_iteration_runner();
73920
74350
  init_escalation();
73921
74351
  init_queue_handler();
73922
74352
  init_ensure_package_dirs();
@@ -73928,6 +74358,7 @@ var init_execution2 = __esm(() => {
73928
74358
  init_story_orchestrator();
73929
74359
  init_story_orchestrator_logging();
73930
74360
  init_plan_inputs();
74361
+ init_non_blocking_fix();
73931
74362
  init_build_plan_for_strategy();
73932
74363
  init_checkpoint();
73933
74364
  init_runner_completion();