@nathapp/nax 0.75.2 → 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,
@@ -36877,6 +36905,7 @@ var init_adversarial_review = __esm(() => {
36877
36905
  return {
36878
36906
  ...parsed,
36879
36907
  passed,
36908
+ blockingThreshold: threshold,
36880
36909
  modelPassed: parsed.passed,
36881
36910
  findings: accepted,
36882
36911
  normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
@@ -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.2",
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("8538e8f4"))
42600
- return "8538e8f4";
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
  }
@@ -43420,7 +43508,7 @@ var init_language_commands = __esm(() => {
43420
43508
  });
43421
43509
 
43422
43510
  // src/review/scoped-lint.ts
43423
- import { join as join27, relative as relative10 } from "path";
43511
+ import { join as join27, relative as relative11 } from "path";
43424
43512
  function shellQuotePath4(path6) {
43425
43513
  return `'${path6.replaceAll("'", "'\\''")}'`;
43426
43514
  }
@@ -43456,7 +43544,7 @@ async function listChangedFiles(workdir, baseRef) {
43456
43544
  function inferActivePackageDir(workdir, projectDir) {
43457
43545
  if (!projectDir)
43458
43546
  return;
43459
- const rel = normalizePath3(relative10(projectDir, workdir));
43547
+ const rel = normalizePath3(relative11(projectDir, workdir));
43460
43548
  if (!rel || rel === "." || rel.startsWith(".."))
43461
43549
  return;
43462
43550
  return rel;
@@ -44610,6 +44698,7 @@ var init_runner2 = __esm(() => {
44610
44698
  // src/review/index.ts
44611
44699
  var init_review = __esm(() => {
44612
44700
  init_semantic_helpers();
44701
+ init_adversarial_helpers();
44613
44702
  init_category_fix_target();
44614
44703
  init_finding_filters();
44615
44704
  init_ac_quote_validator();
@@ -44894,7 +44983,10 @@ REASON: <one paragraph: which mock is wrong vs which dispatch the new code uses,
44894
44983
  Rules:
44895
44984
  - Do NOT make any edits yourself; the test-writer will fulfill.
44896
44985
  - Do NOT also emit \`UNRESOLVED:\` in the same turn \u2014 this declaration IS the handoff.
44897
- - FILES must list real test files. Each path must exist and be a test file.`, SINGLE_SESSION_PERMIT_HEADLINE = "You authored these tests in the same session as the implementation, so you MAY edit test files \u2014 but ONLY to resolve a genuine contradiction between a test and this story's acceptance criteria (or between two acceptance criteria). NEVER weaken, delete, loosen, or skip a test merely to make it pass. See the test-edit guidance appended below.", SINGLE_SESSION_TEST_EDIT_POLICY = `
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 = `
44898
44990
 
44899
44991
  ## Test-edit guidance (single-session implementer)
44900
44992
 
@@ -48676,7 +48768,7 @@ var init_pid_registry = __esm(() => {
48676
48768
  // src/session/manager-deps.ts
48677
48769
  import { randomUUID as randomUUID3 } from "crypto";
48678
48770
  import { mkdir as mkdir5 } from "fs/promises";
48679
- 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";
48680
48772
  function resolveProjectDirFromScratchDir(scratchDir) {
48681
48773
  const marker = `${sep2}.nax${sep2}features${sep2}`;
48682
48774
  const markerIdx = scratchDir.lastIndexOf(marker);
@@ -48688,7 +48780,7 @@ function resolveProjectDirFromScratchDir(scratchDir) {
48688
48780
  return;
48689
48781
  }
48690
48782
  function toProjectRelativePath(projectDir, pathValue) {
48691
- const relativePath = isAbsolute10(pathValue) ? relative11(projectDir, pathValue) : pathValue;
48783
+ const relativePath = isAbsolute11(pathValue) ? relative12(projectDir, pathValue) : pathValue;
48692
48784
  return relativePath === "" ? "." : relativePath;
48693
48785
  }
48694
48786
  var _sessionManagerDeps;
@@ -50198,7 +50290,7 @@ var init_windsurf = __esm(() => {
50198
50290
 
50199
50291
  // src/context/generator.ts
50200
50292
  import { existsSync as existsSync9 } from "fs";
50201
- import { join as join33, relative as relative12 } from "path";
50293
+ import { join as join33, relative as relative13 } from "path";
50202
50294
  async function loadContextContent(options, config2) {
50203
50295
  if (!_generatorDeps.existsSync(options.contextPath)) {
50204
50296
  throw new Error(`Context file not found: ${options.contextPath}`);
@@ -50326,7 +50418,7 @@ async function discoverWorkspacePackages2(repoRoot) {
50326
50418
  }
50327
50419
  async function generateForPackage(packageDir, config2, dryRun = false, repoRoot) {
50328
50420
  const resolvedRepoRoot = repoRoot ?? packageDir;
50329
- const relativePkgPath = relative12(resolvedRepoRoot, packageDir);
50421
+ const relativePkgPath = relative13(resolvedRepoRoot, packageDir);
50330
50422
  const contextPath = join33(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
50331
50423
  if (!_generatorDeps.existsSync(contextPath)) {
50332
50424
  return [
@@ -54173,7 +54265,7 @@ var init_checks_blockers = __esm(() => {
54173
54265
 
54174
54266
  // src/precheck/checks-warnings.ts
54175
54267
  import { existsSync as existsSync14 } from "fs";
54176
- import { isAbsolute as isAbsolute11 } from "path";
54268
+ import { isAbsolute as isAbsolute12 } from "path";
54177
54269
  async function checkClaudeMdExists(workdir) {
54178
54270
  const claudeMdPath = `${workdir}/CLAUDE.md`;
54179
54271
  const passed = existsSync14(claudeMdPath);
@@ -54308,7 +54400,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
54308
54400
  }
54309
54401
  async function checkHomeEnvValid() {
54310
54402
  const home = process.env.HOME ?? "";
54311
- const passed = home !== "" && isAbsolute11(home);
54403
+ const passed = home !== "" && isAbsolute12(home);
54312
54404
  return {
54313
54405
  name: "home-env-valid",
54314
54406
  tier: "warning",
@@ -57012,6 +57104,79 @@ ${stderr}`;
57012
57104
  };
57013
57105
  });
57014
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
+
57015
57180
  // src/pipeline/stages/acceptance-setup.ts
57016
57181
  var exports_acceptance_setup = {};
57017
57182
  __export(exports_acceptance_setup, {
@@ -57027,6 +57192,214 @@ function computeACFingerprint(criteria) {
57027
57192
  hasher.update(sorted);
57028
57193
  return `sha256:${hasher.digest("hex")}`;
57029
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
+ }
57030
57403
  var _acceptanceSetupDeps, acceptanceSetupStage;
57031
57404
  var init_acceptance_setup = __esm(() => {
57032
57405
  init_acceptance2();
@@ -57035,6 +57408,7 @@ var init_acceptance_setup = __esm(() => {
57035
57408
  init_logger2();
57036
57409
  init_operations();
57037
57410
  init_git();
57411
+ init_event_bus();
57038
57412
  _acceptanceSetupDeps = {
57039
57413
  getAgent: (_name) => {
57040
57414
  return;
@@ -57134,189 +57508,19 @@ ${stderr}` };
57134
57508
  if (!ctx.featureDir) {
57135
57509
  return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
57136
57510
  }
57137
- const language = ctx.config.project?.language;
57138
- const testPathConfig = ctx.config.acceptance.testPath;
57139
- const metaPath = path12.join(ctx.featureDir, "acceptance-meta.json");
57140
- const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
57141
- const featureName = ctx.prd.feature ?? ctx.prd.featureName;
57142
- const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
57143
- const nonFixStories = groups.flatMap((g) => g.stories);
57144
- let totalCriteria = 0;
57145
- let testableCount = 0;
57146
- const fingerprint = computeACFingerprint(allCriteria);
57147
- const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
57148
- getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
57149
- currentFingerprint: fingerprint,
57150
- storedFingerprint: meta3?.acFingerprint ?? "none",
57151
- match: meta3?.acFingerprint === fingerprint
57152
- });
57153
- let shouldGenerate = false;
57154
- if (!meta3 || meta3.acFingerprint !== fingerprint) {
57155
- if (!meta3) {
57156
- getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
57157
- } else {
57158
- getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
57159
- reason: "fingerprint mismatch",
57160
- currentFingerprint: fingerprint,
57161
- storedFingerprint: meta3.acFingerprint
57162
- });
57163
- }
57164
- for (const { testPath } of groups) {
57165
- if (await _acceptanceSetupDeps.fileExists(testPath)) {
57166
- await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
57167
- await _acceptanceSetupDeps.deleteFile(testPath);
57168
- }
57169
- }
57170
- await _acceptanceSetupDeps.deleteSemanticVerdicts(ctx.featureDir);
57171
- shouldGenerate = true;
57172
- } else {
57173
- getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
57174
- }
57175
- if (shouldGenerate) {
57176
- totalCriteria = allCriteria.length;
57177
- let allRefinedCriteria;
57178
- if (ctx.config.acceptance.refinement) {
57179
- const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
57180
- const results = new Array(nonFixStories.length);
57181
- const executing = new Set;
57182
- for (let i = 0;i < nonFixStories.length; i++) {
57183
- const story = nonFixStories[i];
57184
- const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
57185
- criteria: story.acceptanceCriteria,
57186
- codebaseContext: "",
57187
- storyId: story.id,
57188
- testStrategy: ctx.config.acceptance.testStrategy,
57189
- testFramework: ctx.config.acceptance.testFramework,
57190
- storyTitle: story.title,
57191
- storyDescription: story.description
57192
- }, story.id).then((refined) => {
57193
- results[i] = refined;
57194
- }).catch(() => {
57195
- getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
57196
- storyId: story.id
57197
- });
57198
- results[i] = story.acceptanceCriteria.map((c) => ({
57199
- original: c,
57200
- refined: c,
57201
- testable: true,
57202
- storyId: story.id
57203
- }));
57204
- }).finally(() => {
57205
- executing.delete(task);
57206
- });
57207
- executing.add(task);
57208
- if (executing.size >= maxConcurrency) {
57209
- await Promise.race(executing);
57210
- }
57211
- }
57212
- await Promise.all(executing);
57213
- allRefinedCriteria = results.flat();
57214
- } else {
57215
- allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
57216
- original: c,
57217
- refined: c,
57218
- testable: true,
57219
- storyId: story.id
57220
- })));
57221
- }
57222
- testableCount = allRefinedCriteria.filter((r) => r.testable).length;
57223
- for (const group of groups) {
57224
- const { testPath, packageDir } = group;
57225
- const groupStoryIds = new Set(group.stories.map((s) => s.id));
57226
- const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
57227
- const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
57228
- `);
57229
- const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
57230
- [FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
57231
- const groupStoryId = group.stories[0]?.id;
57232
- const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
57233
- featureName: featureName ?? "",
57234
- criteriaList,
57235
- frameworkOverrideLine,
57236
- targetTestFilePath: testPath,
57237
- ..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
57238
- }, groupStoryId);
57239
- const testCode = genResult.testCode;
57240
- if (testCode) {
57241
- await _acceptanceSetupDeps.writeFile(testPath, testCode);
57242
- } else {
57243
- const skeletonCriteria = groupRefined.map((c, i) => ({
57244
- id: `AC-${i + 1}`,
57245
- text: c.refined,
57246
- lineNumber: i + 1
57247
- }));
57248
- const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
57249
- await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
57250
- getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
57251
- storyId: groupStoryId,
57252
- testPath
57253
- });
57254
- }
57255
- }
57256
- if (allRefinedCriteria.length > 0) {
57257
- const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
57258
- acId: `AC-${i + 1}`,
57259
- original: c.original,
57260
- refined: c.refined,
57261
- testable: c.testable,
57262
- storyId: c.storyId
57263
- })), null, 2);
57264
- await _acceptanceSetupDeps.writeFile(path12.join(ctx.featureDir, "acceptance-refined.json"), refinedJsonContent);
57265
- }
57266
- const fingerprint2 = computeACFingerprint(allCriteria);
57267
- await _acceptanceSetupDeps.writeMeta(metaPath, {
57268
- generatedAt: new Date().toISOString(),
57269
- acFingerprint: fingerprint2,
57270
- storyCount: ctx.prd.userStories.length,
57271
- acCount: totalCriteria,
57272
- generator: "nax"
57273
- });
57274
- await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
57275
- }
57276
- const acceptanceTestPaths = [];
57277
- for (const g of groups) {
57278
- const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
57279
- let groupConfig = ctx.config;
57280
- if (relativeWorkdir && relativeWorkdir !== ".") {
57281
- try {
57282
- groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
57283
- } catch {
57284
- groupConfig = ctx.config;
57285
- }
57286
- }
57287
- acceptanceTestPaths.push({
57288
- testPath: g.testPath,
57289
- packageDir: g.packageDir,
57290
- testFramework: groupConfig.project?.testFramework,
57291
- commandOverride: groupConfig.acceptance.command
57292
- });
57293
- }
57294
- ctx.acceptanceTestPaths = acceptanceTestPaths;
57295
- if (ctx.config.acceptance.redGate === false) {
57296
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57297
- return { action: "continue" };
57298
- }
57299
- let redFailCount = 0;
57300
- for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
57301
- const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
57302
- getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
57303
- cmd: runCmd.join(" "),
57304
- packageDir
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
57305
57521
  });
57306
- const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
57307
- if (exitCode !== 0) {
57308
- redFailCount++;
57309
- }
57310
- }
57311
- if (redFailCount === 0) {
57312
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
57313
- return {
57314
- action: "skip",
57315
- reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
57316
- };
57522
+ throw err;
57317
57523
  }
57318
- ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
57319
- return { action: "continue" };
57320
57524
  }
57321
57525
  };
57322
57526
  });
@@ -57462,79 +57666,6 @@ async function appendProgress(featureDir, storyId, status, message) {
57462
57666
  }
57463
57667
  var init_progress = () => {};
57464
57668
 
57465
- // src/pipeline/event-bus.ts
57466
- class PipelineEventBus {
57467
- subscribers = new Map;
57468
- _pending = new Set;
57469
- on(eventType, subscriber) {
57470
- const list = this.subscribers.get(eventType) ?? [];
57471
- list.push(subscriber);
57472
- this.subscribers.set(eventType, list);
57473
- return () => {
57474
- const current = this.subscribers.get(eventType) ?? [];
57475
- this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
57476
- };
57477
- }
57478
- onAll(subscriber) {
57479
- const list = this.subscribers.get("*") ?? [];
57480
- list.push(subscriber);
57481
- this.subscribers.set("*", list);
57482
- return () => {
57483
- const current = this.subscribers.get("*") ?? [];
57484
- this.subscribers.set("*", current.filter((s) => s !== subscriber));
57485
- };
57486
- }
57487
- emit(event) {
57488
- const logger = getLogger();
57489
- const specific = this.subscribers.get(event.type) ?? [];
57490
- const all = this.subscribers.get("*") ?? [];
57491
- const targets = [...specific, ...all];
57492
- for (const sub of targets) {
57493
- try {
57494
- const result = sub(event);
57495
- if (result instanceof Promise) {
57496
- const tracked = result.catch((err) => {
57497
- logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57498
- });
57499
- this._pending.add(tracked);
57500
- tracked.finally(() => this._pending.delete(tracked));
57501
- }
57502
- } catch (err) {
57503
- logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
57504
- }
57505
- }
57506
- }
57507
- async emitAsync(event) {
57508
- const logger = getLogger();
57509
- const specific = this.subscribers.get(event.type) ?? [];
57510
- const all = this.subscribers.get("*") ?? [];
57511
- const targets = [...specific, ...all];
57512
- await Promise.allSettled(targets.map(async (sub) => {
57513
- try {
57514
- await sub(event);
57515
- } catch (err) {
57516
- logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
57517
- }
57518
- }));
57519
- }
57520
- async drain() {
57521
- if (this._pending.size === 0)
57522
- return;
57523
- await Promise.allSettled([...this._pending]);
57524
- }
57525
- clear() {
57526
- this.subscribers.clear();
57527
- }
57528
- subscriberCount(eventType) {
57529
- return (this.subscribers.get(eventType) ?? []).length;
57530
- }
57531
- }
57532
- var pipelineEventBus;
57533
- var init_event_bus = __esm(() => {
57534
- init_logger2();
57535
- pipelineEventBus = new PipelineEventBus;
57536
- });
57537
-
57538
57669
  // src/pipeline/stages/completion.ts
57539
57670
  async function getDiffText(workdir, baseRef) {
57540
57671
  if (!baseRef)
@@ -58326,11 +58457,11 @@ var init_rollback = __esm(() => {
58326
58457
  });
58327
58458
 
58328
58459
  // src/utils/paths.ts
58329
- 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";
58330
58461
  function packageDirRelative(projectDir, workdir) {
58331
58462
  if (!projectDir || !workdir || workdir === projectDir)
58332
58463
  return;
58333
- const rel = relative13(projectDir, workdir);
58464
+ const rel = relative14(projectDir, workdir);
58334
58465
  if (rel === ".." || rel.startsWith(`..${sep4}`))
58335
58466
  return;
58336
58467
  return rel && rel !== "." ? rel : undefined;
@@ -58346,6 +58477,9 @@ var init_paths3 = __esm(() => {
58346
58477
  });
58347
58478
 
58348
58479
  // src/execution/non-blocking-fix.ts
58480
+ function actionableAdvisoryFindings(findings) {
58481
+ return findings.filter((f) => f.actionRequired !== false);
58482
+ }
58349
58483
  function shouldRunNonBlockingFix(cfg, advisoryCount) {
58350
58484
  return cfg?.enabled === true && advisoryCount > 0;
58351
58485
  }
@@ -58418,9 +58552,16 @@ async function runNonBlockingFix(args, overrides = {}) {
58418
58552
  exhausted = true;
58419
58553
  }
58420
58554
  if (!exhausted) {
58421
- if (args.keptTreeRegressed?.()) {
58555
+ const gateVerdict = args.keptTreeRegressed?.();
58556
+ if (gateVerdict?.regressed) {
58422
58557
  logger?.info("non-blocking-fix", "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", {
58423
- 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
58424
58565
  });
58425
58566
  return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
58426
58567
  }
@@ -58464,7 +58605,7 @@ async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot,
58464
58605
  });
58465
58606
  return { ran: true, kept: false, restored: true };
58466
58607
  }
58467
- var REVIEW_PHASE_KINDS, _nonBlockingFixDeps, DEFAULT_DEPS;
58608
+ var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
58468
58609
  var init_non_blocking_fix = __esm(() => {
58469
58610
  init_logger2();
58470
58611
  init_rollback();
@@ -58607,13 +58748,29 @@ function gateFailureKeys(gateOutput) {
58607
58748
  }
58608
58749
  return keys;
58609
58750
  }
58610
- function gateRegressedAfterRectification(finalGateOutput, baselineKeys, gateName, storyId) {
58611
- if (phasePassed(gateName, finalGateOutput, storyId))
58612
- return false;
58613
- const finalKeys = gateFailureKeys(finalGateOutput);
58614
- const hasNewStructuredKey = [...finalKeys].some((k) => !baselineKeys.has(k));
58615
- const isKeylessFailure = finalKeys.size === 0 || finalKeys.has(KEYLESS_GATE_FAILURE_KEY);
58616
- return hasNewStructuredKey || isKeylessFailure;
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
+ };
58617
58774
  }
58618
58775
  function phasesToRevalidate(strategiesRun, allPhases) {
58619
58776
  if (!strategiesRun || strategiesRun.length === 0)
@@ -58760,6 +58917,9 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
58760
58917
  const data = { storyId, phase: opName, durationMs };
58761
58918
  if (findingsCount !== undefined)
58762
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;
58763
58923
  if (status !== undefined)
58764
58924
  data.status = status;
58765
58925
  if (typeof r.failureCategory === "string")
@@ -58791,8 +58951,10 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
58791
58951
  logger?.warn("story-orchestrator", message, data);
58792
58952
  }
58793
58953
  }
58954
+ var MAX_LOGGED_FINDING_IDENTITIES = 10;
58794
58955
  var init_story_orchestrator_logging = __esm(() => {
58795
58956
  init_logger2();
58957
+ init_phase_eval();
58796
58958
  });
58797
58959
  // src/verification/flake-baseline-diff.ts
58798
58960
  async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
@@ -59065,10 +59227,13 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59065
59227
  const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
59066
59228
  let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
59067
59229
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
59230
+ let advIterationBefore = 0;
59068
59231
  if (opName === "adversarial-review" && ctx.storyId) {
59232
+ const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59233
+ advIterationBefore = priorIterations.length;
59069
59234
  dispatchInput = {
59070
59235
  ...dispatchInput,
59071
- priorAdversarialIterations: getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId)
59236
+ priorAdversarialIterations: priorIterations
59072
59237
  };
59073
59238
  }
59074
59239
  if (isTddPhase) {
@@ -59085,6 +59250,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59085
59250
  }
59086
59251
  const phaseStartedAt = Date.now();
59087
59252
  const scope = ctx.runtime.costAggregator.openScope();
59253
+ let outcome = "passed";
59088
59254
  try {
59089
59255
  const output = await _storyOrchestratorDeps.callOp({ ...ctx, scopeId: scope.scopeId }, slot.op, dispatchInput);
59090
59256
  phaseOutputs[opName] = output;
@@ -59098,6 +59264,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59098
59264
  }
59099
59265
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
59100
59266
  logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
59267
+ outcome = derivePhaseOutcome(output);
59101
59268
  if (isTddPhase) {
59102
59269
  const durationMs = Date.now() - phaseStartedAt;
59103
59270
  logger?.info("tdd", `Session complete: ${opName}`, {
@@ -59127,11 +59294,95 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59127
59294
  }
59128
59295
  }
59129
59296
  return output;
59297
+ } catch (err) {
59298
+ outcome = "error";
59299
+ throw err;
59130
59300
  } finally {
59131
- phaseCosts[opName] = (phaseCosts[opName] ?? 0) + scope.snapshot().totalCostUsd;
59301
+ const snapshot = scope.snapshot();
59302
+ phaseCosts[opName] = (phaseCosts[opName] ?? 0) + snapshot.totalCostUsd;
59132
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
+ }
59133
59322
  }
59134
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
+ }
59135
59386
  function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
59136
59387
  if (!enabled)
59137
59388
  return strategies;
@@ -59155,7 +59406,7 @@ function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
59155
59406
  }
59156
59407
  }));
59157
59408
  }
59158
- var _storyOrchestratorDeps;
59409
+ var _storyOrchestratorDeps, ALL_FINDING_SEVERITIES;
59159
59410
  var init_run_phase = __esm(() => {
59160
59411
  init_findings();
59161
59412
  init_logger2();
@@ -59185,6 +59436,14 @@ var init_run_phase = __esm(() => {
59185
59436
  },
59186
59437
  loadCheckpoints: async (_featureDir) => new Map
59187
59438
  };
59439
+ ALL_FINDING_SEVERITIES = [
59440
+ "critical",
59441
+ "error",
59442
+ "warning",
59443
+ "info",
59444
+ "low",
59445
+ "unverifiable"
59446
+ ];
59188
59447
  });
59189
59448
 
59190
59449
  // src/execution/story-orchestrator/rectification.ts
@@ -59408,6 +59667,15 @@ class ExecutionPlan {
59408
59667
  this.state = state;
59409
59668
  this.isThreeSession = isThreeSession;
59410
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
+ }
59411
59679
  phaseNames() {
59412
59680
  const names = collectOrderedPhases(this.state).map((p) => p.slot.op.name);
59413
59681
  if (this.state.rectification) {
@@ -59544,7 +59812,7 @@ class ExecutionPlan {
59544
59812
  const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
59545
59813
  const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
59546
59814
  const advisoryOut = phaseOutputs["adversarial-review"];
59547
- const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
59815
+ const advisoryFindings = actionableAdvisoryFindings(advisoryOut?.advisoryFindings ?? []);
59548
59816
  if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
59549
59817
  await _storyOrchestratorDeps.runNonBlockingFix({
59550
59818
  workdir: this.ctx.packageDir,
@@ -59561,7 +59829,7 @@ class ExecutionPlan {
59561
59829
  maxAttempts,
59562
59830
  postValidate: this.state.nonBlockingFixPostValidate
59563
59831
  }),
59564
- keptTreeRegressed: () => gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId)
59832
+ keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
59565
59833
  }, {
59566
59834
  measureSourceDiff: createMeasureSourceDiff({
59567
59835
  config: this.ctx.runtime.configLoader.current(),
@@ -59572,7 +59840,7 @@ class ExecutionPlan {
59572
59840
  }
59573
59841
  const verifierName = this.state.verifier?.slot.op.name;
59574
59842
  const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
59575
- const gateRegressedDuringRect = gateName !== undefined && gateRegressedAfterRectification(phaseOutputs[gateName], preRectGateFailureKeys, gateName, this.ctx.storyId);
59843
+ const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
59576
59844
  const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
59577
59845
  if (verifierExplicitlyPassed && gateRegressedDuringRect) {
59578
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 });
@@ -59792,8 +60060,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59792
60060
  if (inputs.adversarialReview) {
59793
60061
  builder.addAdversarialReview(inputs.adversarialReview);
59794
60062
  }
59795
- const packageDir = join50(ctx.packageDir, story.workdir ?? "");
59796
- 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);
59797
60066
  if (shouldRunRectification(config2) && inputs.rectification) {
59798
60067
  const sink = makeDeclarationSink();
59799
60068
  const strategies = [];
@@ -59829,7 +60098,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59829
60098
  files: h.files,
59830
60099
  reasonDetail: h.reasonDetail
59831
60100
  }));
59832
- const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
60101
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
60102
+ repoRoot
60103
+ });
59833
60104
  sink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
59834
60105
  const allDeclarations = [...sink.testEdits, ...valid];
59835
60106
  sink.testEdits = [];
@@ -59888,7 +60159,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
59888
60159
  files: h.files,
59889
60160
  reasonDetail: h.reasonDetail
59890
60161
  }));
59891
- const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir);
60162
+ const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
60163
+ repoRoot
60164
+ });
59892
60165
  nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
59893
60166
  const allDeclarations = [...nbSink.testEdits, ...valid];
59894
60167
  nbSink.testEdits = [];
@@ -60827,7 +61100,12 @@ var init_execution = __esm(() => {
60827
61100
  featureName: ctx.prd.feature,
60828
61101
  story: ctx.story,
60829
61102
  ...ctx.featureDir ? { featureDir: ctx.featureDir } : {},
60830
- ...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
+ }
60831
61109
  };
60832
61110
  let capturedTokenUsage;
60833
61111
  let capturedResponse = "";
@@ -61590,6 +61868,196 @@ var init_stages = __esm(() => {
61590
61868
  preRunPipeline = [acceptanceSetupStage];
61591
61869
  });
61592
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
+
61593
62061
  // src/pipeline/index.ts
61594
62062
  var init_pipeline = __esm(() => {
61595
62063
  init_runner4();
@@ -61597,7 +62065,9 @@ var init_pipeline = __esm(() => {
61597
62065
  init_stages();
61598
62066
  init_queue_check();
61599
62067
  init_execution_helpers();
62068
+ init_acceptance_setup();
61600
62069
  init_event_bus();
62070
+ init_reporters();
61601
62071
  });
61602
62072
 
61603
62073
  // src/cli/prompts-shared.ts
@@ -64352,6 +64822,19 @@ var init_telegram2 = __esm(() => {
64352
64822
 
64353
64823
  // src/plugins/builtin/nax-finish/index.ts
64354
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
+ }
64355
64838
  async function defaultRun2(cmd, opts) {
64356
64839
  const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
64357
64840
  let timedOut = false;
@@ -64425,7 +64908,7 @@ function buildFlowEnv(cfg) {
64425
64908
  env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
64426
64909
  return env2;
64427
64910
  }
64428
- 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;
64429
64912
  var init_nax_finish = __esm(() => {
64430
64913
  init_config2();
64431
64914
  init_telegram2();
@@ -64476,7 +64959,16 @@ var init_nax_finish = __esm(() => {
64476
64959
  });
64477
64960
  const result = await _naxFinishDeps.readResult(ctx.workdir);
64478
64961
  if (!result) {
64479
- 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
+ };
64480
64972
  }
64481
64973
  if (result.status === "escalated" && escalateTelegram && creds) {
64482
64974
  await _naxFinishDeps.notify(creds, `nax-finish escalated *${result.feature}*: ${result.escalationReason ?? ""}`);
@@ -64554,16 +65046,72 @@ var init_reporter_shared = __esm(() => {
64554
65046
  init_post_json();
64555
65047
  });
64556
65048
 
64557
- // src/plugins/builtin/otel-reporter/ids.ts
64558
- function randomHex(bytes) {
64559
- const arr = new Uint8Array(bytes);
64560
- crypto.getRandomValues(arr);
64561
- let out = "";
64562
- for (const b of arr)
64563
- out += b.toString(16).padStart(2, "0");
64564
- return out;
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
+ };
64565
65110
  }
64566
- 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
+ });
64567
65115
 
64568
65116
  // src/plugins/builtin/otel-reporter/otlp.ts
64569
65117
  function attr(key, value) {
@@ -64572,10 +65120,27 @@ function attr(key, value) {
64572
65120
  function msToUnixNano(ms) {
64573
65121
  return (BigInt(Math.round(ms)) * 1000000n).toString();
64574
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
+ }
64575
65139
  function buildTracesPayload(p) {
64576
65140
  const span = {
64577
65141
  traceId: p.traceId,
64578
65142
  spanId: p.spanId,
65143
+ ...p.parentSpanId ? { parentSpanId: p.parentSpanId } : {},
64579
65144
  name: "nax.run",
64580
65145
  kind: 1,
64581
65146
  startTimeUnixNano: p.startUnixNano,
@@ -64596,7 +65161,7 @@ function buildTracesPayload(p) {
64596
65161
  resourceSpans: [
64597
65162
  {
64598
65163
  resource: { attributes: [attr("service.name", p.serviceName)] },
64599
- scopeSpans: [{ scope: { name: "nax" }, spans: [span] }]
65164
+ scopeSpans: [{ scope: { name: "nax" }, spans: [span, ...p.extraSpans ?? []] }]
64600
65165
  }
64601
65166
  ]
64602
65167
  };
@@ -64634,16 +65199,356 @@ function buildMetricsPayload(p) {
64634
65199
  };
64635
65200
  }
64636
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
+
64637
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
+ }
64638
65487
  function createOtelReporterPlugin(cfg, deps) {
64639
65488
  const states = new Map;
64640
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
+ };
64641
65546
  const flush = async (st, endMs, e) => {
64642
65547
  if (!base)
64643
65548
  return;
64644
65549
  const { resolved, missing } = interpolateHeaders(cfg.headers);
64645
65550
  if (missing.length > 0) {
64646
- getSafeLogger()?.warn(STAGE, "Skipping OTLP export \u2014 unresolved env vars", { missing });
65551
+ getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
64647
65552
  return;
64648
65553
  }
64649
65554
  const startUnixNano = msToUnixNano(st.startMs);
@@ -64652,6 +65557,7 @@ function createOtelReporterPlugin(cfg, deps) {
64652
65557
  serviceName: cfg.serviceName,
64653
65558
  traceId: st.traceId,
64654
65559
  spanId: st.spanId,
65560
+ parentSpanId: st.parentSpanId,
64655
65561
  startUnixNano,
64656
65562
  endUnixNano,
64657
65563
  feature: st.feature,
@@ -64668,20 +65574,35 @@ function createOtelReporterPlugin(cfg, deps) {
64668
65574
  totalCost: e.totalCost,
64669
65575
  totalDurationMs: e.totalDurationMs
64670
65576
  });
64671
- 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 };
64672
65580
  await postJson(`${base}/v1/traces`, traces, opts);
64673
65581
  await postJson(`${base}/v1/metrics`, metrics, opts);
64674
65582
  };
64675
65583
  const reporter = {
64676
- name: STAGE,
65584
+ name: STAGE3,
64677
65585
  async onRunStart(event) {
64678
- states.set(event.runId, {
64679
- traceId: newTraceId(),
64680
- spanId: newSpanId(),
65586
+ const identity = rootSpanIdentity();
65587
+ const runId = event.runId;
65588
+ const state = {
65589
+ ...identity,
64681
65590
  startMs: Date.parse(event.startTime),
64682
65591
  feature: event.feature,
64683
- events: []
64684
- });
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);
64685
65606
  },
64686
65607
  async onStoryComplete(event) {
64687
65608
  const st = states.get(event.runId);
@@ -64698,32 +65619,98 @@ function createOtelReporterPlugin(cfg, deps) {
64698
65619
  attr("testStrategy", event.testStrategy)
64699
65620
  ]
64700
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);
64701
65668
  },
64702
65669
  async onRunEnd(event) {
64703
65670
  const existing = states.get(event.runId);
65671
+ existing?.heartbeat.stop();
64704
65672
  const startMs = existing?.startMs ?? Date.now() - event.totalDurationMs;
64705
- const st = existing ?? {
64706
- traceId: newTraceId(),
64707
- spanId: newSpanId(),
64708
- startMs,
64709
- feature: "",
64710
- events: []
64711
- };
65673
+ const st = existing ?? buildOrphanState(startMs);
64712
65674
  states.delete(event.runId);
65675
+ await st.spanQueue.flushNow();
65676
+ st.spanQueue.teardown();
64713
65677
  await flush(st, startMs + event.totalDurationMs, event);
64714
65678
  }
64715
65679
  };
64716
65680
  return {
64717
- name: STAGE,
65681
+ name: STAGE3,
64718
65682
  version: "1.0.0",
64719
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
+ },
64720
65703
  extensions: { reporter }
64721
65704
  };
64722
65705
  }
64723
- var STAGE = "otel-reporter";
65706
+ var STAGE3 = "otel-reporter", DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048;
64724
65707
  var init_otel_reporter = __esm(() => {
64725
65708
  init_logger2();
64726
65709
  init_reporter_shared();
65710
+ init_batch_queue();
65711
+ init_heartbeat();
65712
+ init_span_tree();
65713
+ init_traceparent();
64727
65714
  });
64728
65715
 
64729
65716
  // src/plugins/builtin/webhook-reporter/index.ts
@@ -64734,25 +65721,27 @@ function createWebhookReporterPlugin(cfg, deps) {
64734
65721
  return;
64735
65722
  const { resolved, missing } = interpolateHeaders(cfg.headers);
64736
65723
  if (missing.length > 0) {
64737
- getSafeLogger()?.warn(STAGE2, "Skipping webhook \u2014 unresolved env vars", { missing });
65724
+ getSafeLogger()?.warn(STAGE4, "Skipping webhook \u2014 unresolved env vars", { missing });
64738
65725
  return;
64739
65726
  }
64740
- 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 });
64741
65728
  };
64742
65729
  const reporter = {
64743
- name: STAGE2,
65730
+ name: STAGE4,
64744
65731
  onRunStart: (event) => emit("onRunStart", event),
64745
65732
  onStoryComplete: (event) => emit("onStoryComplete", event),
64746
- onRunEnd: (event) => emit("onRunEnd", event)
65733
+ onRunEnd: (event) => emit("onRunEnd", event),
65734
+ onPhaseStart: (event) => emit("onPhaseStart", event),
65735
+ onPhaseComplete: (event) => emit("onPhaseComplete", event)
64747
65736
  };
64748
65737
  return {
64749
- name: STAGE2,
65738
+ name: STAGE4,
64750
65739
  version: "1.0.0",
64751
65740
  provides: ["reporter"],
64752
65741
  extensions: { reporter }
64753
65742
  };
64754
65743
  }
64755
- var STAGE2 = "webhook-reporter";
65744
+ var STAGE4 = "webhook-reporter";
64756
65745
  var init_webhook_reporter = __esm(() => {
64757
65746
  init_logger2();
64758
65747
  init_reporter_shared();
@@ -65787,7 +66776,7 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
65787
66776
  }
65788
66777
  }
65789
66778
  }
65790
- function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
66779
+ function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
65791
66780
  const logger = _heartbeatDeps.getSafeLogger();
65792
66781
  _heartbeatActive = true;
65793
66782
  const gen = ++_heartbeatGen;
@@ -67381,28 +68370,47 @@ async function handleRunCompletion(options) {
67381
68370
  const regressionMode = config2.execution.regressionGate?.mode;
67382
68371
  if (options.skipRegression) {} else if ((regressionMode === "deferred" || regressionMode === "per-story") && config2.quality.commands.test) {
67383
68372
  statusWriter.setPostRunPhase("regression", { status: "running" });
68373
+ const regressionStartTime = Date.now();
67384
68374
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "regression" });
67385
- const regressionResult = await _runCompletionDeps.runDeferredRegression({
67386
- config: config2,
67387
- prd,
67388
- workdir,
67389
- runtime: options.runtime,
67390
- quarantineMemo: options.runtime.quarantineMemo,
67391
- storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
67392
- storyId: m.storyId,
67393
- completedAt: m.completedAt,
67394
- failingTestFiles: m.failingTestFiles
67395
- }))
67396
- });
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
+ }
67397
68398
  const lastRunAt = new Date().toISOString();
67398
68399
  logger?.info("regression", "Deferred regression gate completed", {
67399
68400
  success: regressionResult.success,
67400
68401
  failedTests: regressionResult.failedTests,
67401
68402
  affectedStories: regressionResult.affectedStories
67402
68403
  });
68404
+ const regressionDurationMs = Date.now() - regressionStartTime;
67403
68405
  if (regressionResult.success) {
67404
68406
  statusWriter.setPostRunPhase("regression", { status: "passed", lastRunAt });
67405
- 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
+ });
67406
68414
  } else {
67407
68415
  statusWriter.setPostRunPhase("regression", {
67408
68416
  status: "failed",
@@ -67410,7 +68418,16 @@ async function handleRunCompletion(options) {
67410
68418
  affectedStories: regressionResult.affectedStories,
67411
68419
  lastRunAt
67412
68420
  });
67413
- 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
+ });
67414
68431
  for (const storyId of regressionResult.affectedStories) {
67415
68432
  const story = prd.userStories.find((s) => s.id === storyId);
67416
68433
  if (story) {
@@ -67478,7 +68495,15 @@ async function handleRunCompletion(options) {
67478
68495
  let pluginGateFailed = false;
67479
68496
  const deferredReview = options.deferredReview;
67480
68497
  if (deferredReview !== undefined) {
67481
- 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
+ });
67482
68507
  }
67483
68508
  if (deferredReview?.anyFailed) {
67484
68509
  const failedReviewers = deferredReview.reviewerResults.filter((r) => !r.passed).map((r) => r.name);
@@ -67727,6 +68752,7 @@ async function runCompletionPhase(options) {
67727
68752
  logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
67728
68753
  } else if (options.config.acceptance.enabled && isComplete(options.prd)) {
67729
68754
  options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
68755
+ const acceptanceStartTime = Date.now();
67730
68756
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance" });
67731
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) => {
67732
68758
  const relativeWorkdir = path25.relative(options.workdir, g.packageDir);
@@ -67749,32 +68775,54 @@ async function runCompletionPhase(options) {
67749
68775
  commandOverride: groupConfig.acceptance.command
67750
68776
  };
67751
68777
  })) : undefined;
67752
- const acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
67753
- config: options.config,
67754
- prd: options.prd,
67755
- prdPath: options.prdPath,
67756
- workdir: options.workdir,
67757
- featureDir: options.featureDir,
67758
- hooks: options.hooks,
67759
- feature: options.feature,
67760
- totalCost: options.totalCost,
67761
- iterations: options.iterations,
67762
- storiesCompleted: options.storiesCompleted,
67763
- allStoryMetrics: options.allStoryMetrics,
67764
- pluginRegistry: options.pluginRegistry,
67765
- eventEmitter: options.eventEmitter,
67766
- statusWriter: options.statusWriter,
67767
- agentGetFn: options.agentGetFn,
67768
- agentManager: options.agentManager,
67769
- sessionManager: options.sessionManager,
67770
- runtime: options.runtime,
67771
- abortSignal: options.abortSignal,
67772
- acceptanceTestPaths
67773
- });
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
+ }
67774
68811
  const lastRunAt = new Date().toISOString();
68812
+ const acceptanceDurationMs = Date.now() - acceptanceStartTime;
67775
68813
  if (acceptanceResult.success) {
67776
68814
  options.statusWriter.setPostRunPhase("acceptance", { status: "passed", lastRunAt });
67777
- 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
+ });
67778
68826
  } else {
67779
68827
  acceptancePassed = false;
67780
68828
  options.statusWriter.setPostRunPhase("acceptance", {
@@ -67783,7 +68831,17 @@ async function runCompletionPhase(options) {
67783
68831
  retries: acceptanceResult.retries ?? 0,
67784
68832
  lastRunAt
67785
68833
  });
67786
- 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
+ });
67787
68845
  }
67788
68846
  Object.assign(options, {
67789
68847
  prd: acceptanceResult.prd,
@@ -67814,6 +68872,7 @@ async function runCompletionPhase(options) {
67814
68872
  sessionManager: options.sessionManager,
67815
68873
  pluginProviderCache: options.pluginProviderCache,
67816
68874
  deferredReview: options.deferredReview,
68875
+ deferredReviewStartedAt: options.deferredReviewStartedAt,
67817
68876
  exitReason: options.exitReason,
67818
68877
  runtime: options.runtime,
67819
68878
  abortSignal: options.abortSignal
@@ -67890,7 +68949,7 @@ var init_runner_completion = __esm(() => {
67890
68949
  });
67891
68950
 
67892
68951
  // src/execution/batching.ts
67893
- function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
68952
+ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
67894
68953
  const batches = [];
67895
68954
  let currentBatch = [];
67896
68955
  for (const story of stories) {
@@ -67923,7 +68982,7 @@ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE)
67923
68982
  }
67924
68983
  return batches;
67925
68984
  }
67926
- function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
68985
+ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
67927
68986
  const batches = [];
67928
68987
  let currentBatch = [];
67929
68988
  for (const story of stories) {
@@ -67960,7 +69019,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
67960
69019
  }
67961
69020
  return batches;
67962
69021
  }
67963
- var DEFAULT_MAX_BATCH_SIZE = 4;
69022
+ var DEFAULT_MAX_BATCH_SIZE2 = 4;
67964
69023
 
67965
69024
  // src/execution/ensure-package-dirs.ts
67966
69025
  import path26 from "path";
@@ -68240,131 +69299,6 @@ var init_registry6 = __esm(() => {
68240
69299
  init_paths3();
68241
69300
  });
68242
69301
 
68243
- // src/pipeline/subscribers/reporters.ts
68244
- function wireReporters(bus, pluginRegistry, runId, startTime) {
68245
- const logger = getSafeLogger();
68246
- const safe = (name, fn) => {
68247
- return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
68248
- };
68249
- const unsubs = [];
68250
- unsubs.push(bus.on("run:started", (ev) => {
68251
- return safe("onRunStart", async () => {
68252
- const reporters = pluginRegistry.getReporters();
68253
- for (const r of reporters) {
68254
- if (r.onRunStart) {
68255
- try {
68256
- await r.onRunStart({
68257
- runId,
68258
- feature: ev.feature,
68259
- totalStories: ev.totalStories,
68260
- startTime: new Date(startTime).toISOString()
68261
- });
68262
- } catch (err) {
68263
- logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
68264
- }
68265
- }
68266
- }
68267
- });
68268
- }));
68269
- unsubs.push(bus.on("story:completed", (ev) => {
68270
- return safe("onStoryComplete(completed)", async () => {
68271
- const reporters = pluginRegistry.getReporters();
68272
- for (const r of reporters) {
68273
- if (r.onStoryComplete) {
68274
- try {
68275
- await r.onStoryComplete({
68276
- runId,
68277
- storyId: ev.storyId,
68278
- status: "completed",
68279
- runElapsedMs: ev.runElapsedMs,
68280
- cost: ev.cost ?? 0,
68281
- tier: ev.modelTier ?? "balanced",
68282
- testStrategy: ev.testStrategy ?? "test-after"
68283
- });
68284
- } catch (err) {
68285
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68286
- }
68287
- }
68288
- }
68289
- });
68290
- }));
68291
- unsubs.push(bus.on("story:failed", (ev) => {
68292
- return safe("onStoryComplete(failed)", async () => {
68293
- const reporters = pluginRegistry.getReporters();
68294
- for (const r of reporters) {
68295
- if (r.onStoryComplete) {
68296
- try {
68297
- await r.onStoryComplete({
68298
- runId,
68299
- storyId: ev.storyId,
68300
- status: "failed",
68301
- runElapsedMs: Date.now() - startTime,
68302
- cost: 0,
68303
- tier: "balanced",
68304
- testStrategy: "test-after"
68305
- });
68306
- } catch (err) {
68307
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68308
- }
68309
- }
68310
- }
68311
- });
68312
- }));
68313
- unsubs.push(bus.on("story:paused", (ev) => {
68314
- return safe("onStoryComplete(paused)", async () => {
68315
- const reporters = pluginRegistry.getReporters();
68316
- for (const r of reporters) {
68317
- if (r.onStoryComplete) {
68318
- try {
68319
- await r.onStoryComplete({
68320
- runId,
68321
- storyId: ev.storyId,
68322
- status: "paused",
68323
- runElapsedMs: Date.now() - startTime,
68324
- cost: 0,
68325
- tier: "balanced",
68326
- testStrategy: "test-after"
68327
- });
68328
- } catch (err) {
68329
- logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
68330
- }
68331
- }
68332
- }
68333
- });
68334
- }));
68335
- unsubs.push(bus.on("run:completed", (ev) => {
68336
- return safe("onRunEnd", async () => {
68337
- const reporters = pluginRegistry.getReporters();
68338
- for (const r of reporters) {
68339
- if (r.onRunEnd) {
68340
- try {
68341
- await r.onRunEnd({
68342
- runId,
68343
- totalDurationMs: Date.now() - startTime,
68344
- totalCost: ev.totalCost ?? 0,
68345
- storySummary: {
68346
- completed: ev.passedStories,
68347
- failed: ev.failedStories,
68348
- skipped: ev.skippedStories,
68349
- paused: ev.pausedStories
68350
- }
68351
- });
68352
- } catch (err) {
68353
- logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
68354
- }
68355
- }
68356
- }
68357
- });
68358
- }));
68359
- return () => {
68360
- for (const u of unsubs)
68361
- u();
68362
- };
68363
- }
68364
- var init_reporters = __esm(() => {
68365
- init_logger2();
68366
- });
68367
-
68368
69302
  // src/execution/deferred-review.ts
68369
69303
  var {spawn: spawn4 } = globalThis.Bun;
68370
69304
  async function captureRunStartRef(workdir) {
@@ -70378,6 +71312,7 @@ async function executeUnified(ctx, initialPrd) {
70378
71312
  const allStoryMetrics = [];
70379
71313
  let warningSent = false;
70380
71314
  let deferredReview;
71315
+ let deferredReviewStartedAt;
70381
71316
  const runStartRef = await captureRunStartRef(ctx.workdir);
70382
71317
  let cachedNaxIgnoreKey;
70383
71318
  const getRunNaxIgnoreIndex = async (currentPrd) => {
@@ -70416,14 +71351,16 @@ async function executeUnified(ctx, initialPrd) {
70416
71351
  totalCost: totalCost2,
70417
71352
  allStoryMetrics,
70418
71353
  exitReason,
70419
- deferredReview
71354
+ deferredReview,
71355
+ deferredReviewStartedAt
70420
71356
  });
70421
- startHeartbeat(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
71357
+ startHeartbeat2(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
70422
71358
  let _executeThrew = false;
70423
71359
  try {
70424
71360
  if (isComplete(prd)) {
70425
71361
  logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
70426
71362
  const naxIgnoreIndex = await getRunNaxIgnoreIndex(prd);
71363
+ deferredReviewStartedAt = Date.now();
70427
71364
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
70428
71365
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
70429
71366
  return buildResult2("completed");
@@ -70478,6 +71415,7 @@ async function executeUnified(ctx, initialPrd) {
70478
71415
  return buildResult2("pre-merge-aborted");
70479
71416
  }
70480
71417
  logger?.debug("execution", "Running deferred review");
71418
+ deferredReviewStartedAt = Date.now();
70481
71419
  pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
70482
71420
  deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
70483
71421
  logger?.debug("execution", "Deferred review done \u2014 returning completed");
@@ -70988,6 +71926,7 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
70988
71926
  totalCost: totalCost2,
70989
71927
  allStoryMetrics,
70990
71928
  deferredReview: unifiedResult.deferredReview,
71929
+ deferredReviewStartedAt: unifiedResult.deferredReviewStartedAt,
70991
71930
  exitReason: unifiedResult.exitReason
70992
71931
  };
70993
71932
  }
@@ -72241,10 +73180,10 @@ async function cleanupRun(options) {
72241
73180
  }
72242
73181
  const actions = pluginRegistry.getPostRunActions();
72243
73182
  const pluginLogger = {
72244
- debug: (msg) => logger?.debug("post-run", msg),
72245
- info: (msg) => logger?.info("post-run", msg),
72246
- warn: (msg) => logger?.warn("post-run", msg),
72247
- error: (msg) => logger?.error("post-run", msg)
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)
72248
73187
  };
72249
73188
  const ctx = buildPostRunContext(options, durationMs, pluginLogger);
72250
73189
  for (const action of actions) {
@@ -72440,6 +73379,7 @@ async function run(options) {
72440
73379
  agentManager,
72441
73380
  pluginProviderCache,
72442
73381
  deferredReview: executionResult.deferredReview,
73382
+ deferredReviewStartedAt: executionResult.deferredReviewStartedAt,
72443
73383
  exitReason: executionResult.exitReason,
72444
73384
  runtime,
72445
73385
  abortSignal: shutdownController.signal
@@ -72538,8 +73478,11 @@ __export(exports_execution, {
72538
73478
  withIncreasingFailuresBail: () => withIncreasingFailuresBail,
72539
73479
  synthesizeBackfillMetric: () => synthesizeBackfillMetric,
72540
73480
  stopHeartbeat: () => stopHeartbeat,
72541
- startHeartbeat: () => startHeartbeat,
73481
+ startHeartbeat: () => startHeartbeat2,
73482
+ runRectification: () => runRectification,
73483
+ runPhase: () => runPhase,
72542
73484
  runDeferredRegression: () => runDeferredRegression,
73485
+ runCompletionPhase: () => runCompletionPhase,
72543
73486
  run: () => run,
72544
73487
  resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
72545
73488
  resetCrashHandlers: () => resetCrashHandlers,
@@ -72565,7 +73508,6 @@ __export(exports_execution, {
72565
73508
  getTierConfig: () => getTierConfig,
72566
73509
  getOscillations: () => getOscillations,
72567
73510
  getAllReadyStories: () => getAllReadyStories,
72568
- gateRegressedAfterRectification: () => gateRegressedAfterRectification,
72569
73511
  gateFailureKeys: () => gateFailureKeys,
72570
73512
  formatProgress: () => formatProgress,
72571
73513
  formatPhaseResultMessage: () => formatPhaseResultMessage,
@@ -72574,11 +73516,13 @@ __export(exports_execution, {
72574
73516
  extractPauseReason: () => extractPauseReason,
72575
73517
  escalateTier: () => escalateTier,
72576
73518
  ensureStoryPackageDirs: () => ensureStoryPackageDirs,
73519
+ describeGateRegression: () => describeGateRegression,
72577
73520
  deriveTddFailureCategory: () => deriveTddFailureCategory,
72578
73521
  decideStageAction: () => decideStageAction,
72579
73522
  createCheckpointWriter: () => createCheckpointWriter,
72580
73523
  countOscillationOutcomes: () => countOscillationOutcomes,
72581
73524
  clearQueueFile: () => clearQueueFile,
73525
+ cleanupRun: () => cleanupRun,
72582
73526
  captureTreeState: () => captureTreeState,
72583
73527
  calculateMaxIterations: () => calculateMaxIterations,
72584
73528
  buildStoryContext: () => buildStoryContext,
@@ -72595,6 +73539,7 @@ __export(exports_execution, {
72595
73539
  _storyOrchestratorDeps: () => _storyOrchestratorDeps,
72596
73540
  _runnerReentrancyGuard: () => _runnerReentrancyGuard,
72597
73541
  _runnerDeps: () => _runnerDeps,
73542
+ _runnerCompletionDeps: () => _runnerCompletionDeps,
72598
73543
  _runCompletionDeps: () => _runCompletionDeps,
72599
73544
  _regressionDeps: () => _regressionDeps,
72600
73545
  _postRunDeps: () => _postRunDeps,
@@ -72626,6 +73571,7 @@ var init_execution2 = __esm(() => {
72626
73571
  init_plan_inputs();
72627
73572
  init_build_plan_for_strategy();
72628
73573
  init_checkpoint();
73574
+ init_runner_completion();
72629
73575
  init_post_run();
72630
73576
  });
72631
73577
 
@@ -104405,7 +105351,7 @@ async function resolveRunProfileOverride(opts) {
104405
105351
  // src/cli/features-resolve.ts
104406
105352
  init_config();
104407
105353
  import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
104408
- import { join as join74, relative as relative16 } from "path";
105354
+ import { join as join74, relative as relative17 } from "path";
104409
105355
 
104410
105356
  // src/cli/features-acceptance.ts
104411
105357
  init_acceptance2();
@@ -104413,7 +105359,7 @@ init_config();
104413
105359
  init_logger2();
104414
105360
  init_prd();
104415
105361
  import { existsSync as existsSync27 } from "fs";
104416
- import { join as join73, relative as relative15 } from "path";
105362
+ import { join as join73, relative as relative16 } from "path";
104417
105363
  async function resolveFeatureAcceptance(featureName, workdir) {
104418
105364
  let enabled = true;
104419
105365
  try {
@@ -104434,11 +105380,11 @@ async function resolveFeatureAcceptance(featureName, workdir) {
104434
105380
  const prd = await loadPRD(prdPath);
104435
105381
  const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
104436
105382
  const groups = await Promise.all(testGroups.map(async (g) => {
104437
- const packageDir = relative15(repoRoot, g.packageDir);
105383
+ const packageDir = relative16(repoRoot, g.packageDir);
104438
105384
  const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
104439
105385
  return {
104440
105386
  packageDir,
104441
- testPath: relative15(repoRoot, g.testPath),
105387
+ testPath: relative16(repoRoot, g.testPath),
104442
105388
  exists: await Bun.file(g.testPath).exists(),
104443
105389
  command,
104444
105390
  cwd: packageDir,
@@ -104475,17 +105421,17 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104475
105421
  ];
104476
105422
  const docsSpecExact = join74(repoRoot, "docs", "specs", `SPEC-${name}.md`);
104477
105423
  candidates.push({ abs: docsSpecExact, kind: "markdown" });
104478
- const checked = candidates.map((c) => relative16(repoRoot, c.abs));
105424
+ const checked = candidates.map((c) => relative17(repoRoot, c.abs));
104479
105425
  for (const { abs, kind } of candidates.slice(0, 2)) {
104480
105426
  if (kind === "markdown") {
104481
105427
  const nonEmpty = await isNonEmptyFile(abs);
104482
105428
  if (nonEmpty) {
104483
- return { source: { kind, path: relative16(repoRoot, abs) }, checked };
105429
+ return { source: { kind, path: relative17(repoRoot, abs) }, checked };
104484
105430
  }
104485
105431
  }
104486
105432
  }
104487
105433
  if (await isNonEmptyFile(docsSpecExact)) {
104488
- return { source: { kind: "markdown", path: relative16(repoRoot, docsSpecExact) }, checked };
105434
+ return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
104489
105435
  }
104490
105436
  const docsSpecsDir = join74(repoRoot, "docs", "specs");
104491
105437
  if (existsSync28(docsSpecsDir)) {
@@ -104493,7 +105439,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104493
105439
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
104494
105440
  const abs = join74(docsSpecsDir, match);
104495
105441
  if (await isNonEmptyFile(abs)) {
104496
- const relPath = relative16(repoRoot, abs);
105442
+ const relPath = relative17(repoRoot, abs);
104497
105443
  if (!checked.includes(relPath))
104498
105444
  checked.push(relPath);
104499
105445
  return { source: { kind: "markdown", path: relPath }, checked };
@@ -104501,7 +105447,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
104501
105447
  }
104502
105448
  }
104503
105449
  const prdAbs = join74(naxDir, "features", name, "prd.json");
104504
- const prdRel = relative16(repoRoot, prdAbs);
105450
+ const prdRel = relative17(repoRoot, prdAbs);
104505
105451
  if (!checked.includes(prdRel))
104506
105452
  checked.push(prdRel);
104507
105453
  if (existsSync28(prdAbs)) {
@@ -104553,8 +105499,8 @@ async function resolveFeatureSpec(name, workdir) {
104553
105499
  return {
104554
105500
  status: "ok",
104555
105501
  featureName: null,
104556
- specSource: { kind: "markdown", path: relative16(repoRoot, abs) },
104557
- message: `resolved spec: ${relative16(repoRoot, abs)}`
105502
+ specSource: { kind: "markdown", path: relative17(repoRoot, abs) },
105503
+ message: `resolved spec: ${relative17(repoRoot, abs)}`
104558
105504
  };
104559
105505
  }
104560
105506
  if (name !== undefined && name.trim() !== "") {
@@ -106052,9 +106998,9 @@ function parseSchedule(input, now2) {
106052
106998
  const trimmed = input.trim();
106053
106999
  if (trimmed === "")
106054
107000
  return { ok: false, error: `Empty schedule value. ${ACCEPTED}` };
106055
- const relative17 = parseRelative(trimmed, now2);
106056
- if (relative17)
106057
- return relative17;
107001
+ const relative18 = parseRelative(trimmed, now2);
107002
+ if (relative18)
107003
+ return relative18;
106058
107004
  const timeOfDay = parseTimeOfDay(trimmed, now2);
106059
107005
  if (timeOfDay)
106060
107006
  return timeOfDay;