@nathapp/nax 0.75.1 → 0.75.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,23 @@ 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()
17315
17321
  }).default({
17316
17322
  enabled: false,
17317
17323
  headers: {},
17318
17324
  serviceName: "nax",
17319
- timeoutMs: 5000
17325
+ timeoutMs: 5000,
17326
+ detail: "counts",
17327
+ heartbeatIntervalMs: 1e4,
17328
+ maxBatchSize: 64,
17329
+ flushIntervalMs: 5000,
17330
+ maxQueueSize: 2048
17320
17331
  });
17321
17332
  ReportersConfigSchema = exports_external.object({
17322
17333
  webhook: WebhookReporterConfigSchema,
@@ -17331,7 +17342,12 @@ var init_schemas_reporters = __esm(() => {
17331
17342
  enabled: false,
17332
17343
  headers: {},
17333
17344
  serviceName: "nax",
17334
- timeoutMs: 5000
17345
+ timeoutMs: 5000,
17346
+ detail: "counts",
17347
+ heartbeatIntervalMs: 1e4,
17348
+ maxBatchSize: 64,
17349
+ flushIntervalMs: 5000,
17350
+ maxQueueSize: 2048
17335
17351
  }
17336
17352
  });
17337
17353
  });
@@ -18222,6 +18238,10 @@ function formatAdvisorySummary(findings, options) {
18222
18238
  if (coverageGapCount > 0) {
18223
18239
  lines.push(c.gray(` ${coverageGapCount} of ${findings.length} were coverage-gap demotions (recurred past the block limit \u2014 candidate for spec/AC review)`));
18224
18240
  }
18241
+ const noActionCount = findings.filter((f) => f.actionRequired === false).length;
18242
+ if (noActionCount > 0) {
18243
+ lines.push(c.gray(` ${noActionCount} of ${findings.length} asked for no change (compliance notes \u2014 the best-effort fix pass skipped them)`));
18244
+ }
18225
18245
  lines.push(c.yellow("\u2500".repeat(60)));
18226
18246
  for (const f of sorted) {
18227
18247
  const location = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : undefined;
@@ -18230,7 +18250,8 @@ function formatAdvisorySummary(findings, options) {
18230
18250
  f.storyId ?? "unknown",
18231
18251
  location,
18232
18252
  f.category,
18233
- f.coverageGap ? "coverage-gap" : undefined
18253
+ f.coverageGap ? "coverage-gap" : undefined,
18254
+ f.actionRequired === false ? "no-action" : undefined
18234
18255
  ].filter((v) => typeof v === "string" && v.length > 0);
18235
18256
  lines.push(` ${c.gray(parts.join(" \xB7 "))}`);
18236
18257
  lines.push(` ${f.issue}`);
@@ -33112,6 +33133,7 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
33112
33133
  "acIndex": 2,
33113
33134
  "scopeQuote": "<out-of-scope findings ONLY: verbatim substring of one Out of Scope entry>",
33114
33135
  "scopeIndex": 1,
33136
+ "actionRequired": true,
33115
33137
  "verifiedBy": {
33116
33138
  "command": "command used to inspect the current codebase",
33117
33139
  "file": "relative/path/to/file.ts",
@@ -33165,6 +33187,11 @@ A finding about code that crossed one of these boundaries must NOT cite an AC \u
33165
33187
  - When the boundary is only a description "Out:" bullet, quote it in \`issue\` and leave \`scopeQuote\`/\`scopeIndex\` unset.
33166
33188
  - Emit scope-violation findings as \`"warning"\` \u2014 never \`"error"\`. Reporting the boundary is the goal; it does not block the story.
33167
33189
 
33190
+ **Do not report compliance as a finding:**
33191
+ 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.
33192
+
33193
+ 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.
33194
+
33168
33195
  Never use \`acIndex: 0\`; \`acIndex\` is 1-based (first AC bullet = 1). The same applies to \`scopeIndex\`.
33169
33196
 
33170
33197
  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 +35914,7 @@ function toAdversarialReviewFindings(findings, opts = {}) {
35887
35914
  message: f.issue,
35888
35915
  suggestion: f.suggestion,
35889
35916
  fixTarget: resolveFixTarget({ base: categoryToFixTarget(f.category), file: f.file, isTestFile: opts.isTestFile }),
35917
+ ...f.actionRequired === false ? { actionRequired: false } : {},
35890
35918
  meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
35891
35919
  };
35892
35920
  });
@@ -36826,11 +36854,11 @@ var init_adversarial_review = __esm(() => {
36826
36854
  throw new ParseValidationError("[adversarial-review] parse failed: invalid JSON shape");
36827
36855
  },
36828
36856
  async verify(parsed, input, _verifyCtx) {
36857
+ const threshold = input.blockingThreshold ?? "error";
36829
36858
  if (parsed.failOpen || parsed.looksLikeFail)
36830
- return parsed;
36859
+ return { ...parsed, blockingThreshold: threshold };
36831
36860
  if (parsed.findings.length === 0)
36832
- return parsed;
36833
- const threshold = input.blockingThreshold ?? "error";
36861
+ return { ...parsed, blockingThreshold: threshold };
36834
36862
  const findings = parsed.findings;
36835
36863
  const substantiated = await substantiateAdversarialFindings({
36836
36864
  findings,
@@ -36873,11 +36901,12 @@ var init_adversarial_review = __esm(() => {
36873
36901
  category: f.category
36874
36902
  });
36875
36903
  }
36876
- const hadBlockingSeverity = accepted.some((f) => isBlockingSeverity(f.severity, threshold));
36877
- const passed = blocking.length === 0 && (parsed.passed || hadBlockingSeverity);
36904
+ const passed = blocking.length === 0 && (parsed.passed || accepted.length > 0);
36878
36905
  return {
36879
36906
  ...parsed,
36880
36907
  passed,
36908
+ blockingThreshold: threshold,
36909
+ modelPassed: parsed.passed,
36881
36910
  findings: accepted,
36882
36911
  normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
36883
36912
  advisoryFindings: [
@@ -39901,9 +39930,29 @@ var init_apply_test_edit_declarations = __esm(() => {
39901
39930
  });
39902
39931
 
39903
39932
  // 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;
39933
+ import { isAbsolute as isAbsolute9, join as join23, relative as relative9 } from "path";
39934
+ function resolutionCandidates(file3, packageDir, repoRoot) {
39935
+ if (isAbsolute9(file3))
39936
+ return [file3];
39937
+ const viaPackageDir = join23(packageDir, file3);
39938
+ if (repoRoot === undefined)
39939
+ return [viaPackageDir];
39940
+ const viaRepoRoot = join23(repoRoot, file3);
39941
+ return viaRepoRoot === viaPackageDir ? [viaPackageDir] : [viaPackageDir, viaRepoRoot];
39942
+ }
39943
+ async function resolvePackageRelative(file3, opts) {
39944
+ for (const candidate of resolutionCandidates(file3, opts.packageDir, opts.repoRoot)) {
39945
+ if (!await opts.fileExists(candidate))
39946
+ continue;
39947
+ const rel = relative9(opts.packageDir, candidate);
39948
+ if (rel === "" || rel.startsWith("..") || isAbsolute9(rel))
39949
+ return null;
39950
+ return rel;
39951
+ }
39952
+ return null;
39953
+ }
39954
+ async function validateMockStructureFiles(declarations, resolvedTestPatterns, packageDir, opts) {
39955
+ const fileExists = opts?.fileExists ?? defaultFileExists;
39907
39956
  const valid = [];
39908
39957
  const invalid = [];
39909
39958
  for (const d of declarations) {
@@ -39914,13 +39963,16 @@ async function validateMockStructureFiles(declarations, resolvedTestPatterns, pa
39914
39963
  const files = d.files ?? [d.file];
39915
39964
  let allValid = true;
39916
39965
  for (const file3 of files) {
39917
- const absolutePath = join23(packageDir, file3);
39918
- const exists = await fileExists(absolutePath);
39919
- if (!exists) {
39966
+ const packageRelative = await resolvePackageRelative(file3, {
39967
+ packageDir,
39968
+ repoRoot: opts?.repoRoot,
39969
+ fileExists
39970
+ });
39971
+ if (packageRelative === null) {
39920
39972
  allValid = false;
39921
39973
  break;
39922
39974
  }
39923
- const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(file3));
39975
+ const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(packageRelative));
39924
39976
  if (!matchesPattern) {
39925
39977
  allValid = false;
39926
39978
  break;
@@ -41458,7 +41510,7 @@ var init_mutation = __esm(() => {
41458
41510
  });
41459
41511
 
41460
41512
  // src/operations/mutation-check.ts
41461
- import { isAbsolute as isAbsolute9, join as join24 } from "path";
41513
+ import { isAbsolute as isAbsolute10, join as join24 } from "path";
41462
41514
  var _mutationCheckDeps, mutationCheckOp;
41463
41515
  var init_mutation_check = __esm(() => {
41464
41516
  init_config();
@@ -41501,7 +41553,7 @@ var init_mutation_check = __esm(() => {
41501
41553
  }
41502
41554
  const changedFiles = await deps.getChangedNonTestFiles(input.workdir, input.storyGitRef, input.packagePrefix, [...input.resolvedTestPatterns.regex], undefined, input.repoRoot);
41503
41555
  const anchor = input.repoRoot ?? input.workdir;
41504
- const absoluteChangedFiles = changedFiles.map((f) => isAbsolute9(f) ? f : join24(anchor, f));
41556
+ const absoluteChangedFiles = changedFiles.map((f) => isAbsolute10(f) ? f : join24(anchor, f));
41505
41557
  const survivors = [];
41506
41558
  const mutants = [];
41507
41559
  for (const file3 of absoluteChangedFiles) {
@@ -41626,6 +41678,31 @@ var init_operations = __esm(() => {
41626
41678
  init_mutation_check();
41627
41679
  });
41628
41680
 
41681
+ // src/findings/cycle-retirement.ts
41682
+ function createDeclineLedger() {
41683
+ const declinedByStrategy = new Map;
41684
+ const hasDeclined = (strategyName, finding) => declinedByStrategy.get(strategyName)?.has(findingKey(finding)) === true;
41685
+ const isRetiredFor = (strategy, findings) => {
41686
+ const claimed = findings.filter((f) => strategy.appliesTo(f));
41687
+ return claimed.length > 0 && claimed.every((f) => hasDeclined(strategy.name, f));
41688
+ };
41689
+ return {
41690
+ recordDeclined(strategy, dispatched) {
41691
+ const declined = declinedByStrategy.get(strategy.name) ?? new Set;
41692
+ for (const f of dispatched.filter((x) => strategy.appliesTo(x)))
41693
+ declined.add(findingKey(f));
41694
+ declinedByStrategy.set(strategy.name, declined);
41695
+ },
41696
+ isRetiredFor,
41697
+ retiredNames(strategies, findings) {
41698
+ return strategies.filter((s) => isRetiredFor(s, findings)).map((s) => s.name);
41699
+ }
41700
+ };
41701
+ }
41702
+ var init_cycle_retirement = __esm(() => {
41703
+ init_types6();
41704
+ });
41705
+
41629
41706
  // src/findings/cycle.ts
41630
41707
  function normalizeValidateResult(r) {
41631
41708
  return Array.isArray(r) ? { findings: r, shortCircuited: false } : r;
@@ -41695,17 +41772,18 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41695
41772
  const storyId = ctx.storyId;
41696
41773
  const packageDir = ctx.packageDir;
41697
41774
  let totalCostUsd = 0;
41698
- const spentStrategies = new Set;
41775
+ const declines = createDeclineLedger();
41699
41776
  let unresolvedDetail;
41700
41777
  const finish = (result) => unresolvedDetail !== undefined && result.unresolvedDetail === undefined ? { ...result, unresolvedDetail } : result;
41701
41778
  for (;; ) {
41702
41779
  if (cycle.findings.length === 0 && cycle.verdict === undefined) {
41703
41780
  return { iterations: cycle.iterations, finalFindings: [], exitReason: "resolved", costUsd: totalCostUsd };
41704
41781
  }
41705
- const selectable = cycle.strategies.filter((s) => !spentStrategies.has(s.name));
41782
+ const selectable = cycle.strategies.filter((s) => !declines.isRetiredFor(s, cycle.findings));
41706
41783
  const active = selectActiveStrategies(selectable, cycle.findings, cycle.verdict);
41707
41784
  if (active.length === 0) {
41708
41785
  const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
41786
+ const retiredStrategies = declines.retiredNames(cycle.strategies, cycle.findings);
41709
41787
  logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
41710
41788
  storyId,
41711
41789
  packageDir,
@@ -41713,7 +41791,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41713
41791
  reason: "no-strategy",
41714
41792
  findingsCount: cycle.findings.length,
41715
41793
  orphanSources,
41716
- ...spentStrategies.size > 0 ? { retiredStrategies: [...spentStrategies] } : {}
41794
+ ...retiredStrategies.length > 0 ? { retiredStrategies } : {}
41717
41795
  });
41718
41796
  return finish({
41719
41797
  iterations: cycle.iterations,
@@ -41784,7 +41862,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41784
41862
  for (const strategy of group) {
41785
41863
  const relevantFindings = findingsBefore.filter((f) => strategy.appliesTo(f));
41786
41864
  const input = strategy.buildInput(relevantFindings, cycle.iterations, ctx);
41787
- const output = await doCallOp(ctx, strategy.fixOp, input);
41865
+ const fixCtx = {
41866
+ ...ctx,
41867
+ fixStrategy: { name: strategy.name, findingsBefore: findingsBefore.length }
41868
+ };
41869
+ const output = await doCallOp(fixCtx, strategy.fixOp, input);
41788
41870
  const extracted = await (strategy.extractApplied?.(output, input) ?? {});
41789
41871
  fixesApplied.push({
41790
41872
  strategyName: strategy.name,
@@ -41799,8 +41881,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
41799
41881
  if (unresolvedFas.length > 0) {
41800
41882
  const firstUnresolved = unresolvedFas[0];
41801
41883
  unresolvedDetail = firstUnresolved.unresolved;
41802
- for (const fa of unresolvedFas)
41803
- spentStrategies.add(fa.strategyName);
41884
+ for (const fa of unresolvedFas) {
41885
+ const strategy = group.find((s) => s.name === fa.strategyName);
41886
+ if (strategy)
41887
+ declines.recordDeclined(strategy, findingsBefore);
41888
+ }
41804
41889
  const allGaveUp = unresolvedFas.length === fixesApplied.length;
41805
41890
  if (allGaveUp) {
41806
41891
  const finishedAt2 = now();
@@ -42015,6 +42100,7 @@ var _cycleDeps;
42015
42100
  var init_cycle = __esm(() => {
42016
42101
  init_logger2();
42017
42102
  init_operations();
42103
+ init_cycle_retirement();
42018
42104
  init_types6();
42019
42105
  _cycleDeps = {
42020
42106
  callOp,
@@ -42346,13 +42432,13 @@ var init_finding_projection = __esm(() => {
42346
42432
  });
42347
42433
 
42348
42434
  // src/review/prepare-inputs.ts
42349
- import { relative as relative9, sep } from "path";
42435
+ import { relative as relative10, sep } from "path";
42350
42436
  function derivePackageDirs(workdir, projectDir) {
42351
42437
  const repoRoot = projectDir ?? workdir;
42352
42438
  const packageDir = workdir !== repoRoot ? workdir : undefined;
42353
42439
  let packageDirRelative;
42354
42440
  if (projectDir && workdir !== projectDir) {
42355
- const rel = relative9(projectDir, workdir);
42441
+ const rel = relative10(projectDir, workdir);
42356
42442
  if (rel !== ".." && !rel.startsWith(`..${sep}`)) {
42357
42443
  packageDirRelative = rel && rel !== "." ? rel : undefined;
42358
42444
  }
@@ -42493,7 +42579,7 @@ var package_default;
42493
42579
  var init_package = __esm(() => {
42494
42580
  package_default = {
42495
42581
  name: "@nathapp/nax",
42496
- version: "0.75.1",
42582
+ version: "0.75.3",
42497
42583
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42498
42584
  type: "module",
42499
42585
  bin: {
@@ -42504,9 +42590,10 @@ var init_package = __esm(() => {
42504
42590
  dev: "bun run bin/nax.ts",
42505
42591
  build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
42506
42592
  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",
42593
+ 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
42594
  "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
42595
  "lint:fix": "bun x biome check --write src/ bin/ flows/",
42596
+ "check:flows-no-bun": "bun run scripts/check-flows-no-bun.ts",
42510
42597
  "check:no-real-global-nax": "bun run scripts/check-no-real-global-nax.ts",
42511
42598
  "check:alias-internals": "bun run scripts/check-alias-internals.ts",
42512
42599
  "check:deep-relatives": "bun run scripts/check-deep-relatives.ts",
@@ -42596,8 +42683,8 @@ var init_version = __esm(() => {
42596
42683
  NAX_VERSION = package_default.version;
42597
42684
  NAX_COMMIT = (() => {
42598
42685
  try {
42599
- if (/^[0-9a-f]{6,10}$/.test("928c0354"))
42600
- return "928c0354";
42686
+ if (/^[0-9a-f]{6,10}$/.test("ae716fd4"))
42687
+ return "ae716fd4";
42601
42688
  } catch {}
42602
42689
  try {
42603
42690
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42686,7 +42773,8 @@ function toAdvisorySummaryEntries(entry) {
42686
42773
  file: f.file,
42687
42774
  line: f.line,
42688
42775
  issue: f.issue ?? "(no description)",
42689
- coverageGap: f.meta?.coverageGap === true ? true : undefined
42776
+ coverageGap: f.meta?.coverageGap === true ? true : undefined,
42777
+ actionRequired: f.actionRequired === false ? false : undefined
42690
42778
  };
42691
42779
  });
42692
42780
  }
@@ -43110,12 +43198,10 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
43110
43198
  cost: llmCost
43111
43199
  };
43112
43200
  }
43113
- if (!opResult.passed && acDropped.length > 0) {
43114
- const allHallucinated = acDropped.every((d) => d.code === "ac_quote_not_substring");
43115
- if (allHallucinated) {
43201
+ if (!opResult.modelPassed && acDropped.length > 0) {
43202
+ if (acDropped.every((d) => d.code === "ac_quote_not_substring")) {
43116
43203
  const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })), { isTestFile: testFileMatch });
43117
- const existingAdvisory = advisoryFindings.length > 0 ? advisoryFindingsAsFindings : [];
43118
- const allAdvisory = [...existingAdvisory, ...demotedFindings];
43204
+ const allAdvisory = [...advisoryFindingsAsFindings, ...demotedFindings];
43119
43205
  logger?.warn("review", "Adversarial review passed: all blocking findings discarded as hallucinated AC quotes", {
43120
43206
  storyId: story.id,
43121
43207
  durationMs,
@@ -43422,7 +43508,7 @@ var init_language_commands = __esm(() => {
43422
43508
  });
43423
43509
 
43424
43510
  // src/review/scoped-lint.ts
43425
- import { join as join27, relative as relative10 } from "path";
43511
+ import { join as join27, relative as relative11 } from "path";
43426
43512
  function shellQuotePath4(path6) {
43427
43513
  return `'${path6.replaceAll("'", "'\\''")}'`;
43428
43514
  }
@@ -43458,7 +43544,7 @@ async function listChangedFiles(workdir, baseRef) {
43458
43544
  function inferActivePackageDir(workdir, projectDir) {
43459
43545
  if (!projectDir)
43460
43546
  return;
43461
- const rel = normalizePath3(relative10(projectDir, workdir));
43547
+ const rel = normalizePath3(relative11(projectDir, workdir));
43462
43548
  if (!rel || rel === "." || rel.startsWith(".."))
43463
43549
  return;
43464
43550
  return rel;
@@ -44612,6 +44698,7 @@ var init_runner2 = __esm(() => {
44612
44698
  // src/review/index.ts
44613
44699
  var init_review = __esm(() => {
44614
44700
  init_semantic_helpers();
44701
+ init_adversarial_helpers();
44615
44702
  init_category_fix_target();
44616
44703
  init_finding_filters();
44617
44704
  init_ac_quote_validator();
@@ -44896,7 +44983,10 @@ REASON: <one paragraph: which mock is wrong vs which dispatch the new code uses,
44896
44983
  Rules:
44897
44984
  - Do NOT make any edits yourself; the test-writer will fulfill.
44898
44985
  - Do NOT also emit \`UNRESOLVED:\` in the same turn \u2014 this declaration IS the handoff.
44899
- - 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 = `
44986
+ - FILES must list real test files. Each path must exist and be a test file.
44987
+ - Write each path exactly as it appears in the findings above (repository-relative).
44988
+ Paths that resolve under neither the repository root nor the package directory are
44989
+ 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 = `
44900
44990
 
44901
44991
  ## Test-edit guidance (single-session implementer)
44902
44992
 
@@ -48678,7 +48768,7 @@ var init_pid_registry = __esm(() => {
48678
48768
  // src/session/manager-deps.ts
48679
48769
  import { randomUUID as randomUUID3 } from "crypto";
48680
48770
  import { mkdir as mkdir5 } from "fs/promises";
48681
- import { isAbsolute as isAbsolute10, join as join30, relative as relative11, sep as sep2 } from "path";
48771
+ import { isAbsolute as isAbsolute11, join as join30, relative as relative12, sep as sep2 } from "path";
48682
48772
  function resolveProjectDirFromScratchDir(scratchDir) {
48683
48773
  const marker = `${sep2}.nax${sep2}features${sep2}`;
48684
48774
  const markerIdx = scratchDir.lastIndexOf(marker);
@@ -48690,7 +48780,7 @@ function resolveProjectDirFromScratchDir(scratchDir) {
48690
48780
  return;
48691
48781
  }
48692
48782
  function toProjectRelativePath(projectDir, pathValue) {
48693
- const relativePath = isAbsolute10(pathValue) ? relative11(projectDir, pathValue) : pathValue;
48783
+ const relativePath = isAbsolute11(pathValue) ? relative12(projectDir, pathValue) : pathValue;
48694
48784
  return relativePath === "" ? "." : relativePath;
48695
48785
  }
48696
48786
  var _sessionManagerDeps;
@@ -50200,7 +50290,7 @@ var init_windsurf = __esm(() => {
50200
50290
 
50201
50291
  // src/context/generator.ts
50202
50292
  import { existsSync as existsSync9 } from "fs";
50203
- import { join as join33, relative as relative12 } from "path";
50293
+ import { join as join33, relative as relative13 } from "path";
50204
50294
  async function loadContextContent(options, config2) {
50205
50295
  if (!_generatorDeps.existsSync(options.contextPath)) {
50206
50296
  throw new Error(`Context file not found: ${options.contextPath}`);
@@ -50328,7 +50418,7 @@ async function discoverWorkspacePackages2(repoRoot) {
50328
50418
  }
50329
50419
  async function generateForPackage(packageDir, config2, dryRun = false, repoRoot) {
50330
50420
  const resolvedRepoRoot = repoRoot ?? packageDir;
50331
- const relativePkgPath = relative12(resolvedRepoRoot, packageDir);
50421
+ const relativePkgPath = relative13(resolvedRepoRoot, packageDir);
50332
50422
  const contextPath = join33(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
50333
50423
  if (!_generatorDeps.existsSync(contextPath)) {
50334
50424
  return [
@@ -54175,7 +54265,7 @@ var init_checks_blockers = __esm(() => {
54175
54265
 
54176
54266
  // src/precheck/checks-warnings.ts
54177
54267
  import { existsSync as existsSync14 } from "fs";
54178
- import { isAbsolute as isAbsolute11 } from "path";
54268
+ import { isAbsolute as isAbsolute12 } from "path";
54179
54269
  async function checkClaudeMdExists(workdir) {
54180
54270
  const claudeMdPath = `${workdir}/CLAUDE.md`;
54181
54271
  const passed = existsSync14(claudeMdPath);
@@ -54310,7 +54400,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
54310
54400
  }
54311
54401
  async function checkHomeEnvValid() {
54312
54402
  const home = process.env.HOME ?? "";
54313
- const passed = home !== "" && isAbsolute11(home);
54403
+ const passed = home !== "" && isAbsolute12(home);
54314
54404
  return {
54315
54405
  name: "home-env-valid",
54316
54406
  tier: "warning",
@@ -57014,6 +57104,79 @@ ${stderr}`;
57014
57104
  };
57015
57105
  });
57016
57106
 
57107
+ // src/pipeline/event-bus.ts
57108
+ class PipelineEventBus {
57109
+ subscribers = new Map;
57110
+ _pending = new Set;
57111
+ on(eventType, subscriber) {
57112
+ const list = this.subscribers.get(eventType) ?? [];
57113
+ list.push(subscriber);
57114
+ this.subscribers.set(eventType, list);
57115
+ return () => {
57116
+ const current = this.subscribers.get(eventType) ?? [];
57117
+ this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
57118
+ };
57119
+ }
57120
+ onAll(subscriber) {
57121
+ const list = this.subscribers.get("*") ?? [];
57122
+ list.push(subscriber);
57123
+ this.subscribers.set("*", list);
57124
+ return () => {
57125
+ const current = this.subscribers.get("*") ?? [];
57126
+ this.subscribers.set("*", current.filter((s) => s !== subscriber));
57127
+ };
57128
+ }
57129
+ emit(event) {
57130
+ const logger = getLogger();
57131
+ const specific = this.subscribers.get(event.type) ?? [];
57132
+ const all = this.subscribers.get("*") ?? [];
57133
+ const targets = [...specific, ...all];
57134
+ for (const sub of targets) {
57135
+ try {
57136
+ const result = sub(event);
57137
+ if (result instanceof Promise) {
57138
+ const tracked = result.catch((err) => {
57139
+ logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57140
+ });
57141
+ this._pending.add(tracked);
57142
+ tracked.finally(() => this._pending.delete(tracked));
57143
+ }
57144
+ } catch (err) {
57145
+ logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
57146
+ }
57147
+ }
57148
+ }
57149
+ async emitAsync(event) {
57150
+ const logger = getLogger();
57151
+ const specific = this.subscribers.get(event.type) ?? [];
57152
+ const all = this.subscribers.get("*") ?? [];
57153
+ const targets = [...specific, ...all];
57154
+ await Promise.allSettled(targets.map(async (sub) => {
57155
+ try {
57156
+ await sub(event);
57157
+ } catch (err) {
57158
+ logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57159
+ }
57160
+ }));
57161
+ }
57162
+ async drain() {
57163
+ if (this._pending.size === 0)
57164
+ return;
57165
+ await Promise.allSettled([...this._pending]);
57166
+ }
57167
+ clear() {
57168
+ this.subscribers.clear();
57169
+ }
57170
+ subscriberCount(eventType) {
57171
+ return (this.subscribers.get(eventType) ?? []).length;
57172
+ }
57173
+ }
57174
+ var pipelineEventBus;
57175
+ var init_event_bus = __esm(() => {
57176
+ init_logger2();
57177
+ pipelineEventBus = new PipelineEventBus;
57178
+ });
57179
+
57017
57180
  // src/pipeline/stages/acceptance-setup.ts
57018
57181
  var exports_acceptance_setup = {};
57019
57182
  __export(exports_acceptance_setup, {
@@ -57029,6 +57192,214 @@ function computeACFingerprint(criteria) {
57029
57192
  hasher.update(sorted);
57030
57193
  return `sha256:${hasher.digest("hex")}`;
57031
57194
  }
57195
+ async function runAcceptanceSetup(ctx, featureDir, phaseStartTime) {
57196
+ const language = ctx.config.project?.language;
57197
+ const testPathConfig = ctx.config.acceptance.testPath;
57198
+ const metaPath = path12.join(featureDir, "acceptance-meta.json");
57199
+ const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
57200
+ const featureName = ctx.prd.feature ?? ctx.prd.featureName;
57201
+ const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
57202
+ const nonFixStories = groups.flatMap((g) => g.stories);
57203
+ let totalCriteria = 0;
57204
+ let testableCount = 0;
57205
+ const fingerprint = computeACFingerprint(allCriteria);
57206
+ const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
57207
+ getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
57208
+ currentFingerprint: fingerprint,
57209
+ storedFingerprint: meta3?.acFingerprint ?? "none",
57210
+ match: meta3?.acFingerprint === fingerprint
57211
+ });
57212
+ let shouldGenerate = false;
57213
+ let regenerated = false;
57214
+ if (!meta3 || meta3.acFingerprint !== fingerprint) {
57215
+ if (!meta3) {
57216
+ getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
57217
+ } else {
57218
+ getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
57219
+ reason: "fingerprint mismatch",
57220
+ currentFingerprint: fingerprint,
57221
+ storedFingerprint: meta3.acFingerprint
57222
+ });
57223
+ }
57224
+ for (const { testPath } of groups) {
57225
+ if (await _acceptanceSetupDeps.fileExists(testPath)) {
57226
+ await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
57227
+ await _acceptanceSetupDeps.deleteFile(testPath);
57228
+ }
57229
+ }
57230
+ await _acceptanceSetupDeps.deleteSemanticVerdicts(featureDir);
57231
+ shouldGenerate = true;
57232
+ regenerated = true;
57233
+ } else {
57234
+ getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
57235
+ }
57236
+ if (shouldGenerate) {
57237
+ totalCriteria = allCriteria.length;
57238
+ let allRefinedCriteria;
57239
+ if (ctx.config.acceptance.refinement) {
57240
+ const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
57241
+ const results = new Array(nonFixStories.length);
57242
+ const executing = new Set;
57243
+ for (let i = 0;i < nonFixStories.length; i++) {
57244
+ const story = nonFixStories[i];
57245
+ const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
57246
+ criteria: story.acceptanceCriteria,
57247
+ codebaseContext: "",
57248
+ storyId: story.id,
57249
+ testStrategy: ctx.config.acceptance.testStrategy,
57250
+ testFramework: ctx.config.acceptance.testFramework,
57251
+ storyTitle: story.title,
57252
+ storyDescription: story.description
57253
+ }, story.id).then((refined) => {
57254
+ results[i] = refined;
57255
+ }).catch(() => {
57256
+ getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
57257
+ storyId: story.id
57258
+ });
57259
+ results[i] = story.acceptanceCriteria.map((c) => ({
57260
+ original: c,
57261
+ refined: c,
57262
+ testable: true,
57263
+ storyId: story.id
57264
+ }));
57265
+ }).finally(() => {
57266
+ executing.delete(task);
57267
+ });
57268
+ executing.add(task);
57269
+ if (executing.size >= maxConcurrency) {
57270
+ await Promise.race(executing);
57271
+ }
57272
+ }
57273
+ await Promise.all(executing);
57274
+ allRefinedCriteria = results.flat();
57275
+ } else {
57276
+ allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
57277
+ original: c,
57278
+ refined: c,
57279
+ testable: true,
57280
+ storyId: story.id
57281
+ })));
57282
+ }
57283
+ testableCount = allRefinedCriteria.filter((r) => r.testable).length;
57284
+ for (const group of groups) {
57285
+ const { testPath, packageDir } = group;
57286
+ const groupStoryIds = new Set(group.stories.map((s) => s.id));
57287
+ const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
57288
+ const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
57289
+ `);
57290
+ const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
57291
+ [FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
57292
+ const groupStoryId = group.stories[0]?.id;
57293
+ const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
57294
+ featureName: featureName ?? "",
57295
+ criteriaList,
57296
+ frameworkOverrideLine,
57297
+ targetTestFilePath: testPath,
57298
+ ..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
57299
+ }, groupStoryId);
57300
+ const testCode = genResult.testCode;
57301
+ if (testCode) {
57302
+ await _acceptanceSetupDeps.writeFile(testPath, testCode);
57303
+ } else {
57304
+ const skeletonCriteria = groupRefined.map((c, i) => ({
57305
+ id: `AC-${i + 1}`,
57306
+ text: c.refined,
57307
+ lineNumber: i + 1
57308
+ }));
57309
+ const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
57310
+ await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
57311
+ getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
57312
+ storyId: groupStoryId,
57313
+ testPath
57314
+ });
57315
+ }
57316
+ }
57317
+ if (allRefinedCriteria.length > 0) {
57318
+ const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
57319
+ acId: `AC-${i + 1}`,
57320
+ original: c.original,
57321
+ refined: c.refined,
57322
+ testable: c.testable,
57323
+ storyId: c.storyId
57324
+ })), null, 2);
57325
+ await _acceptanceSetupDeps.writeFile(path12.join(featureDir, "acceptance-refined.json"), refinedJsonContent);
57326
+ }
57327
+ const fingerprint2 = computeACFingerprint(allCriteria);
57328
+ await _acceptanceSetupDeps.writeMeta(metaPath, {
57329
+ generatedAt: new Date().toISOString(),
57330
+ acFingerprint: fingerprint2,
57331
+ storyCount: ctx.prd.userStories.length,
57332
+ acCount: totalCriteria,
57333
+ generator: "nax"
57334
+ });
57335
+ await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
57336
+ }
57337
+ const acceptanceTestPaths = [];
57338
+ for (const g of groups) {
57339
+ const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
57340
+ let groupConfig = ctx.config;
57341
+ if (relativeWorkdir && relativeWorkdir !== ".") {
57342
+ try {
57343
+ groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
57344
+ } catch {
57345
+ groupConfig = ctx.config;
57346
+ }
57347
+ }
57348
+ acceptanceTestPaths.push({
57349
+ testPath: g.testPath,
57350
+ packageDir: g.packageDir,
57351
+ testFramework: groupConfig.project?.testFramework,
57352
+ commandOverride: groupConfig.acceptance.command
57353
+ });
57354
+ }
57355
+ ctx.acceptanceTestPaths = acceptanceTestPaths;
57356
+ if (ctx.config.acceptance.redGate === false) {
57357
+ ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57358
+ pipelineEventBus.emit({
57359
+ type: "postrun:phase:completed",
57360
+ phase: "acceptance-setup",
57361
+ passed: true,
57362
+ durationMs: Date.now() - phaseStartTime,
57363
+ details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
57364
+ });
57365
+ return { action: "continue" };
57366
+ }
57367
+ let redFailCount = 0;
57368
+ for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
57369
+ const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
57370
+ getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
57371
+ cmd: runCmd.join(" "),
57372
+ packageDir
57373
+ });
57374
+ const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
57375
+ if (exitCode !== 0) {
57376
+ redFailCount++;
57377
+ }
57378
+ }
57379
+ if (redFailCount === 0) {
57380
+ ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57381
+ pipelineEventBus.emit({
57382
+ type: "postrun:phase:completed",
57383
+ phase: "acceptance-setup",
57384
+ passed: true,
57385
+ durationMs: Date.now() - phaseStartTime,
57386
+ details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
57387
+ });
57388
+ return {
57389
+ action: "skip",
57390
+ reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
57391
+ };
57392
+ }
57393
+ ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
57394
+ pipelineEventBus.emit({
57395
+ type: "postrun:phase:completed",
57396
+ phase: "acceptance-setup",
57397
+ passed: true,
57398
+ durationMs: Date.now() - phaseStartTime,
57399
+ details: { totalCriteria, testableCount, redFailCount, regenerated }
57400
+ });
57401
+ return { action: "continue" };
57402
+ }
57032
57403
  var _acceptanceSetupDeps, acceptanceSetupStage;
57033
57404
  var init_acceptance_setup = __esm(() => {
57034
57405
  init_acceptance2();
@@ -57037,6 +57408,7 @@ var init_acceptance_setup = __esm(() => {
57037
57408
  init_logger2();
57038
57409
  init_operations();
57039
57410
  init_git();
57411
+ init_event_bus();
57040
57412
  _acceptanceSetupDeps = {
57041
57413
  getAgent: (_name) => {
57042
57414
  return;
@@ -57136,189 +57508,19 @@ ${stderr}` };
57136
57508
  if (!ctx.featureDir) {
57137
57509
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
57138
57510
  }
57139
- const language = ctx.config.project?.language;
57140
- const testPathConfig = ctx.config.acceptance.testPath;
57141
- const metaPath = path12.join(ctx.featureDir, "acceptance-meta.json");
57142
- const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
57143
- const featureName = ctx.prd.feature ?? ctx.prd.featureName;
57144
- const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
57145
- const nonFixStories = groups.flatMap((g) => g.stories);
57146
- let totalCriteria = 0;
57147
- let testableCount = 0;
57148
- const fingerprint = computeACFingerprint(allCriteria);
57149
- const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
57150
- getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
57151
- currentFingerprint: fingerprint,
57152
- storedFingerprint: meta3?.acFingerprint ?? "none",
57153
- match: meta3?.acFingerprint === fingerprint
57154
- });
57155
- let shouldGenerate = false;
57156
- if (!meta3 || meta3.acFingerprint !== fingerprint) {
57157
- if (!meta3) {
57158
- getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
57159
- } else {
57160
- getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
57161
- reason: "fingerprint mismatch",
57162
- currentFingerprint: fingerprint,
57163
- storedFingerprint: meta3.acFingerprint
57164
- });
57165
- }
57166
- for (const { testPath } of groups) {
57167
- if (await _acceptanceSetupDeps.fileExists(testPath)) {
57168
- await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
57169
- await _acceptanceSetupDeps.deleteFile(testPath);
57170
- }
57171
- }
57172
- await _acceptanceSetupDeps.deleteSemanticVerdicts(ctx.featureDir);
57173
- shouldGenerate = true;
57174
- } else {
57175
- getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
57176
- }
57177
- if (shouldGenerate) {
57178
- totalCriteria = allCriteria.length;
57179
- let allRefinedCriteria;
57180
- if (ctx.config.acceptance.refinement) {
57181
- const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
57182
- const results = new Array(nonFixStories.length);
57183
- const executing = new Set;
57184
- for (let i = 0;i < nonFixStories.length; i++) {
57185
- const story = nonFixStories[i];
57186
- const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
57187
- criteria: story.acceptanceCriteria,
57188
- codebaseContext: "",
57189
- storyId: story.id,
57190
- testStrategy: ctx.config.acceptance.testStrategy,
57191
- testFramework: ctx.config.acceptance.testFramework,
57192
- storyTitle: story.title,
57193
- storyDescription: story.description
57194
- }, story.id).then((refined) => {
57195
- results[i] = refined;
57196
- }).catch(() => {
57197
- getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
57198
- storyId: story.id
57199
- });
57200
- results[i] = story.acceptanceCriteria.map((c) => ({
57201
- original: c,
57202
- refined: c,
57203
- testable: true,
57204
- storyId: story.id
57205
- }));
57206
- }).finally(() => {
57207
- executing.delete(task);
57208
- });
57209
- executing.add(task);
57210
- if (executing.size >= maxConcurrency) {
57211
- await Promise.race(executing);
57212
- }
57213
- }
57214
- await Promise.all(executing);
57215
- allRefinedCriteria = results.flat();
57216
- } else {
57217
- allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
57218
- original: c,
57219
- refined: c,
57220
- testable: true,
57221
- storyId: story.id
57222
- })));
57223
- }
57224
- testableCount = allRefinedCriteria.filter((r) => r.testable).length;
57225
- for (const group of groups) {
57226
- const { testPath, packageDir } = group;
57227
- const groupStoryIds = new Set(group.stories.map((s) => s.id));
57228
- const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
57229
- const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
57230
- `);
57231
- const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
57232
- [FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
57233
- const groupStoryId = group.stories[0]?.id;
57234
- const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
57235
- featureName: featureName ?? "",
57236
- criteriaList,
57237
- frameworkOverrideLine,
57238
- targetTestFilePath: testPath,
57239
- ..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
57240
- }, groupStoryId);
57241
- const testCode = genResult.testCode;
57242
- if (testCode) {
57243
- await _acceptanceSetupDeps.writeFile(testPath, testCode);
57244
- } else {
57245
- const skeletonCriteria = groupRefined.map((c, i) => ({
57246
- id: `AC-${i + 1}`,
57247
- text: c.refined,
57248
- lineNumber: i + 1
57249
- }));
57250
- const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
57251
- await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
57252
- getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
57253
- storyId: groupStoryId,
57254
- testPath
57255
- });
57256
- }
57257
- }
57258
- if (allRefinedCriteria.length > 0) {
57259
- const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
57260
- acId: `AC-${i + 1}`,
57261
- original: c.original,
57262
- refined: c.refined,
57263
- testable: c.testable,
57264
- storyId: c.storyId
57265
- })), null, 2);
57266
- await _acceptanceSetupDeps.writeFile(path12.join(ctx.featureDir, "acceptance-refined.json"), refinedJsonContent);
57267
- }
57268
- const fingerprint2 = computeACFingerprint(allCriteria);
57269
- await _acceptanceSetupDeps.writeMeta(metaPath, {
57270
- generatedAt: new Date().toISOString(),
57271
- acFingerprint: fingerprint2,
57272
- storyCount: ctx.prd.userStories.length,
57273
- acCount: totalCriteria,
57274
- generator: "nax"
57275
- });
57276
- await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
57277
- }
57278
- const acceptanceTestPaths = [];
57279
- for (const g of groups) {
57280
- const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
57281
- let groupConfig = ctx.config;
57282
- if (relativeWorkdir && relativeWorkdir !== ".") {
57283
- try {
57284
- groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
57285
- } catch {
57286
- groupConfig = ctx.config;
57287
- }
57288
- }
57289
- acceptanceTestPaths.push({
57290
- testPath: g.testPath,
57291
- packageDir: g.packageDir,
57292
- testFramework: groupConfig.project?.testFramework,
57293
- commandOverride: groupConfig.acceptance.command
57294
- });
57295
- }
57296
- ctx.acceptanceTestPaths = acceptanceTestPaths;
57297
- if (ctx.config.acceptance.redGate === false) {
57298
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57299
- return { action: "continue" };
57300
- }
57301
- let redFailCount = 0;
57302
- for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
57303
- const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
57304
- getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
57305
- cmd: runCmd.join(" "),
57306
- packageDir
57511
+ const phaseStartTime = Date.now();
57512
+ pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance-setup" });
57513
+ try {
57514
+ return await runAcceptanceSetup(ctx, ctx.featureDir, phaseStartTime);
57515
+ } catch (err) {
57516
+ pipelineEventBus.emit({
57517
+ type: "postrun:phase:completed",
57518
+ phase: "acceptance-setup",
57519
+ passed: false,
57520
+ durationMs: Date.now() - phaseStartTime
57307
57521
  });
57308
- const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
57309
- if (exitCode !== 0) {
57310
- redFailCount++;
57311
- }
57312
- }
57313
- if (redFailCount === 0) {
57314
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57315
- return {
57316
- action: "skip",
57317
- reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
57318
- };
57522
+ throw err;
57319
57523
  }
57320
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
57321
- return { action: "continue" };
57322
57524
  }
57323
57525
  };
57324
57526
  });
@@ -57464,79 +57666,6 @@ async function appendProgress(featureDir, storyId, status, message) {
57464
57666
  }
57465
57667
  var init_progress = () => {};
57466
57668
 
57467
- // src/pipeline/event-bus.ts
57468
- class PipelineEventBus {
57469
- subscribers = new Map;
57470
- _pending = new Set;
57471
- on(eventType, subscriber) {
57472
- const list = this.subscribers.get(eventType) ?? [];
57473
- list.push(subscriber);
57474
- this.subscribers.set(eventType, list);
57475
- return () => {
57476
- const current = this.subscribers.get(eventType) ?? [];
57477
- this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
57478
- };
57479
- }
57480
- onAll(subscriber) {
57481
- const list = this.subscribers.get("*") ?? [];
57482
- list.push(subscriber);
57483
- this.subscribers.set("*", list);
57484
- return () => {
57485
- const current = this.subscribers.get("*") ?? [];
57486
- this.subscribers.set("*", current.filter((s) => s !== subscriber));
57487
- };
57488
- }
57489
- emit(event) {
57490
- const logger = getLogger();
57491
- const specific = this.subscribers.get(event.type) ?? [];
57492
- const all = this.subscribers.get("*") ?? [];
57493
- const targets = [...specific, ...all];
57494
- for (const sub of targets) {
57495
- try {
57496
- const result = sub(event);
57497
- if (result instanceof Promise) {
57498
- const tracked = result.catch((err) => {
57499
- logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57500
- });
57501
- this._pending.add(tracked);
57502
- tracked.finally(() => this._pending.delete(tracked));
57503
- }
57504
- } catch (err) {
57505
- logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
57506
- }
57507
- }
57508
- }
57509
- async emitAsync(event) {
57510
- const logger = getLogger();
57511
- const specific = this.subscribers.get(event.type) ?? [];
57512
- const all = this.subscribers.get("*") ?? [];
57513
- const targets = [...specific, ...all];
57514
- await Promise.allSettled(targets.map(async (sub) => {
57515
- try {
57516
- await sub(event);
57517
- } catch (err) {
57518
- logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57519
- }
57520
- }));
57521
- }
57522
- async drain() {
57523
- if (this._pending.size === 0)
57524
- return;
57525
- await Promise.allSettled([...this._pending]);
57526
- }
57527
- clear() {
57528
- this.subscribers.clear();
57529
- }
57530
- subscriberCount(eventType) {
57531
- return (this.subscribers.get(eventType) ?? []).length;
57532
- }
57533
- }
57534
- var pipelineEventBus;
57535
- var init_event_bus = __esm(() => {
57536
- init_logger2();
57537
- pipelineEventBus = new PipelineEventBus;
57538
- });
57539
-
57540
57669
  // src/pipeline/stages/completion.ts
57541
57670
  async function getDiffText(workdir, baseRef) {
57542
57671
  if (!baseRef)
@@ -58328,11 +58457,11 @@ var init_rollback = __esm(() => {
58328
58457
  });
58329
58458
 
58330
58459
  // src/utils/paths.ts
58331
- import { join as join49, relative as relative13, sep as sep4 } from "path";
58460
+ import { join as join49, relative as relative14, sep as sep4 } from "path";
58332
58461
  function packageDirRelative(projectDir, workdir) {
58333
58462
  if (!projectDir || !workdir || workdir === projectDir)
58334
58463
  return;
58335
- const rel = relative13(projectDir, workdir);
58464
+ const rel = relative14(projectDir, workdir);
58336
58465
  if (rel === ".." || rel.startsWith(`..${sep4}`))
58337
58466
  return;
58338
58467
  return rel && rel !== "." ? rel : undefined;
@@ -58348,6 +58477,9 @@ var init_paths3 = __esm(() => {
58348
58477
  });
58349
58478
 
58350
58479
  // src/execution/non-blocking-fix.ts
58480
+ function actionableAdvisoryFindings(findings) {
58481
+ return findings.filter((f) => f.actionRequired !== false);
58482
+ }
58351
58483
  function shouldRunNonBlockingFix(cfg, advisoryCount) {
58352
58484
  return cfg?.enabled === true && advisoryCount > 0;
58353
58485
  }
@@ -58420,9 +58552,16 @@ async function runNonBlockingFix(args, overrides = {}) {
58420
58552
  exhausted = true;
58421
58553
  }
58422
58554
  if (!exhausted) {
58423
- if (args.keptTreeRegressed?.()) {
58555
+ const gateVerdict = args.keptTreeRegressed?.();
58556
+ if (gateVerdict?.regressed) {
58424
58557
  logger?.info("non-blocking-fix", "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", {
58425
- storyId: args.storyId
58558
+ storyId: args.storyId,
58559
+ regressedKeys: gateVerdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
58560
+ regressedKeyCount: gateVerdict.regressedKeys.length,
58561
+ baselineKeySize: gateVerdict.baselineKeySize,
58562
+ keyless: gateVerdict.keyless,
58563
+ memoExcludedKeyCount: gateVerdict.memoExcludedKeys.length,
58564
+ flakeTriageRan: false
58426
58565
  });
58427
58566
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58428
58567
  }
@@ -58466,7 +58605,7 @@ async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot,
58466
58605
  });
58467
58606
  return { ran: true, kept: false, restored: true };
58468
58607
  }
58469
- var REVIEW_PHASE_KINDS, _nonBlockingFixDeps, DEFAULT_DEPS;
58608
+ var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
58470
58609
  var init_non_blocking_fix = __esm(() => {
58471
58610
  init_logger2();
58472
58611
  init_rollback();
@@ -58609,13 +58748,29 @@ function gateFailureKeys(gateOutput) {
58609
58748
  }
58610
58749
  return keys;
58611
58750
  }
58612
- function gateRegressedAfterRectification(finalGateOutput, baselineKeys, gateName, storyId) {
58613
- if (phasePassed(gateName, finalGateOutput, storyId))
58614
- return false;
58615
- const finalKeys = gateFailureKeys(finalGateOutput);
58616
- const hasNewStructuredKey = [...finalKeys].some((k) => !baselineKeys.has(k));
58617
- const isKeylessFailure = finalKeys.size === 0 || finalKeys.has(KEYLESS_GATE_FAILURE_KEY);
58618
- return hasNewStructuredKey || isKeylessFailure;
58751
+ function describeGateRegression(input) {
58752
+ const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
58753
+ const notRegressed = {
58754
+ regressed: false,
58755
+ regressedKeys: [],
58756
+ memoExcludedKeys: [],
58757
+ baselineKeySize: baselineKeys.size,
58758
+ keyless: false
58759
+ };
58760
+ if (gateName === undefined || phasePassed(gateName, gateOutput, storyId))
58761
+ return notRegressed;
58762
+ const allKeys = gateFailureKeys(gateOutput);
58763
+ const memoExcludedKeys = quarantineMemo ? [...allKeys].filter((k) => quarantineMemo.has(k)) : [];
58764
+ const excluded = new Set(memoExcludedKeys);
58765
+ const regressedKeys = [...allKeys].filter((k) => !baselineKeys.has(k) && !excluded.has(k));
58766
+ const keyless = allKeys.size === 0 || allKeys.has(KEYLESS_GATE_FAILURE_KEY);
58767
+ return {
58768
+ regressed: regressedKeys.length > 0 || keyless,
58769
+ regressedKeys,
58770
+ memoExcludedKeys,
58771
+ baselineKeySize: baselineKeys.size,
58772
+ keyless
58773
+ };
58619
58774
  }
58620
58775
  function phasesToRevalidate(strategiesRun, allPhases) {
58621
58776
  if (!strategiesRun || strategiesRun.length === 0)
@@ -58762,6 +58917,9 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
58762
58917
  const data = { storyId, phase: opName, durationMs };
58763
58918
  if (findingsCount !== undefined)
58764
58919
  data.findingsCount = findingsCount;
58920
+ const identities = extractPhaseFindings(output).slice(0, MAX_LOGGED_FINDING_IDENTITIES).map((f) => `${f.file ?? ""}::${f.rule ?? ""}`);
58921
+ if (identities.length > 0)
58922
+ data.findingIdentities = identities;
58765
58923
  if (status !== undefined)
58766
58924
  data.status = status;
58767
58925
  if (typeof r.failureCategory === "string")
@@ -58793,8 +58951,10 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
58793
58951
  logger?.warn("story-orchestrator", message, data);
58794
58952
  }
58795
58953
  }
58954
+ var MAX_LOGGED_FINDING_IDENTITIES = 10;
58796
58955
  var init_story_orchestrator_logging = __esm(() => {
58797
58956
  init_logger2();
58957
+ init_phase_eval();
58798
58958
  });
58799
58959
  // src/verification/flake-baseline-diff.ts
58800
58960
  async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
@@ -59067,10 +59227,13 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59067
59227
  const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
59068
59228
  let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
59069
59229
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
59230
+ let advIterationBefore = 0;
59070
59231
  if (opName === "adversarial-review" && ctx.storyId) {
59232
+ const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59233
+ advIterationBefore = priorIterations.length;
59071
59234
  dispatchInput = {
59072
59235
  ...dispatchInput,
59073
- priorAdversarialIterations: getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId)
59236
+ priorAdversarialIterations: priorIterations
59074
59237
  };
59075
59238
  }
59076
59239
  if (isTddPhase) {
@@ -59087,6 +59250,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59087
59250
  }
59088
59251
  const phaseStartedAt = Date.now();
59089
59252
  const scope = ctx.runtime.costAggregator.openScope();
59253
+ let outcome = "passed";
59090
59254
  try {
59091
59255
  const output = await _storyOrchestratorDeps.callOp({ ...ctx, scopeId: scope.scopeId }, slot.op, dispatchInput);
59092
59256
  phaseOutputs[opName] = output;
@@ -59100,6 +59264,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59100
59264
  }
59101
59265
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
59102
59266
  logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
59267
+ outcome = derivePhaseOutcome(output);
59103
59268
  if (isTddPhase) {
59104
59269
  const durationMs = Date.now() - phaseStartedAt;
59105
59270
  logger?.info("tdd", `Session complete: ${opName}`, {
@@ -59129,11 +59294,95 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59129
59294
  }
59130
59295
  }
59131
59296
  return output;
59297
+ } catch (err) {
59298
+ outcome = "error";
59299
+ throw err;
59132
59300
  } finally {
59133
- phaseCosts[opName] = (phaseCosts[opName] ?? 0) + scope.snapshot().totalCostUsd;
59301
+ const snapshot = scope.snapshot();
59302
+ phaseCosts[opName] = (phaseCosts[opName] ?? 0) + snapshot.totalCostUsd;
59134
59303
  scope.close();
59304
+ if (ctx.storyId) {
59305
+ const phaseDetails = buildPhaseDetails(opName, phaseOutputs[opName], isThreeSession, ctx.packageView.config, advIterationBefore, ctx.fixStrategy);
59306
+ const event = {
59307
+ type: "story:phase:completed",
59308
+ storyId: ctx.storyId,
59309
+ phase: opName,
59310
+ outcome,
59311
+ durationMs: Date.now() - phaseStartedAt,
59312
+ costUsd: snapshot.totalCostUsd,
59313
+ ...ctx.phaseTelemetry ? {
59314
+ tier: ctx.phaseTelemetry.tier,
59315
+ testStrategy: ctx.phaseTelemetry.testStrategy,
59316
+ sessionModel: ctx.phaseTelemetry.sessionModel
59317
+ } : {},
59318
+ ...phaseDetails !== undefined ? { details: phaseDetails } : {}
59319
+ };
59320
+ pipelineEventBus.emit(event);
59321
+ }
59135
59322
  }
59136
59323
  }
59324
+ function buildPhaseDetails(opName, output, isThreeSession, config2, advIterationBefore, fixStrategy) {
59325
+ if (fixStrategy) {
59326
+ return { kind: "fix", strategy: fixStrategy.name, findingsBefore: fixStrategy.findingsBefore };
59327
+ }
59328
+ if (output === null || typeof output !== "object")
59329
+ return;
59330
+ if (opName === "adversarial-review") {
59331
+ const adv = output;
59332
+ const findings = adv?.normalizedFindings ?? [];
59333
+ const threshold = adv?.blockingThreshold ?? "error";
59334
+ const bySeverity = Object.fromEntries(ALL_FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]));
59335
+ const blockingCount = findings.filter((f) => isBlockingSeverity(f.severity, threshold)).length;
59336
+ const advisoryCount = findings.length - blockingCount;
59337
+ return {
59338
+ kind: "review",
59339
+ reviewer: "adversarial",
59340
+ iteration: advIterationBefore,
59341
+ bySeverity,
59342
+ blockingCount,
59343
+ advisoryCount,
59344
+ ...config2.reporters?.otel?.detail === "verbose" ? {
59345
+ items: findings.map((f) => ({
59346
+ message: f.message,
59347
+ severity: f.severity,
59348
+ ...f.rule ? { rule: f.rule } : {},
59349
+ ...f.file ? { file: f.file } : {}
59350
+ }))
59351
+ } : {}
59352
+ };
59353
+ }
59354
+ if (opName === "implementer") {
59355
+ const impl = output;
59356
+ const filesChanged = (impl?.filesChanged ?? []).length;
59357
+ if (isThreeSession) {
59358
+ return { kind: "authoring", role: "implementer", filesChanged, isolationPassed: impl?.isolation?.passed };
59359
+ }
59360
+ return { kind: "authoring", role: "implementer", filesChanged };
59361
+ }
59362
+ if (opName === "test-writer") {
59363
+ const tw = output;
59364
+ return { kind: "authoring", role: "test-writer", filesChanged: (tw?.filesChanged ?? []).length };
59365
+ }
59366
+ if (opName === "full-suite-gate") {
59367
+ const gate = output;
59368
+ return { kind: "gate", gate: "full-suite", failureCount: gate?.failureCount ?? 0 };
59369
+ }
59370
+ if (opName === "verifier" || opName === "verify-scoped") {
59371
+ const verdict = output;
59372
+ return { kind: "verdict", role: opName, passed: verdict.passed ?? false, failureCount: verdict.failureCount ?? 0 };
59373
+ }
59374
+ return;
59375
+ }
59376
+ function derivePhaseOutcome(output) {
59377
+ const built = buildPhaseOutcomeLogData(undefined, "", output, 0);
59378
+ if (!built)
59379
+ return "passed";
59380
+ if (built.success)
59381
+ return "passed";
59382
+ if (built.data.status === "skipped")
59383
+ return "skipped";
59384
+ return "failed";
59385
+ }
59137
59386
  function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
59138
59387
  if (!enabled)
59139
59388
  return strategies;
@@ -59157,7 +59406,7 @@ function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
59157
59406
  }
59158
59407
  }));
59159
59408
  }
59160
- var _storyOrchestratorDeps;
59409
+ var _storyOrchestratorDeps, ALL_FINDING_SEVERITIES;
59161
59410
  var init_run_phase = __esm(() => {
59162
59411
  init_findings();
59163
59412
  init_logger2();
@@ -59187,6 +59436,14 @@ var init_run_phase = __esm(() => {
59187
59436
  },
59188
59437
  loadCheckpoints: async (_featureDir) => new Map
59189
59438
  };
59439
+ ALL_FINDING_SEVERITIES = [
59440
+ "critical",
59441
+ "error",
59442
+ "warning",
59443
+ "info",
59444
+ "low",
59445
+ "unverifiable"
59446
+ ];
59190
59447
  });
59191
59448
 
59192
59449
  // src/execution/story-orchestrator/rectification.ts
@@ -59410,6 +59667,15 @@ class ExecutionPlan {
59410
59667
  this.state = state;
59411
59668
  this.isThreeSession = isThreeSession;
59412
59669
  }
59670
+ describeGateRegressionNow(phaseOutputs, gateName, baselineKeys) {
59671
+ return describeGateRegression({
59672
+ gateOutput: gateName === undefined ? undefined : phaseOutputs[gateName],
59673
+ baselineKeys,
59674
+ gateName,
59675
+ storyId: this.ctx.storyId,
59676
+ quarantineMemo: this.ctx.runtime.quarantineMemo
59677
+ });
59678
+ }
59413
59679
  phaseNames() {
59414
59680
  const names = collectOrderedPhases(this.state).map((p) => p.slot.op.name);
59415
59681
  if (this.state.rectification) {
@@ -59546,7 +59812,7 @@ class ExecutionPlan {
59546
59812
  const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
59547
59813
  const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
59548
59814
  const advisoryOut = phaseOutputs["adversarial-review"];
59549
- const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
59815
+ const advisoryFindings = actionableAdvisoryFindings(advisoryOut?.advisoryFindings ?? []);
59550
59816
  if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
59551
59817
  await _storyOrchestratorDeps.runNonBlockingFix({
59552
59818
  workdir: this.ctx.packageDir,
@@ -59563,7 +59829,7 @@ class ExecutionPlan {
59563
59829
  maxAttempts,
59564
59830
  postValidate: this.state.nonBlockingFixPostValidate
59565
59831
  }),
59566
- keptTreeRegressed: () => gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId)
59832
+ keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
59567
59833
  }, {
59568
59834
  measureSourceDiff: createMeasureSourceDiff({
59569
59835
  config: this.ctx.runtime.configLoader.current(),
@@ -59574,7 +59840,7 @@ class ExecutionPlan {
59574
59840
  }
59575
59841
  const verifierName = this.state.verifier?.slot.op.name;
59576
59842
  const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
59577
- const gateRegressedDuringRect = gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId);
59843
+ const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
59578
59844
  const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
59579
59845
  if (verifierExplicitlyPassed && gateRegressedDuringRect) {
59580
59846
  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 });
@@ -59794,8 +60060,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59794
60060
  if (inputs.adversarialReview) {
59795
60061
  builder.addAdversarialReview(inputs.adversarialReview);
59796
60062
  }
59797
- const packageDir = join50(ctx.packageDir, story.workdir ?? "");
59798
- const resolvedTestPatterns = await resolveTestFilePatterns(config2, ctx.packageDir, story.workdir);
60063
+ const repoRoot = ctx.packageDir;
60064
+ const packageDir = join50(repoRoot, story.workdir ?? "");
60065
+ const resolvedTestPatterns = await resolveTestFilePatterns(config2, repoRoot, story.workdir);
59799
60066
  if (shouldRunRectification(config2) && inputs.rectification) {
59800
60067
  const sink = makeDeclarationSink();
59801
60068
  const strategies = [];
@@ -59831,7 +60098,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59831
60098
  files: h.files,
59832
60099
  reasonDetail: h.reasonDetail
59833
60100
  }));
59834
- const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
60101
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
60102
+ repoRoot
60103
+ });
59835
60104
  sink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
59836
60105
  const allDeclarations = [...sink.testEdits, ...valid];
59837
60106
  sink.testEdits = [];
@@ -59890,7 +60159,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59890
60159
  files: h.files,
59891
60160
  reasonDetail: h.reasonDetail
59892
60161
  }));
59893
- const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
60162
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
60163
+ repoRoot
60164
+ });
59894
60165
  nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
59895
60166
  const allDeclarations = [...nbSink.testEdits, ...valid];
59896
60167
  nbSink.testEdits = [];
@@ -60829,7 +61100,12 @@ var init_execution = __esm(() => {
60829
61100
  featureName: ctx.prd.feature,
60830
61101
  story: ctx.story,
60831
61102
  ...ctx.featureDir ? { featureDir: ctx.featureDir } : {},
60832
- ...interactionBridge ? { interactionBridge } : {}
61103
+ ...interactionBridge ? { interactionBridge } : {},
61104
+ phaseTelemetry: {
61105
+ testStrategy: ctx.routing.testStrategy,
61106
+ sessionModel: isThreeSessionStrategy(ctx.routing.testStrategy) ? "three-session" : "single-session",
61107
+ tier: effectiveTier
61108
+ }
60833
61109
  };
60834
61110
  let capturedTokenUsage;
60835
61111
  let capturedResponse = "";
@@ -61592,6 +61868,196 @@ var init_stages = __esm(() => {
61592
61868
  preRunPipeline = [acceptanceSetupStage];
61593
61869
  });
61594
61870
 
61871
+ // src/pipeline/subscribers/reporters.ts
61872
+ async function fanOutReporters(reporters, hook, invoke) {
61873
+ const logger = getSafeLogger();
61874
+ for (const reporter of reporters) {
61875
+ try {
61876
+ await invoke(reporter);
61877
+ } catch (err) {
61878
+ try {
61879
+ logger?.warn("plugins", `Reporter '${reporter.name}' ${hook} failed`, { error: err });
61880
+ } catch {}
61881
+ }
61882
+ }
61883
+ }
61884
+ function wireReporters(bus, pluginRegistry, runId, startTime) {
61885
+ const logger = getSafeLogger();
61886
+ const safe = (name, fn) => {
61887
+ return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
61888
+ };
61889
+ const unsubs = [];
61890
+ const phaseStart = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseStart", (reporter) => reporter.onPhaseStart?.(event));
61891
+ const phaseComplete = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseComplete", (reporter) => reporter.onPhaseComplete?.(event));
61892
+ unsubs.push(bus.on("story:step", (ev) => phaseStart({
61893
+ runId,
61894
+ scope: "story",
61895
+ storyId: ev.storyId,
61896
+ phase: ev.step,
61897
+ startTime: new Date().toISOString()
61898
+ })), bus.on("story:phase:completed", (phaseEvent) => phaseComplete({
61899
+ runId,
61900
+ scope: "story",
61901
+ storyId: phaseEvent.storyId,
61902
+ phase: phaseEvent.phase,
61903
+ outcome: phaseEvent.outcome,
61904
+ durationMs: phaseEvent.durationMs,
61905
+ costUsd: phaseEvent.costUsd,
61906
+ tier: phaseEvent.tier,
61907
+ testStrategy: phaseEvent.testStrategy,
61908
+ sessionModel: phaseEvent.sessionModel,
61909
+ details: phaseEvent.details
61910
+ })), bus.on("postrun:phase:started", (ev) => phaseStart({
61911
+ runId,
61912
+ scope: "run",
61913
+ phase: ev.phase,
61914
+ startTime: new Date().toISOString()
61915
+ })), bus.on("postrun:phase:completed", (phaseEvent) => phaseComplete({
61916
+ runId,
61917
+ scope: "run",
61918
+ phase: phaseEvent.phase,
61919
+ outcome: phaseEvent.passed ? "passed" : "failed",
61920
+ durationMs: phaseEvent.durationMs ?? 0,
61921
+ costUsd: phaseEvent.costUsd,
61922
+ details: phaseEvent.details
61923
+ })));
61924
+ unsubs.push(bus.on("run:started", (ev) => {
61925
+ return safe("onRunStart", async () => {
61926
+ const reporters = pluginRegistry.getReporters();
61927
+ for (const r of reporters) {
61928
+ if (r.onRunStart) {
61929
+ try {
61930
+ await r.onRunStart({
61931
+ runId,
61932
+ feature: ev.feature,
61933
+ totalStories: ev.totalStories,
61934
+ startTime: new Date(startTime).toISOString()
61935
+ });
61936
+ } catch (err) {
61937
+ logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
61938
+ }
61939
+ }
61940
+ }
61941
+ });
61942
+ }));
61943
+ unsubs.push(bus.on("story:completed", (ev) => {
61944
+ return safe("onStoryComplete(completed)", async () => {
61945
+ const reporters = pluginRegistry.getReporters();
61946
+ for (const r of reporters) {
61947
+ if (r.onStoryComplete) {
61948
+ try {
61949
+ await r.onStoryComplete({
61950
+ runId,
61951
+ storyId: ev.storyId,
61952
+ status: "completed",
61953
+ runElapsedMs: ev.runElapsedMs,
61954
+ cost: ev.cost ?? 0,
61955
+ tier: ev.modelTier ?? "balanced",
61956
+ testStrategy: ev.testStrategy ?? "test-after"
61957
+ });
61958
+ } catch (err) {
61959
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
61960
+ }
61961
+ }
61962
+ }
61963
+ });
61964
+ }));
61965
+ unsubs.push(bus.on("story:failed", (ev) => {
61966
+ return safe("onStoryComplete(failed)", async () => {
61967
+ const reporters = pluginRegistry.getReporters();
61968
+ for (const r of reporters) {
61969
+ if (r.onStoryComplete) {
61970
+ try {
61971
+ await r.onStoryComplete({
61972
+ runId,
61973
+ storyId: ev.storyId,
61974
+ status: "failed",
61975
+ runElapsedMs: Date.now() - startTime,
61976
+ cost: 0,
61977
+ tier: "balanced",
61978
+ testStrategy: "test-after"
61979
+ });
61980
+ } catch (err) {
61981
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
61982
+ }
61983
+ }
61984
+ }
61985
+ });
61986
+ }));
61987
+ unsubs.push(bus.on("story:paused", (ev) => {
61988
+ return safe("onStoryComplete(paused)", async () => {
61989
+ const reporters = pluginRegistry.getReporters();
61990
+ for (const r of reporters) {
61991
+ if (r.onStoryComplete) {
61992
+ try {
61993
+ await r.onStoryComplete({
61994
+ runId,
61995
+ storyId: ev.storyId,
61996
+ status: "paused",
61997
+ runElapsedMs: Date.now() - startTime,
61998
+ cost: 0,
61999
+ tier: "balanced",
62000
+ testStrategy: "test-after"
62001
+ });
62002
+ } catch (err) {
62003
+ logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
62004
+ }
62005
+ }
62006
+ }
62007
+ });
62008
+ }));
62009
+ unsubs.push(bus.on("story:escalated", (ev) => {
62010
+ return safe("onEscalation", async () => {
62011
+ const reporters = pluginRegistry.getReporters();
62012
+ for (const r of reporters) {
62013
+ if (r.onEscalation) {
62014
+ try {
62015
+ await r.onEscalation({
62016
+ runId,
62017
+ storyId: ev.storyId,
62018
+ fromTier: ev.fromTier,
62019
+ toTier: ev.toTier
62020
+ });
62021
+ } catch (err) {
62022
+ logger?.warn("plugins", `Reporter '${r.name}' onEscalation failed`, { error: err });
62023
+ }
62024
+ }
62025
+ }
62026
+ });
62027
+ }));
62028
+ unsubs.push(bus.on("run:completed", (ev) => {
62029
+ return safe("onRunEnd", async () => {
62030
+ const reporters = pluginRegistry.getReporters();
62031
+ for (const r of reporters) {
62032
+ if (r.onRunEnd) {
62033
+ try {
62034
+ await r.onRunEnd({
62035
+ runId,
62036
+ totalDurationMs: Date.now() - startTime,
62037
+ totalCost: ev.totalCost ?? 0,
62038
+ storySummary: {
62039
+ completed: ev.passedStories,
62040
+ failed: ev.failedStories,
62041
+ skipped: ev.skippedStories,
62042
+ paused: ev.pausedStories
62043
+ }
62044
+ });
62045
+ } catch (err) {
62046
+ logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
62047
+ }
62048
+ }
62049
+ }
62050
+ });
62051
+ }));
62052
+ return () => {
62053
+ for (const u of unsubs)
62054
+ u();
62055
+ };
62056
+ }
62057
+ var init_reporters = __esm(() => {
62058
+ init_logger2();
62059
+ });
62060
+
61595
62061
  // src/pipeline/index.ts
61596
62062
  var init_pipeline = __esm(() => {
61597
62063
  init_runner4();
@@ -61599,7 +62065,9 @@ var init_pipeline = __esm(() => {
61599
62065
  init_stages();
61600
62066
  init_queue_check();
61601
62067
  init_execution_helpers();
62068
+ init_acceptance_setup();
61602
62069
  init_event_bus();
62070
+ init_reporters();
61603
62071
  });
61604
62072
 
61605
62073
  // src/cli/prompts-shared.ts
@@ -64354,6 +64822,19 @@ var init_telegram2 = __esm(() => {
64354
64822
 
64355
64823
  // src/plugins/builtin/nax-finish/index.ts
64356
64824
  import * as path21 from "path";
64825
+ function logTail(stream) {
64826
+ if (stream.length <= LOG_TAIL_CHARS)
64827
+ return stream;
64828
+ return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
64829
+ ${stream.slice(-LOG_TAIL_CHARS)}`;
64830
+ }
64831
+ function stderrTail(stderr) {
64832
+ const trimmed = stderr.trim();
64833
+ if (!trimmed)
64834
+ return "";
64835
+ const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
64836
+ return tail.replace(/\s+/g, " ");
64837
+ }
64357
64838
  async function defaultRun2(cmd, opts) {
64358
64839
  const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
64359
64840
  let timedOut = false;
@@ -64427,7 +64908,7 @@ function buildFlowEnv(cfg) {
64427
64908
  env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
64428
64909
  return env2;
64429
64910
  }
64430
- var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
64911
+ var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, STDERR_TAIL_CHARS = 400, LOG_TAIL_CHARS = 20000, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
64431
64912
  var init_nax_finish = __esm(() => {
64432
64913
  init_config2();
64433
64914
  init_telegram2();
@@ -64478,7 +64959,16 @@ var init_nax_finish = __esm(() => {
64478
64959
  });
64479
64960
  const result = await _naxFinishDeps.readResult(ctx.workdir);
64480
64961
  if (!result) {
64481
- return { success: res.exitCode === 0, message: `nax-finish flow exited ${res.exitCode} (no result file)` };
64962
+ ctx.logger.warn("nax-finish flow produced no result file", {
64963
+ exitCode: res.exitCode,
64964
+ stdout: logTail(res.stdout),
64965
+ stderr: logTail(res.stderr)
64966
+ });
64967
+ const tail = stderrTail(res.stderr);
64968
+ return {
64969
+ success: false,
64970
+ message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
64971
+ };
64482
64972
  }
64483
64973
  if (result.status === "escalated" && escalateTelegram && creds) {
64484
64974
  await _naxFinishDeps.notify(creds, `nax-finish escalated *${result.feature}*: ${result.escalationReason ?? ""}`);
@@ -64556,16 +65046,72 @@ var init_reporter_shared = __esm(() => {
64556
65046
  init_post_json();
64557
65047
  });
64558
65048
 
64559
- // src/plugins/builtin/otel-reporter/ids.ts
64560
- function randomHex(bytes) {
64561
- const arr = new Uint8Array(bytes);
64562
- crypto.getRandomValues(arr);
64563
- let out = "";
64564
- for (const b of arr)
64565
- out += b.toString(16).padStart(2, "0");
64566
- return out;
65049
+ // src/plugins/builtin/otel-reporter/batch-queue.ts
65050
+ function createBatchQueue(opts) {
65051
+ const { maxBatchSize, flushIntervalMs, maxQueueSize, send } = opts;
65052
+ let queue = [];
65053
+ let dropCount = 0;
65054
+ let overflowing = false;
65055
+ let tornDown = false;
65056
+ let timer;
65057
+ const armTimer = () => {
65058
+ timer = setTimeout(() => {
65059
+ doFlush();
65060
+ if (!tornDown)
65061
+ armTimer();
65062
+ }, flushIntervalMs);
65063
+ };
65064
+ const sendWithRetry = async (batch) => {
65065
+ for (let attempt = 0;attempt < RETRY_ATTEMPTS; attempt++) {
65066
+ try {
65067
+ const ok = await send(batch);
65068
+ if (ok)
65069
+ return;
65070
+ } catch (err) {
65071
+ getSafeLogger()?.warn(STAGE, "Batch export threw", { error: err instanceof Error ? err.message : String(err) });
65072
+ }
65073
+ }
65074
+ };
65075
+ const doFlush = () => {
65076
+ if (tornDown || queue.length === 0)
65077
+ return Promise.resolve();
65078
+ const batch = queue;
65079
+ queue = [];
65080
+ sendWithRetry(batch);
65081
+ return Promise.resolve();
65082
+ };
65083
+ const enqueue = (item) => {
65084
+ queue.push(item);
65085
+ if (queue.length > maxQueueSize) {
65086
+ queue.shift();
65087
+ dropCount++;
65088
+ if (!overflowing) {
65089
+ overflowing = true;
65090
+ getSafeLogger()?.warn(STAGE, "Batch queue overflow \u2014 dropping oldest entries", { maxQueueSize });
65091
+ }
65092
+ } else {
65093
+ overflowing = false;
65094
+ }
65095
+ if (queue.length >= maxBatchSize) {
65096
+ doFlush();
65097
+ }
65098
+ };
65099
+ armTimer();
65100
+ return {
65101
+ enqueue,
65102
+ flushNow: () => doFlush(),
65103
+ teardown: () => {
65104
+ tornDown = true;
65105
+ if (timer !== undefined)
65106
+ clearTimeout(timer);
65107
+ },
65108
+ getMetrics: () => ({ size: queue.length, dropCount })
65109
+ };
64567
65110
  }
64568
- var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
65111
+ var STAGE = "otel-batch-queue", RETRY_ATTEMPTS = 2;
65112
+ var init_batch_queue = __esm(() => {
65113
+ init_logger2();
65114
+ });
64569
65115
 
64570
65116
  // src/plugins/builtin/otel-reporter/otlp.ts
64571
65117
  function attr(key, value) {
@@ -64574,10 +65120,27 @@ function attr(key, value) {
64574
65120
  function msToUnixNano(ms) {
64575
65121
  return (BigInt(Math.round(ms)) * 1000000n).toString();
64576
65122
  }
65123
+ function buildHistogramPoint(values, bounds, attributes, timeUnixNano) {
65124
+ const bucketCounts = new Array(bounds.length + 1).fill(0);
65125
+ let sum = 0;
65126
+ for (const value of values) {
65127
+ sum += value;
65128
+ const bucketIndex = bounds.findIndex((bound) => value <= bound);
65129
+ bucketCounts[bucketIndex === -1 ? bounds.length : bucketIndex]++;
65130
+ }
65131
+ return { attributes, timeUnixNano, count: values.length, sum, bucketCounts, explicitBounds: bounds };
65132
+ }
65133
+ function buildCounterPoint(count, attributes, timeUnixNano) {
65134
+ return { attributes, timeUnixNano, asInt: String(count) };
65135
+ }
65136
+ function buildResourceAttributes(serviceName, runId) {
65137
+ return [attr("service.name", serviceName), attr("nax.run_id", runId)];
65138
+ }
64577
65139
  function buildTracesPayload(p) {
64578
65140
  const span = {
64579
65141
  traceId: p.traceId,
64580
65142
  spanId: p.spanId,
65143
+ ...p.parentSpanId ? { parentSpanId: p.parentSpanId } : {},
64581
65144
  name: "nax.run",
64582
65145
  kind: 1,
64583
65146
  startTimeUnixNano: p.startUnixNano,
@@ -64598,7 +65161,7 @@ function buildTracesPayload(p) {
64598
65161
  resourceSpans: [
64599
65162
  {
64600
65163
  resource: { attributes: [attr("service.name", p.serviceName)] },
64601
- scopeSpans: [{ scope: { name: "nax" }, spans: [span] }]
65164
+ scopeSpans: [{ scope: { name: "nax" }, spans: [span, ...p.extraSpans ?? []] }]
64602
65165
  }
64603
65166
  ]
64604
65167
  };
@@ -64636,16 +65199,356 @@ function buildMetricsPayload(p) {
64636
65199
  };
64637
65200
  }
64638
65201
 
65202
+ // src/plugins/builtin/otel-reporter/heartbeat.ts
65203
+ function startHeartbeat(opts) {
65204
+ const { intervalMs, getSnapshot, onTick } = opts;
65205
+ if (intervalMs <= 0)
65206
+ return { stop() {} };
65207
+ let stopped = false;
65208
+ let timer;
65209
+ const armTimer = () => {
65210
+ timer = setTimeout(() => {
65211
+ try {
65212
+ Promise.resolve(onTick(getSnapshot())).catch((err) => getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
65213
+ error: err instanceof Error ? err.message : String(err)
65214
+ }));
65215
+ } catch (err) {
65216
+ getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
65217
+ error: err instanceof Error ? err.message : String(err)
65218
+ });
65219
+ }
65220
+ if (!stopped)
65221
+ armTimer();
65222
+ }, intervalMs);
65223
+ };
65224
+ armTimer();
65225
+ return {
65226
+ stop() {
65227
+ stopped = true;
65228
+ if (timer !== undefined)
65229
+ clearTimeout(timer);
65230
+ }
65231
+ };
65232
+ }
65233
+ function heartbeatAttributes(a) {
65234
+ return [
65235
+ attr("run_id", a.runId),
65236
+ attr("feature", a.feature),
65237
+ attr("project", a.project),
65238
+ attr("story_id", a.storyId),
65239
+ attr("phase", a.phase),
65240
+ attr("tier", a.tier),
65241
+ attr("test_strategy", a.testStrategy)
65242
+ ];
65243
+ }
65244
+ function buildHeartbeatMetricsPayload(p) {
65245
+ const attributes = heartbeatAttributes(p.snapshot.attributes);
65246
+ const gauge = (name, value) => ({
65247
+ name,
65248
+ gauge: { dataPoints: [{ asDouble: value, timeUnixNano: p.timeUnixNano, attributes }] }
65249
+ });
65250
+ return {
65251
+ resourceMetrics: [
65252
+ {
65253
+ resource: { attributes: [attr("service.name", p.serviceName)] },
65254
+ scopeMetrics: [
65255
+ {
65256
+ scope: { name: "nax" },
65257
+ metrics: [
65258
+ gauge("nax.run.active", 1),
65259
+ gauge("nax.run.phase_elapsed_ms", p.snapshot.phaseElapsedMs),
65260
+ gauge("nax.run.cost_usd", p.snapshot.costUsd)
65261
+ ]
65262
+ }
65263
+ ]
65264
+ }
65265
+ ]
65266
+ };
65267
+ }
65268
+ var STAGE2 = "otel-reporter-heartbeat";
65269
+ var init_heartbeat = __esm(() => {
65270
+ init_logger2();
65271
+ });
65272
+
65273
+ // src/plugins/builtin/otel-reporter/ids.ts
65274
+ function randomHex(bytes) {
65275
+ const arr = new Uint8Array(bytes);
65276
+ crypto.getRandomValues(arr);
65277
+ let out = "";
65278
+ for (const b of arr)
65279
+ out += b.toString(16).padStart(2, "0");
65280
+ return out;
65281
+ }
65282
+ var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
65283
+
65284
+ // src/plugins/builtin/otel-reporter/span-tree.ts
65285
+ function createSpanTree(traceId, runSpanId) {
65286
+ const storySpanIds = new Map;
65287
+ function storySpanId(storyId) {
65288
+ let spanId = storySpanIds.get(storyId);
65289
+ if (!spanId) {
65290
+ spanId = newSpanId();
65291
+ storySpanIds.set(storyId, spanId);
65292
+ }
65293
+ return spanId;
65294
+ }
65295
+ function buildStorySpan(storyId, startUnixNano, endUnixNano) {
65296
+ return {
65297
+ traceId,
65298
+ spanId: storySpanId(storyId),
65299
+ parentSpanId: runSpanId,
65300
+ name: "nax.story",
65301
+ startTimeUnixNano: startUnixNano,
65302
+ endTimeUnixNano: endUnixNano,
65303
+ attributes: [attr("nax.story_id", storyId)]
65304
+ };
65305
+ }
65306
+ function buildPhaseSpan({ event, traceId: spanTraceId, startUnixNano, endUnixNano }) {
65307
+ const parentSpanId = event.scope === "story" && event.storyId !== undefined ? storySpanId(event.storyId) : runSpanId;
65308
+ const attributes = [attr("phase", event.phase), attr("outcome", event.outcome)];
65309
+ if (event.testStrategy)
65310
+ attributes.push(attr("nax.test_strategy", event.testStrategy));
65311
+ return {
65312
+ traceId: spanTraceId,
65313
+ spanId: newSpanId(),
65314
+ parentSpanId,
65315
+ name: "nax.phase",
65316
+ startTimeUnixNano: startUnixNano,
65317
+ endTimeUnixNano: endUnixNano,
65318
+ attributes
65319
+ };
65320
+ }
65321
+ return { traceId, runSpanId, storySpanId, buildStorySpan, buildPhaseSpan };
65322
+ }
65323
+ function counterKey(attributes) {
65324
+ return attributes.map((a) => `${a.key}=${a.value.stringValue ?? a.value.doubleValue}`).join("|");
65325
+ }
65326
+ function bumpCounter(groups, attributes, count) {
65327
+ const key = counterKey(attributes);
65328
+ const existing = groups.get(key);
65329
+ if (existing) {
65330
+ existing.count += count;
65331
+ } else {
65332
+ groups.set(key, { attributes, count });
65333
+ }
65334
+ }
65335
+ function createPhaseMetricsAggregator() {
65336
+ const phaseGroups = new Map;
65337
+ const reviewFindings = new Map;
65338
+ const fixIterations = new Map;
65339
+ const escalations = new Map;
65340
+ function recordPhase(event) {
65341
+ const attributes = [
65342
+ attr("phase", event.phase),
65343
+ attr("outcome", event.outcome),
65344
+ attr("tier", event.tier ?? "unknown"),
65345
+ attr("test_strategy", event.testStrategy ?? "unknown"),
65346
+ attr("session_model", event.sessionModel ?? "unknown")
65347
+ ];
65348
+ const key = counterKey(attributes);
65349
+ let group = phaseGroups.get(key);
65350
+ if (!group) {
65351
+ group = { attributes, durations: [], costs: [] };
65352
+ phaseGroups.set(key, group);
65353
+ }
65354
+ group.durations.push(event.durationMs);
65355
+ if (event.costUsd !== undefined)
65356
+ group.costs.push(event.costUsd);
65357
+ }
65358
+ function recordReviewFindings(phase, severity2, count) {
65359
+ bumpCounter(reviewFindings, [attr("phase", phase), attr("severity", severity2)], count);
65360
+ }
65361
+ function recordFixIterations(phase, strategy, count) {
65362
+ bumpCounter(fixIterations, [attr("phase", phase), attr("strategy", strategy)], count);
65363
+ }
65364
+ function recordEscalation(toTier, count) {
65365
+ bumpCounter(escalations, [attr("to_tier", toTier)], count);
65366
+ }
65367
+ function buildMetricsPayload2(serviceName, runId, timeUnixNano) {
65368
+ const groups = [...phaseGroups.values()];
65369
+ const counterMetric = (name, source) => ({
65370
+ name,
65371
+ sum: {
65372
+ aggregationTemporality: 2,
65373
+ isMonotonic: true,
65374
+ dataPoints: [...source.values()].map((g) => buildCounterPoint(g.count, g.attributes, timeUnixNano))
65375
+ }
65376
+ });
65377
+ const metrics = [];
65378
+ if (groups.length > 0) {
65379
+ metrics.push({
65380
+ name: "nax.phase.duration",
65381
+ histogram: {
65382
+ aggregationTemporality: 2,
65383
+ dataPoints: groups.map((g) => buildHistogramPoint(g.durations, PHASE_DURATION_BOUNDS, g.attributes, timeUnixNano))
65384
+ }
65385
+ });
65386
+ metrics.push({
65387
+ name: "nax.phase.cost_usd",
65388
+ histogram: {
65389
+ aggregationTemporality: 2,
65390
+ dataPoints: groups.map((g) => buildHistogramPoint(g.costs, PHASE_COST_BOUNDS, g.attributes, timeUnixNano))
65391
+ }
65392
+ });
65393
+ }
65394
+ if (reviewFindings.size > 0)
65395
+ metrics.push(counterMetric("nax.review.findings", reviewFindings));
65396
+ if (fixIterations.size > 0)
65397
+ metrics.push(counterMetric("nax.fix.iterations", fixIterations));
65398
+ if (escalations.size > 0)
65399
+ metrics.push(counterMetric("nax.escalations", escalations));
65400
+ return {
65401
+ resourceMetrics: [
65402
+ {
65403
+ resource: { attributes: buildResourceAttributes(serviceName, runId) },
65404
+ scopeMetrics: [{ scope: { name: "nax" }, metrics }]
65405
+ }
65406
+ ]
65407
+ };
65408
+ }
65409
+ return { recordPhase, recordReviewFindings, recordFixIterations, recordEscalation, buildMetricsPayload: buildMetricsPayload2 };
65410
+ }
65411
+ var PHASE_DURATION_BOUNDS, PHASE_COST_BOUNDS;
65412
+ var init_span_tree = __esm(() => {
65413
+ PHASE_DURATION_BOUNDS = [100, 500, 1000, 5000, 15000, 60000, 300000, 900000];
65414
+ PHASE_COST_BOUNDS = [0.001, 0.01, 0.05, 0.1, 0.5, 1, 5];
65415
+ });
65416
+
65417
+ // src/plugins/builtin/otel-reporter/traceparent.ts
65418
+ function parseTraceparent(value) {
65419
+ if (!value)
65420
+ return null;
65421
+ const match = TRACEPARENT_PATTERN.exec(value);
65422
+ if (!match)
65423
+ return null;
65424
+ const [, traceId, spanId] = match;
65425
+ if (!traceId || !spanId || ALL_ZERO.test(traceId))
65426
+ return null;
65427
+ return { traceId, spanId };
65428
+ }
65429
+ var TRACEPARENT_PATTERN, ALL_ZERO;
65430
+ var init_traceparent = __esm(() => {
65431
+ TRACEPARENT_PATTERN = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
65432
+ ALL_ZERO = /^0+$/;
65433
+ });
65434
+
64639
65435
  // src/plugins/builtin/otel-reporter/index.ts
65436
+ function rootSpanIdentity() {
65437
+ const adopted = parseTraceparent(process.env.TRACEPARENT);
65438
+ if (!adopted)
65439
+ return { traceId: newTraceId(), spanId: newSpanId() };
65440
+ return { traceId: adopted.traceId, spanId: newSpanId(), parentSpanId: adopted.spanId };
65441
+ }
65442
+ function heartbeatSnapshotOf(runId, st) {
65443
+ const last = st.lastPhase;
65444
+ return {
65445
+ attributes: {
65446
+ runId,
65447
+ feature: st.feature,
65448
+ project: st.project,
65449
+ storyId: last?.storyId ?? "",
65450
+ phase: last?.phase ?? "",
65451
+ tier: last?.tier ?? "",
65452
+ testStrategy: last?.testStrategy ?? ""
65453
+ },
65454
+ phaseElapsedMs: last ? Date.now() - last.atMs : 0,
65455
+ costUsd: st.costUsd
65456
+ };
65457
+ }
65458
+ function recordDetailMetrics(metrics, phase, details) {
65459
+ if (typeof details !== "object" || details === null)
65460
+ return;
65461
+ const record2 = details;
65462
+ if (record2.kind === "review" && typeof record2.bySeverity === "object" && record2.bySeverity !== null) {
65463
+ for (const [severity2, count] of Object.entries(record2.bySeverity)) {
65464
+ if (count > 0)
65465
+ metrics.recordReviewFindings(phase, severity2, count);
65466
+ }
65467
+ } else if (record2.kind === "fix" && typeof record2.strategy === "string") {
65468
+ metrics.recordFixIterations(phase, record2.strategy, 1);
65469
+ }
65470
+ }
65471
+ function reviewSpanEvents(details, timeUnixNano, verbose) {
65472
+ if (!verbose)
65473
+ return [];
65474
+ if (typeof details !== "object" || details === null)
65475
+ return [];
65476
+ const record2 = details;
65477
+ if (record2.kind !== "review" || !Array.isArray(record2.items))
65478
+ return [];
65479
+ return record2.items.map((item) => {
65480
+ const finding = typeof item === "object" && item !== null ? item : {};
65481
+ const attributes = [attr("message", String(finding.message ?? ""))];
65482
+ if (typeof finding.file === "string")
65483
+ attributes.push(attr("file", finding.file));
65484
+ return { timeUnixNano, name: "review.finding", attributes };
65485
+ });
65486
+ }
64640
65487
  function createOtelReporterPlugin(cfg, deps) {
64641
65488
  const states = new Map;
64642
65489
  const base = cfg.endpoint?.replace(/\/$/, "");
65490
+ let tornDown = false;
65491
+ const sendSpanBatch = async (batch) => {
65492
+ if (!base || batch.length === 0)
65493
+ return true;
65494
+ const { resolved, missing } = interpolateHeaders(cfg.headers);
65495
+ if (missing.length > 0) {
65496
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65497
+ return true;
65498
+ }
65499
+ const payload = {
65500
+ resourceSpans: [
65501
+ {
65502
+ resource: { attributes: [attr("service.name", cfg.serviceName)] },
65503
+ scopeSpans: [{ scope: { name: "nax" }, spans: batch }]
65504
+ }
65505
+ ]
65506
+ };
65507
+ return postJson(`${base}/v1/traces`, payload, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
65508
+ };
65509
+ const makeSpanQueue = () => createBatchQueue({
65510
+ maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
65511
+ flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
65512
+ maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
65513
+ send: sendSpanBatch
65514
+ });
65515
+ const buildOrphanState = (startMs) => {
65516
+ const identity = rootSpanIdentity();
65517
+ return {
65518
+ ...identity,
65519
+ startMs,
65520
+ feature: "",
65521
+ project: "",
65522
+ events: [],
65523
+ spanTree: createSpanTree(identity.traceId, identity.spanId),
65524
+ spanQueue: makeSpanQueue(),
65525
+ metrics: createPhaseMetricsAggregator(),
65526
+ storyBounds: new Map,
65527
+ costUsd: 0,
65528
+ heartbeat: { stop() {} }
65529
+ };
65530
+ };
65531
+ const exportHeartbeat = async (snapshot) => {
65532
+ if (!base)
65533
+ return;
65534
+ const { resolved, missing } = interpolateHeaders(cfg.headers);
65535
+ if (missing.length > 0) {
65536
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65537
+ return;
65538
+ }
65539
+ const metrics = buildHeartbeatMetricsPayload({
65540
+ serviceName: cfg.serviceName,
65541
+ timeUnixNano: msToUnixNano(Date.now()),
65542
+ snapshot
65543
+ });
65544
+ await postJson(`${base}/v1/metrics`, metrics, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
65545
+ };
64643
65546
  const flush = async (st, endMs, e) => {
64644
65547
  if (!base)
64645
65548
  return;
64646
65549
  const { resolved, missing } = interpolateHeaders(cfg.headers);
64647
65550
  if (missing.length > 0) {
64648
- getSafeLogger()?.warn(STAGE, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65551
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
64649
65552
  return;
64650
65553
  }
64651
65554
  const startUnixNano = msToUnixNano(st.startMs);
@@ -64654,6 +65557,7 @@ function createOtelReporterPlugin(cfg, deps) {
64654
65557
  serviceName: cfg.serviceName,
64655
65558
  traceId: st.traceId,
64656
65559
  spanId: st.spanId,
65560
+ parentSpanId: st.parentSpanId,
64657
65561
  startUnixNano,
64658
65562
  endUnixNano,
64659
65563
  feature: st.feature,
@@ -64670,20 +65574,35 @@ function createOtelReporterPlugin(cfg, deps) {
64670
65574
  totalCost: e.totalCost,
64671
65575
  totalDurationMs: e.totalDurationMs
64672
65576
  });
64673
- const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE, deps };
65577
+ const aggMetrics = st.metrics.buildMetricsPayload(cfg.serviceName, e.runId, endUnixNano);
65578
+ metrics.resourceMetrics[0].scopeMetrics[0].metrics.push(...aggMetrics.resourceMetrics[0].scopeMetrics[0].metrics);
65579
+ const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps };
64674
65580
  await postJson(`${base}/v1/traces`, traces, opts);
64675
65581
  await postJson(`${base}/v1/metrics`, metrics, opts);
64676
65582
  };
64677
65583
  const reporter = {
64678
- name: STAGE,
65584
+ name: STAGE3,
64679
65585
  async onRunStart(event) {
64680
- states.set(event.runId, {
64681
- traceId: newTraceId(),
64682
- spanId: newSpanId(),
65586
+ const identity = rootSpanIdentity();
65587
+ const runId = event.runId;
65588
+ const state = {
65589
+ ...identity,
64683
65590
  startMs: Date.parse(event.startTime),
64684
65591
  feature: event.feature,
64685
- events: []
64686
- });
65592
+ project: event.project ?? "",
65593
+ events: [],
65594
+ spanTree: createSpanTree(identity.traceId, identity.spanId),
65595
+ spanQueue: makeSpanQueue(),
65596
+ metrics: createPhaseMetricsAggregator(),
65597
+ storyBounds: new Map,
65598
+ costUsd: 0,
65599
+ heartbeat: startHeartbeat({
65600
+ intervalMs: cfg.heartbeatIntervalMs ?? 0,
65601
+ getSnapshot: () => heartbeatSnapshotOf(runId, state),
65602
+ onTick: (snapshot) => exportHeartbeat(snapshot)
65603
+ })
65604
+ };
65605
+ states.set(runId, state);
64687
65606
  },
64688
65607
  async onStoryComplete(event) {
64689
65608
  const st = states.get(event.runId);
@@ -64700,32 +65619,98 @@ function createOtelReporterPlugin(cfg, deps) {
64700
65619
  attr("testStrategy", event.testStrategy)
64701
65620
  ]
64702
65621
  });
65622
+ const bounds = st.storyBounds.get(event.storyId);
65623
+ if (bounds) {
65624
+ st.spanQueue.enqueue(st.spanTree.buildStorySpan(event.storyId, msToUnixNano(bounds.startMs), msToUnixNano(bounds.endMs)));
65625
+ st.storyBounds.delete(event.storyId);
65626
+ }
65627
+ },
65628
+ async onPhaseComplete(event) {
65629
+ const st = states.get(event.runId);
65630
+ if (!st)
65631
+ return;
65632
+ st.costUsd += event.costUsd ?? 0;
65633
+ st.lastPhase = {
65634
+ phase: event.phase,
65635
+ storyId: event.storyId ?? "",
65636
+ tier: event.tier ?? "",
65637
+ testStrategy: event.testStrategy ?? "",
65638
+ atMs: Date.now()
65639
+ };
65640
+ const endMs = Date.now();
65641
+ const startMs = endMs - event.durationMs;
65642
+ const endUnixNano = msToUnixNano(endMs);
65643
+ const span = st.spanTree.buildPhaseSpan({
65644
+ event,
65645
+ traceId: st.traceId,
65646
+ startUnixNano: msToUnixNano(startMs),
65647
+ endUnixNano
65648
+ });
65649
+ const events = reviewSpanEvents(event.details, endUnixNano, cfg.detail === "verbose");
65650
+ if (events.length > 0)
65651
+ span.events = events;
65652
+ st.spanQueue.enqueue(span);
65653
+ st.metrics.recordPhase(event);
65654
+ recordDetailMetrics(st.metrics, event.phase, event.details);
65655
+ if (event.scope === "story" && event.storyId !== undefined) {
65656
+ const bounds = st.storyBounds.get(event.storyId);
65657
+ st.storyBounds.set(event.storyId, {
65658
+ startMs: bounds ? Math.min(bounds.startMs, startMs) : startMs,
65659
+ endMs: bounds ? Math.max(bounds.endMs, endMs) : endMs
65660
+ });
65661
+ }
65662
+ },
65663
+ async onEscalation(event) {
65664
+ const st = states.get(event.runId);
65665
+ if (!st)
65666
+ return;
65667
+ st.metrics.recordEscalation(event.toTier, 1);
64703
65668
  },
64704
65669
  async onRunEnd(event) {
64705
65670
  const existing = states.get(event.runId);
65671
+ existing?.heartbeat.stop();
64706
65672
  const startMs = existing?.startMs ?? Date.now() - event.totalDurationMs;
64707
- const st = existing ?? {
64708
- traceId: newTraceId(),
64709
- spanId: newSpanId(),
64710
- startMs,
64711
- feature: "",
64712
- events: []
64713
- };
65673
+ const st = existing ?? buildOrphanState(startMs);
64714
65674
  states.delete(event.runId);
65675
+ await st.spanQueue.flushNow();
65676
+ st.spanQueue.teardown();
64715
65677
  await flush(st, startMs + event.totalDurationMs, event);
64716
65678
  }
64717
65679
  };
64718
65680
  return {
64719
- name: STAGE,
65681
+ name: STAGE3,
64720
65682
  version: "1.0.0",
64721
65683
  provides: ["reporter"],
65684
+ async teardown() {
65685
+ if (tornDown)
65686
+ return;
65687
+ tornDown = true;
65688
+ const entries = [...states.entries()];
65689
+ states.clear();
65690
+ for (const [runId, st] of entries) {
65691
+ st.heartbeat.stop();
65692
+ await st.spanQueue.flushNow();
65693
+ st.spanQueue.teardown();
65694
+ const endMs = Date.now();
65695
+ await flush(st, endMs, {
65696
+ runId,
65697
+ totalDurationMs: endMs - st.startMs,
65698
+ totalCost: st.costUsd,
65699
+ storySummary: { completed: 0, failed: 0, skipped: 0, paused: 0 }
65700
+ });
65701
+ }
65702
+ },
64722
65703
  extensions: { reporter }
64723
65704
  };
64724
65705
  }
64725
- var STAGE = "otel-reporter";
65706
+ var STAGE3 = "otel-reporter", DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048;
64726
65707
  var init_otel_reporter = __esm(() => {
64727
65708
  init_logger2();
64728
65709
  init_reporter_shared();
65710
+ init_batch_queue();
65711
+ init_heartbeat();
65712
+ init_span_tree();
65713
+ init_traceparent();
64729
65714
  });
64730
65715
 
64731
65716
  // src/plugins/builtin/webhook-reporter/index.ts
@@ -64736,25 +65721,27 @@ function createWebhookReporterPlugin(cfg, deps) {
64736
65721
  return;
64737
65722
  const { resolved, missing } = interpolateHeaders(cfg.headers);
64738
65723
  if (missing.length > 0) {
64739
- getSafeLogger()?.warn(STAGE2, "Skipping webhook \u2014 unresolved env vars", { missing });
65724
+ getSafeLogger()?.warn(STAGE4, "Skipping webhook \u2014 unresolved env vars", { missing });
64740
65725
  return;
64741
65726
  }
64742
- await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE2, deps });
65727
+ await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE4, deps });
64743
65728
  };
64744
65729
  const reporter = {
64745
- name: STAGE2,
65730
+ name: STAGE4,
64746
65731
  onRunStart: (event) => emit("onRunStart", event),
64747
65732
  onStoryComplete: (event) => emit("onStoryComplete", event),
64748
- onRunEnd: (event) => emit("onRunEnd", event)
65733
+ onRunEnd: (event) => emit("onRunEnd", event),
65734
+ onPhaseStart: (event) => emit("onPhaseStart", event),
65735
+ onPhaseComplete: (event) => emit("onPhaseComplete", event)
64749
65736
  };
64750
65737
  return {
64751
- name: STAGE2,
65738
+ name: STAGE4,
64752
65739
  version: "1.0.0",
64753
65740
  provides: ["reporter"],
64754
65741
  extensions: { reporter }
64755
65742
  };
64756
65743
  }
64757
- var STAGE2 = "webhook-reporter";
65744
+ var STAGE4 = "webhook-reporter";
64758
65745
  var init_webhook_reporter = __esm(() => {
64759
65746
  init_logger2();
64760
65747
  init_reporter_shared();
@@ -65789,7 +66776,7 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
65789
66776
  }
65790
66777
  }
65791
66778
  }
65792
- function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
66779
+ function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
65793
66780
  const logger = _heartbeatDeps.getSafeLogger();
65794
66781
  _heartbeatActive = true;
65795
66782
  const gen = ++_heartbeatGen;
@@ -67383,28 +68370,47 @@ async function handleRunCompletion(options) {
67383
68370
  const regressionMode = config2.execution.regressionGate?.mode;
67384
68371
  if (options.skipRegression) {} else if ((regressionMode === "deferred" || regressionMode === "per-story") && config2.quality.commands.test) {
67385
68372
  statusWriter.setPostRunPhase("regression", { status: "running" });
68373
+ const regressionStartTime = Date.now();
67386
68374
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "regression" });
67387
- const regressionResult = await _runCompletionDeps.runDeferredRegression({
67388
- config: config2,
67389
- prd,
67390
- workdir,
67391
- runtime: options.runtime,
67392
- quarantineMemo: options.runtime.quarantineMemo,
67393
- storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
67394
- storyId: m.storyId,
67395
- completedAt: m.completedAt,
67396
- failingTestFiles: m.failingTestFiles
67397
- }))
67398
- });
68375
+ let regressionResult;
68376
+ try {
68377
+ regressionResult = await _runCompletionDeps.runDeferredRegression({
68378
+ config: config2,
68379
+ prd,
68380
+ workdir,
68381
+ runtime: options.runtime,
68382
+ quarantineMemo: options.runtime.quarantineMemo,
68383
+ storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
68384
+ storyId: m.storyId,
68385
+ completedAt: m.completedAt,
68386
+ failingTestFiles: m.failingTestFiles
68387
+ }))
68388
+ });
68389
+ } catch (err) {
68390
+ pipelineEventBus.emit({
68391
+ type: "postrun:phase:completed",
68392
+ phase: "regression",
68393
+ passed: false,
68394
+ durationMs: Date.now() - regressionStartTime
68395
+ });
68396
+ throw err;
68397
+ }
67399
68398
  const lastRunAt = new Date().toISOString();
67400
68399
  logger?.info("regression", "Deferred regression gate completed", {
67401
68400
  success: regressionResult.success,
67402
68401
  failedTests: regressionResult.failedTests,
67403
68402
  affectedStories: regressionResult.affectedStories
67404
68403
  });
68404
+ const regressionDurationMs = Date.now() - regressionStartTime;
67405
68405
  if (regressionResult.success) {
67406
68406
  statusWriter.setPostRunPhase("regression", { status: "passed", lastRunAt });
67407
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "regression", passed: true });
68407
+ pipelineEventBus.emit({
68408
+ type: "postrun:phase:completed",
68409
+ phase: "regression",
68410
+ passed: true,
68411
+ durationMs: regressionDurationMs,
68412
+ details: { mode: regressionMode, failedTests: 0 }
68413
+ });
67408
68414
  } else {
67409
68415
  statusWriter.setPostRunPhase("regression", {
67410
68416
  status: "failed",
@@ -67412,7 +68418,16 @@ async function handleRunCompletion(options) {
67412
68418
  affectedStories: regressionResult.affectedStories,
67413
68419
  lastRunAt
67414
68420
  });
67415
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "regression", passed: false });
68421
+ pipelineEventBus.emit({
68422
+ type: "postrun:phase:completed",
68423
+ phase: "regression",
68424
+ passed: false,
68425
+ durationMs: regressionDurationMs,
68426
+ details: {
68427
+ mode: regressionMode,
68428
+ failedTests: regressionResult.failedTests
68429
+ }
68430
+ });
67416
68431
  for (const storyId of regressionResult.affectedStories) {
67417
68432
  const story = prd.userStories.find((s) => s.id === storyId);
67418
68433
  if (story) {
@@ -67480,7 +68495,15 @@ async function handleRunCompletion(options) {
67480
68495
  let pluginGateFailed = false;
67481
68496
  const deferredReview = options.deferredReview;
67482
68497
  if (deferredReview !== undefined) {
67483
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "review", passed: !deferredReview.anyFailed });
68498
+ const findingCount = deferredReview.reviewerResults.filter((r) => !r.passed).length;
68499
+ const reviewDurationMs = Date.now() - (options.deferredReviewStartedAt ?? Date.now());
68500
+ pipelineEventBus.emit({
68501
+ type: "postrun:phase:completed",
68502
+ phase: "review",
68503
+ passed: !deferredReview.anyFailed,
68504
+ durationMs: reviewDurationMs,
68505
+ details: { findingCount, anyFailed: deferredReview.anyFailed }
68506
+ });
67484
68507
  }
67485
68508
  if (deferredReview?.anyFailed) {
67486
68509
  const failedReviewers = deferredReview.reviewerResults.filter((r) => !r.passed).map((r) => r.name);
@@ -67729,6 +68752,7 @@ async function runCompletionPhase(options) {
67729
68752
  logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
67730
68753
  } else if (options.config.acceptance.enabled && isComplete(options.prd)) {
67731
68754
  options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
68755
+ const acceptanceStartTime = Date.now();
67732
68756
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance" });
67733
68757
  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) => {
67734
68758
  const relativeWorkdir = path25.relative(options.workdir, g.packageDir);
@@ -67751,32 +68775,54 @@ async function runCompletionPhase(options) {
67751
68775
  commandOverride: groupConfig.acceptance.command
67752
68776
  };
67753
68777
  })) : undefined;
67754
- const acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
67755
- config: options.config,
67756
- prd: options.prd,
67757
- prdPath: options.prdPath,
67758
- workdir: options.workdir,
67759
- featureDir: options.featureDir,
67760
- hooks: options.hooks,
67761
- feature: options.feature,
67762
- totalCost: options.totalCost,
67763
- iterations: options.iterations,
67764
- storiesCompleted: options.storiesCompleted,
67765
- allStoryMetrics: options.allStoryMetrics,
67766
- pluginRegistry: options.pluginRegistry,
67767
- eventEmitter: options.eventEmitter,
67768
- statusWriter: options.statusWriter,
67769
- agentGetFn: options.agentGetFn,
67770
- agentManager: options.agentManager,
67771
- sessionManager: options.sessionManager,
67772
- runtime: options.runtime,
67773
- abortSignal: options.abortSignal,
67774
- acceptanceTestPaths
67775
- });
68778
+ let acceptanceResult;
68779
+ try {
68780
+ acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
68781
+ config: options.config,
68782
+ prd: options.prd,
68783
+ prdPath: options.prdPath,
68784
+ workdir: options.workdir,
68785
+ featureDir: options.featureDir,
68786
+ hooks: options.hooks,
68787
+ feature: options.feature,
68788
+ totalCost: options.totalCost,
68789
+ iterations: options.iterations,
68790
+ storiesCompleted: options.storiesCompleted,
68791
+ allStoryMetrics: options.allStoryMetrics,
68792
+ pluginRegistry: options.pluginRegistry,
68793
+ eventEmitter: options.eventEmitter,
68794
+ statusWriter: options.statusWriter,
68795
+ agentGetFn: options.agentGetFn,
68796
+ agentManager: options.agentManager,
68797
+ sessionManager: options.sessionManager,
68798
+ runtime: options.runtime,
68799
+ abortSignal: options.abortSignal,
68800
+ acceptanceTestPaths
68801
+ });
68802
+ } catch (err) {
68803
+ pipelineEventBus.emit({
68804
+ type: "postrun:phase:completed",
68805
+ phase: "acceptance",
68806
+ passed: false,
68807
+ durationMs: Date.now() - acceptanceStartTime
68808
+ });
68809
+ throw err;
68810
+ }
67776
68811
  const lastRunAt = new Date().toISOString();
68812
+ const acceptanceDurationMs = Date.now() - acceptanceStartTime;
67777
68813
  if (acceptanceResult.success) {
67778
68814
  options.statusWriter.setPostRunPhase("acceptance", { status: "passed", lastRunAt });
67779
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "acceptance", passed: true });
68815
+ pipelineEventBus.emit({
68816
+ type: "postrun:phase:completed",
68817
+ phase: "acceptance",
68818
+ passed: true,
68819
+ durationMs: acceptanceDurationMs,
68820
+ details: {
68821
+ retries: acceptanceResult.retries ?? 0,
68822
+ failedACCount: acceptanceResult.failedACs?.length ?? 0,
68823
+ fixStoriesCreated: 0
68824
+ }
68825
+ });
67780
68826
  } else {
67781
68827
  acceptancePassed = false;
67782
68828
  options.statusWriter.setPostRunPhase("acceptance", {
@@ -67785,7 +68831,17 @@ async function runCompletionPhase(options) {
67785
68831
  retries: acceptanceResult.retries ?? 0,
67786
68832
  lastRunAt
67787
68833
  });
67788
- pipelineEventBus.emit({ type: "postrun:phase:completed", phase: "acceptance", passed: false });
68834
+ pipelineEventBus.emit({
68835
+ type: "postrun:phase:completed",
68836
+ phase: "acceptance",
68837
+ passed: false,
68838
+ durationMs: acceptanceDurationMs,
68839
+ details: {
68840
+ retries: acceptanceResult.retries ?? 0,
68841
+ failedACCount: acceptanceResult.failedACs?.length ?? 0,
68842
+ fixStoriesCreated: 0
68843
+ }
68844
+ });
67789
68845
  }
67790
68846
  Object.assign(options, {
67791
68847
  prd: acceptanceResult.prd,
@@ -67816,6 +68872,7 @@ async function runCompletionPhase(options) {
67816
68872
  sessionManager: options.sessionManager,
67817
68873
  pluginProviderCache: options.pluginProviderCache,
67818
68874
  deferredReview: options.deferredReview,
68875
+ deferredReviewStartedAt: options.deferredReviewStartedAt,
67819
68876
  exitReason: options.exitReason,
67820
68877
  runtime: options.runtime,
67821
68878
  abortSignal: options.abortSignal
@@ -67892,7 +68949,7 @@ var init_runner_completion = __esm(() => {
67892
68949
  });
67893
68950
 
67894
68951
  // src/execution/batching.ts
67895
- function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
68952
+ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
67896
68953
  const batches = [];
67897
68954
  let currentBatch = [];
67898
68955
  for (const story of stories) {
@@ -67925,7 +68982,7 @@ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE)
67925
68982
  }
67926
68983
  return batches;
67927
68984
  }
67928
- function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
68985
+ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
67929
68986
  const batches = [];
67930
68987
  let currentBatch = [];
67931
68988
  for (const story of stories) {
@@ -67962,7 +69019,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
67962
69019
  }
67963
69020
  return batches;
67964
69021
  }
67965
- var DEFAULT_MAX_BATCH_SIZE = 4;
69022
+ var DEFAULT_MAX_BATCH_SIZE2 = 4;
67966
69023
 
67967
69024
  // src/execution/ensure-package-dirs.ts
67968
69025
  import path26 from "path";
@@ -68242,131 +69299,6 @@ var init_registry6 = __esm(() => {
68242
69299
  init_paths3();
68243
69300
  });
68244
69301
 
68245
- // src/pipeline/subscribers/reporters.ts
68246
- function wireReporters(bus, pluginRegistry, runId, startTime) {
68247
- const logger = getSafeLogger();
68248
- const safe = (name, fn) => {
68249
- return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
68250
- };
68251
- const unsubs = [];
68252
- unsubs.push(bus.on("run:started", (ev) => {
68253
- return safe("onRunStart", async () => {
68254
- const reporters = pluginRegistry.getReporters();
68255
- for (const r of reporters) {
68256
- if (r.onRunStart) {
68257
- try {
68258
- await r.onRunStart({
68259
- runId,
68260
- feature: ev.feature,
68261
- totalStories: ev.totalStories,
68262
- startTime: new Date(startTime).toISOString()
68263
- });
68264
- } catch (err) {
68265
- logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
68266
- }
68267
- }
68268
- }
68269
- });
68270
- }));
68271
- unsubs.push(bus.on("story:completed", (ev) => {
68272
- return safe("onStoryComplete(completed)", async () => {
68273
- const reporters = pluginRegistry.getReporters();
68274
- for (const r of reporters) {
68275
- if (r.onStoryComplete) {
68276
- try {
68277
- await r.onStoryComplete({
68278
- runId,
68279
- storyId: ev.storyId,
68280
- status: "completed",
68281
- runElapsedMs: ev.runElapsedMs,
68282
- cost: ev.cost ?? 0,
68283
- tier: ev.modelTier ?? "balanced",
68284
- testStrategy: ev.testStrategy ?? "test-after"
68285
- });
68286
- } catch (err) {
68287
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68288
- }
68289
- }
68290
- }
68291
- });
68292
- }));
68293
- unsubs.push(bus.on("story:failed", (ev) => {
68294
- return safe("onStoryComplete(failed)", async () => {
68295
- const reporters = pluginRegistry.getReporters();
68296
- for (const r of reporters) {
68297
- if (r.onStoryComplete) {
68298
- try {
68299
- await r.onStoryComplete({
68300
- runId,
68301
- storyId: ev.storyId,
68302
- status: "failed",
68303
- runElapsedMs: Date.now() - startTime,
68304
- cost: 0,
68305
- tier: "balanced",
68306
- testStrategy: "test-after"
68307
- });
68308
- } catch (err) {
68309
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68310
- }
68311
- }
68312
- }
68313
- });
68314
- }));
68315
- unsubs.push(bus.on("story:paused", (ev) => {
68316
- return safe("onStoryComplete(paused)", async () => {
68317
- const reporters = pluginRegistry.getReporters();
68318
- for (const r of reporters) {
68319
- if (r.onStoryComplete) {
68320
- try {
68321
- await r.onStoryComplete({
68322
- runId,
68323
- storyId: ev.storyId,
68324
- status: "paused",
68325
- runElapsedMs: Date.now() - startTime,
68326
- cost: 0,
68327
- tier: "balanced",
68328
- testStrategy: "test-after"
68329
- });
68330
- } catch (err) {
68331
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68332
- }
68333
- }
68334
- }
68335
- });
68336
- }));
68337
- unsubs.push(bus.on("run:completed", (ev) => {
68338
- return safe("onRunEnd", async () => {
68339
- const reporters = pluginRegistry.getReporters();
68340
- for (const r of reporters) {
68341
- if (r.onRunEnd) {
68342
- try {
68343
- await r.onRunEnd({
68344
- runId,
68345
- totalDurationMs: Date.now() - startTime,
68346
- totalCost: ev.totalCost ?? 0,
68347
- storySummary: {
68348
- completed: ev.passedStories,
68349
- failed: ev.failedStories,
68350
- skipped: ev.skippedStories,
68351
- paused: ev.pausedStories
68352
- }
68353
- });
68354
- } catch (err) {
68355
- logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
68356
- }
68357
- }
68358
- }
68359
- });
68360
- }));
68361
- return () => {
68362
- for (const u of unsubs)
68363
- u();
68364
- };
68365
- }
68366
- var init_reporters = __esm(() => {
68367
- init_logger2();
68368
- });
68369
-
68370
69302
  // src/execution/deferred-review.ts
68371
69303
  var {spawn: spawn4 } = globalThis.Bun;
68372
69304
  async function captureRunStartRef(workdir) {
@@ -70380,6 +71312,7 @@ async function executeUnified(ctx, initialPrd) {
70380
71312
  const allStoryMetrics = [];
70381
71313
  let warningSent = false;
70382
71314
  let deferredReview;
71315
+ let deferredReviewStartedAt;
70383
71316
  const runStartRef = await captureRunStartRef(ctx.workdir);
70384
71317
  let cachedNaxIgnoreKey;
70385
71318
  const getRunNaxIgnoreIndex = async (currentPrd) => {
@@ -70418,14 +71351,16 @@ async function executeUnified(ctx, initialPrd) {
70418
71351
  totalCost: totalCost2,
70419
71352
  allStoryMetrics,
70420
71353
  exitReason,
70421
- deferredReview
71354
+ deferredReview,
71355
+ deferredReviewStartedAt
70422
71356
  });
70423
- startHeartbeat(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
71357
+ startHeartbeat2(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
70424
71358
  let _executeThrew = false;
70425
71359
  try {
70426
71360
  if (isComplete(prd)) {
70427
71361
  logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
70428
71362
  const naxIgnoreIndex = await getRunNaxIgnoreIndex(prd);
71363
+ deferredReviewStartedAt = Date.now();
70429
71364
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
70430
71365
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
70431
71366
  return buildResult2("completed");
@@ -70480,6 +71415,7 @@ async function executeUnified(ctx, initialPrd) {
70480
71415
  return buildResult2("pre-merge-aborted");
70481
71416
  }
70482
71417
  logger?.debug("execution", "Running deferred review");
71418
+ deferredReviewStartedAt = Date.now();
70483
71419
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
70484
71420
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
70485
71421
  logger?.debug("execution", "Deferred review done \u2014 returning completed");
@@ -70990,6 +71926,7 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
70990
71926
  totalCost: totalCost2,
70991
71927
  allStoryMetrics,
70992
71928
  deferredReview: unifiedResult.deferredReview,
71929
+ deferredReviewStartedAt: unifiedResult.deferredReviewStartedAt,
70993
71930
  exitReason: unifiedResult.exitReason
70994
71931
  };
70995
71932
  }
@@ -72243,10 +73180,10 @@ async function cleanupRun(options) {
72243
73180
  }
72244
73181
  const actions = pluginRegistry.getPostRunActions();
72245
73182
  const pluginLogger = {
72246
- debug: (msg) => logger?.debug("post-run", msg),
72247
- info: (msg) => logger?.info("post-run", msg),
72248
- warn: (msg) => logger?.warn("post-run", msg),
72249
- error: (msg) => logger?.error("post-run", msg)
73183
+ debug: (msg, data) => logger?.debug("post-run", msg, data),
73184
+ info: (msg, data) => logger?.info("post-run", msg, data),
73185
+ warn: (msg, data) => logger?.warn("post-run", msg, data),
73186
+ error: (msg, data) => logger?.error("post-run", msg, data)
72250
73187
  };
72251
73188
  const ctx = buildPostRunContext(options, durationMs, pluginLogger);
72252
73189
  for (const action of actions) {
@@ -72442,6 +73379,7 @@ async function run(options) {
72442
73379
  agentManager,
72443
73380
  pluginProviderCache,
72444
73381
  deferredReview: executionResult.deferredReview,
73382
+ deferredReviewStartedAt: executionResult.deferredReviewStartedAt,
72445
73383
  exitReason: executionResult.exitReason,
72446
73384
  runtime,
72447
73385
  abortSignal: shutdownController.signal
@@ -72540,8 +73478,11 @@ __export(exports_execution, {
72540
73478
  withIncreasingFailuresBail: () => withIncreasingFailuresBail,
72541
73479
  synthesizeBackfillMetric: () => synthesizeBackfillMetric,
72542
73480
  stopHeartbeat: () => stopHeartbeat,
72543
- startHeartbeat: () => startHeartbeat,
73481
+ startHeartbeat: () => startHeartbeat2,
73482
+ runRectification: () => runRectification,
73483
+ runPhase: () => runPhase,
72544
73484
  runDeferredRegression: () => runDeferredRegression,
73485
+ runCompletionPhase: () => runCompletionPhase,
72545
73486
  run: () => run,
72546
73487
  resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
72547
73488
  resetCrashHandlers: () => resetCrashHandlers,
@@ -72567,7 +73508,6 @@ __export(exports_execution, {
72567
73508
  getTierConfig: () => getTierConfig,
72568
73509
  getOscillations: () => getOscillations,
72569
73510
  getAllReadyStories: () => getAllReadyStories,
72570
- gateRegressedAfterRectification: () => gateRegressedAfterRectification,
72571
73511
  gateFailureKeys: () => gateFailureKeys,
72572
73512
  formatProgress: () => formatProgress,
72573
73513
  formatPhaseResultMessage: () => formatPhaseResultMessage,
@@ -72576,11 +73516,13 @@ __export(exports_execution, {
72576
73516
  extractPauseReason: () => extractPauseReason,
72577
73517
  escalateTier: () => escalateTier,
72578
73518
  ensureStoryPackageDirs: () => ensureStoryPackageDirs,
73519
+ describeGateRegression: () => describeGateRegression,
72579
73520
  deriveTddFailureCategory: () => deriveTddFailureCategory,
72580
73521
  decideStageAction: () => decideStageAction,
72581
73522
  createCheckpointWriter: () => createCheckpointWriter,
72582
73523
  countOscillationOutcomes: () => countOscillationOutcomes,
72583
73524
  clearQueueFile: () => clearQueueFile,
73525
+ cleanupRun: () => cleanupRun,
72584
73526
  captureTreeState: () => captureTreeState,
72585
73527
  calculateMaxIterations: () => calculateMaxIterations,
72586
73528
  buildStoryContext: () => buildStoryContext,
@@ -72597,6 +73539,7 @@ __export(exports_execution, {
72597
73539
  _storyOrchestratorDeps: () => _storyOrchestratorDeps,
72598
73540
  _runnerReentrancyGuard: () => _runnerReentrancyGuard,
72599
73541
  _runnerDeps: () => _runnerDeps,
73542
+ _runnerCompletionDeps: () => _runnerCompletionDeps,
72600
73543
  _runCompletionDeps: () => _runCompletionDeps,
72601
73544
  _regressionDeps: () => _regressionDeps,
72602
73545
  _postRunDeps: () => _postRunDeps,
@@ -72628,6 +73571,7 @@ var init_execution2 = __esm(() => {
72628
73571
  init_plan_inputs();
72629
73572
  init_build_plan_for_strategy();
72630
73573
  init_checkpoint();
73574
+ init_runner_completion();
72631
73575
  init_post_run();
72632
73576
  });
72633
73577
 
@@ -104407,7 +105351,7 @@ async function resolveRunProfileOverride(opts) {
104407
105351
  // src/cli/features-resolve.ts
104408
105352
  init_config();
104409
105353
  import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
104410
- import { join as join74, relative as relative16 } from "path";
105354
+ import { join as join74, relative as relative17 } from "path";
104411
105355
 
104412
105356
  // src/cli/features-acceptance.ts
104413
105357
  init_acceptance2();
@@ -104415,7 +105359,7 @@ init_config();
104415
105359
  init_logger2();
104416
105360
  init_prd();
104417
105361
  import { existsSync as existsSync27 } from "fs";
104418
- import { join as join73, relative as relative15 } from "path";
105362
+ import { join as join73, relative as relative16 } from "path";
104419
105363
  async function resolveFeatureAcceptance(featureName, workdir) {
104420
105364
  let enabled = true;
104421
105365
  try {
@@ -104436,11 +105380,11 @@ async function resolveFeatureAcceptance(featureName, workdir) {
104436
105380
  const prd = await loadPRD(prdPath);
104437
105381
  const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
104438
105382
  const groups = await Promise.all(testGroups.map(async (g) => {
104439
- const packageDir = relative15(repoRoot, g.packageDir);
105383
+ const packageDir = relative16(repoRoot, g.packageDir);
104440
105384
  const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
104441
105385
  return {
104442
105386
  packageDir,
104443
- testPath: relative15(repoRoot, g.testPath),
105387
+ testPath: relative16(repoRoot, g.testPath),
104444
105388
  exists: await Bun.file(g.testPath).exists(),
104445
105389
  command,
104446
105390
  cwd: packageDir,
@@ -104477,17 +105421,17 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104477
105421
  ];
104478
105422
  const docsSpecExact = join74(repoRoot, "docs", "specs", `SPEC-${name}.md`);
104479
105423
  candidates.push({ abs: docsSpecExact, kind: "markdown" });
104480
- const checked = candidates.map((c) => relative16(repoRoot, c.abs));
105424
+ const checked = candidates.map((c) => relative17(repoRoot, c.abs));
104481
105425
  for (const { abs, kind } of candidates.slice(0, 2)) {
104482
105426
  if (kind === "markdown") {
104483
105427
  const nonEmpty = await isNonEmptyFile(abs);
104484
105428
  if (nonEmpty) {
104485
- return { source: { kind, path: relative16(repoRoot, abs) }, checked };
105429
+ return { source: { kind, path: relative17(repoRoot, abs) }, checked };
104486
105430
  }
104487
105431
  }
104488
105432
  }
104489
105433
  if (await isNonEmptyFile(docsSpecExact)) {
104490
- return { source: { kind: "markdown", path: relative16(repoRoot, docsSpecExact) }, checked };
105434
+ return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
104491
105435
  }
104492
105436
  const docsSpecsDir = join74(repoRoot, "docs", "specs");
104493
105437
  if (existsSync28(docsSpecsDir)) {
@@ -104495,7 +105439,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104495
105439
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
104496
105440
  const abs = join74(docsSpecsDir, match);
104497
105441
  if (await isNonEmptyFile(abs)) {
104498
- const relPath = relative16(repoRoot, abs);
105442
+ const relPath = relative17(repoRoot, abs);
104499
105443
  if (!checked.includes(relPath))
104500
105444
  checked.push(relPath);
104501
105445
  return { source: { kind: "markdown", path: relPath }, checked };
@@ -104503,7 +105447,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104503
105447
  }
104504
105448
  }
104505
105449
  const prdAbs = join74(naxDir, "features", name, "prd.json");
104506
- const prdRel = relative16(repoRoot, prdAbs);
105450
+ const prdRel = relative17(repoRoot, prdAbs);
104507
105451
  if (!checked.includes(prdRel))
104508
105452
  checked.push(prdRel);
104509
105453
  if (existsSync28(prdAbs)) {
@@ -104555,8 +105499,8 @@ async function resolveFeatureSpec(name, workdir) {
104555
105499
  return {
104556
105500
  status: "ok",
104557
105501
  featureName: null,
104558
- specSource: { kind: "markdown", path: relative16(repoRoot, abs) },
104559
- message: `resolved spec: ${relative16(repoRoot, abs)}`
105502
+ specSource: { kind: "markdown", path: relative17(repoRoot, abs) },
105503
+ message: `resolved spec: ${relative17(repoRoot, abs)}`
104560
105504
  };
104561
105505
  }
104562
105506
  if (name !== undefined && name.trim() !== "") {
@@ -106054,9 +106998,9 @@ function parseSchedule(input, now2) {
106054
106998
  const trimmed = input.trim();
106055
106999
  if (trimmed === "")
106056
107000
  return { ok: false, error: `Empty schedule value. ${ACCEPTED}` };
106057
- const relative17 = parseRelative(trimmed, now2);
106058
- if (relative17)
106059
- return relative17;
107001
+ const relative18 = parseRelative(trimmed, now2);
107002
+ if (relative18)
107003
+ return relative18;
106060
107004
  const timeOfDay = parseTimeOfDay(trimmed, now2);
106061
107005
  if (timeOfDay)
106062
107006
  return timeOfDay;