@nathapp/nax 0.75.2 → 0.75.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nax.js CHANGED
@@ -17294,7 +17294,7 @@ var HeadersSchema, ReporterEventSchema, WebhookReporterConfigSchema, OtelReporte
17294
17294
  var init_schemas_reporters = __esm(() => {
17295
17295
  init_zod();
17296
17296
  HeadersSchema = exports_external.record(exports_external.string(), exports_external.string()).default({});
17297
- ReporterEventSchema = exports_external.enum(["onRunStart", "onStoryComplete", "onRunEnd"]);
17297
+ ReporterEventSchema = exports_external.enum(["onRunStart", "onStoryComplete", "onRunEnd", "onPhaseStart", "onPhaseComplete"]);
17298
17298
  WebhookReporterConfigSchema = exports_external.object({
17299
17299
  enabled: exports_external.boolean().default(false),
17300
17300
  url: exports_external.string().url().optional(),
@@ -17311,12 +17311,28 @@ var init_schemas_reporters = __esm(() => {
17311
17311
  endpoint: exports_external.string().url().optional(),
17312
17312
  headers: HeadersSchema,
17313
17313
  serviceName: exports_external.string().default("nax"),
17314
- timeoutMs: exports_external.number().int().positive().default(5000)
17314
+ timeoutMs: exports_external.number().int().positive().default(5000),
17315
+ detail: exports_external.enum(["counts", "verbose"]).default("counts"),
17316
+ heartbeatIntervalMs: exports_external.number().int().nonnegative().default(1e4),
17317
+ maxBatchSize: exports_external.number().int().positive().default(64),
17318
+ flushIntervalMs: exports_external.number().int().positive().default(5000),
17319
+ maxQueueSize: exports_external.number().int().positive().default(2048),
17320
+ phases: exports_external.array(exports_external.string()).optional(),
17321
+ logs: exports_external.object({
17322
+ enabled: exports_external.boolean().default(false),
17323
+ level: exports_external.enum(["silent", "error", "warn", "info", "debug"]).default("info")
17324
+ }).default({ enabled: false, level: "info" })
17315
17325
  }).default({
17316
17326
  enabled: false,
17317
17327
  headers: {},
17318
17328
  serviceName: "nax",
17319
- timeoutMs: 5000
17329
+ timeoutMs: 5000,
17330
+ detail: "counts",
17331
+ heartbeatIntervalMs: 1e4,
17332
+ maxBatchSize: 64,
17333
+ flushIntervalMs: 5000,
17334
+ maxQueueSize: 2048,
17335
+ logs: { enabled: false, level: "info" }
17320
17336
  });
17321
17337
  ReportersConfigSchema = exports_external.object({
17322
17338
  webhook: WebhookReporterConfigSchema,
@@ -17331,7 +17347,13 @@ var init_schemas_reporters = __esm(() => {
17331
17347
  enabled: false,
17332
17348
  headers: {},
17333
17349
  serviceName: "nax",
17334
- timeoutMs: 5000
17350
+ timeoutMs: 5000,
17351
+ detail: "counts",
17352
+ heartbeatIntervalMs: 1e4,
17353
+ maxBatchSize: 64,
17354
+ flushIntervalMs: 5000,
17355
+ maxQueueSize: 2048,
17356
+ logs: { enabled: false, level: "info" }
17335
17357
  }
17336
17358
  });
17337
17359
  });
@@ -18222,6 +18244,10 @@ function formatAdvisorySummary(findings, options) {
18222
18244
  if (coverageGapCount > 0) {
18223
18245
  lines.push(c.gray(` ${coverageGapCount} of ${findings.length} were coverage-gap demotions (recurred past the block limit \u2014 candidate for spec/AC review)`));
18224
18246
  }
18247
+ const noActionCount = findings.filter((f) => f.actionRequired === false).length;
18248
+ if (noActionCount > 0) {
18249
+ lines.push(c.gray(` ${noActionCount} of ${findings.length} asked for no change (compliance notes \u2014 the best-effort fix pass skipped them)`));
18250
+ }
18225
18251
  lines.push(c.yellow("\u2500".repeat(60)));
18226
18252
  for (const f of sorted) {
18227
18253
  const location = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : undefined;
@@ -18230,7 +18256,8 @@ function formatAdvisorySummary(findings, options) {
18230
18256
  f.storyId ?? "unknown",
18231
18257
  location,
18232
18258
  f.category,
18233
- f.coverageGap ? "coverage-gap" : undefined
18259
+ f.coverageGap ? "coverage-gap" : undefined,
18260
+ f.actionRequired === false ? "no-action" : undefined
18234
18261
  ].filter((v) => typeof v === "string" && v.length > 0);
18235
18262
  lines.push(` ${c.gray(parts.join(" \xB7 "))}`);
18236
18263
  lines.push(` ${f.issue}`);
@@ -18367,6 +18394,30 @@ var init_redact = __esm(() => {
18367
18394
  ];
18368
18395
  });
18369
18396
 
18397
+ // src/logger/sink-registry.ts
18398
+ class SinkRegistry {
18399
+ sinks = [];
18400
+ add(sink) {
18401
+ this.sinks.push(sink);
18402
+ return () => {
18403
+ const idx = this.sinks.indexOf(sink);
18404
+ if (idx !== -1) {
18405
+ this.sinks.splice(idx, 1);
18406
+ }
18407
+ };
18408
+ }
18409
+ dispatch(entry) {
18410
+ for (const sink of this.sinks) {
18411
+ try {
18412
+ sink({ ...entry });
18413
+ } catch (error48) {
18414
+ process.stderr.write(`[logger] Sink threw: ${error48}
18415
+ `);
18416
+ }
18417
+ }
18418
+ }
18419
+ }
18420
+
18370
18421
  // src/logger/logger.ts
18371
18422
  import { mkdirSync } from "fs";
18372
18423
  import { appendFile } from "fs/promises";
@@ -18379,6 +18430,7 @@ class Logger {
18379
18430
  suppressConsole;
18380
18431
  writeQueueTail = Promise.resolve();
18381
18432
  pendingLines = [];
18433
+ sinkRegistry = new SinkRegistry;
18382
18434
  constructor(options) {
18383
18435
  this.level = options.level;
18384
18436
  this.filePath = options.filePath;
@@ -18422,10 +18474,11 @@ class Logger {
18422
18474
  ...sessionRole && { sessionRole },
18423
18475
  ...strippedData && { data: strippedData }
18424
18476
  };
18477
+ const entry = redactEntry(rawEntry);
18478
+ this.sinkRegistry.dispatch(entry);
18425
18479
  const consoleEnabled = this.shouldLog(level) && !this.suppressConsole;
18426
18480
  if (!consoleEnabled && !this.filePath)
18427
18481
  return;
18428
- const entry = redactEntry(rawEntry);
18429
18482
  if (consoleEnabled) {
18430
18483
  let consoleOutput = null;
18431
18484
  if (this.formatterMode) {
@@ -18509,8 +18562,19 @@ ${JSON.stringify(entry.data, null, 2)}`;
18509
18562
  debug: (stage, message, data) => this.log("debug", stage, message, data, storyId)
18510
18563
  };
18511
18564
  }
18565
+ addSink(sink) {
18566
+ return this.sinkRegistry.add(sink);
18567
+ }
18512
18568
  close() {}
18513
18569
  }
18570
+ function addSink(sink) {
18571
+ if (!instance) {
18572
+ throw new NaxError("Logger not initialized. Call initLogger() before addSink().", "LOGGER_NOT_INITIALIZED", {
18573
+ stage: "logger"
18574
+ });
18575
+ }
18576
+ return instance.addSink(sink);
18577
+ }
18514
18578
  function initLogger(options = { level: "silent" }) {
18515
18579
  if (instance) {
18516
18580
  throw new Error("Logger already initialized. Call getLogger() to access existing instance.");
@@ -18539,6 +18603,7 @@ function resetLogger() {
18539
18603
  }
18540
18604
  var LOG_LEVEL_PRIORITY, MAX_BATCH_BYTES, instance = null, noopLogger;
18541
18605
  var init_logger = __esm(() => {
18606
+ init_errors();
18542
18607
  init_log_format();
18543
18608
  init_formatters();
18544
18609
  init_redact();
@@ -33112,6 +33177,7 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
33112
33177
  "acIndex": 2,
33113
33178
  "scopeQuote": "<out-of-scope findings ONLY: verbatim substring of one Out of Scope entry>",
33114
33179
  "scopeIndex": 1,
33180
+ "actionRequired": true,
33115
33181
  "verifiedBy": {
33116
33182
  "command": "command used to inspect the current codebase",
33117
33183
  "file": "relative/path/to/file.ts",
@@ -33165,6 +33231,11 @@ A finding about code that crossed one of these boundaries must NOT cite an AC \u
33165
33231
  - When the boundary is only a description "Out:" bullet, quote it in \`issue\` and leave \`scopeQuote\`/\`scopeIndex\` unset.
33166
33232
  - Emit scope-violation findings as \`"warning"\` \u2014 never \`"error"\`. Reporting the boundary is the goal; it does not block the story.
33167
33233
 
33234
+ **Do not report compliance as a finding:**
33235
+ A finding is a request for a change. If you inspected something and the code was CORRECT \u2014 it honoured an out-of-scope boundary, satisfied a convention, handled the edge case \u2014 that is not a finding. Say it in \`passed\`, not in \`findings\`. Emitting "this is correct per Out of Scope #10 / no action needed" as a finding causes an automated fix pass to be dispatched against code that needs no fix; one such report edited working code and broke a test.
33236
+
33237
+ If you judge a no-change note genuinely worth recording, set \`actionRequired: false\` on it. Every finding that asks for a change must leave \`actionRequired\` unset or \`true\`. Never pair \`actionRequired: false\` with a \`suggestion\` that requests an edit \u2014 pick one.
33238
+
33168
33239
  Never use \`acIndex: 0\`; \`acIndex\` is 1-based (first AC bullet = 1). The same applies to \`scopeIndex\`.
33169
33240
 
33170
33241
  If you cannot find an AC that names the **specific symbol** in your finding, downgrade to \`"info"\` or \`"warning"\`. A finding dropped by the validator is worse than one correctly classified as advisory.`, AdversarialReviewPromptBuilder;
@@ -35887,6 +35958,7 @@ function toAdversarialReviewFindings(findings, opts = {}) {
35887
35958
  message: f.issue,
35888
35959
  suggestion: f.suggestion,
35889
35960
  fixTarget: resolveFixTarget({ base: categoryToFixTarget(f.category), file: f.file, isTestFile: opts.isTestFile }),
35961
+ ...f.actionRequired === false ? { actionRequired: false } : {},
35890
35962
  meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
35891
35963
  };
35892
35964
  });
@@ -36826,11 +36898,11 @@ var init_adversarial_review = __esm(() => {
36826
36898
  throw new ParseValidationError("[adversarial-review] parse failed: invalid JSON shape");
36827
36899
  },
36828
36900
  async verify(parsed, input, _verifyCtx) {
36901
+ const threshold = input.blockingThreshold ?? "error";
36829
36902
  if (parsed.failOpen || parsed.looksLikeFail)
36830
- return parsed;
36903
+ return { ...parsed, blockingThreshold: threshold };
36831
36904
  if (parsed.findings.length === 0)
36832
- return parsed;
36833
- const threshold = input.blockingThreshold ?? "error";
36905
+ return { ...parsed, blockingThreshold: threshold };
36834
36906
  const findings = parsed.findings;
36835
36907
  const substantiated = await substantiateAdversarialFindings({
36836
36908
  findings,
@@ -36877,6 +36949,7 @@ var init_adversarial_review = __esm(() => {
36877
36949
  return {
36878
36950
  ...parsed,
36879
36951
  passed,
36952
+ blockingThreshold: threshold,
36880
36953
  modelPassed: parsed.passed,
36881
36954
  findings: accepted,
36882
36955
  normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
@@ -39901,9 +39974,29 @@ var init_apply_test_edit_declarations = __esm(() => {
39901
39974
  });
39902
39975
 
39903
39976
  // src/operations/validate-mock-structure-files.ts
39904
- import { join as join23 } from "path";
39905
- async function validateMockStructureFiles(declarations, resolvedTestPatterns, packageDir, deps) {
39906
- const fileExists = deps?.fileExists ?? defaultFileExists;
39977
+ import { isAbsolute as isAbsolute9, join as join23, relative as relative9 } from "path";
39978
+ function resolutionCandidates(file3, packageDir, repoRoot) {
39979
+ if (isAbsolute9(file3))
39980
+ return [file3];
39981
+ const viaPackageDir = join23(packageDir, file3);
39982
+ if (repoRoot === undefined)
39983
+ return [viaPackageDir];
39984
+ const viaRepoRoot = join23(repoRoot, file3);
39985
+ return viaRepoRoot === viaPackageDir ? [viaPackageDir] : [viaPackageDir, viaRepoRoot];
39986
+ }
39987
+ async function resolvePackageRelative(file3, opts) {
39988
+ for (const candidate of resolutionCandidates(file3, opts.packageDir, opts.repoRoot)) {
39989
+ if (!await opts.fileExists(candidate))
39990
+ continue;
39991
+ const rel = relative9(opts.packageDir, candidate);
39992
+ if (rel === "" || rel.startsWith("..") || isAbsolute9(rel))
39993
+ return null;
39994
+ return rel;
39995
+ }
39996
+ return null;
39997
+ }
39998
+ async function validateMockStructureFiles(declarations, resolvedTestPatterns, packageDir, opts) {
39999
+ const fileExists = opts?.fileExists ?? defaultFileExists;
39907
40000
  const valid = [];
39908
40001
  const invalid = [];
39909
40002
  for (const d of declarations) {
@@ -39914,13 +40007,16 @@ async function validateMockStructureFiles(declarations, resolvedTestPatterns, pa
39914
40007
  const files = d.files ?? [d.file];
39915
40008
  let allValid = true;
39916
40009
  for (const file3 of files) {
39917
- const absolutePath = join23(packageDir, file3);
39918
- const exists = await fileExists(absolutePath);
39919
- if (!exists) {
40010
+ const packageRelative = await resolvePackageRelative(file3, {
40011
+ packageDir,
40012
+ repoRoot: opts?.repoRoot,
40013
+ fileExists
40014
+ });
40015
+ if (packageRelative === null) {
39920
40016
  allValid = false;
39921
40017
  break;
39922
40018
  }
39923
- const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(file3));
40019
+ const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(packageRelative));
39924
40020
  if (!matchesPattern) {
39925
40021
  allValid = false;
39926
40022
  break;
@@ -41458,7 +41554,7 @@ var init_mutation = __esm(() => {
41458
41554
  });
41459
41555
 
41460
41556
  // src/operations/mutation-check.ts
41461
- import { isAbsolute as isAbsolute9, join as join24 } from "path";
41557
+ import { isAbsolute as isAbsolute10, join as join24 } from "path";
41462
41558
  var _mutationCheckDeps, mutationCheckOp;
41463
41559
  var init_mutation_check = __esm(() => {
41464
41560
  init_config();
@@ -41501,7 +41597,7 @@ var init_mutation_check = __esm(() => {
41501
41597
  }
41502
41598
  const changedFiles = await deps.getChangedNonTestFiles(input.workdir, input.storyGitRef, input.packagePrefix, [...input.resolvedTestPatterns.regex], undefined, input.repoRoot);
41503
41599
  const anchor = input.repoRoot ?? input.workdir;
41504
- const absoluteChangedFiles = changedFiles.map((f) => isAbsolute9(f) ? f : join24(anchor, f));
41600
+ const absoluteChangedFiles = changedFiles.map((f) => isAbsolute10(f) ? f : join24(anchor, f));
41505
41601
  const survivors = [];
41506
41602
  const mutants = [];
41507
41603
  for (const file3 of absoluteChangedFiles) {
@@ -41626,6 +41722,31 @@ var init_operations = __esm(() => {
41626
41722
  init_mutation_check();
41627
41723
  });
41628
41724
 
41725
+ // src/findings/cycle-retirement.ts
41726
+ function createDeclineLedger() {
41727
+ const declinedByStrategy = new Map;
41728
+ const hasDeclined = (strategyName, finding) => declinedByStrategy.get(strategyName)?.has(findingKey(finding)) === true;
41729
+ const isRetiredFor = (strategy, findings) => {
41730
+ const claimed = findings.filter((f) => strategy.appliesTo(f));
41731
+ return claimed.length > 0 && claimed.every((f) => hasDeclined(strategy.name, f));
41732
+ };
41733
+ return {
41734
+ recordDeclined(strategy, dispatched) {
41735
+ const declined = declinedByStrategy.get(strategy.name) ?? new Set;
41736
+ for (const f of dispatched.filter((x) => strategy.appliesTo(x)))
41737
+ declined.add(findingKey(f));
41738
+ declinedByStrategy.set(strategy.name, declined);
41739
+ },
41740
+ isRetiredFor,
41741
+ retiredNames(strategies, findings) {
41742
+ return strategies.filter((s) => isRetiredFor(s, findings)).map((s) => s.name);
41743
+ }
41744
+ };
41745
+ }
41746
+ var init_cycle_retirement = __esm(() => {
41747
+ init_types6();
41748
+ });
41749
+
41629
41750
  // src/findings/cycle.ts
41630
41751
  function normalizeValidateResult(r) {
41631
41752
  return Array.isArray(r) ? { findings: r, shortCircuited: false } : r;
@@ -41695,17 +41816,18 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41695
41816
  const storyId = ctx.storyId;
41696
41817
  const packageDir = ctx.packageDir;
41697
41818
  let totalCostUsd = 0;
41698
- const spentStrategies = new Set;
41819
+ const declines = createDeclineLedger();
41699
41820
  let unresolvedDetail;
41700
41821
  const finish = (result) => unresolvedDetail !== undefined && result.unresolvedDetail === undefined ? { ...result, unresolvedDetail } : result;
41701
41822
  for (;; ) {
41702
41823
  if (cycle.findings.length === 0 && cycle.verdict === undefined) {
41703
41824
  return { iterations: cycle.iterations, finalFindings: [], exitReason: "resolved", costUsd: totalCostUsd };
41704
41825
  }
41705
- const selectable = cycle.strategies.filter((s) => !spentStrategies.has(s.name));
41826
+ const selectable = cycle.strategies.filter((s) => !declines.isRetiredFor(s, cycle.findings));
41706
41827
  const active = selectActiveStrategies(selectable, cycle.findings, cycle.verdict);
41707
41828
  if (active.length === 0) {
41708
41829
  const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
41830
+ const retiredStrategies = declines.retiredNames(cycle.strategies, cycle.findings);
41709
41831
  logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
41710
41832
  storyId,
41711
41833
  packageDir,
@@ -41713,7 +41835,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41713
41835
  reason: "no-strategy",
41714
41836
  findingsCount: cycle.findings.length,
41715
41837
  orphanSources,
41716
- ...spentStrategies.size > 0 ? { retiredStrategies: [...spentStrategies] } : {}
41838
+ ...retiredStrategies.length > 0 ? { retiredStrategies } : {}
41717
41839
  });
41718
41840
  return finish({
41719
41841
  iterations: cycle.iterations,
@@ -41784,7 +41906,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41784
41906
  for (const strategy of group) {
41785
41907
  const relevantFindings = findingsBefore.filter((f) => strategy.appliesTo(f));
41786
41908
  const input = strategy.buildInput(relevantFindings, cycle.iterations, ctx);
41787
- const output = await doCallOp(ctx, strategy.fixOp, input);
41909
+ const fixCtx = {
41910
+ ...ctx,
41911
+ fixStrategy: { name: strategy.name, findingsBefore: findingsBefore.length }
41912
+ };
41913
+ const output = await doCallOp(fixCtx, strategy.fixOp, input);
41788
41914
  const extracted = await (strategy.extractApplied?.(output, input) ?? {});
41789
41915
  fixesApplied.push({
41790
41916
  strategyName: strategy.name,
@@ -41799,8 +41925,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41799
41925
  if (unresolvedFas.length > 0) {
41800
41926
  const firstUnresolved = unresolvedFas[0];
41801
41927
  unresolvedDetail = firstUnresolved.unresolved;
41802
- for (const fa of unresolvedFas)
41803
- spentStrategies.add(fa.strategyName);
41928
+ for (const fa of unresolvedFas) {
41929
+ const strategy = group.find((s) => s.name === fa.strategyName);
41930
+ if (strategy)
41931
+ declines.recordDeclined(strategy, findingsBefore);
41932
+ }
41804
41933
  const allGaveUp = unresolvedFas.length === fixesApplied.length;
41805
41934
  if (allGaveUp) {
41806
41935
  const finishedAt2 = now();
@@ -42015,6 +42144,7 @@ var _cycleDeps;
42015
42144
  var init_cycle = __esm(() => {
42016
42145
  init_logger2();
42017
42146
  init_operations();
42147
+ init_cycle_retirement();
42018
42148
  init_types6();
42019
42149
  _cycleDeps = {
42020
42150
  callOp,
@@ -42346,13 +42476,13 @@ var init_finding_projection = __esm(() => {
42346
42476
  });
42347
42477
 
42348
42478
  // src/review/prepare-inputs.ts
42349
- import { relative as relative9, sep } from "path";
42479
+ import { relative as relative10, sep } from "path";
42350
42480
  function derivePackageDirs(workdir, projectDir) {
42351
42481
  const repoRoot = projectDir ?? workdir;
42352
42482
  const packageDir = workdir !== repoRoot ? workdir : undefined;
42353
42483
  let packageDirRelative;
42354
42484
  if (projectDir && workdir !== projectDir) {
42355
- const rel = relative9(projectDir, workdir);
42485
+ const rel = relative10(projectDir, workdir);
42356
42486
  if (rel !== ".." && !rel.startsWith(`..${sep}`)) {
42357
42487
  packageDirRelative = rel && rel !== "." ? rel : undefined;
42358
42488
  }
@@ -42493,7 +42623,7 @@ var package_default;
42493
42623
  var init_package = __esm(() => {
42494
42624
  package_default = {
42495
42625
  name: "@nathapp/nax",
42496
- version: "0.75.2",
42626
+ version: "0.75.4",
42497
42627
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42498
42628
  type: "module",
42499
42629
  bin: {
@@ -42504,9 +42634,10 @@ var init_package = __esm(() => {
42504
42634
  dev: "bun run bin/nax.ts",
42505
42635
  build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
42506
42636
  typecheck: "bun x tsc --noEmit && bun x tsc --noEmit -p tsconfig.contracts.json",
42507
- lint: "bun x biome check src/ bin/ flows/ && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
42637
+ lint: "bun x biome check src/ bin/ flows/ && bun run check:flows-no-bun && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
42508
42638
  "lint:json": "bun x biome check src/ bin/ flows/ --reporter json && bun run check:nax-error 1>&2 && bun run check:logger-storyid 1>&2",
42509
42639
  "lint:fix": "bun x biome check --write src/ bin/ flows/",
42640
+ "check:flows-no-bun": "bun run scripts/check-flows-no-bun.ts",
42510
42641
  "check:no-real-global-nax": "bun run scripts/check-no-real-global-nax.ts",
42511
42642
  "check:alias-internals": "bun run scripts/check-alias-internals.ts",
42512
42643
  "check:deep-relatives": "bun run scripts/check-deep-relatives.ts",
@@ -42596,8 +42727,8 @@ var init_version = __esm(() => {
42596
42727
  NAX_VERSION = package_default.version;
42597
42728
  NAX_COMMIT = (() => {
42598
42729
  try {
42599
- if (/^[0-9a-f]{6,10}$/.test("8538e8f4"))
42600
- return "8538e8f4";
42730
+ if (/^[0-9a-f]{6,10}$/.test("5aee16bf"))
42731
+ return "5aee16bf";
42601
42732
  } catch {}
42602
42733
  try {
42603
42734
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42686,7 +42817,8 @@ function toAdvisorySummaryEntries(entry) {
42686
42817
  file: f.file,
42687
42818
  line: f.line,
42688
42819
  issue: f.issue ?? "(no description)",
42689
- coverageGap: f.meta?.coverageGap === true ? true : undefined
42820
+ coverageGap: f.meta?.coverageGap === true ? true : undefined,
42821
+ actionRequired: f.actionRequired === false ? false : undefined
42690
42822
  };
42691
42823
  });
42692
42824
  }
@@ -43420,7 +43552,7 @@ var init_language_commands = __esm(() => {
43420
43552
  });
43421
43553
 
43422
43554
  // src/review/scoped-lint.ts
43423
- import { join as join27, relative as relative10 } from "path";
43555
+ import { join as join27, relative as relative11 } from "path";
43424
43556
  function shellQuotePath4(path6) {
43425
43557
  return `'${path6.replaceAll("'", "'\\''")}'`;
43426
43558
  }
@@ -43456,7 +43588,7 @@ async function listChangedFiles(workdir, baseRef) {
43456
43588
  function inferActivePackageDir(workdir, projectDir) {
43457
43589
  if (!projectDir)
43458
43590
  return;
43459
- const rel = normalizePath3(relative10(projectDir, workdir));
43591
+ const rel = normalizePath3(relative11(projectDir, workdir));
43460
43592
  if (!rel || rel === "." || rel.startsWith(".."))
43461
43593
  return;
43462
43594
  return rel;
@@ -44610,6 +44742,7 @@ var init_runner2 = __esm(() => {
44610
44742
  // src/review/index.ts
44611
44743
  var init_review = __esm(() => {
44612
44744
  init_semantic_helpers();
44745
+ init_adversarial_helpers();
44613
44746
  init_category_fix_target();
44614
44747
  init_finding_filters();
44615
44748
  init_ac_quote_validator();
@@ -44894,7 +45027,10 @@ REASON: <one paragraph: which mock is wrong vs which dispatch the new code uses,
44894
45027
  Rules:
44895
45028
  - Do NOT make any edits yourself; the test-writer will fulfill.
44896
45029
  - Do NOT also emit \`UNRESOLVED:\` in the same turn \u2014 this declaration IS the handoff.
44897
- - FILES must list real test files. Each path must exist and be a test file.`, SINGLE_SESSION_PERMIT_HEADLINE = "You authored these tests in the same session as the implementation, so you MAY edit test files \u2014 but ONLY to resolve a genuine contradiction between a test and this story's acceptance criteria (or between two acceptance criteria). NEVER weaken, delete, loosen, or skip a test merely to make it pass. See the test-edit guidance appended below.", SINGLE_SESSION_TEST_EDIT_POLICY = `
45030
+ - FILES must list real test files. Each path must exist and be a test file.
45031
+ - Write each path exactly as it appears in the findings above (repository-relative).
45032
+ Paths that resolve under neither the repository root nor the package directory are
45033
+ rejected and the handoff is dropped \u2014 the findings then have no owner.`, SINGLE_SESSION_PERMIT_HEADLINE = "You authored these tests in the same session as the implementation, so you MAY edit test files \u2014 but ONLY to resolve a genuine contradiction between a test and this story's acceptance criteria (or between two acceptance criteria). NEVER weaken, delete, loosen, or skip a test merely to make it pass. See the test-edit guidance appended below.", SINGLE_SESSION_TEST_EDIT_POLICY = `
44898
45034
 
44899
45035
  ## Test-edit guidance (single-session implementer)
44900
45036
 
@@ -48676,7 +48812,7 @@ var init_pid_registry = __esm(() => {
48676
48812
  // src/session/manager-deps.ts
48677
48813
  import { randomUUID as randomUUID3 } from "crypto";
48678
48814
  import { mkdir as mkdir5 } from "fs/promises";
48679
- import { isAbsolute as isAbsolute10, join as join30, relative as relative11, sep as sep2 } from "path";
48815
+ import { isAbsolute as isAbsolute11, join as join30, relative as relative12, sep as sep2 } from "path";
48680
48816
  function resolveProjectDirFromScratchDir(scratchDir) {
48681
48817
  const marker = `${sep2}.nax${sep2}features${sep2}`;
48682
48818
  const markerIdx = scratchDir.lastIndexOf(marker);
@@ -48688,7 +48824,7 @@ function resolveProjectDirFromScratchDir(scratchDir) {
48688
48824
  return;
48689
48825
  }
48690
48826
  function toProjectRelativePath(projectDir, pathValue) {
48691
- const relativePath = isAbsolute10(pathValue) ? relative11(projectDir, pathValue) : pathValue;
48827
+ const relativePath = isAbsolute11(pathValue) ? relative12(projectDir, pathValue) : pathValue;
48692
48828
  return relativePath === "" ? "." : relativePath;
48693
48829
  }
48694
48830
  var _sessionManagerDeps;
@@ -50198,7 +50334,7 @@ var init_windsurf = __esm(() => {
50198
50334
 
50199
50335
  // src/context/generator.ts
50200
50336
  import { existsSync as existsSync9 } from "fs";
50201
- import { join as join33, relative as relative12 } from "path";
50337
+ import { join as join33, relative as relative13 } from "path";
50202
50338
  async function loadContextContent(options, config2) {
50203
50339
  if (!_generatorDeps.existsSync(options.contextPath)) {
50204
50340
  throw new Error(`Context file not found: ${options.contextPath}`);
@@ -50326,7 +50462,7 @@ async function discoverWorkspacePackages2(repoRoot) {
50326
50462
  }
50327
50463
  async function generateForPackage(packageDir, config2, dryRun = false, repoRoot) {
50328
50464
  const resolvedRepoRoot = repoRoot ?? packageDir;
50329
- const relativePkgPath = relative12(resolvedRepoRoot, packageDir);
50465
+ const relativePkgPath = relative13(resolvedRepoRoot, packageDir);
50330
50466
  const contextPath = join33(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
50331
50467
  if (!_generatorDeps.existsSync(contextPath)) {
50332
50468
  return [
@@ -54173,7 +54309,7 @@ var init_checks_blockers = __esm(() => {
54173
54309
 
54174
54310
  // src/precheck/checks-warnings.ts
54175
54311
  import { existsSync as existsSync14 } from "fs";
54176
- import { isAbsolute as isAbsolute11 } from "path";
54312
+ import { isAbsolute as isAbsolute12 } from "path";
54177
54313
  async function checkClaudeMdExists(workdir) {
54178
54314
  const claudeMdPath = `${workdir}/CLAUDE.md`;
54179
54315
  const passed = existsSync14(claudeMdPath);
@@ -54308,7 +54444,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
54308
54444
  }
54309
54445
  async function checkHomeEnvValid() {
54310
54446
  const home = process.env.HOME ?? "";
54311
- const passed = home !== "" && isAbsolute11(home);
54447
+ const passed = home !== "" && isAbsolute12(home);
54312
54448
  return {
54313
54449
  name: "home-env-valid",
54314
54450
  tier: "warning",
@@ -57012,6 +57148,79 @@ ${stderr}`;
57012
57148
  };
57013
57149
  });
57014
57150
 
57151
+ // src/pipeline/event-bus.ts
57152
+ class PipelineEventBus {
57153
+ subscribers = new Map;
57154
+ _pending = new Set;
57155
+ on(eventType, subscriber) {
57156
+ const list = this.subscribers.get(eventType) ?? [];
57157
+ list.push(subscriber);
57158
+ this.subscribers.set(eventType, list);
57159
+ return () => {
57160
+ const current = this.subscribers.get(eventType) ?? [];
57161
+ this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
57162
+ };
57163
+ }
57164
+ onAll(subscriber) {
57165
+ const list = this.subscribers.get("*") ?? [];
57166
+ list.push(subscriber);
57167
+ this.subscribers.set("*", list);
57168
+ return () => {
57169
+ const current = this.subscribers.get("*") ?? [];
57170
+ this.subscribers.set("*", current.filter((s) => s !== subscriber));
57171
+ };
57172
+ }
57173
+ emit(event) {
57174
+ const logger = getLogger();
57175
+ const specific = this.subscribers.get(event.type) ?? [];
57176
+ const all = this.subscribers.get("*") ?? [];
57177
+ const targets = [...specific, ...all];
57178
+ for (const sub of targets) {
57179
+ try {
57180
+ const result = sub(event);
57181
+ if (result instanceof Promise) {
57182
+ const tracked = result.catch((err) => {
57183
+ logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57184
+ });
57185
+ this._pending.add(tracked);
57186
+ tracked.finally(() => this._pending.delete(tracked));
57187
+ }
57188
+ } catch (err) {
57189
+ logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
57190
+ }
57191
+ }
57192
+ }
57193
+ async emitAsync(event) {
57194
+ const logger = getLogger();
57195
+ const specific = this.subscribers.get(event.type) ?? [];
57196
+ const all = this.subscribers.get("*") ?? [];
57197
+ const targets = [...specific, ...all];
57198
+ await Promise.allSettled(targets.map(async (sub) => {
57199
+ try {
57200
+ await sub(event);
57201
+ } catch (err) {
57202
+ logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57203
+ }
57204
+ }));
57205
+ }
57206
+ async drain() {
57207
+ if (this._pending.size === 0)
57208
+ return;
57209
+ await Promise.allSettled([...this._pending]);
57210
+ }
57211
+ clear() {
57212
+ this.subscribers.clear();
57213
+ }
57214
+ subscriberCount(eventType) {
57215
+ return (this.subscribers.get(eventType) ?? []).length;
57216
+ }
57217
+ }
57218
+ var pipelineEventBus;
57219
+ var init_event_bus = __esm(() => {
57220
+ init_logger2();
57221
+ pipelineEventBus = new PipelineEventBus;
57222
+ });
57223
+
57015
57224
  // src/pipeline/stages/acceptance-setup.ts
57016
57225
  var exports_acceptance_setup = {};
57017
57226
  __export(exports_acceptance_setup, {
@@ -57027,6 +57236,214 @@ function computeACFingerprint(criteria) {
57027
57236
  hasher.update(sorted);
57028
57237
  return `sha256:${hasher.digest("hex")}`;
57029
57238
  }
57239
+ async function runAcceptanceSetup(ctx, featureDir, phaseStartTime) {
57240
+ const language = ctx.config.project?.language;
57241
+ const testPathConfig = ctx.config.acceptance.testPath;
57242
+ const metaPath = path12.join(featureDir, "acceptance-meta.json");
57243
+ const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
57244
+ const featureName = ctx.prd.feature ?? ctx.prd.featureName;
57245
+ const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
57246
+ const nonFixStories = groups.flatMap((g) => g.stories);
57247
+ let totalCriteria = 0;
57248
+ let testableCount = 0;
57249
+ const fingerprint = computeACFingerprint(allCriteria);
57250
+ const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
57251
+ getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
57252
+ currentFingerprint: fingerprint,
57253
+ storedFingerprint: meta3?.acFingerprint ?? "none",
57254
+ match: meta3?.acFingerprint === fingerprint
57255
+ });
57256
+ let shouldGenerate = false;
57257
+ let regenerated = false;
57258
+ if (!meta3 || meta3.acFingerprint !== fingerprint) {
57259
+ if (!meta3) {
57260
+ getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
57261
+ } else {
57262
+ getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
57263
+ reason: "fingerprint mismatch",
57264
+ currentFingerprint: fingerprint,
57265
+ storedFingerprint: meta3.acFingerprint
57266
+ });
57267
+ }
57268
+ for (const { testPath } of groups) {
57269
+ if (await _acceptanceSetupDeps.fileExists(testPath)) {
57270
+ await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
57271
+ await _acceptanceSetupDeps.deleteFile(testPath);
57272
+ }
57273
+ }
57274
+ await _acceptanceSetupDeps.deleteSemanticVerdicts(featureDir);
57275
+ shouldGenerate = true;
57276
+ regenerated = true;
57277
+ } else {
57278
+ getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
57279
+ }
57280
+ if (shouldGenerate) {
57281
+ totalCriteria = allCriteria.length;
57282
+ let allRefinedCriteria;
57283
+ if (ctx.config.acceptance.refinement) {
57284
+ const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
57285
+ const results = new Array(nonFixStories.length);
57286
+ const executing = new Set;
57287
+ for (let i = 0;i < nonFixStories.length; i++) {
57288
+ const story = nonFixStories[i];
57289
+ const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
57290
+ criteria: story.acceptanceCriteria,
57291
+ codebaseContext: "",
57292
+ storyId: story.id,
57293
+ testStrategy: ctx.config.acceptance.testStrategy,
57294
+ testFramework: ctx.config.acceptance.testFramework,
57295
+ storyTitle: story.title,
57296
+ storyDescription: story.description
57297
+ }, story.id).then((refined) => {
57298
+ results[i] = refined;
57299
+ }).catch(() => {
57300
+ getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
57301
+ storyId: story.id
57302
+ });
57303
+ results[i] = story.acceptanceCriteria.map((c) => ({
57304
+ original: c,
57305
+ refined: c,
57306
+ testable: true,
57307
+ storyId: story.id
57308
+ }));
57309
+ }).finally(() => {
57310
+ executing.delete(task);
57311
+ });
57312
+ executing.add(task);
57313
+ if (executing.size >= maxConcurrency) {
57314
+ await Promise.race(executing);
57315
+ }
57316
+ }
57317
+ await Promise.all(executing);
57318
+ allRefinedCriteria = results.flat();
57319
+ } else {
57320
+ allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
57321
+ original: c,
57322
+ refined: c,
57323
+ testable: true,
57324
+ storyId: story.id
57325
+ })));
57326
+ }
57327
+ testableCount = allRefinedCriteria.filter((r) => r.testable).length;
57328
+ for (const group of groups) {
57329
+ const { testPath, packageDir } = group;
57330
+ const groupStoryIds = new Set(group.stories.map((s) => s.id));
57331
+ const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
57332
+ const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
57333
+ `);
57334
+ const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
57335
+ [FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
57336
+ const groupStoryId = group.stories[0]?.id;
57337
+ const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
57338
+ featureName: featureName ?? "",
57339
+ criteriaList,
57340
+ frameworkOverrideLine,
57341
+ targetTestFilePath: testPath,
57342
+ ..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
57343
+ }, groupStoryId);
57344
+ const testCode = genResult.testCode;
57345
+ if (testCode) {
57346
+ await _acceptanceSetupDeps.writeFile(testPath, testCode);
57347
+ } else {
57348
+ const skeletonCriteria = groupRefined.map((c, i) => ({
57349
+ id: `AC-${i + 1}`,
57350
+ text: c.refined,
57351
+ lineNumber: i + 1
57352
+ }));
57353
+ const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
57354
+ await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
57355
+ getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
57356
+ storyId: groupStoryId,
57357
+ testPath
57358
+ });
57359
+ }
57360
+ }
57361
+ if (allRefinedCriteria.length > 0) {
57362
+ const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
57363
+ acId: `AC-${i + 1}`,
57364
+ original: c.original,
57365
+ refined: c.refined,
57366
+ testable: c.testable,
57367
+ storyId: c.storyId
57368
+ })), null, 2);
57369
+ await _acceptanceSetupDeps.writeFile(path12.join(featureDir, "acceptance-refined.json"), refinedJsonContent);
57370
+ }
57371
+ const fingerprint2 = computeACFingerprint(allCriteria);
57372
+ await _acceptanceSetupDeps.writeMeta(metaPath, {
57373
+ generatedAt: new Date().toISOString(),
57374
+ acFingerprint: fingerprint2,
57375
+ storyCount: ctx.prd.userStories.length,
57376
+ acCount: totalCriteria,
57377
+ generator: "nax"
57378
+ });
57379
+ await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
57380
+ }
57381
+ const acceptanceTestPaths = [];
57382
+ for (const g of groups) {
57383
+ const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
57384
+ let groupConfig = ctx.config;
57385
+ if (relativeWorkdir && relativeWorkdir !== ".") {
57386
+ try {
57387
+ groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
57388
+ } catch {
57389
+ groupConfig = ctx.config;
57390
+ }
57391
+ }
57392
+ acceptanceTestPaths.push({
57393
+ testPath: g.testPath,
57394
+ packageDir: g.packageDir,
57395
+ testFramework: groupConfig.project?.testFramework,
57396
+ commandOverride: groupConfig.acceptance.command
57397
+ });
57398
+ }
57399
+ ctx.acceptanceTestPaths = acceptanceTestPaths;
57400
+ if (ctx.config.acceptance.redGate === false) {
57401
+ ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57402
+ pipelineEventBus.emit({
57403
+ type: "postrun:phase:completed",
57404
+ phase: "acceptance-setup",
57405
+ passed: true,
57406
+ durationMs: Date.now() - phaseStartTime,
57407
+ details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
57408
+ });
57409
+ return { action: "continue" };
57410
+ }
57411
+ let redFailCount = 0;
57412
+ for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
57413
+ const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
57414
+ getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
57415
+ cmd: runCmd.join(" "),
57416
+ packageDir
57417
+ });
57418
+ const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
57419
+ if (exitCode !== 0) {
57420
+ redFailCount++;
57421
+ }
57422
+ }
57423
+ if (redFailCount === 0) {
57424
+ ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57425
+ pipelineEventBus.emit({
57426
+ type: "postrun:phase:completed",
57427
+ phase: "acceptance-setup",
57428
+ passed: true,
57429
+ durationMs: Date.now() - phaseStartTime,
57430
+ details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
57431
+ });
57432
+ return {
57433
+ action: "skip",
57434
+ reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
57435
+ };
57436
+ }
57437
+ ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
57438
+ pipelineEventBus.emit({
57439
+ type: "postrun:phase:completed",
57440
+ phase: "acceptance-setup",
57441
+ passed: true,
57442
+ durationMs: Date.now() - phaseStartTime,
57443
+ details: { totalCriteria, testableCount, redFailCount, regenerated }
57444
+ });
57445
+ return { action: "continue" };
57446
+ }
57030
57447
  var _acceptanceSetupDeps, acceptanceSetupStage;
57031
57448
  var init_acceptance_setup = __esm(() => {
57032
57449
  init_acceptance2();
@@ -57035,6 +57452,7 @@ var init_acceptance_setup = __esm(() => {
57035
57452
  init_logger2();
57036
57453
  init_operations();
57037
57454
  init_git();
57455
+ init_event_bus();
57038
57456
  _acceptanceSetupDeps = {
57039
57457
  getAgent: (_name) => {
57040
57458
  return;
@@ -57134,189 +57552,19 @@ ${stderr}` };
57134
57552
  if (!ctx.featureDir) {
57135
57553
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
57136
57554
  }
57137
- const language = ctx.config.project?.language;
57138
- const testPathConfig = ctx.config.acceptance.testPath;
57139
- const metaPath = path12.join(ctx.featureDir, "acceptance-meta.json");
57140
- const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
57141
- const featureName = ctx.prd.feature ?? ctx.prd.featureName;
57142
- const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
57143
- const nonFixStories = groups.flatMap((g) => g.stories);
57144
- let totalCriteria = 0;
57145
- let testableCount = 0;
57146
- const fingerprint = computeACFingerprint(allCriteria);
57147
- const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
57148
- getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
57149
- currentFingerprint: fingerprint,
57150
- storedFingerprint: meta3?.acFingerprint ?? "none",
57151
- match: meta3?.acFingerprint === fingerprint
57152
- });
57153
- let shouldGenerate = false;
57154
- if (!meta3 || meta3.acFingerprint !== fingerprint) {
57155
- if (!meta3) {
57156
- getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
57157
- } else {
57158
- getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
57159
- reason: "fingerprint mismatch",
57160
- currentFingerprint: fingerprint,
57161
- storedFingerprint: meta3.acFingerprint
57162
- });
57163
- }
57164
- for (const { testPath } of groups) {
57165
- if (await _acceptanceSetupDeps.fileExists(testPath)) {
57166
- await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
57167
- await _acceptanceSetupDeps.deleteFile(testPath);
57168
- }
57169
- }
57170
- await _acceptanceSetupDeps.deleteSemanticVerdicts(ctx.featureDir);
57171
- shouldGenerate = true;
57172
- } else {
57173
- getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
57174
- }
57175
- if (shouldGenerate) {
57176
- totalCriteria = allCriteria.length;
57177
- let allRefinedCriteria;
57178
- if (ctx.config.acceptance.refinement) {
57179
- const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
57180
- const results = new Array(nonFixStories.length);
57181
- const executing = new Set;
57182
- for (let i = 0;i < nonFixStories.length; i++) {
57183
- const story = nonFixStories[i];
57184
- const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
57185
- criteria: story.acceptanceCriteria,
57186
- codebaseContext: "",
57187
- storyId: story.id,
57188
- testStrategy: ctx.config.acceptance.testStrategy,
57189
- testFramework: ctx.config.acceptance.testFramework,
57190
- storyTitle: story.title,
57191
- storyDescription: story.description
57192
- }, story.id).then((refined) => {
57193
- results[i] = refined;
57194
- }).catch(() => {
57195
- getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
57196
- storyId: story.id
57197
- });
57198
- results[i] = story.acceptanceCriteria.map((c) => ({
57199
- original: c,
57200
- refined: c,
57201
- testable: true,
57202
- storyId: story.id
57203
- }));
57204
- }).finally(() => {
57205
- executing.delete(task);
57206
- });
57207
- executing.add(task);
57208
- if (executing.size >= maxConcurrency) {
57209
- await Promise.race(executing);
57210
- }
57211
- }
57212
- await Promise.all(executing);
57213
- allRefinedCriteria = results.flat();
57214
- } else {
57215
- allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
57216
- original: c,
57217
- refined: c,
57218
- testable: true,
57219
- storyId: story.id
57220
- })));
57221
- }
57222
- testableCount = allRefinedCriteria.filter((r) => r.testable).length;
57223
- for (const group of groups) {
57224
- const { testPath, packageDir } = group;
57225
- const groupStoryIds = new Set(group.stories.map((s) => s.id));
57226
- const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
57227
- const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
57228
- `);
57229
- const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
57230
- [FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
57231
- const groupStoryId = group.stories[0]?.id;
57232
- const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
57233
- featureName: featureName ?? "",
57234
- criteriaList,
57235
- frameworkOverrideLine,
57236
- targetTestFilePath: testPath,
57237
- ..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
57238
- }, groupStoryId);
57239
- const testCode = genResult.testCode;
57240
- if (testCode) {
57241
- await _acceptanceSetupDeps.writeFile(testPath, testCode);
57242
- } else {
57243
- const skeletonCriteria = groupRefined.map((c, i) => ({
57244
- id: `AC-${i + 1}`,
57245
- text: c.refined,
57246
- lineNumber: i + 1
57247
- }));
57248
- const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
57249
- await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
57250
- getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
57251
- storyId: groupStoryId,
57252
- testPath
57253
- });
57254
- }
57255
- }
57256
- if (allRefinedCriteria.length > 0) {
57257
- const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
57258
- acId: `AC-${i + 1}`,
57259
- original: c.original,
57260
- refined: c.refined,
57261
- testable: c.testable,
57262
- storyId: c.storyId
57263
- })), null, 2);
57264
- await _acceptanceSetupDeps.writeFile(path12.join(ctx.featureDir, "acceptance-refined.json"), refinedJsonContent);
57265
- }
57266
- const fingerprint2 = computeACFingerprint(allCriteria);
57267
- await _acceptanceSetupDeps.writeMeta(metaPath, {
57268
- generatedAt: new Date().toISOString(),
57269
- acFingerprint: fingerprint2,
57270
- storyCount: ctx.prd.userStories.length,
57271
- acCount: totalCriteria,
57272
- generator: "nax"
57273
- });
57274
- await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
57275
- }
57276
- const acceptanceTestPaths = [];
57277
- for (const g of groups) {
57278
- const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
57279
- let groupConfig = ctx.config;
57280
- if (relativeWorkdir && relativeWorkdir !== ".") {
57281
- try {
57282
- groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
57283
- } catch {
57284
- groupConfig = ctx.config;
57285
- }
57286
- }
57287
- acceptanceTestPaths.push({
57288
- testPath: g.testPath,
57289
- packageDir: g.packageDir,
57290
- testFramework: groupConfig.project?.testFramework,
57291
- commandOverride: groupConfig.acceptance.command
57292
- });
57293
- }
57294
- ctx.acceptanceTestPaths = acceptanceTestPaths;
57295
- if (ctx.config.acceptance.redGate === false) {
57296
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57297
- return { action: "continue" };
57298
- }
57299
- let redFailCount = 0;
57300
- for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
57301
- const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
57302
- getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
57303
- cmd: runCmd.join(" "),
57304
- packageDir
57555
+ const phaseStartTime = Date.now();
57556
+ pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance-setup" });
57557
+ try {
57558
+ return await runAcceptanceSetup(ctx, ctx.featureDir, phaseStartTime);
57559
+ } catch (err) {
57560
+ pipelineEventBus.emit({
57561
+ type: "postrun:phase:completed",
57562
+ phase: "acceptance-setup",
57563
+ passed: false,
57564
+ durationMs: Date.now() - phaseStartTime
57305
57565
  });
57306
- const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
57307
- if (exitCode !== 0) {
57308
- redFailCount++;
57309
- }
57310
- }
57311
- if (redFailCount === 0) {
57312
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57313
- return {
57314
- action: "skip",
57315
- reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
57316
- };
57566
+ throw err;
57317
57567
  }
57318
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
57319
- return { action: "continue" };
57320
57568
  }
57321
57569
  };
57322
57570
  });
@@ -57462,79 +57710,6 @@ async function appendProgress(featureDir, storyId, status, message) {
57462
57710
  }
57463
57711
  var init_progress = () => {};
57464
57712
 
57465
- // src/pipeline/event-bus.ts
57466
- class PipelineEventBus {
57467
- subscribers = new Map;
57468
- _pending = new Set;
57469
- on(eventType, subscriber) {
57470
- const list = this.subscribers.get(eventType) ?? [];
57471
- list.push(subscriber);
57472
- this.subscribers.set(eventType, list);
57473
- return () => {
57474
- const current = this.subscribers.get(eventType) ?? [];
57475
- this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
57476
- };
57477
- }
57478
- onAll(subscriber) {
57479
- const list = this.subscribers.get("*") ?? [];
57480
- list.push(subscriber);
57481
- this.subscribers.set("*", list);
57482
- return () => {
57483
- const current = this.subscribers.get("*") ?? [];
57484
- this.subscribers.set("*", current.filter((s) => s !== subscriber));
57485
- };
57486
- }
57487
- emit(event) {
57488
- const logger = getLogger();
57489
- const specific = this.subscribers.get(event.type) ?? [];
57490
- const all = this.subscribers.get("*") ?? [];
57491
- const targets = [...specific, ...all];
57492
- for (const sub of targets) {
57493
- try {
57494
- const result = sub(event);
57495
- if (result instanceof Promise) {
57496
- const tracked = result.catch((err) => {
57497
- logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57498
- });
57499
- this._pending.add(tracked);
57500
- tracked.finally(() => this._pending.delete(tracked));
57501
- }
57502
- } catch (err) {
57503
- logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
57504
- }
57505
- }
57506
- }
57507
- async emitAsync(event) {
57508
- const logger = getLogger();
57509
- const specific = this.subscribers.get(event.type) ?? [];
57510
- const all = this.subscribers.get("*") ?? [];
57511
- const targets = [...specific, ...all];
57512
- await Promise.allSettled(targets.map(async (sub) => {
57513
- try {
57514
- await sub(event);
57515
- } catch (err) {
57516
- logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57517
- }
57518
- }));
57519
- }
57520
- async drain() {
57521
- if (this._pending.size === 0)
57522
- return;
57523
- await Promise.allSettled([...this._pending]);
57524
- }
57525
- clear() {
57526
- this.subscribers.clear();
57527
- }
57528
- subscriberCount(eventType) {
57529
- return (this.subscribers.get(eventType) ?? []).length;
57530
- }
57531
- }
57532
- var pipelineEventBus;
57533
- var init_event_bus = __esm(() => {
57534
- init_logger2();
57535
- pipelineEventBus = new PipelineEventBus;
57536
- });
57537
-
57538
57713
  // src/pipeline/stages/completion.ts
57539
57714
  async function getDiffText(workdir, baseRef) {
57540
57715
  if (!baseRef)
@@ -58326,11 +58501,11 @@ var init_rollback = __esm(() => {
58326
58501
  });
58327
58502
 
58328
58503
  // src/utils/paths.ts
58329
- import { join as join49, relative as relative13, sep as sep4 } from "path";
58504
+ import { join as join49, relative as relative14, sep as sep4 } from "path";
58330
58505
  function packageDirRelative(projectDir, workdir) {
58331
58506
  if (!projectDir || !workdir || workdir === projectDir)
58332
58507
  return;
58333
- const rel = relative13(projectDir, workdir);
58508
+ const rel = relative14(projectDir, workdir);
58334
58509
  if (rel === ".." || rel.startsWith(`..${sep4}`))
58335
58510
  return;
58336
58511
  return rel && rel !== "." ? rel : undefined;
@@ -58346,6 +58521,9 @@ var init_paths3 = __esm(() => {
58346
58521
  });
58347
58522
 
58348
58523
  // src/execution/non-blocking-fix.ts
58524
+ function actionableAdvisoryFindings(findings) {
58525
+ return findings.filter((f) => f.actionRequired !== false);
58526
+ }
58349
58527
  function shouldRunNonBlockingFix(cfg, advisoryCount) {
58350
58528
  return cfg?.enabled === true && advisoryCount > 0;
58351
58529
  }
@@ -58418,10 +58596,9 @@ async function runNonBlockingFix(args, overrides = {}) {
58418
58596
  exhausted = true;
58419
58597
  }
58420
58598
  if (!exhausted) {
58421
- if (args.keptTreeRegressed?.()) {
58422
- logger?.info("non-blocking-fix", "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", {
58423
- storyId: args.storyId
58424
- });
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);
58425
58602
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58426
58603
  }
58427
58604
  const cap = args.cfg.sourceDiffCap;
@@ -58449,8 +58626,23 @@ async function runNonBlockingFix(args, overrides = {}) {
58449
58626
  logger?.info("non-blocking-fix", "best-effort fix kept", { storyId: args.storyId });
58450
58627
  return { ran: true, kept: true, restored: false };
58451
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
+ }
58452
58633
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58453
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
+ }
58454
58646
  async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger) {
58455
58647
  await _deps.rollbackToRef(args.workdir, restoreRef);
58456
58648
  for (const key of Object.keys(args.phaseOutputs))
@@ -58464,7 +58656,7 @@ async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot,
58464
58656
  });
58465
58657
  return { ran: true, kept: false, restored: true };
58466
58658
  }
58467
- var REVIEW_PHASE_KINDS, _nonBlockingFixDeps, DEFAULT_DEPS;
58659
+ var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
58468
58660
  var init_non_blocking_fix = __esm(() => {
58469
58661
  init_logger2();
58470
58662
  init_rollback();
@@ -58603,17 +58795,41 @@ function gateFailureKeys(gateOutput) {
58603
58795
  continue;
58604
58796
  if (f.category === "flaky-test")
58605
58797
  continue;
58606
- keys.add(`${f.file ?? ""}::${f.rule ?? ""}`);
58798
+ keys.add(gateFindingKey(f));
58607
58799
  }
58608
58800
  return keys;
58609
58801
  }
58610
- function gateRegressedAfterRectification(finalGateOutput, baselineKeys, gateName, storyId) {
58611
- if (phasePassed(gateName, finalGateOutput, storyId))
58802
+ function gateFindingKey(finding) {
58803
+ return `${finding.file ?? ""}::${finding.rule ?? ""}`;
58804
+ }
58805
+ function isQuarantinedFlake(finding, quarantineMemo) {
58806
+ if (finding.source !== "test-runner")
58612
58807
  return false;
58613
- const finalKeys = gateFailureKeys(finalGateOutput);
58614
- const hasNewStructuredKey = [...finalKeys].some((k) => !baselineKeys.has(k));
58615
- const isKeylessFailure = finalKeys.size === 0 || finalKeys.has(KEYLESS_GATE_FAILURE_KEY);
58616
- return hasNewStructuredKey || isKeylessFailure;
58808
+ return quarantineMemo?.has(gateFindingKey(finding)) === true;
58809
+ }
58810
+ function describeGateRegression(input) {
58811
+ const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
58812
+ const notRegressed = {
58813
+ regressed: false,
58814
+ regressedKeys: [],
58815
+ memoExcludedKeys: [],
58816
+ baselineKeySize: baselineKeys.size,
58817
+ keyless: false
58818
+ };
58819
+ if (gateName === undefined || phasePassed(gateName, gateOutput, storyId))
58820
+ return notRegressed;
58821
+ const allKeys = gateFailureKeys(gateOutput);
58822
+ const memoExcludedKeys = quarantineMemo ? [...allKeys].filter((k) => quarantineMemo.has(k)) : [];
58823
+ const excluded = new Set(memoExcludedKeys);
58824
+ const regressedKeys = [...allKeys].filter((k) => !baselineKeys.has(k) && !excluded.has(k));
58825
+ const keyless = allKeys.size === 0 || allKeys.has(KEYLESS_GATE_FAILURE_KEY);
58826
+ return {
58827
+ regressed: regressedKeys.length > 0 || keyless,
58828
+ regressedKeys,
58829
+ memoExcludedKeys,
58830
+ baselineKeySize: baselineKeys.size,
58831
+ keyless
58832
+ };
58617
58833
  }
58618
58834
  function phasesToRevalidate(strategiesRun, allPhases) {
58619
58835
  if (!strategiesRun || strategiesRun.length === 0)
@@ -58760,6 +58976,9 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
58760
58976
  const data = { storyId, phase: opName, durationMs };
58761
58977
  if (findingsCount !== undefined)
58762
58978
  data.findingsCount = findingsCount;
58979
+ const identities = extractPhaseFindings(output).slice(0, MAX_LOGGED_FINDING_IDENTITIES).map((f) => `${f.file ?? ""}::${f.rule ?? ""}`);
58980
+ if (identities.length > 0)
58981
+ data.findingIdentities = identities;
58763
58982
  if (status !== undefined)
58764
58983
  data.status = status;
58765
58984
  if (typeof r.failureCategory === "string")
@@ -58791,8 +59010,10 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
58791
59010
  logger?.warn("story-orchestrator", message, data);
58792
59011
  }
58793
59012
  }
59013
+ var MAX_LOGGED_FINDING_IDENTITIES = 10;
58794
59014
  var init_story_orchestrator_logging = __esm(() => {
58795
59015
  init_logger2();
59016
+ init_phase_eval();
58796
59017
  });
58797
59018
  // src/verification/flake-baseline-diff.ts
58798
59019
  async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
@@ -59065,10 +59286,13 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59065
59286
  const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
59066
59287
  let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
59067
59288
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
59289
+ let advIterationBefore = 0;
59068
59290
  if (opName === "adversarial-review" && ctx.storyId) {
59291
+ const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59292
+ advIterationBefore = priorIterations.length;
59069
59293
  dispatchInput = {
59070
59294
  ...dispatchInput,
59071
- priorAdversarialIterations: getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId)
59295
+ priorAdversarialIterations: priorIterations
59072
59296
  };
59073
59297
  }
59074
59298
  if (isTddPhase) {
@@ -59085,6 +59309,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59085
59309
  }
59086
59310
  const phaseStartedAt = Date.now();
59087
59311
  const scope = ctx.runtime.costAggregator.openScope();
59312
+ let outcome = "passed";
59088
59313
  try {
59089
59314
  const output = await _storyOrchestratorDeps.callOp({ ...ctx, scopeId: scope.scopeId }, slot.op, dispatchInput);
59090
59315
  phaseOutputs[opName] = output;
@@ -59098,6 +59323,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59098
59323
  }
59099
59324
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
59100
59325
  logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
59326
+ outcome = derivePhaseOutcome(output);
59101
59327
  if (isTddPhase) {
59102
59328
  const durationMs = Date.now() - phaseStartedAt;
59103
59329
  logger?.info("tdd", `Session complete: ${opName}`, {
@@ -59127,10 +59353,94 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59127
59353
  }
59128
59354
  }
59129
59355
  return output;
59356
+ } catch (err) {
59357
+ outcome = "error";
59358
+ throw err;
59130
59359
  } finally {
59131
- phaseCosts[opName] = (phaseCosts[opName] ?? 0) + scope.snapshot().totalCostUsd;
59360
+ const snapshot = scope.snapshot();
59361
+ phaseCosts[opName] = (phaseCosts[opName] ?? 0) + snapshot.totalCostUsd;
59132
59362
  scope.close();
59363
+ if (ctx.storyId) {
59364
+ const phaseDetails = buildPhaseDetails(opName, phaseOutputs[opName], isThreeSession, ctx.packageView.config, advIterationBefore, ctx.fixStrategy);
59365
+ const event = {
59366
+ type: "story:phase:completed",
59367
+ storyId: ctx.storyId,
59368
+ phase: opName,
59369
+ outcome,
59370
+ durationMs: Date.now() - phaseStartedAt,
59371
+ costUsd: snapshot.totalCostUsd,
59372
+ ...ctx.phaseTelemetry ? {
59373
+ tier: ctx.phaseTelemetry.tier,
59374
+ testStrategy: ctx.phaseTelemetry.testStrategy,
59375
+ sessionModel: ctx.phaseTelemetry.sessionModel
59376
+ } : {},
59377
+ ...phaseDetails !== undefined ? { details: phaseDetails } : {}
59378
+ };
59379
+ pipelineEventBus.emit(event);
59380
+ }
59381
+ }
59382
+ }
59383
+ function buildPhaseDetails(opName, output, isThreeSession, config2, advIterationBefore, fixStrategy) {
59384
+ if (fixStrategy) {
59385
+ return { kind: "fix", strategy: fixStrategy.name, findingsBefore: fixStrategy.findingsBefore };
59386
+ }
59387
+ if (output === null || typeof output !== "object")
59388
+ return;
59389
+ if (opName === "adversarial-review") {
59390
+ const adv = output;
59391
+ const findings = adv?.normalizedFindings ?? [];
59392
+ const threshold = adv?.blockingThreshold ?? "error";
59393
+ const bySeverity = Object.fromEntries(ALL_FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]));
59394
+ const blockingCount = findings.filter((f) => isBlockingSeverity(f.severity, threshold)).length;
59395
+ const advisoryCount = findings.length - blockingCount;
59396
+ return {
59397
+ kind: "review",
59398
+ reviewer: "adversarial",
59399
+ iteration: advIterationBefore,
59400
+ bySeverity,
59401
+ blockingCount,
59402
+ advisoryCount,
59403
+ ...config2.reporters?.otel?.detail === "verbose" ? {
59404
+ items: findings.map((f) => ({
59405
+ message: f.message,
59406
+ severity: f.severity,
59407
+ ...f.rule ? { rule: f.rule } : {},
59408
+ ...f.file ? { file: f.file } : {}
59409
+ }))
59410
+ } : {}
59411
+ };
59412
+ }
59413
+ if (opName === "implementer") {
59414
+ const impl = output;
59415
+ const filesChanged = (impl?.filesChanged ?? []).length;
59416
+ if (isThreeSession) {
59417
+ return { kind: "authoring", role: "implementer", filesChanged, isolationPassed: impl?.isolation?.passed };
59418
+ }
59419
+ return { kind: "authoring", role: "implementer", filesChanged };
59420
+ }
59421
+ if (opName === "test-writer") {
59422
+ const tw = output;
59423
+ return { kind: "authoring", role: "test-writer", filesChanged: (tw?.filesChanged ?? []).length };
59424
+ }
59425
+ if (opName === "full-suite-gate") {
59426
+ const gate = output;
59427
+ return { kind: "gate", gate: "full-suite", failureCount: gate?.failureCount ?? 0 };
59428
+ }
59429
+ if (opName === "verifier" || opName === "verify-scoped") {
59430
+ const verdict = output;
59431
+ return { kind: "verdict", role: opName, passed: verdict.passed ?? false, failureCount: verdict.failureCount ?? 0 };
59133
59432
  }
59433
+ return;
59434
+ }
59435
+ function derivePhaseOutcome(output) {
59436
+ const built = buildPhaseOutcomeLogData(undefined, "", output, 0);
59437
+ if (!built)
59438
+ return "passed";
59439
+ if (built.success)
59440
+ return "passed";
59441
+ if (built.data.status === "skipped")
59442
+ return "skipped";
59443
+ return "failed";
59134
59444
  }
59135
59445
  function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
59136
59446
  if (!enabled)
@@ -59155,7 +59465,7 @@ function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
59155
59465
  }
59156
59466
  }));
59157
59467
  }
59158
- var _storyOrchestratorDeps;
59468
+ var _storyOrchestratorDeps, ALL_FINDING_SEVERITIES;
59159
59469
  var init_run_phase = __esm(() => {
59160
59470
  init_findings();
59161
59471
  init_logger2();
@@ -59185,12 +59495,23 @@ var init_run_phase = __esm(() => {
59185
59495
  },
59186
59496
  loadCheckpoints: async (_featureDir) => new Map
59187
59497
  };
59498
+ ALL_FINDING_SEVERITIES = [
59499
+ "critical",
59500
+ "error",
59501
+ "warning",
59502
+ "info",
59503
+ "low",
59504
+ "unverifiable"
59505
+ ];
59188
59506
  });
59189
59507
 
59190
59508
  // src/execution/story-orchestrator/rectification.ts
59191
- function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
59509
+ function shouldSkipPhaseForRectification(input) {
59510
+ const { phase, state, phaseOutputs, nbfPath } = input;
59192
59511
  if (phase.kind !== "full-suite-gate")
59193
59512
  return false;
59513
+ if (nbfPath)
59514
+ return false;
59194
59515
  const verifierName = state.verifier?.slot.op.name;
59195
59516
  if (!verifierName)
59196
59517
  return false;
@@ -59199,7 +59520,7 @@ function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
59199
59520
  function gatherRectificationFindings(phaseOutputs, phases, state) {
59200
59521
  const findings = [];
59201
59522
  for (const phase of phases) {
59202
- if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
59523
+ if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs }))
59203
59524
  continue;
59204
59525
  for (const f of extractPhaseFindings(phaseOutputs[phase.slot.op.name])) {
59205
59526
  if (f.category === "flaky-test")
@@ -59280,7 +59601,9 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59280
59601
  return {};
59281
59602
  }
59282
59603
  let initialFindings;
59604
+ let nbfPath = false;
59283
59605
  if (overrides?.initialFindings) {
59606
+ nbfPath = true;
59284
59607
  initialFindings = [...overrides.initialFindings];
59285
59608
  } else {
59286
59609
  const gateName = state.fullSuiteGate?.slot.op.name;
@@ -59330,10 +59653,11 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
59330
59653
  let shortCircuited = false;
59331
59654
  for (const phase of phases) {
59332
59655
  await runPhase(ctx, phase.slot, phaseCosts, phaseOutputs);
59333
- if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
59656
+ if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs, nbfPath }))
59334
59657
  continue;
59335
59658
  const output = phaseOutputs[phase.slot.op.name];
59336
- findings.push(...extractPhaseFindings(output));
59659
+ const phaseFindings = extractPhaseFindings(output);
59660
+ findings.push(...nbfPath ? phaseFindings.filter((f) => !isQuarantinedFlake(f, ctx.runtime.quarantineMemo)) : phaseFindings);
59337
59661
  if (!phasePassed(phase.slot.op.name, output, ctx.storyId)) {
59338
59662
  getSafeLogger()?.warn("story-orchestrator", "Short-circuiting revalidation on phase failure", {
59339
59663
  storyId: ctx.storyId,
@@ -59408,6 +59732,15 @@ class ExecutionPlan {
59408
59732
  this.state = state;
59409
59733
  this.isThreeSession = isThreeSession;
59410
59734
  }
59735
+ describeGateRegressionNow(phaseOutputs, gateName, baselineKeys) {
59736
+ return describeGateRegression({
59737
+ gateOutput: gateName === undefined ? undefined : phaseOutputs[gateName],
59738
+ baselineKeys,
59739
+ gateName,
59740
+ storyId: this.ctx.storyId,
59741
+ quarantineMemo: this.ctx.runtime.quarantineMemo
59742
+ });
59743
+ }
59411
59744
  phaseNames() {
59412
59745
  const names = collectOrderedPhases(this.state).map((p) => p.slot.op.name);
59413
59746
  if (this.state.rectification) {
@@ -59544,7 +59877,7 @@ class ExecutionPlan {
59544
59877
  const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
59545
59878
  const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
59546
59879
  const advisoryOut = phaseOutputs["adversarial-review"];
59547
- const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
59880
+ const advisoryFindings = actionableAdvisoryFindings(advisoryOut?.advisoryFindings ?? []);
59548
59881
  if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
59549
59882
  await _storyOrchestratorDeps.runNonBlockingFix({
59550
59883
  workdir: this.ctx.packageDir,
@@ -59561,7 +59894,7 @@ class ExecutionPlan {
59561
59894
  maxAttempts,
59562
59895
  postValidate: this.state.nonBlockingFixPostValidate
59563
59896
  }),
59564
- keptTreeRegressed: () => gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId)
59897
+ keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
59565
59898
  }, {
59566
59899
  measureSourceDiff: createMeasureSourceDiff({
59567
59900
  config: this.ctx.runtime.configLoader.current(),
@@ -59572,7 +59905,7 @@ class ExecutionPlan {
59572
59905
  }
59573
59906
  const verifierName = this.state.verifier?.slot.op.name;
59574
59907
  const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
59575
- const gateRegressedDuringRect = gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId);
59908
+ const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
59576
59909
  const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
59577
59910
  if (verifierExplicitlyPassed && gateRegressedDuringRect) {
59578
59911
  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 });
@@ -59792,8 +60125,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59792
60125
  if (inputs.adversarialReview) {
59793
60126
  builder.addAdversarialReview(inputs.adversarialReview);
59794
60127
  }
59795
- const packageDir = join50(ctx.packageDir, story.workdir ?? "");
59796
- const resolvedTestPatterns = await resolveTestFilePatterns(config2, ctx.packageDir, story.workdir);
60128
+ const repoRoot = ctx.packageDir;
60129
+ const packageDir = join50(repoRoot, story.workdir ?? "");
60130
+ const resolvedTestPatterns = await resolveTestFilePatterns(config2, repoRoot, story.workdir);
59797
60131
  if (shouldRunRectification(config2) && inputs.rectification) {
59798
60132
  const sink = makeDeclarationSink();
59799
60133
  const strategies = [];
@@ -59829,7 +60163,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59829
60163
  files: h.files,
59830
60164
  reasonDetail: h.reasonDetail
59831
60165
  }));
59832
- const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
60166
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
60167
+ repoRoot
60168
+ });
59833
60169
  sink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
59834
60170
  const allDeclarations = [...sink.testEdits, ...valid];
59835
60171
  sink.testEdits = [];
@@ -59888,7 +60224,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59888
60224
  files: h.files,
59889
60225
  reasonDetail: h.reasonDetail
59890
60226
  }));
59891
- const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
60227
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
60228
+ repoRoot
60229
+ });
59892
60230
  nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
59893
60231
  const allDeclarations = [...nbSink.testEdits, ...valid];
59894
60232
  nbSink.testEdits = [];
@@ -60827,7 +61165,12 @@ var init_execution = __esm(() => {
60827
61165
  featureName: ctx.prd.feature,
60828
61166
  story: ctx.story,
60829
61167
  ...ctx.featureDir ? { featureDir: ctx.featureDir } : {},
60830
- ...interactionBridge ? { interactionBridge } : {}
61168
+ ...interactionBridge ? { interactionBridge } : {},
61169
+ phaseTelemetry: {
61170
+ testStrategy: ctx.routing.testStrategy,
61171
+ sessionModel: isThreeSessionStrategy(ctx.routing.testStrategy) ? "three-session" : "single-session",
61172
+ tier: effectiveTier
61173
+ }
60831
61174
  };
60832
61175
  let capturedTokenUsage;
60833
61176
  let capturedResponse = "";
@@ -61590,6 +61933,196 @@ var init_stages = __esm(() => {
61590
61933
  preRunPipeline = [acceptanceSetupStage];
61591
61934
  });
61592
61935
 
61936
+ // src/pipeline/subscribers/reporters.ts
61937
+ async function fanOutReporters(reporters, hook, invoke) {
61938
+ const logger = getSafeLogger();
61939
+ for (const reporter of reporters) {
61940
+ try {
61941
+ await invoke(reporter);
61942
+ } catch (err) {
61943
+ try {
61944
+ logger?.warn("plugins", `Reporter '${reporter.name}' ${hook} failed`, { error: err });
61945
+ } catch {}
61946
+ }
61947
+ }
61948
+ }
61949
+ function wireReporters(bus, pluginRegistry, runId, startTime) {
61950
+ const logger = getSafeLogger();
61951
+ const safe = (name, fn) => {
61952
+ return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
61953
+ };
61954
+ const unsubs = [];
61955
+ const phaseStart = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseStart", (reporter) => reporter.onPhaseStart?.(event));
61956
+ const phaseComplete = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseComplete", (reporter) => reporter.onPhaseComplete?.(event));
61957
+ unsubs.push(bus.on("story:step", (ev) => phaseStart({
61958
+ runId,
61959
+ scope: "story",
61960
+ storyId: ev.storyId,
61961
+ phase: ev.step,
61962
+ startTime: new Date().toISOString()
61963
+ })), bus.on("story:phase:completed", (phaseEvent) => phaseComplete({
61964
+ runId,
61965
+ scope: "story",
61966
+ storyId: phaseEvent.storyId,
61967
+ phase: phaseEvent.phase,
61968
+ outcome: phaseEvent.outcome,
61969
+ durationMs: phaseEvent.durationMs,
61970
+ costUsd: phaseEvent.costUsd,
61971
+ tier: phaseEvent.tier,
61972
+ testStrategy: phaseEvent.testStrategy,
61973
+ sessionModel: phaseEvent.sessionModel,
61974
+ details: phaseEvent.details
61975
+ })), bus.on("postrun:phase:started", (ev) => phaseStart({
61976
+ runId,
61977
+ scope: "run",
61978
+ phase: ev.phase,
61979
+ startTime: new Date().toISOString()
61980
+ })), bus.on("postrun:phase:completed", (phaseEvent) => phaseComplete({
61981
+ runId,
61982
+ scope: "run",
61983
+ phase: phaseEvent.phase,
61984
+ outcome: phaseEvent.passed ? "passed" : "failed",
61985
+ durationMs: phaseEvent.durationMs ?? 0,
61986
+ costUsd: phaseEvent.costUsd,
61987
+ details: phaseEvent.details
61988
+ })));
61989
+ unsubs.push(bus.on("run:started", (ev) => {
61990
+ return safe("onRunStart", async () => {
61991
+ const reporters = pluginRegistry.getReporters();
61992
+ for (const r of reporters) {
61993
+ if (r.onRunStart) {
61994
+ try {
61995
+ await r.onRunStart({
61996
+ runId,
61997
+ feature: ev.feature,
61998
+ totalStories: ev.totalStories,
61999
+ startTime: new Date(startTime).toISOString()
62000
+ });
62001
+ } catch (err) {
62002
+ logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
62003
+ }
62004
+ }
62005
+ }
62006
+ });
62007
+ }));
62008
+ unsubs.push(bus.on("story:completed", (ev) => {
62009
+ return safe("onStoryComplete(completed)", async () => {
62010
+ const reporters = pluginRegistry.getReporters();
62011
+ for (const r of reporters) {
62012
+ if (r.onStoryComplete) {
62013
+ try {
62014
+ await r.onStoryComplete({
62015
+ runId,
62016
+ storyId: ev.storyId,
62017
+ status: "completed",
62018
+ runElapsedMs: ev.runElapsedMs,
62019
+ cost: ev.cost ?? 0,
62020
+ tier: ev.modelTier ?? "balanced",
62021
+ testStrategy: ev.testStrategy ?? "test-after"
62022
+ });
62023
+ } catch (err) {
62024
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
62025
+ }
62026
+ }
62027
+ }
62028
+ });
62029
+ }));
62030
+ unsubs.push(bus.on("story:failed", (ev) => {
62031
+ return safe("onStoryComplete(failed)", async () => {
62032
+ const reporters = pluginRegistry.getReporters();
62033
+ for (const r of reporters) {
62034
+ if (r.onStoryComplete) {
62035
+ try {
62036
+ await r.onStoryComplete({
62037
+ runId,
62038
+ storyId: ev.storyId,
62039
+ status: "failed",
62040
+ runElapsedMs: Date.now() - startTime,
62041
+ cost: 0,
62042
+ tier: "balanced",
62043
+ testStrategy: "test-after"
62044
+ });
62045
+ } catch (err) {
62046
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
62047
+ }
62048
+ }
62049
+ }
62050
+ });
62051
+ }));
62052
+ unsubs.push(bus.on("story:paused", (ev) => {
62053
+ return safe("onStoryComplete(paused)", async () => {
62054
+ const reporters = pluginRegistry.getReporters();
62055
+ for (const r of reporters) {
62056
+ if (r.onStoryComplete) {
62057
+ try {
62058
+ await r.onStoryComplete({
62059
+ runId,
62060
+ storyId: ev.storyId,
62061
+ status: "paused",
62062
+ runElapsedMs: Date.now() - startTime,
62063
+ cost: 0,
62064
+ tier: "balanced",
62065
+ testStrategy: "test-after"
62066
+ });
62067
+ } catch (err) {
62068
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
62069
+ }
62070
+ }
62071
+ }
62072
+ });
62073
+ }));
62074
+ unsubs.push(bus.on("story:escalated", (ev) => {
62075
+ return safe("onEscalation", async () => {
62076
+ const reporters = pluginRegistry.getReporters();
62077
+ for (const r of reporters) {
62078
+ if (r.onEscalation) {
62079
+ try {
62080
+ await r.onEscalation({
62081
+ runId,
62082
+ storyId: ev.storyId,
62083
+ fromTier: ev.fromTier,
62084
+ toTier: ev.toTier
62085
+ });
62086
+ } catch (err) {
62087
+ logger?.warn("plugins", `Reporter '${r.name}' onEscalation failed`, { error: err });
62088
+ }
62089
+ }
62090
+ }
62091
+ });
62092
+ }));
62093
+ unsubs.push(bus.on("run:completed", (ev) => {
62094
+ return safe("onRunEnd", async () => {
62095
+ const reporters = pluginRegistry.getReporters();
62096
+ for (const r of reporters) {
62097
+ if (r.onRunEnd) {
62098
+ try {
62099
+ await r.onRunEnd({
62100
+ runId,
62101
+ totalDurationMs: Date.now() - startTime,
62102
+ totalCost: ev.totalCost ?? 0,
62103
+ storySummary: {
62104
+ completed: ev.passedStories,
62105
+ failed: ev.failedStories,
62106
+ skipped: ev.skippedStories,
62107
+ paused: ev.pausedStories
62108
+ }
62109
+ });
62110
+ } catch (err) {
62111
+ logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
62112
+ }
62113
+ }
62114
+ }
62115
+ });
62116
+ }));
62117
+ return () => {
62118
+ for (const u of unsubs)
62119
+ u();
62120
+ };
62121
+ }
62122
+ var init_reporters = __esm(() => {
62123
+ init_logger2();
62124
+ });
62125
+
61593
62126
  // src/pipeline/index.ts
61594
62127
  var init_pipeline = __esm(() => {
61595
62128
  init_runner4();
@@ -61597,7 +62130,9 @@ var init_pipeline = __esm(() => {
61597
62130
  init_stages();
61598
62131
  init_queue_check();
61599
62132
  init_execution_helpers();
62133
+ init_acceptance_setup();
61600
62134
  init_event_bus();
62135
+ init_reporters();
61601
62136
  });
61602
62137
 
61603
62138
  // src/cli/prompts-shared.ts
@@ -64337,21 +64872,55 @@ var init_config2 = __esm(() => {
64337
64872
  });
64338
64873
 
64339
64874
  // src/plugins/builtin/nax-finish/telegram.ts
64875
+ function buildEscalationMessage(feature, reason, findings) {
64876
+ const head = `nax-finish escalated ${feature}: ${reason}`;
64877
+ if (findings.length === 0)
64878
+ return head;
64879
+ const footerReserve = `
64880
+ \u2026and ${findings.length} more`.length;
64881
+ const lines = [];
64882
+ let used = head.length;
64883
+ for (const f of findings) {
64884
+ const title = f.title.length > MAX_FINDING_TITLE_CHARS ? `${f.title.slice(0, MAX_FINDING_TITLE_CHARS)}\u2026` : f.title;
64885
+ const line = `
64886
+ - [${f.severity}] ${title}`;
64887
+ if (used + line.length + footerReserve > TELEGRAM_MAX_MESSAGE_CHARS)
64888
+ break;
64889
+ lines.push(line);
64890
+ used += line.length;
64891
+ }
64892
+ const omitted = findings.length - lines.length;
64893
+ return `${head}${lines.join("")}${omitted > 0 ? `
64894
+ \u2026and ${omitted} more` : ""}`;
64895
+ }
64340
64896
  async function sendTelegramNotify(cfg, text) {
64341
64897
  const res = await _telegramDeps.fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
64342
64898
  method: "POST",
64343
64899
  headers: { "content-type": "application/json" },
64344
- body: JSON.stringify({ chat_id: cfg.chatId, text, parse_mode: "Markdown" })
64900
+ body: JSON.stringify({ chat_id: cfg.chatId, text })
64345
64901
  });
64346
64902
  return res.ok;
64347
64903
  }
64348
- var _telegramDeps;
64904
+ var TELEGRAM_MAX_MESSAGE_CHARS = 4096, MAX_FINDING_TITLE_CHARS = 120, _telegramDeps;
64349
64905
  var init_telegram2 = __esm(() => {
64350
64906
  _telegramDeps = { fetch: (...a) => fetch(...a) };
64351
64907
  });
64352
64908
 
64353
64909
  // src/plugins/builtin/nax-finish/index.ts
64354
64910
  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
+ }
64355
64924
  async function defaultRun2(cmd, opts) {
64356
64925
  const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
64357
64926
  let timedOut = false;
@@ -64425,7 +64994,7 @@ function buildFlowEnv(cfg) {
64425
64994
  env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
64426
64995
  return env2;
64427
64996
  }
64428
- var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
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;
64429
64998
  var init_nax_finish = __esm(() => {
64430
64999
  init_config2();
64431
65000
  init_telegram2();
@@ -64476,10 +65045,43 @@ var init_nax_finish = __esm(() => {
64476
65045
  });
64477
65046
  const result = await _naxFinishDeps.readResult(ctx.workdir);
64478
65047
  if (!result) {
64479
- return { success: res.exitCode === 0, message: `nax-finish flow exited ${res.exitCode} (no result file)` };
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
+ };
64480
65058
  }
64481
- if (result.status === "escalated" && escalateTelegram && creds) {
64482
- await _naxFinishDeps.notify(creds, `nax-finish escalated *${result.feature}*: ${result.escalationReason ?? ""}`);
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
+ }
64483
65085
  }
64484
65086
  return { success: true, message: `nax-finish: ${result.status}`, url: result.url };
64485
65087
  } catch (err) {
@@ -64554,28 +65156,119 @@ var init_reporter_shared = __esm(() => {
64554
65156
  init_post_json();
64555
65157
  });
64556
65158
 
64557
- // src/plugins/builtin/otel-reporter/ids.ts
64558
- function randomHex(bytes) {
64559
- const arr = new Uint8Array(bytes);
64560
- crypto.getRandomValues(arr);
64561
- let out = "";
64562
- for (const b of arr)
64563
- out += b.toString(16).padStart(2, "0");
64564
- return out;
65159
+ // src/plugins/builtin/otel-reporter/batch-queue.ts
65160
+ function createBatchQueue(opts) {
65161
+ const { maxBatchSize, flushIntervalMs, maxQueueSize, send } = opts;
65162
+ let queue = [];
65163
+ let dropCount = 0;
65164
+ let overflowing = false;
65165
+ let tornDown = false;
65166
+ let timer;
65167
+ const armTimer = () => {
65168
+ timer = setTimeout(() => {
65169
+ doFlush();
65170
+ if (!tornDown)
65171
+ armTimer();
65172
+ }, flushIntervalMs);
65173
+ };
65174
+ const sendWithRetry = async (batch) => {
65175
+ for (let attempt = 0;attempt < RETRY_ATTEMPTS; attempt++) {
65176
+ try {
65177
+ const ok = await send(batch);
65178
+ if (ok)
65179
+ return;
65180
+ } catch (err) {
65181
+ getSafeLogger()?.warn(STAGE, "Batch export threw", { error: err instanceof Error ? err.message : String(err) });
65182
+ }
65183
+ }
65184
+ };
65185
+ const doFlush = () => {
65186
+ if (tornDown || queue.length === 0)
65187
+ return Promise.resolve();
65188
+ const batch = queue;
65189
+ queue = [];
65190
+ sendWithRetry(batch);
65191
+ return Promise.resolve();
65192
+ };
65193
+ const enqueue = (item) => {
65194
+ queue.push(item);
65195
+ if (queue.length > maxQueueSize) {
65196
+ queue.shift();
65197
+ dropCount++;
65198
+ if (!overflowing) {
65199
+ overflowing = true;
65200
+ getSafeLogger()?.warn(STAGE, "Batch queue overflow \u2014 dropping oldest entries", { maxQueueSize });
65201
+ }
65202
+ } else {
65203
+ overflowing = false;
65204
+ }
65205
+ if (queue.length >= maxBatchSize) {
65206
+ doFlush();
65207
+ }
65208
+ };
65209
+ armTimer();
65210
+ return {
65211
+ enqueue,
65212
+ flushNow: () => doFlush(),
65213
+ teardown: () => {
65214
+ tornDown = true;
65215
+ if (timer !== undefined)
65216
+ clearTimeout(timer);
65217
+ },
65218
+ getMetrics: () => ({ size: queue.length, dropCount })
65219
+ };
64565
65220
  }
64566
- var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
65221
+ var STAGE = "otel-batch-queue", RETRY_ATTEMPTS = 2;
65222
+ var init_batch_queue = __esm(() => {
65223
+ init_logger2();
65224
+ });
64567
65225
 
64568
65226
  // src/plugins/builtin/otel-reporter/otlp.ts
65227
+ import { hostname as hostname3 } from "os";
64569
65228
  function attr(key, value) {
64570
65229
  return typeof value === "number" ? { key, value: { doubleValue: value } } : { key, value: { stringValue: value } };
64571
65230
  }
64572
65231
  function msToUnixNano(ms) {
64573
65232
  return (BigInt(Math.round(ms)) * 1000000n).toString();
64574
65233
  }
65234
+ function buildHistogramPoint(values, bounds, attributes, timeUnixNano) {
65235
+ const bucketCounts = new Array(bounds.length + 1).fill(0);
65236
+ let sum = 0;
65237
+ for (const value of values) {
65238
+ sum += value;
65239
+ const bucketIndex = bounds.findIndex((bound) => value <= bound);
65240
+ bucketCounts[bucketIndex === -1 ? bounds.length : bucketIndex]++;
65241
+ }
65242
+ return { attributes, timeUnixNano, count: values.length, sum, bucketCounts, explicitBounds: bounds };
65243
+ }
65244
+ function buildCounterPoint(count, attributes, timeUnixNano) {
65245
+ return { attributes, timeUnixNano, asInt: String(count) };
65246
+ }
65247
+ function buildResourceAttributes(input) {
65248
+ const attrs = [
65249
+ attr("service.name", input.serviceName),
65250
+ attr("nax.run_id", input.runId),
65251
+ attr("nax.version", NAX_VERSION),
65252
+ attr("process.pid", process.pid)
65253
+ ];
65254
+ try {
65255
+ attrs.push(attr("host.name", hostname3()));
65256
+ } catch {}
65257
+ if (input.feature !== undefined)
65258
+ attrs.push(attr("nax.feature", input.feature));
65259
+ if (input.project !== undefined)
65260
+ attrs.push(attr("nax.project", input.project));
65261
+ if (input.git?.branch !== undefined)
65262
+ attrs.push(attr("nax.git.branch", input.git.branch));
65263
+ if (input.git?.sha !== undefined)
65264
+ attrs.push(attr("nax.git.sha", input.git.sha));
65265
+ return attrs;
65266
+ }
64575
65267
  function buildTracesPayload(p) {
64576
65268
  const span = {
64577
65269
  traceId: p.traceId,
64578
65270
  spanId: p.spanId,
65271
+ ...p.parentSpanId ? { parentSpanId: p.parentSpanId } : {},
64579
65272
  name: "nax.run",
64580
65273
  kind: 1,
64581
65274
  startTimeUnixNano: p.startUnixNano,
@@ -64595,8 +65288,19 @@ function buildTracesPayload(p) {
64595
65288
  return {
64596
65289
  resourceSpans: [
64597
65290
  {
64598
- resource: { attributes: [attr("service.name", p.serviceName)] },
64599
- scopeSpans: [{ scope: { name: "nax" }, spans: [span] }]
65291
+ resource: {
65292
+ attributes: buildResourceAttributes({
65293
+ serviceName: p.serviceName,
65294
+ runId: p.runId,
65295
+ feature: p.feature,
65296
+ project: p.project,
65297
+ git: {
65298
+ branch: p.gitBranch,
65299
+ sha: p.gitSha
65300
+ }
65301
+ })
65302
+ },
65303
+ scopeSpans: [{ scope: { name: "nax" }, spans: [span, ...p.extraSpans ?? []] }]
64600
65304
  }
64601
65305
  ]
64602
65306
  };
@@ -64622,7 +65326,18 @@ function buildMetricsPayload(p) {
64622
65326
  return {
64623
65327
  resourceMetrics: [
64624
65328
  {
64625
- resource: { attributes: [attr("service.name", p.serviceName)] },
65329
+ resource: {
65330
+ attributes: buildResourceAttributes({
65331
+ serviceName: p.serviceName,
65332
+ runId: p.runId,
65333
+ feature: p.feature,
65334
+ project: p.project,
65335
+ git: {
65336
+ branch: p.gitBranch,
65337
+ sha: p.gitSha
65338
+ }
65339
+ })
65340
+ },
64626
65341
  scopeMetrics: [
64627
65342
  {
64628
65343
  scope: { name: "nax" },
@@ -64633,17 +65348,482 @@ function buildMetricsPayload(p) {
64633
65348
  ]
64634
65349
  };
64635
65350
  }
65351
+ var init_otlp = __esm(() => {
65352
+ init_version();
65353
+ });
65354
+
65355
+ // src/plugins/builtin/otel-reporter/heartbeat.ts
65356
+ function startHeartbeat(opts) {
65357
+ const { intervalMs, getSnapshot, onTick } = opts;
65358
+ if (intervalMs <= 0)
65359
+ return { stop() {} };
65360
+ let stopped = false;
65361
+ let timer;
65362
+ const armTimer = () => {
65363
+ timer = setTimeout(() => {
65364
+ try {
65365
+ Promise.resolve(onTick(getSnapshot())).catch((err) => getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
65366
+ error: err instanceof Error ? err.message : String(err)
65367
+ }));
65368
+ } catch (err) {
65369
+ getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
65370
+ error: err instanceof Error ? err.message : String(err)
65371
+ });
65372
+ }
65373
+ if (!stopped)
65374
+ armTimer();
65375
+ }, intervalMs);
65376
+ };
65377
+ armTimer();
65378
+ return {
65379
+ stop() {
65380
+ stopped = true;
65381
+ if (timer !== undefined)
65382
+ clearTimeout(timer);
65383
+ }
65384
+ };
65385
+ }
65386
+ function heartbeatAttributes(a) {
65387
+ return [
65388
+ attr("run_id", a.runId),
65389
+ attr("feature", a.feature),
65390
+ attr("project", a.project),
65391
+ attr("story_id", a.storyId),
65392
+ attr("phase", a.phase),
65393
+ attr("tier", a.tier),
65394
+ attr("test_strategy", a.testStrategy)
65395
+ ];
65396
+ }
65397
+ function buildHeartbeatMetricsPayload(p) {
65398
+ const attributes = heartbeatAttributes(p.snapshot.attributes);
65399
+ const gauge = (name, value) => ({
65400
+ name,
65401
+ gauge: { dataPoints: [{ asDouble: value, timeUnixNano: p.timeUnixNano, attributes }] }
65402
+ });
65403
+ return {
65404
+ resourceMetrics: [
65405
+ {
65406
+ resource: {
65407
+ attributes: buildResourceAttributes({
65408
+ serviceName: p.serviceName,
65409
+ runId: p.snapshot.attributes.runId
65410
+ })
65411
+ },
65412
+ scopeMetrics: [
65413
+ {
65414
+ scope: { name: "nax" },
65415
+ metrics: [
65416
+ gauge("nax.run.active", 1),
65417
+ gauge("nax.run.phase_elapsed_ms", p.snapshot.phaseElapsedMs),
65418
+ gauge("nax.run.cost_usd", p.snapshot.costUsd)
65419
+ ]
65420
+ }
65421
+ ]
65422
+ }
65423
+ ]
65424
+ };
65425
+ }
65426
+ var STAGE2 = "otel-reporter-heartbeat";
65427
+ var init_heartbeat = __esm(() => {
65428
+ init_logger2();
65429
+ init_otlp();
65430
+ });
65431
+
65432
+ // src/plugins/builtin/otel-reporter/ids.ts
65433
+ function randomHex(bytes) {
65434
+ const arr = new Uint8Array(bytes);
65435
+ crypto.getRandomValues(arr);
65436
+ let out = "";
65437
+ for (const b of arr)
65438
+ out += b.toString(16).padStart(2, "0");
65439
+ return out;
65440
+ }
65441
+ var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
65442
+
65443
+ // src/plugins/builtin/otel-reporter/logs.ts
65444
+ function entryTimestampMs(entry) {
65445
+ return new Date(entry.timestamp).getTime();
65446
+ }
65447
+ function toLogRecord(entry) {
65448
+ const timeUnixNano = msToUnixNano(entryTimestampMs(entry));
65449
+ const { number: severityNumber, text: severityText } = SEVERITY[entry.level];
65450
+ const attributes = [attr("nax.stage", entry.stage)];
65451
+ if (entry.storyId !== undefined)
65452
+ attributes.push(attr("nax.story_id", entry.storyId));
65453
+ if (entry.sessionRole !== undefined)
65454
+ attributes.push(attr("nax.session_role", entry.sessionRole));
65455
+ const data = entry.data ?? {};
65456
+ const nonScalars = {};
65457
+ for (const [key, value] of Object.entries(data)) {
65458
+ if (typeof value === "string") {
65459
+ attributes.push(attr(`nax.data.${key}`, truncate3(value)));
65460
+ } else if (typeof value === "number") {
65461
+ if (Number.isFinite(value)) {
65462
+ attributes.push(attr(`nax.data.${key}`, value));
65463
+ } else {
65464
+ nonScalars[key] = value;
65465
+ }
65466
+ } else if (typeof value === "boolean") {
65467
+ attributes.push(attr(`nax.data.${key}`, String(value)));
65468
+ } else {
65469
+ nonScalars[key] = value;
65470
+ }
65471
+ }
65472
+ if (Object.keys(nonScalars).length > 0) {
65473
+ attributes.push(attr("nax.data_json", truncate3(JSON.stringify(nonScalars))));
65474
+ }
65475
+ return {
65476
+ body: { stringValue: entry.message },
65477
+ timeUnixNano,
65478
+ severityNumber,
65479
+ severityText,
65480
+ attributes
65481
+ };
65482
+ }
65483
+ function buildLogsPayload(entries, resource) {
65484
+ const logRecords = entries.map(toLogRecord);
65485
+ return {
65486
+ resourceLogs: [
65487
+ {
65488
+ resource: {
65489
+ attributes: buildResourceAttributes(resource)
65490
+ },
65491
+ scopeLogs: [{ scope: { name: "nax" }, logRecords }]
65492
+ }
65493
+ ]
65494
+ };
65495
+ }
65496
+ function truncate3(value) {
65497
+ if (value.length <= DATA_JSON_MAX)
65498
+ return value;
65499
+ const marker = TRUNCATION_MARKER;
65500
+ const keep = DATA_JSON_MAX - marker.length;
65501
+ return `${value.slice(0, keep)}${marker}`;
65502
+ }
65503
+ var SEVERITY, DATA_JSON_MAX = 2048, TRUNCATION_MARKER = "...[truncated]";
65504
+ var init_logs = __esm(() => {
65505
+ init_otlp();
65506
+ SEVERITY = {
65507
+ silent: { number: 0, text: "SILENT" },
65508
+ error: { number: 17, text: "ERROR" },
65509
+ warn: { number: 13, text: "WARN" },
65510
+ info: { number: 9, text: "INFO" },
65511
+ debug: { number: 5, text: "DEBUG" }
65512
+ };
65513
+ });
65514
+
65515
+ // src/plugins/builtin/otel-reporter/span-tree.ts
65516
+ function createSpanTree(traceId, runSpanId) {
65517
+ const storySpanIds = new Map;
65518
+ function storySpanId(storyId) {
65519
+ let spanId = storySpanIds.get(storyId);
65520
+ if (!spanId) {
65521
+ spanId = newSpanId();
65522
+ storySpanIds.set(storyId, spanId);
65523
+ }
65524
+ return spanId;
65525
+ }
65526
+ function buildStorySpan(storyId, startUnixNano, endUnixNano) {
65527
+ return {
65528
+ traceId,
65529
+ spanId: storySpanId(storyId),
65530
+ parentSpanId: runSpanId,
65531
+ name: "nax.story",
65532
+ startTimeUnixNano: startUnixNano,
65533
+ endTimeUnixNano: endUnixNano,
65534
+ attributes: [attr("nax.story_id", storyId)]
65535
+ };
65536
+ }
65537
+ function buildPhaseSpan({ event, traceId: spanTraceId, startUnixNano, endUnixNano }) {
65538
+ const parentSpanId = event.scope === "story" && event.storyId !== undefined ? storySpanId(event.storyId) : runSpanId;
65539
+ const attributes = [attr("phase", event.phase), attr("outcome", event.outcome)];
65540
+ if (event.testStrategy)
65541
+ attributes.push(attr("nax.test_strategy", event.testStrategy));
65542
+ return {
65543
+ traceId: spanTraceId,
65544
+ spanId: newSpanId(),
65545
+ parentSpanId,
65546
+ name: "nax.phase",
65547
+ startTimeUnixNano: startUnixNano,
65548
+ endTimeUnixNano: endUnixNano,
65549
+ attributes
65550
+ };
65551
+ }
65552
+ return { traceId, runSpanId, storySpanId, buildStorySpan, buildPhaseSpan };
65553
+ }
65554
+ function counterKey(attributes) {
65555
+ return attributes.map((a) => `${a.key}=${a.value.stringValue ?? a.value.doubleValue}`).join("|");
65556
+ }
65557
+ function bumpCounter(groups, attributes, count) {
65558
+ const key = counterKey(attributes);
65559
+ const existing = groups.get(key);
65560
+ if (existing) {
65561
+ existing.count += count;
65562
+ } else {
65563
+ groups.set(key, { attributes, count });
65564
+ }
65565
+ }
65566
+ function createPhaseMetricsAggregator() {
65567
+ const phaseGroups = new Map;
65568
+ const reviewFindings = new Map;
65569
+ const fixIterations = new Map;
65570
+ const escalations = new Map;
65571
+ function recordPhase(event) {
65572
+ const attributes = [
65573
+ attr("phase", event.phase),
65574
+ attr("outcome", event.outcome),
65575
+ attr("tier", event.tier ?? "unknown"),
65576
+ attr("test_strategy", event.testStrategy ?? "unknown"),
65577
+ attr("session_model", event.sessionModel ?? "unknown")
65578
+ ];
65579
+ const key = counterKey(attributes);
65580
+ let group = phaseGroups.get(key);
65581
+ if (!group) {
65582
+ group = { attributes, durations: [], costs: [] };
65583
+ phaseGroups.set(key, group);
65584
+ }
65585
+ group.durations.push(event.durationMs);
65586
+ if (event.costUsd !== undefined)
65587
+ group.costs.push(event.costUsd);
65588
+ }
65589
+ function recordReviewFindings(phase, severity2, count) {
65590
+ bumpCounter(reviewFindings, [attr("phase", phase), attr("severity", severity2)], count);
65591
+ }
65592
+ function recordFixIterations(phase, strategy, count) {
65593
+ bumpCounter(fixIterations, [attr("phase", phase), attr("strategy", strategy)], count);
65594
+ }
65595
+ function recordEscalation(toTier, count) {
65596
+ bumpCounter(escalations, [attr("to_tier", toTier)], count);
65597
+ }
65598
+ function buildMetricsPayload2(input) {
65599
+ const { serviceName, runId, timeUnixNano, feature, project, gitBranch, gitSha } = input;
65600
+ const groups = [...phaseGroups.values()];
65601
+ const counterMetric = (name, source) => ({
65602
+ name,
65603
+ sum: {
65604
+ aggregationTemporality: 2,
65605
+ isMonotonic: true,
65606
+ dataPoints: [...source.values()].map((g) => buildCounterPoint(g.count, g.attributes, timeUnixNano))
65607
+ }
65608
+ });
65609
+ const metrics = [];
65610
+ if (groups.length > 0) {
65611
+ metrics.push({
65612
+ name: "nax.phase.duration",
65613
+ histogram: {
65614
+ aggregationTemporality: 2,
65615
+ dataPoints: groups.map((g) => buildHistogramPoint(g.durations, PHASE_DURATION_BOUNDS, g.attributes, timeUnixNano))
65616
+ }
65617
+ });
65618
+ metrics.push({
65619
+ name: "nax.phase.cost_usd",
65620
+ histogram: {
65621
+ aggregationTemporality: 2,
65622
+ dataPoints: groups.map((g) => buildHistogramPoint(g.costs, PHASE_COST_BOUNDS, g.attributes, timeUnixNano))
65623
+ }
65624
+ });
65625
+ }
65626
+ if (reviewFindings.size > 0)
65627
+ metrics.push(counterMetric("nax.review.findings", reviewFindings));
65628
+ if (fixIterations.size > 0)
65629
+ metrics.push(counterMetric("nax.fix.iterations", fixIterations));
65630
+ if (escalations.size > 0)
65631
+ metrics.push(counterMetric("nax.escalations", escalations));
65632
+ return {
65633
+ resourceMetrics: [
65634
+ {
65635
+ resource: {
65636
+ attributes: buildResourceAttributes({
65637
+ serviceName,
65638
+ runId,
65639
+ feature,
65640
+ project,
65641
+ git: { branch: gitBranch, sha: gitSha }
65642
+ })
65643
+ },
65644
+ scopeMetrics: [{ scope: { name: "nax" }, metrics }]
65645
+ }
65646
+ ]
65647
+ };
65648
+ }
65649
+ return { recordPhase, recordReviewFindings, recordFixIterations, recordEscalation, buildMetricsPayload: buildMetricsPayload2 };
65650
+ }
65651
+ var PHASE_DURATION_BOUNDS, PHASE_COST_BOUNDS;
65652
+ var init_span_tree = __esm(() => {
65653
+ init_otlp();
65654
+ PHASE_DURATION_BOUNDS = [100, 500, 1000, 5000, 15000, 60000, 300000, 900000];
65655
+ PHASE_COST_BOUNDS = [0.001, 0.01, 0.05, 0.1, 0.5, 1, 5];
65656
+ });
65657
+
65658
+ // src/plugins/builtin/otel-reporter/traceparent.ts
65659
+ function parseTraceparent(value) {
65660
+ if (!value)
65661
+ return null;
65662
+ const match = TRACEPARENT_PATTERN.exec(value);
65663
+ if (!match)
65664
+ return null;
65665
+ const [, traceId, spanId] = match;
65666
+ if (!traceId || !spanId || ALL_ZERO.test(traceId))
65667
+ return null;
65668
+ return { traceId, spanId };
65669
+ }
65670
+ var TRACEPARENT_PATTERN, ALL_ZERO;
65671
+ var init_traceparent = __esm(() => {
65672
+ TRACEPARENT_PATTERN = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
65673
+ ALL_ZERO = /^0+$/;
65674
+ });
64636
65675
 
64637
65676
  // src/plugins/builtin/otel-reporter/index.ts
64638
- function createOtelReporterPlugin(cfg, deps) {
65677
+ function rootSpanIdentity() {
65678
+ const adopted = parseTraceparent(process.env.TRACEPARENT);
65679
+ if (!adopted)
65680
+ return { traceId: newTraceId(), spanId: newSpanId() };
65681
+ return { traceId: adopted.traceId, spanId: newSpanId(), parentSpanId: adopted.spanId };
65682
+ }
65683
+ function heartbeatSnapshotOf(runId, st) {
65684
+ const last = st.lastPhase;
65685
+ return {
65686
+ attributes: {
65687
+ runId,
65688
+ feature: st.feature,
65689
+ project: st.project,
65690
+ storyId: last?.storyId ?? "",
65691
+ phase: last?.phase ?? "",
65692
+ tier: last?.tier ?? "",
65693
+ testStrategy: last?.testStrategy ?? ""
65694
+ },
65695
+ phaseElapsedMs: last ? Date.now() - last.atMs : 0,
65696
+ costUsd: st.costUsd
65697
+ };
65698
+ }
65699
+ function recordDetailMetrics(metrics, phase, details) {
65700
+ if (typeof details !== "object" || details === null)
65701
+ return;
65702
+ const record2 = details;
65703
+ if (record2.kind === "review" && typeof record2.bySeverity === "object" && record2.bySeverity !== null) {
65704
+ for (const [severity2, count] of Object.entries(record2.bySeverity)) {
65705
+ if (count > 0)
65706
+ metrics.recordReviewFindings(phase, severity2, count);
65707
+ }
65708
+ } else if (record2.kind === "fix" && typeof record2.strategy === "string") {
65709
+ metrics.recordFixIterations(phase, record2.strategy, 1);
65710
+ }
65711
+ }
65712
+ function reviewSpanEvents(details, timeUnixNano, verbose) {
65713
+ if (!verbose)
65714
+ return [];
65715
+ if (typeof details !== "object" || details === null)
65716
+ return [];
65717
+ const record2 = details;
65718
+ if (record2.kind !== "review" || !Array.isArray(record2.items))
65719
+ return [];
65720
+ return record2.items.map((item) => {
65721
+ const finding = typeof item === "object" && item !== null ? item : {};
65722
+ const attributes = [attr("message", String(finding.message ?? ""))];
65723
+ if (typeof finding.file === "string")
65724
+ attributes.push(attr("file", finding.file));
65725
+ return { timeUnixNano, name: "review.finding", attributes };
65726
+ });
65727
+ }
65728
+ function createOtelReporterPlugin(cfg, deps, workdir) {
64639
65729
  const states = new Map;
64640
65730
  const base = cfg.endpoint?.replace(/\/$/, "");
65731
+ let tornDown = false;
65732
+ const makeSendSpanBatch = (resourceAttrs) => async (batch) => {
65733
+ if (!base || batch.length === 0)
65734
+ return true;
65735
+ const { resolved, missing } = interpolateHeaders(cfg.headers);
65736
+ if (missing.length > 0) {
65737
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65738
+ return true;
65739
+ }
65740
+ const payload = {
65741
+ resourceSpans: [
65742
+ {
65743
+ resource: { attributes: resourceAttrs },
65744
+ scopeSpans: [{ scope: { name: "nax" }, spans: batch }]
65745
+ }
65746
+ ]
65747
+ };
65748
+ return postJson(`${base}/v1/traces`, payload, {
65749
+ headers: resolved,
65750
+ timeoutMs: cfg.timeoutMs,
65751
+ stage: STAGE3,
65752
+ deps
65753
+ });
65754
+ };
65755
+ const makeSpanQueue = (resourceAttrs) => createBatchQueue({
65756
+ maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
65757
+ flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
65758
+ maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
65759
+ send: makeSendSpanBatch(resourceAttrs)
65760
+ });
65761
+ const makeSendLogsBatch = (resource) => async (batch) => {
65762
+ if (!base || batch.length === 0)
65763
+ return true;
65764
+ const { resolved, missing } = interpolateHeaders(cfg.headers);
65765
+ if (missing.length > 0) {
65766
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65767
+ return true;
65768
+ }
65769
+ const payload = buildLogsPayload(batch, {
65770
+ serviceName: resource.serviceName,
65771
+ runId: resource.runId,
65772
+ feature: resource.feature,
65773
+ project: resource.project,
65774
+ git: { branch: resource.gitBranch, sha: resource.gitSha }
65775
+ });
65776
+ return postJson(`${base}/v1/logs`, payload, {
65777
+ headers: resolved,
65778
+ timeoutMs: cfg.timeoutMs,
65779
+ stage: STAGE3,
65780
+ deps
65781
+ });
65782
+ };
65783
+ const makeLogsQueue = (resource) => createBatchQueue({
65784
+ maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
65785
+ flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
65786
+ maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
65787
+ send: makeSendLogsBatch(resource)
65788
+ });
65789
+ const buildOrphanState = (startMs) => {
65790
+ const identity = rootSpanIdentity();
65791
+ const orphanAttrs = buildResourceAttributes({ serviceName: cfg.serviceName, runId: "orphan" });
65792
+ return {
65793
+ ...identity,
65794
+ startMs,
65795
+ feature: "",
65796
+ project: "",
65797
+ events: [],
65798
+ spanTree: createSpanTree(identity.traceId, identity.spanId),
65799
+ spanQueue: makeSpanQueue(orphanAttrs),
65800
+ metrics: createPhaseMetricsAggregator(),
65801
+ storyBounds: new Map,
65802
+ costUsd: 0,
65803
+ heartbeat: { stop() {} }
65804
+ };
65805
+ };
65806
+ const exportHeartbeat = async (snapshot) => {
65807
+ if (!base)
65808
+ return;
65809
+ const { resolved, missing } = interpolateHeaders(cfg.headers);
65810
+ if (missing.length > 0) {
65811
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65812
+ return;
65813
+ }
65814
+ const metrics = buildHeartbeatMetricsPayload({
65815
+ serviceName: cfg.serviceName,
65816
+ timeUnixNano: msToUnixNano(Date.now()),
65817
+ snapshot
65818
+ });
65819
+ await postJson(`${base}/v1/metrics`, metrics, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
65820
+ };
64641
65821
  const flush = async (st, endMs, e) => {
64642
65822
  if (!base)
64643
65823
  return;
64644
65824
  const { resolved, missing } = interpolateHeaders(cfg.headers);
64645
65825
  if (missing.length > 0) {
64646
- getSafeLogger()?.warn(STAGE, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65826
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
64647
65827
  return;
64648
65828
  }
64649
65829
  const startUnixNano = msToUnixNano(st.startMs);
@@ -64652,9 +65832,13 @@ function createOtelReporterPlugin(cfg, deps) {
64652
65832
  serviceName: cfg.serviceName,
64653
65833
  traceId: st.traceId,
64654
65834
  spanId: st.spanId,
65835
+ parentSpanId: st.parentSpanId,
64655
65836
  startUnixNano,
64656
65837
  endUnixNano,
64657
65838
  feature: st.feature,
65839
+ project: st.project,
65840
+ gitBranch: st.gitBranch,
65841
+ gitSha: st.gitSha,
64658
65842
  runId: e.runId,
64659
65843
  storySummary: e.storySummary,
64660
65844
  totalCost: e.totalCost,
@@ -64664,24 +65848,99 @@ function createOtelReporterPlugin(cfg, deps) {
64664
65848
  serviceName: cfg.serviceName,
64665
65849
  runId: e.runId,
64666
65850
  timeUnixNano: endUnixNano,
65851
+ feature: st.feature,
65852
+ project: st.project,
65853
+ gitBranch: st.gitBranch,
65854
+ gitSha: st.gitSha,
64667
65855
  storySummary: e.storySummary,
64668
65856
  totalCost: e.totalCost,
64669
65857
  totalDurationMs: e.totalDurationMs
64670
65858
  });
64671
- const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE, deps };
65859
+ const aggMetrics = st.metrics.buildMetricsPayload({
65860
+ serviceName: cfg.serviceName,
65861
+ runId: e.runId,
65862
+ timeUnixNano: endUnixNano,
65863
+ feature: st.feature,
65864
+ project: st.project,
65865
+ gitBranch: st.gitBranch,
65866
+ gitSha: st.gitSha
65867
+ });
65868
+ metrics.resourceMetrics[0].scopeMetrics[0].metrics.push(...aggMetrics.resourceMetrics[0].scopeMetrics[0].metrics);
65869
+ const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps };
64672
65870
  await postJson(`${base}/v1/traces`, traces, opts);
64673
65871
  await postJson(`${base}/v1/metrics`, metrics, opts);
64674
65872
  };
64675
65873
  const reporter = {
64676
- name: STAGE,
65874
+ name: STAGE3,
64677
65875
  async onRunStart(event) {
64678
- states.set(event.runId, {
64679
- traceId: newTraceId(),
64680
- spanId: newSpanId(),
64681
- startMs: Date.parse(event.startTime),
65876
+ const identity = rootSpanIdentity();
65877
+ const runId = event.runId;
65878
+ let gitBranch;
65879
+ let gitSha;
65880
+ if (base && workdir) {
65881
+ const [branchResult, shaResult] = await Promise.all([
65882
+ gitWithTimeout(["rev-parse", "--abbrev-ref", "HEAD"], workdir).catch(() => null),
65883
+ gitWithTimeout(["rev-parse", "HEAD"], workdir).catch(() => null)
65884
+ ]);
65885
+ if (branchResult?.exitCode === 0) {
65886
+ const branch = branchResult.stdout.trim();
65887
+ if (branch && branch !== "HEAD")
65888
+ gitBranch = branch;
65889
+ }
65890
+ if (shaResult?.exitCode === 0) {
65891
+ const sha = shaResult.stdout.trim();
65892
+ if (sha)
65893
+ gitSha = sha;
65894
+ }
65895
+ }
65896
+ const resourceAttrs = buildResourceAttributes({
65897
+ serviceName: cfg.serviceName,
65898
+ runId,
64682
65899
  feature: event.feature,
64683
- events: []
65900
+ project: event.project,
65901
+ git: { branch: gitBranch, sha: gitSha }
64684
65902
  });
65903
+ const state = {
65904
+ ...identity,
65905
+ startMs: Date.parse(event.startTime),
65906
+ feature: event.feature,
65907
+ project: event.project ?? "",
65908
+ gitBranch,
65909
+ gitSha,
65910
+ events: [],
65911
+ spanTree: createSpanTree(identity.traceId, identity.spanId),
65912
+ spanQueue: makeSpanQueue(resourceAttrs),
65913
+ metrics: createPhaseMetricsAggregator(),
65914
+ storyBounds: new Map,
65915
+ costUsd: 0,
65916
+ heartbeat: startHeartbeat({
65917
+ intervalMs: cfg.heartbeatIntervalMs ?? 0,
65918
+ getSnapshot: () => heartbeatSnapshotOf(runId, state),
65919
+ onTick: (snapshot) => exportHeartbeat(snapshot)
65920
+ })
65921
+ };
65922
+ states.set(runId, state);
65923
+ if (cfg.logs?.enabled) {
65924
+ const logsQueue = makeLogsQueue({
65925
+ serviceName: cfg.serviceName,
65926
+ runId,
65927
+ feature: event.feature,
65928
+ project: event.project ?? "",
65929
+ gitBranch,
65930
+ gitSha
65931
+ });
65932
+ const floorKey = cfg.logs.level;
65933
+ const sank = (entry) => {
65934
+ if (REENTRY_STAGES.has(entry.stage))
65935
+ return;
65936
+ if (LOG_PRIORITY[entry.level] > LOG_PRIORITY[floorKey])
65937
+ return;
65938
+ logsQueue.enqueue(entry);
65939
+ };
65940
+ const addSinkFn = deps?.addSink ?? addSink;
65941
+ state.logsQueue = logsQueue;
65942
+ state.logUnsubscribe = addSinkFn(sank);
65943
+ }
64685
65944
  },
64686
65945
  async onStoryComplete(event) {
64687
65946
  const st = states.get(event.runId);
@@ -64698,32 +65957,119 @@ function createOtelReporterPlugin(cfg, deps) {
64698
65957
  attr("testStrategy", event.testStrategy)
64699
65958
  ]
64700
65959
  });
65960
+ const bounds = st.storyBounds.get(event.storyId);
65961
+ if (bounds) {
65962
+ st.spanQueue.enqueue(st.spanTree.buildStorySpan(event.storyId, msToUnixNano(bounds.startMs), msToUnixNano(bounds.endMs)));
65963
+ st.storyBounds.delete(event.storyId);
65964
+ }
65965
+ },
65966
+ async onPhaseComplete(event) {
65967
+ const st = states.get(event.runId);
65968
+ if (!st)
65969
+ return;
65970
+ st.costUsd += event.costUsd ?? 0;
65971
+ st.lastPhase = {
65972
+ phase: event.phase,
65973
+ storyId: event.storyId ?? "",
65974
+ tier: event.tier ?? "",
65975
+ testStrategy: event.testStrategy ?? "",
65976
+ atMs: Date.now()
65977
+ };
65978
+ const endMs = Date.now();
65979
+ const startMs = endMs - event.durationMs;
65980
+ const endUnixNano = msToUnixNano(endMs);
65981
+ const span = st.spanTree.buildPhaseSpan({
65982
+ event,
65983
+ traceId: st.traceId,
65984
+ startUnixNano: msToUnixNano(startMs),
65985
+ endUnixNano
65986
+ });
65987
+ const events = reviewSpanEvents(event.details, endUnixNano, cfg.detail === "verbose");
65988
+ if (events.length > 0)
65989
+ span.events = events;
65990
+ st.spanQueue.enqueue(span);
65991
+ st.metrics.recordPhase(event);
65992
+ recordDetailMetrics(st.metrics, event.phase, event.details);
65993
+ if (event.scope === "story" && event.storyId !== undefined) {
65994
+ const bounds = st.storyBounds.get(event.storyId);
65995
+ st.storyBounds.set(event.storyId, {
65996
+ startMs: bounds ? Math.min(bounds.startMs, startMs) : startMs,
65997
+ endMs: bounds ? Math.max(bounds.endMs, endMs) : endMs
65998
+ });
65999
+ }
66000
+ },
66001
+ async onEscalation(event) {
66002
+ const st = states.get(event.runId);
66003
+ if (!st)
66004
+ return;
66005
+ st.metrics.recordEscalation(event.toTier, 1);
64701
66006
  },
64702
66007
  async onRunEnd(event) {
64703
66008
  const existing = states.get(event.runId);
66009
+ existing?.heartbeat.stop();
64704
66010
  const startMs = existing?.startMs ?? Date.now() - event.totalDurationMs;
64705
- const st = existing ?? {
64706
- traceId: newTraceId(),
64707
- spanId: newSpanId(),
64708
- startMs,
64709
- feature: "",
64710
- events: []
64711
- };
66011
+ const st = existing ?? buildOrphanState(startMs);
64712
66012
  states.delete(event.runId);
66013
+ await st.spanQueue.flushNow();
66014
+ st.spanQueue.teardown();
66015
+ if (st.logsQueue) {
66016
+ await st.logsQueue.flushNow();
66017
+ st.logsQueue.teardown();
66018
+ st.logUnsubscribe?.();
66019
+ }
64713
66020
  await flush(st, startMs + event.totalDurationMs, event);
64714
66021
  }
64715
66022
  };
64716
66023
  return {
64717
- name: STAGE,
66024
+ name: STAGE3,
64718
66025
  version: "1.0.0",
64719
66026
  provides: ["reporter"],
66027
+ async teardown() {
66028
+ if (tornDown)
66029
+ return;
66030
+ tornDown = true;
66031
+ const entries = [...states.entries()];
66032
+ states.clear();
66033
+ for (const [runId, st] of entries) {
66034
+ st.heartbeat.stop();
66035
+ await st.spanQueue.flushNow();
66036
+ st.spanQueue.teardown();
66037
+ if (st.logsQueue) {
66038
+ await st.logsQueue.flushNow();
66039
+ st.logsQueue.teardown();
66040
+ st.logUnsubscribe?.();
66041
+ }
66042
+ const endMs = Date.now();
66043
+ await flush(st, endMs, {
66044
+ runId,
66045
+ totalDurationMs: endMs - st.startMs,
66046
+ totalCost: st.costUsd,
66047
+ storySummary: { completed: 0, failed: 0, skipped: 0, paused: 0 }
66048
+ });
66049
+ }
66050
+ },
64720
66051
  extensions: { reporter }
64721
66052
  };
64722
66053
  }
64723
- var STAGE = "otel-reporter";
66054
+ var STAGE3 = "otel-reporter", REENTRY_STAGE = "otel-batch-queue", REENTRY_STAGES, DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048, LOG_PRIORITY;
64724
66055
  var init_otel_reporter = __esm(() => {
64725
66056
  init_logger2();
66057
+ init_git();
64726
66058
  init_reporter_shared();
66059
+ init_batch_queue();
66060
+ init_heartbeat();
66061
+ init_logs();
66062
+ init_otlp();
66063
+ init_span_tree();
66064
+ init_traceparent();
66065
+ REENTRY_STAGES = new Set([STAGE3, REENTRY_STAGE]);
66066
+ LOG_PRIORITY = {
66067
+ silent: -1,
66068
+ error: 0,
66069
+ warn: 1,
66070
+ info: 2,
66071
+ debug: 3
66072
+ };
64727
66073
  });
64728
66074
 
64729
66075
  // src/plugins/builtin/webhook-reporter/index.ts
@@ -64734,25 +66080,27 @@ function createWebhookReporterPlugin(cfg, deps) {
64734
66080
  return;
64735
66081
  const { resolved, missing } = interpolateHeaders(cfg.headers);
64736
66082
  if (missing.length > 0) {
64737
- getSafeLogger()?.warn(STAGE2, "Skipping webhook \u2014 unresolved env vars", { missing });
66083
+ getSafeLogger()?.warn(STAGE4, "Skipping webhook \u2014 unresolved env vars", { missing });
64738
66084
  return;
64739
66085
  }
64740
- await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE2, deps });
66086
+ await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE4, deps });
64741
66087
  };
64742
66088
  const reporter = {
64743
- name: STAGE2,
66089
+ name: STAGE4,
64744
66090
  onRunStart: (event) => emit("onRunStart", event),
64745
66091
  onStoryComplete: (event) => emit("onStoryComplete", event),
64746
- onRunEnd: (event) => emit("onRunEnd", event)
66092
+ onRunEnd: (event) => emit("onRunEnd", event),
66093
+ onPhaseStart: (event) => emit("onPhaseStart", event),
66094
+ onPhaseComplete: (event) => emit("onPhaseComplete", event)
64747
66095
  };
64748
66096
  return {
64749
- name: STAGE2,
66097
+ name: STAGE4,
64750
66098
  version: "1.0.0",
64751
66099
  provides: ["reporter"],
64752
66100
  extensions: { reporter }
64753
66101
  };
64754
66102
  }
64755
- var STAGE2 = "webhook-reporter";
66103
+ var STAGE4 = "webhook-reporter";
64756
66104
  var init_webhook_reporter = __esm(() => {
64757
66105
  init_logger2();
64758
66106
  init_reporter_shared();
@@ -65177,7 +66525,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
65177
66525
  {
65178
66526
  name: "otel-reporter",
65179
66527
  enabled: reporters.otel.enabled,
65180
- make: () => createOtelReporterPlugin(reporters.otel)
66528
+ make: () => createOtelReporterPlugin(reporters.otel, undefined, effectiveProjectRoot)
65181
66529
  }
65182
66530
  ] : [];
65183
66531
  for (const { name, enabled: reporterEnabled, make } of reporterFactories) {
@@ -65787,7 +67135,7 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
65787
67135
  }
65788
67136
  }
65789
67137
  }
65790
- function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
67138
+ function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
65791
67139
  const logger = _heartbeatDeps.getSafeLogger();
65792
67140
  _heartbeatActive = true;
65793
67141
  const gen = ++_heartbeatGen;
@@ -67381,28 +68729,47 @@ async function handleRunCompletion(options) {
67381
68729
  const regressionMode = config2.execution.regressionGate?.mode;
67382
68730
  if (options.skipRegression) {} else if ((regressionMode === "deferred" || regressionMode === "per-story") && config2.quality.commands.test) {
67383
68731
  statusWriter.setPostRunPhase("regression", { status: "running" });
68732
+ const regressionStartTime = Date.now();
67384
68733
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "regression" });
67385
- const regressionResult = await _runCompletionDeps.runDeferredRegression({
67386
- config: config2,
67387
- prd,
67388
- workdir,
67389
- runtime: options.runtime,
67390
- quarantineMemo: options.runtime.quarantineMemo,
67391
- storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
67392
- storyId: m.storyId,
67393
- completedAt: m.completedAt,
67394
- failingTestFiles: m.failingTestFiles
67395
- }))
67396
- });
68734
+ let regressionResult;
68735
+ try {
68736
+ regressionResult = await _runCompletionDeps.runDeferredRegression({
68737
+ config: config2,
68738
+ prd,
68739
+ workdir,
68740
+ runtime: options.runtime,
68741
+ quarantineMemo: options.runtime.quarantineMemo,
68742
+ storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
68743
+ storyId: m.storyId,
68744
+ completedAt: m.completedAt,
68745
+ failingTestFiles: m.failingTestFiles
68746
+ }))
68747
+ });
68748
+ } catch (err) {
68749
+ pipelineEventBus.emit({
68750
+ type: "postrun:phase:completed",
68751
+ phase: "regression",
68752
+ passed: false,
68753
+ durationMs: Date.now() - regressionStartTime
68754
+ });
68755
+ throw err;
68756
+ }
67397
68757
  const lastRunAt = new Date().toISOString();
67398
68758
  logger?.info("regression", "Deferred regression gate completed", {
67399
68759
  success: regressionResult.success,
67400
68760
  failedTests: regressionResult.failedTests,
67401
68761
  affectedStories: regressionResult.affectedStories
67402
68762
  });
68763
+ const regressionDurationMs = Date.now() - regressionStartTime;
67403
68764
  if (regressionResult.success) {
67404
68765
  statusWriter.setPostRunPhase("regression", { status: "passed", lastRunAt });
67405
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "regression", passed: true });
68766
+ pipelineEventBus.emit({
68767
+ type: "postrun:phase:completed",
68768
+ phase: "regression",
68769
+ passed: true,
68770
+ durationMs: regressionDurationMs,
68771
+ details: { mode: regressionMode, failedTests: 0 }
68772
+ });
67406
68773
  } else {
67407
68774
  statusWriter.setPostRunPhase("regression", {
67408
68775
  status: "failed",
@@ -67410,7 +68777,16 @@ async function handleRunCompletion(options) {
67410
68777
  affectedStories: regressionResult.affectedStories,
67411
68778
  lastRunAt
67412
68779
  });
67413
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "regression", passed: false });
68780
+ pipelineEventBus.emit({
68781
+ type: "postrun:phase:completed",
68782
+ phase: "regression",
68783
+ passed: false,
68784
+ durationMs: regressionDurationMs,
68785
+ details: {
68786
+ mode: regressionMode,
68787
+ failedTests: regressionResult.failedTests
68788
+ }
68789
+ });
67414
68790
  for (const storyId of regressionResult.affectedStories) {
67415
68791
  const story = prd.userStories.find((s) => s.id === storyId);
67416
68792
  if (story) {
@@ -67478,7 +68854,15 @@ async function handleRunCompletion(options) {
67478
68854
  let pluginGateFailed = false;
67479
68855
  const deferredReview = options.deferredReview;
67480
68856
  if (deferredReview !== undefined) {
67481
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "review", passed: !deferredReview.anyFailed });
68857
+ const findingCount = deferredReview.reviewerResults.filter((r) => !r.passed).length;
68858
+ const reviewDurationMs = Date.now() - (options.deferredReviewStartedAt ?? Date.now());
68859
+ pipelineEventBus.emit({
68860
+ type: "postrun:phase:completed",
68861
+ phase: "review",
68862
+ passed: !deferredReview.anyFailed,
68863
+ durationMs: reviewDurationMs,
68864
+ details: { findingCount, anyFailed: deferredReview.anyFailed }
68865
+ });
67482
68866
  }
67483
68867
  if (deferredReview?.anyFailed) {
67484
68868
  const failedReviewers = deferredReview.reviewerResults.filter((r) => !r.passed).map((r) => r.name);
@@ -67727,6 +69111,7 @@ async function runCompletionPhase(options) {
67727
69111
  logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
67728
69112
  } else if (options.config.acceptance.enabled && isComplete(options.prd)) {
67729
69113
  options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
69114
+ const acceptanceStartTime = Date.now();
67730
69115
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance" });
67731
69116
  const acceptanceTestPaths = options.featureDir ? await Promise.all((await groupStoriesByPackage(options.prd, options.workdir, options.feature, options.config.acceptance.testPath, options.config.project?.language)).map(async (g) => {
67732
69117
  const relativeWorkdir = path25.relative(options.workdir, g.packageDir);
@@ -67749,32 +69134,54 @@ async function runCompletionPhase(options) {
67749
69134
  commandOverride: groupConfig.acceptance.command
67750
69135
  };
67751
69136
  })) : undefined;
67752
- const acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
67753
- config: options.config,
67754
- prd: options.prd,
67755
- prdPath: options.prdPath,
67756
- workdir: options.workdir,
67757
- featureDir: options.featureDir,
67758
- hooks: options.hooks,
67759
- feature: options.feature,
67760
- totalCost: options.totalCost,
67761
- iterations: options.iterations,
67762
- storiesCompleted: options.storiesCompleted,
67763
- allStoryMetrics: options.allStoryMetrics,
67764
- pluginRegistry: options.pluginRegistry,
67765
- eventEmitter: options.eventEmitter,
67766
- statusWriter: options.statusWriter,
67767
- agentGetFn: options.agentGetFn,
67768
- agentManager: options.agentManager,
67769
- sessionManager: options.sessionManager,
67770
- runtime: options.runtime,
67771
- abortSignal: options.abortSignal,
67772
- acceptanceTestPaths
67773
- });
69137
+ let acceptanceResult;
69138
+ try {
69139
+ acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
69140
+ config: options.config,
69141
+ prd: options.prd,
69142
+ prdPath: options.prdPath,
69143
+ workdir: options.workdir,
69144
+ featureDir: options.featureDir,
69145
+ hooks: options.hooks,
69146
+ feature: options.feature,
69147
+ totalCost: options.totalCost,
69148
+ iterations: options.iterations,
69149
+ storiesCompleted: options.storiesCompleted,
69150
+ allStoryMetrics: options.allStoryMetrics,
69151
+ pluginRegistry: options.pluginRegistry,
69152
+ eventEmitter: options.eventEmitter,
69153
+ statusWriter: options.statusWriter,
69154
+ agentGetFn: options.agentGetFn,
69155
+ agentManager: options.agentManager,
69156
+ sessionManager: options.sessionManager,
69157
+ runtime: options.runtime,
69158
+ abortSignal: options.abortSignal,
69159
+ acceptanceTestPaths
69160
+ });
69161
+ } catch (err) {
69162
+ pipelineEventBus.emit({
69163
+ type: "postrun:phase:completed",
69164
+ phase: "acceptance",
69165
+ passed: false,
69166
+ durationMs: Date.now() - acceptanceStartTime
69167
+ });
69168
+ throw err;
69169
+ }
67774
69170
  const lastRunAt = new Date().toISOString();
69171
+ const acceptanceDurationMs = Date.now() - acceptanceStartTime;
67775
69172
  if (acceptanceResult.success) {
67776
69173
  options.statusWriter.setPostRunPhase("acceptance", { status: "passed", lastRunAt });
67777
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "acceptance", passed: true });
69174
+ pipelineEventBus.emit({
69175
+ type: "postrun:phase:completed",
69176
+ phase: "acceptance",
69177
+ passed: true,
69178
+ durationMs: acceptanceDurationMs,
69179
+ details: {
69180
+ retries: acceptanceResult.retries ?? 0,
69181
+ failedACCount: acceptanceResult.failedACs?.length ?? 0,
69182
+ fixStoriesCreated: 0
69183
+ }
69184
+ });
67778
69185
  } else {
67779
69186
  acceptancePassed = false;
67780
69187
  options.statusWriter.setPostRunPhase("acceptance", {
@@ -67783,7 +69190,17 @@ async function runCompletionPhase(options) {
67783
69190
  retries: acceptanceResult.retries ?? 0,
67784
69191
  lastRunAt
67785
69192
  });
67786
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "acceptance", passed: false });
69193
+ pipelineEventBus.emit({
69194
+ type: "postrun:phase:completed",
69195
+ phase: "acceptance",
69196
+ passed: false,
69197
+ durationMs: acceptanceDurationMs,
69198
+ details: {
69199
+ retries: acceptanceResult.retries ?? 0,
69200
+ failedACCount: acceptanceResult.failedACs?.length ?? 0,
69201
+ fixStoriesCreated: 0
69202
+ }
69203
+ });
67787
69204
  }
67788
69205
  Object.assign(options, {
67789
69206
  prd: acceptanceResult.prd,
@@ -67814,6 +69231,7 @@ async function runCompletionPhase(options) {
67814
69231
  sessionManager: options.sessionManager,
67815
69232
  pluginProviderCache: options.pluginProviderCache,
67816
69233
  deferredReview: options.deferredReview,
69234
+ deferredReviewStartedAt: options.deferredReviewStartedAt,
67817
69235
  exitReason: options.exitReason,
67818
69236
  runtime: options.runtime,
67819
69237
  abortSignal: options.abortSignal
@@ -67890,7 +69308,7 @@ var init_runner_completion = __esm(() => {
67890
69308
  });
67891
69309
 
67892
69310
  // src/execution/batching.ts
67893
- function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
69311
+ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
67894
69312
  const batches = [];
67895
69313
  let currentBatch = [];
67896
69314
  for (const story of stories) {
@@ -67923,7 +69341,7 @@ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE)
67923
69341
  }
67924
69342
  return batches;
67925
69343
  }
67926
- function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
69344
+ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
67927
69345
  const batches = [];
67928
69346
  let currentBatch = [];
67929
69347
  for (const story of stories) {
@@ -67960,7 +69378,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
67960
69378
  }
67961
69379
  return batches;
67962
69380
  }
67963
- var DEFAULT_MAX_BATCH_SIZE = 4;
69381
+ var DEFAULT_MAX_BATCH_SIZE2 = 4;
67964
69382
 
67965
69383
  // src/execution/ensure-package-dirs.ts
67966
69384
  import path26 from "path";
@@ -68240,131 +69658,6 @@ var init_registry6 = __esm(() => {
68240
69658
  init_paths3();
68241
69659
  });
68242
69660
 
68243
- // src/pipeline/subscribers/reporters.ts
68244
- function wireReporters(bus, pluginRegistry, runId, startTime) {
68245
- const logger = getSafeLogger();
68246
- const safe = (name, fn) => {
68247
- return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
68248
- };
68249
- const unsubs = [];
68250
- unsubs.push(bus.on("run:started", (ev) => {
68251
- return safe("onRunStart", async () => {
68252
- const reporters = pluginRegistry.getReporters();
68253
- for (const r of reporters) {
68254
- if (r.onRunStart) {
68255
- try {
68256
- await r.onRunStart({
68257
- runId,
68258
- feature: ev.feature,
68259
- totalStories: ev.totalStories,
68260
- startTime: new Date(startTime).toISOString()
68261
- });
68262
- } catch (err) {
68263
- logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
68264
- }
68265
- }
68266
- }
68267
- });
68268
- }));
68269
- unsubs.push(bus.on("story:completed", (ev) => {
68270
- return safe("onStoryComplete(completed)", async () => {
68271
- const reporters = pluginRegistry.getReporters();
68272
- for (const r of reporters) {
68273
- if (r.onStoryComplete) {
68274
- try {
68275
- await r.onStoryComplete({
68276
- runId,
68277
- storyId: ev.storyId,
68278
- status: "completed",
68279
- runElapsedMs: ev.runElapsedMs,
68280
- cost: ev.cost ?? 0,
68281
- tier: ev.modelTier ?? "balanced",
68282
- testStrategy: ev.testStrategy ?? "test-after"
68283
- });
68284
- } catch (err) {
68285
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68286
- }
68287
- }
68288
- }
68289
- });
68290
- }));
68291
- unsubs.push(bus.on("story:failed", (ev) => {
68292
- return safe("onStoryComplete(failed)", async () => {
68293
- const reporters = pluginRegistry.getReporters();
68294
- for (const r of reporters) {
68295
- if (r.onStoryComplete) {
68296
- try {
68297
- await r.onStoryComplete({
68298
- runId,
68299
- storyId: ev.storyId,
68300
- status: "failed",
68301
- runElapsedMs: Date.now() - startTime,
68302
- cost: 0,
68303
- tier: "balanced",
68304
- testStrategy: "test-after"
68305
- });
68306
- } catch (err) {
68307
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68308
- }
68309
- }
68310
- }
68311
- });
68312
- }));
68313
- unsubs.push(bus.on("story:paused", (ev) => {
68314
- return safe("onStoryComplete(paused)", async () => {
68315
- const reporters = pluginRegistry.getReporters();
68316
- for (const r of reporters) {
68317
- if (r.onStoryComplete) {
68318
- try {
68319
- await r.onStoryComplete({
68320
- runId,
68321
- storyId: ev.storyId,
68322
- status: "paused",
68323
- runElapsedMs: Date.now() - startTime,
68324
- cost: 0,
68325
- tier: "balanced",
68326
- testStrategy: "test-after"
68327
- });
68328
- } catch (err) {
68329
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68330
- }
68331
- }
68332
- }
68333
- });
68334
- }));
68335
- unsubs.push(bus.on("run:completed", (ev) => {
68336
- return safe("onRunEnd", async () => {
68337
- const reporters = pluginRegistry.getReporters();
68338
- for (const r of reporters) {
68339
- if (r.onRunEnd) {
68340
- try {
68341
- await r.onRunEnd({
68342
- runId,
68343
- totalDurationMs: Date.now() - startTime,
68344
- totalCost: ev.totalCost ?? 0,
68345
- storySummary: {
68346
- completed: ev.passedStories,
68347
- failed: ev.failedStories,
68348
- skipped: ev.skippedStories,
68349
- paused: ev.pausedStories
68350
- }
68351
- });
68352
- } catch (err) {
68353
- logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
68354
- }
68355
- }
68356
- }
68357
- });
68358
- }));
68359
- return () => {
68360
- for (const u of unsubs)
68361
- u();
68362
- };
68363
- }
68364
- var init_reporters = __esm(() => {
68365
- init_logger2();
68366
- });
68367
-
68368
69661
  // src/execution/deferred-review.ts
68369
69662
  var {spawn: spawn4 } = globalThis.Bun;
68370
69663
  async function captureRunStartRef(workdir) {
@@ -70378,6 +71671,7 @@ async function executeUnified(ctx, initialPrd) {
70378
71671
  const allStoryMetrics = [];
70379
71672
  let warningSent = false;
70380
71673
  let deferredReview;
71674
+ let deferredReviewStartedAt;
70381
71675
  const runStartRef = await captureRunStartRef(ctx.workdir);
70382
71676
  let cachedNaxIgnoreKey;
70383
71677
  const getRunNaxIgnoreIndex = async (currentPrd) => {
@@ -70416,14 +71710,16 @@ async function executeUnified(ctx, initialPrd) {
70416
71710
  totalCost: totalCost2,
70417
71711
  allStoryMetrics,
70418
71712
  exitReason,
70419
- deferredReview
71713
+ deferredReview,
71714
+ deferredReviewStartedAt
70420
71715
  });
70421
- startHeartbeat(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
71716
+ startHeartbeat2(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
70422
71717
  let _executeThrew = false;
70423
71718
  try {
70424
71719
  if (isComplete(prd)) {
70425
71720
  logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
70426
71721
  const naxIgnoreIndex = await getRunNaxIgnoreIndex(prd);
71722
+ deferredReviewStartedAt = Date.now();
70427
71723
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
70428
71724
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
70429
71725
  return buildResult2("completed");
@@ -70478,6 +71774,7 @@ async function executeUnified(ctx, initialPrd) {
70478
71774
  return buildResult2("pre-merge-aborted");
70479
71775
  }
70480
71776
  logger?.debug("execution", "Running deferred review");
71777
+ deferredReviewStartedAt = Date.now();
70481
71778
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
70482
71779
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
70483
71780
  logger?.debug("execution", "Deferred review done \u2014 returning completed");
@@ -70988,6 +72285,7 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
70988
72285
  totalCost: totalCost2,
70989
72286
  allStoryMetrics,
70990
72287
  deferredReview: unifiedResult.deferredReview,
72288
+ deferredReviewStartedAt: unifiedResult.deferredReviewStartedAt,
70991
72289
  exitReason: unifiedResult.exitReason
70992
72290
  };
70993
72291
  }
@@ -72241,10 +73539,10 @@ async function cleanupRun(options) {
72241
73539
  }
72242
73540
  const actions = pluginRegistry.getPostRunActions();
72243
73541
  const pluginLogger = {
72244
- debug: (msg) => logger?.debug("post-run", msg),
72245
- info: (msg) => logger?.info("post-run", msg),
72246
- warn: (msg) => logger?.warn("post-run", msg),
72247
- error: (msg) => logger?.error("post-run", msg)
73542
+ debug: (msg, data) => logger?.debug("post-run", msg, data),
73543
+ info: (msg, data) => logger?.info("post-run", msg, data),
73544
+ warn: (msg, data) => logger?.warn("post-run", msg, data),
73545
+ error: (msg, data) => logger?.error("post-run", msg, data)
72248
73546
  };
72249
73547
  const ctx = buildPostRunContext(options, durationMs, pluginLogger);
72250
73548
  for (const action of actions) {
@@ -72440,6 +73738,7 @@ async function run(options) {
72440
73738
  agentManager,
72441
73739
  pluginProviderCache,
72442
73740
  deferredReview: executionResult.deferredReview,
73741
+ deferredReviewStartedAt: executionResult.deferredReviewStartedAt,
72443
73742
  exitReason: executionResult.exitReason,
72444
73743
  runtime,
72445
73744
  abortSignal: shutdownController.signal
@@ -72538,8 +73837,11 @@ __export(exports_execution, {
72538
73837
  withIncreasingFailuresBail: () => withIncreasingFailuresBail,
72539
73838
  synthesizeBackfillMetric: () => synthesizeBackfillMetric,
72540
73839
  stopHeartbeat: () => stopHeartbeat,
72541
- startHeartbeat: () => startHeartbeat,
73840
+ startHeartbeat: () => startHeartbeat2,
73841
+ runRectification: () => runRectification,
73842
+ runPhase: () => runPhase,
72542
73843
  runDeferredRegression: () => runDeferredRegression,
73844
+ runCompletionPhase: () => runCompletionPhase,
72543
73845
  run: () => run,
72544
73846
  resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
72545
73847
  resetCrashHandlers: () => resetCrashHandlers,
@@ -72565,7 +73867,6 @@ __export(exports_execution, {
72565
73867
  getTierConfig: () => getTierConfig,
72566
73868
  getOscillations: () => getOscillations,
72567
73869
  getAllReadyStories: () => getAllReadyStories,
72568
- gateRegressedAfterRectification: () => gateRegressedAfterRectification,
72569
73870
  gateFailureKeys: () => gateFailureKeys,
72570
73871
  formatProgress: () => formatProgress,
72571
73872
  formatPhaseResultMessage: () => formatPhaseResultMessage,
@@ -72574,11 +73875,13 @@ __export(exports_execution, {
72574
73875
  extractPauseReason: () => extractPauseReason,
72575
73876
  escalateTier: () => escalateTier,
72576
73877
  ensureStoryPackageDirs: () => ensureStoryPackageDirs,
73878
+ describeGateRegression: () => describeGateRegression,
72577
73879
  deriveTddFailureCategory: () => deriveTddFailureCategory,
72578
73880
  decideStageAction: () => decideStageAction,
72579
73881
  createCheckpointWriter: () => createCheckpointWriter,
72580
73882
  countOscillationOutcomes: () => countOscillationOutcomes,
72581
73883
  clearQueueFile: () => clearQueueFile,
73884
+ cleanupRun: () => cleanupRun,
72582
73885
  captureTreeState: () => captureTreeState,
72583
73886
  calculateMaxIterations: () => calculateMaxIterations,
72584
73887
  buildStoryContext: () => buildStoryContext,
@@ -72595,6 +73898,7 @@ __export(exports_execution, {
72595
73898
  _storyOrchestratorDeps: () => _storyOrchestratorDeps,
72596
73899
  _runnerReentrancyGuard: () => _runnerReentrancyGuard,
72597
73900
  _runnerDeps: () => _runnerDeps,
73901
+ _runnerCompletionDeps: () => _runnerCompletionDeps,
72598
73902
  _runCompletionDeps: () => _runCompletionDeps,
72599
73903
  _regressionDeps: () => _regressionDeps,
72600
73904
  _postRunDeps: () => _postRunDeps,
@@ -72626,6 +73930,7 @@ var init_execution2 = __esm(() => {
72626
73930
  init_plan_inputs();
72627
73931
  init_build_plan_for_strategy();
72628
73932
  init_checkpoint();
73933
+ init_runner_completion();
72629
73934
  init_post_run();
72630
73935
  });
72631
73936
 
@@ -104405,7 +105710,7 @@ async function resolveRunProfileOverride(opts) {
104405
105710
  // src/cli/features-resolve.ts
104406
105711
  init_config();
104407
105712
  import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
104408
- import { join as join74, relative as relative16 } from "path";
105713
+ import { join as join74, relative as relative17 } from "path";
104409
105714
 
104410
105715
  // src/cli/features-acceptance.ts
104411
105716
  init_acceptance2();
@@ -104413,7 +105718,7 @@ init_config();
104413
105718
  init_logger2();
104414
105719
  init_prd();
104415
105720
  import { existsSync as existsSync27 } from "fs";
104416
- import { join as join73, relative as relative15 } from "path";
105721
+ import { join as join73, relative as relative16 } from "path";
104417
105722
  async function resolveFeatureAcceptance(featureName, workdir) {
104418
105723
  let enabled = true;
104419
105724
  try {
@@ -104434,11 +105739,11 @@ async function resolveFeatureAcceptance(featureName, workdir) {
104434
105739
  const prd = await loadPRD(prdPath);
104435
105740
  const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
104436
105741
  const groups = await Promise.all(testGroups.map(async (g) => {
104437
- const packageDir = relative15(repoRoot, g.packageDir);
105742
+ const packageDir = relative16(repoRoot, g.packageDir);
104438
105743
  const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
104439
105744
  return {
104440
105745
  packageDir,
104441
- testPath: relative15(repoRoot, g.testPath),
105746
+ testPath: relative16(repoRoot, g.testPath),
104442
105747
  exists: await Bun.file(g.testPath).exists(),
104443
105748
  command,
104444
105749
  cwd: packageDir,
@@ -104475,17 +105780,17 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104475
105780
  ];
104476
105781
  const docsSpecExact = join74(repoRoot, "docs", "specs", `SPEC-${name}.md`);
104477
105782
  candidates.push({ abs: docsSpecExact, kind: "markdown" });
104478
- const checked = candidates.map((c) => relative16(repoRoot, c.abs));
105783
+ const checked = candidates.map((c) => relative17(repoRoot, c.abs));
104479
105784
  for (const { abs, kind } of candidates.slice(0, 2)) {
104480
105785
  if (kind === "markdown") {
104481
105786
  const nonEmpty = await isNonEmptyFile(abs);
104482
105787
  if (nonEmpty) {
104483
- return { source: { kind, path: relative16(repoRoot, abs) }, checked };
105788
+ return { source: { kind, path: relative17(repoRoot, abs) }, checked };
104484
105789
  }
104485
105790
  }
104486
105791
  }
104487
105792
  if (await isNonEmptyFile(docsSpecExact)) {
104488
- return { source: { kind: "markdown", path: relative16(repoRoot, docsSpecExact) }, checked };
105793
+ return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
104489
105794
  }
104490
105795
  const docsSpecsDir = join74(repoRoot, "docs", "specs");
104491
105796
  if (existsSync28(docsSpecsDir)) {
@@ -104493,7 +105798,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104493
105798
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
104494
105799
  const abs = join74(docsSpecsDir, match);
104495
105800
  if (await isNonEmptyFile(abs)) {
104496
- const relPath = relative16(repoRoot, abs);
105801
+ const relPath = relative17(repoRoot, abs);
104497
105802
  if (!checked.includes(relPath))
104498
105803
  checked.push(relPath);
104499
105804
  return { source: { kind: "markdown", path: relPath }, checked };
@@ -104501,7 +105806,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104501
105806
  }
104502
105807
  }
104503
105808
  const prdAbs = join74(naxDir, "features", name, "prd.json");
104504
- const prdRel = relative16(repoRoot, prdAbs);
105809
+ const prdRel = relative17(repoRoot, prdAbs);
104505
105810
  if (!checked.includes(prdRel))
104506
105811
  checked.push(prdRel);
104507
105812
  if (existsSync28(prdAbs)) {
@@ -104553,8 +105858,8 @@ async function resolveFeatureSpec(name, workdir) {
104553
105858
  return {
104554
105859
  status: "ok",
104555
105860
  featureName: null,
104556
- specSource: { kind: "markdown", path: relative16(repoRoot, abs) },
104557
- message: `resolved spec: ${relative16(repoRoot, abs)}`
105861
+ specSource: { kind: "markdown", path: relative17(repoRoot, abs) },
105862
+ message: `resolved spec: ${relative17(repoRoot, abs)}`
104558
105863
  };
104559
105864
  }
104560
105865
  if (name !== undefined && name.trim() !== "") {
@@ -106052,9 +107357,9 @@ function parseSchedule(input, now2) {
106052
107357
  const trimmed = input.trim();
106053
107358
  if (trimmed === "")
106054
107359
  return { ok: false, error: `Empty schedule value. ${ACCEPTED}` };
106055
- const relative17 = parseRelative(trimmed, now2);
106056
- if (relative17)
106057
- return relative17;
107360
+ const relative18 = parseRelative(trimmed, now2);
107361
+ if (relative18)
107362
+ return relative18;
106058
107363
  const timeOfDay = parseTimeOfDay(trimmed, now2);
106059
107364
  if (timeOfDay)
106060
107365
  return timeOfDay;