@nathapp/nax 0.70.6 → 0.70.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/nax.js +276 -103
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -24693,7 +24693,7 @@ function parsePytestOutput(output) {
24693
24693
  const failures = [];
24694
24694
  for (const line of output.split(`
24695
24695
  `)) {
24696
- const m = line.match(/^FAILED\s+(\S+)(?:\s+-\s+(.*))?$/);
24696
+ const m = line.match(/^(?:\S+:\s+)?FAILED\s+(\S+)(?:\s+-\s+(.*))?$/);
24697
24697
  if (m) {
24698
24698
  const [, location, reason] = m;
24699
24699
  const parts = location.split("::");
@@ -24705,6 +24705,28 @@ function parsePytestOutput(output) {
24705
24705
  });
24706
24706
  }
24707
24707
  }
24708
+ const PLACEHOLDER_REASON = "Collection/import error";
24709
+ const errorByFile = new Map;
24710
+ for (const line of output.split(`
24711
+ `)) {
24712
+ const collecting = line.match(/^(?:\S+:\s+)?_*\s*ERROR\s+collecting\s+(\S+\.\w+)/);
24713
+ const summary = line.match(/^(?:\S+:\s+)?ERROR\s+(\S+\.\w+(?:::\S+)?)\s+-\s+(.*)/);
24714
+ const location = collecting?.[1] ?? summary?.[1];
24715
+ if (!location)
24716
+ continue;
24717
+ const reason = summary?.[2]?.trim();
24718
+ const file3 = location.split("::")[0] ?? location;
24719
+ const existing = errorByFile.get(file3);
24720
+ if (existing && existing.error !== PLACEHOLDER_REASON)
24721
+ continue;
24722
+ errorByFile.set(file3, {
24723
+ file: file3,
24724
+ testName: `collection error: ${file3}`,
24725
+ error: reason || PLACEHOLDER_REASON,
24726
+ stackTrace: []
24727
+ });
24728
+ }
24729
+ failures.push(...errorByFile.values());
24708
24730
  const verboseStacks = parsePytestVerboseStacks(output);
24709
24731
  for (const failure of failures) {
24710
24732
  const leafName = failure.testName.split(" > ").pop() ?? failure.testName;
@@ -24808,6 +24830,10 @@ function parseCommonOutput(output) {
24808
24830
  failed = Number.parseInt(failMatches[failMatches.length - 1][1], 10);
24809
24831
  }
24810
24832
  }
24833
+ const errorMatches = Array.from(output.matchAll(/(\d+)\s+errors?\b(?=[^\n]*\bin\s+[\d.]+\s*s\b)/gi));
24834
+ if (errorMatches.length > 0) {
24835
+ failed += Number.parseInt(errorMatches[errorMatches.length - 1][1], 10);
24836
+ }
24811
24837
  return { passed, failed, failures: [] };
24812
24838
  }
24813
24839
  function formatFailureSummary(failures, maxChars = 2000) {
@@ -24860,9 +24886,10 @@ var init_parser = __esm(() => {
24860
24886
 
24861
24887
  // src/test-runners/ac-parser.ts
24862
24888
  function parseTestFailures(output) {
24863
- const framework = detectFramework(output);
24889
+ const clean = output.replace(ANSI_ESCAPE_PATTERN, "");
24890
+ const framework = detectFramework(clean);
24864
24891
  const failedACs = [];
24865
- const lines = output.split(`
24892
+ const lines = clean.split(`
24866
24893
  `);
24867
24894
  for (const line of lines) {
24868
24895
  if (framework === "bun" || framework === "unknown") {
@@ -24896,7 +24923,7 @@ function parseTestFailures(output) {
24896
24923
  }
24897
24924
  }
24898
24925
  if (framework === "jest" || framework === "vitest" || framework === "unknown") {
24899
- if (/[\u25CF\u00D7\u2715]/.test(line)) {
24926
+ if (/[\u25CF\u00D7\u2715]/.test(line) || /^\s*FAIL\s/.test(line)) {
24900
24927
  const acMatch = line.match(/AC[-_]?(\d+)/i);
24901
24928
  if (acMatch) {
24902
24929
  const acId = `AC-${acMatch[1]}`;
@@ -24913,8 +24940,10 @@ function parseTestFailures(output) {
24913
24940
  }
24914
24941
  return failedACs;
24915
24942
  }
24943
+ var ANSI_ESCAPE_PATTERN;
24916
24944
  var init_ac_parser = __esm(() => {
24917
24945
  init_detector2();
24946
+ ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
24918
24947
  });
24919
24948
 
24920
24949
  // src/utils/git.ts
@@ -30246,11 +30275,7 @@ async function gitLsFiles2(workdir) {
30246
30275
  return null;
30247
30276
  }
30248
30277
  }
30249
- async function hasTestFiles(workdir, patterns) {
30250
- const files = await gitLsFiles2(workdir);
30251
- if (files !== null) {
30252
- return files.some((f) => isTestFileByPatterns(f, patterns));
30253
- }
30278
+ async function hasTestFilesOnDisk(workdir, patterns) {
30254
30279
  for (const pattern of patterns) {
30255
30280
  const g = new Bun.Glob(pattern);
30256
30281
  for await (const path3 of g.scan({ cwd: workdir, onlyFiles: true })) {
@@ -30261,6 +30286,13 @@ async function hasTestFiles(workdir, patterns) {
30261
30286
  }
30262
30287
  return false;
30263
30288
  }
30289
+ async function hasTestFiles(workdir, patterns) {
30290
+ const files = await gitLsFiles2(workdir);
30291
+ if (files !== null) {
30292
+ return files.some((f) => isTestFileByPatterns(f, patterns));
30293
+ }
30294
+ return hasTestFilesOnDisk(workdir, patterns);
30295
+ }
30264
30296
  async function isGreenfieldStory(_story, workdir, patterns) {
30265
30297
  try {
30266
30298
  return !await hasTestFiles(workdir, patterns ?? DEFAULT_TEST_FILE_PATTERNS);
@@ -30291,6 +30323,7 @@ var init_greenfield = __esm(() => {
30291
30323
  "out",
30292
30324
  "tmp",
30293
30325
  "temp",
30326
+ ".nax",
30294
30327
  ".git"
30295
30328
  ]);
30296
30329
  });
@@ -32865,13 +32898,13 @@ var init_semantic_helpers = __esm(() => {
32865
32898
 
32866
32899
  // src/review/semantic-evidence.ts
32867
32900
  import { isAbsolute as isAbsolute8 } from "path";
32868
- async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error") {
32901
+ async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error", repoRoot) {
32869
32902
  if (diffMode !== "ref")
32870
32903
  return findings;
32871
32904
  return Promise.all(findings.map(async (finding) => {
32872
32905
  if (!isBlockingSeverity(finding.severity, blockingThreshold))
32873
32906
  return finding;
32874
- const evidence = await checkFindingEvidence({ finding, workdir });
32907
+ const evidence = await checkFindingEvidence({ finding, workdir, repoRoot });
32875
32908
  if (evidence.status !== "unmatched")
32876
32909
  return finding;
32877
32910
  return downgradeUnsubstantiatedFinding({ finding, storyId, ...evidence });
@@ -32883,7 +32916,8 @@ async function checkFindingEvidence(opts) {
32883
32916
  const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
32884
32917
  if (!observed)
32885
32918
  return { status: "missing-observed", file: file3, line };
32886
- const contents = await readSafeFile(opts.workdir, file3);
32919
+ const roots = opts.repoRoot && opts.repoRoot !== opts.workdir ? [opts.repoRoot, opts.workdir] : [opts.workdir];
32920
+ const contents = await readSafeFile(roots, file3);
32887
32921
  if (contents === null)
32888
32922
  return { status: "unreadable", file: file3, line, observed };
32889
32923
  return matchesEvidence(contents, observed, line) ? { status: "matched", file: file3, line, observed } : { status: "unmatched", file: file3, line, observed };
@@ -32912,13 +32946,13 @@ function downgradeUnsubstantiatedFinding(opts) {
32912
32946
  });
32913
32947
  return { ...opts.finding, severity: "unverifiable" };
32914
32948
  }
32915
- async function readSafeFile(workdir, file3) {
32916
- const validated = validateModulePath(file3, [workdir]);
32917
- if (validated.valid && validated.absolutePath) {
32918
- try {
32919
- return await Bun.file(validated.absolutePath).text();
32920
- } catch {
32921
- return null;
32949
+ async function readSafeFile(roots, file3) {
32950
+ for (const root of roots) {
32951
+ const validated = validateModulePath(file3, [root]);
32952
+ if (validated.valid && validated.absolutePath) {
32953
+ try {
32954
+ return await Bun.file(validated.absolutePath).text();
32955
+ } catch {}
32922
32956
  }
32923
32957
  }
32924
32958
  if (isAbsolute8(file3)) {
@@ -32963,11 +32997,11 @@ function hasInspectionTrail(raw) {
32963
32997
  return Array.isArray(files) && files.some((f) => typeof f === "string" && f.trim().length > 0);
32964
32998
  }
32965
32999
  async function substantiateAdversarialFindings(opts) {
32966
- const { findings, workdir, storyId, blockingThreshold } = opts;
33000
+ const { findings, workdir, storyId, blockingThreshold, repoRoot } = opts;
32967
33001
  return Promise.all(findings.map(async (finding) => {
32968
33002
  if (!isBlockingSeverity(finding.severity, blockingThreshold))
32969
33003
  return finding;
32970
- const evidence = await checkFindingEvidence({ finding, workdir });
33004
+ const evidence = await checkFindingEvidence({ finding, workdir, repoRoot });
32971
33005
  if (evidence.status !== "unmatched" && evidence.status !== "missing-observed")
32972
33006
  return finding;
32973
33007
  return downgradeUnsubstantiatedFinding({
@@ -33072,7 +33106,11 @@ async function requoteBlockingAdversarialFindings(findings, ctx) {
33072
33106
  for (const [index, finding] of next.entries()) {
33073
33107
  if (!isBlockingSeverity(finding.severity, threshold))
33074
33108
  continue;
33075
- const initialEvidence = await checkFindingEvidence({ finding, workdir: ctx.input.workdir });
33109
+ const initialEvidence = await checkFindingEvidence({
33110
+ finding,
33111
+ workdir: ctx.input.workdir,
33112
+ repoRoot: ctx.input.repoRoot
33113
+ });
33076
33114
  if (initialEvidence.status !== "unmatched")
33077
33115
  continue;
33078
33116
  if (used >= maxRequotes)
@@ -33101,7 +33139,8 @@ async function requoteBlockingAdversarialFindings(findings, ctx) {
33101
33139
  };
33102
33140
  const requotedEvidence = await checkFindingEvidence({
33103
33141
  finding: updatedFinding,
33104
- workdir: ctx.input.workdir
33142
+ workdir: ctx.input.workdir,
33143
+ repoRoot: ctx.input.repoRoot
33105
33144
  });
33106
33145
  if (requotedEvidence.status === "matched") {
33107
33146
  getSafeLogger()?.info("review", "Recovered adversarial finding via same-session requote", {
@@ -33329,6 +33368,7 @@ var init_adversarial_review = __esm(() => {
33329
33368
  const substantiated = await substantiateAdversarialFindings({
33330
33369
  findings,
33331
33370
  workdir: input.workdir,
33371
+ repoRoot: input.repoRoot,
33332
33372
  storyId: input.story.id,
33333
33373
  blockingThreshold: threshold
33334
33374
  });
@@ -34026,6 +34066,7 @@ async function runAdversarialReview(opts) {
34026
34066
  try {
34027
34067
  opResult = await _adversarialDeps.callOp(callCtx, adversarialReviewOp, {
34028
34068
  workdir,
34069
+ repoRoot: projectDir ?? workdir,
34029
34070
  story,
34030
34071
  adversarialConfig,
34031
34072
  mode: diffMode,
@@ -35548,6 +35589,10 @@ var init_decompose2 = __esm(() => {
35548
35589
  });
35549
35590
 
35550
35591
  // src/routing/classify.ts
35592
+ function isSecurityCriticalStory(title, tags = []) {
35593
+ const text = [title, ...tags ?? []].join(" ").toLowerCase();
35594
+ return SECURITY_KEYWORDS.some((kw) => text.includes(kw)) || PUBLIC_API_KEYWORDS.some((kw) => text.includes(kw));
35595
+ }
35551
35596
  function classifyComplexity(title, _description, acceptanceCriteria, tags = []) {
35552
35597
  const text = [title, ...acceptanceCriteria ?? [], ...tags ?? []].join(" ").toLowerCase();
35553
35598
  if (EXPERT_KEYWORDS.some((kw) => text.includes(kw)))
@@ -35565,10 +35610,7 @@ function determineTestStrategy(complexity, title, _description, tags = [], tddSt
35565
35610
  return "tdd-simple";
35566
35611
  if (tddStrategy === "off")
35567
35612
  return "test-after";
35568
- const text = [title, ...tags ?? []].join(" ").toLowerCase();
35569
- const isSecurityCritical = SECURITY_KEYWORDS.some((kw) => text.includes(kw));
35570
- const isPublicApi = PUBLIC_API_KEYWORDS.some((kw) => text.includes(kw));
35571
- if (isSecurityCritical || isPublicApi)
35613
+ if (isSecurityCriticalStory(title, tags))
35572
35614
  return "three-session-tdd";
35573
35615
  if (complexity === "expert")
35574
35616
  return "three-session-tdd";
@@ -35956,6 +35998,7 @@ __export(exports_routing, {
35956
35998
  routeTask: () => routeTask,
35957
35999
  routeStory: () => routeStory,
35958
36000
  resolveRouting: () => resolveRouting,
36001
+ isSecurityCriticalStory: () => isSecurityCriticalStory,
35959
36002
  determineTestStrategy: () => determineTestStrategy,
35960
36003
  complexityToModelTier: () => complexityToModelTier,
35961
36004
  clearCache: () => clearCache,
@@ -36801,7 +36844,11 @@ async function requoteBlockingFindings(findings, ctx) {
36801
36844
  for (const [index, finding] of next.entries()) {
36802
36845
  if (!isBlockingSeverity(finding.severity, threshold))
36803
36846
  continue;
36804
- const initialEvidence = await checkFindingEvidence({ finding, workdir: ctx.input.workdir });
36847
+ const initialEvidence = await checkFindingEvidence({
36848
+ finding,
36849
+ workdir: ctx.input.workdir,
36850
+ repoRoot: ctx.input.repoRoot
36851
+ });
36805
36852
  if (initialEvidence.status !== "unmatched")
36806
36853
  continue;
36807
36854
  if (used >= maxRequotes)
@@ -36831,7 +36878,8 @@ async function requoteBlockingFindings(findings, ctx) {
36831
36878
  };
36832
36879
  const requotedEvidence = await checkFindingEvidence({
36833
36880
  finding: updatedFinding,
36834
- workdir: ctx.input.workdir
36881
+ workdir: ctx.input.workdir,
36882
+ repoRoot: ctx.input.repoRoot
36835
36883
  });
36836
36884
  if (requotedEvidence.status === "matched") {
36837
36885
  getSafeLogger()?.info("review", "Recovered semantic finding via same-session requote", {
@@ -36967,7 +37015,7 @@ var init_semantic_review = __esm(() => {
36967
37015
  const threshold = input.blockingThreshold ?? "error";
36968
37016
  const findings = parsed.findings;
36969
37017
  const sanitized = sanitizeRefModeFindings(findings, input.mode, threshold);
36970
- const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold);
37018
+ const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold, input.repoRoot);
36971
37019
  const { accepted, dropped } = filterByAcGroundingMinimal(substantiated, input.story.acceptanceCriteria);
36972
37020
  const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
36973
37021
  const passed = parsed.passed && blocking.length === 0;
@@ -38975,8 +39023,13 @@ var init_greenfield_gate = __esm(() => {
38975
39023
  config: greenfieldGateConfigSelector,
38976
39024
  async execute(input, _ctx) {
38977
39025
  const globs = input.resolvedTestPatterns.globs;
38978
- const isGreenfield = await isGreenfieldStory(input.story, input.workdir, globs);
38979
- if (isGreenfield) {
39026
+ let hasTests;
39027
+ try {
39028
+ hasTests = await hasTestFilesOnDisk(input.workdir, globs);
39029
+ } catch {
39030
+ return { success: true, hasPreExistingTests: true };
39031
+ }
39032
+ if (!hasTests) {
38980
39033
  return { success: false, hasPreExistingTests: false, pauseReason: "greenfield-no-tests" };
38981
39034
  }
38982
39035
  return { success: true, hasPreExistingTests: true };
@@ -38984,6 +39037,33 @@ var init_greenfield_gate = __esm(() => {
38984
39037
  };
38985
39038
  });
38986
39039
 
39040
+ // src/operations/test-presence-gate.ts
39041
+ var testPresenceGateConfigSelector, testPresenceGateOp;
39042
+ var init_test_presence_gate = __esm(() => {
39043
+ init_config();
39044
+ init_greenfield();
39045
+ testPresenceGateConfigSelector = pickSelector("test-presence-gate", "execution");
39046
+ testPresenceGateOp = {
39047
+ kind: "deterministic",
39048
+ name: "test-presence-gate",
39049
+ stage: "verify",
39050
+ config: testPresenceGateConfigSelector,
39051
+ async execute(input, _ctx) {
39052
+ const globs = input.resolvedTestPatterns.globs;
39053
+ let hasTests;
39054
+ try {
39055
+ hasTests = await hasTestFilesOnDisk(input.workdir, globs);
39056
+ } catch {
39057
+ return { success: true, hasTests: true };
39058
+ }
39059
+ if (!hasTests) {
39060
+ return { success: false, hasTests: false, pauseReason: "no-tests-authored" };
39061
+ }
39062
+ return { success: true, hasTests: true };
39063
+ }
39064
+ };
39065
+ });
39066
+
38987
39067
  // src/utils/command-argv.ts
38988
39068
  function parseCommandToArgv(command) {
38989
39069
  const safeEnv = buildAllowedEnv();
@@ -40667,6 +40747,7 @@ var init_operations = __esm(() => {
40667
40747
  init_plan_critic_llm();
40668
40748
  init_execution_gates();
40669
40749
  init_greenfield_gate();
40750
+ init_test_presence_gate();
40670
40751
  init_full_suite_gate();
40671
40752
  init_full_suite_rectify();
40672
40753
  init_full_suite_rectify_op();
@@ -42048,6 +42129,7 @@ async function runSemanticReview(opts) {
42048
42129
  try {
42049
42130
  opResult = await _semanticDeps.callOp(callCtx, semanticReviewOp, {
42050
42131
  workdir,
42132
+ repoRoot: projectDir ?? workdir,
42051
42133
  story,
42052
42134
  semanticConfig,
42053
42135
  mode: diffMode,
@@ -42602,6 +42684,7 @@ var init_runner2 = __esm(() => {
42602
42684
  var init_review = __esm(() => {
42603
42685
  init_semantic_helpers();
42604
42686
  init_category_fix_target();
42687
+ init_finding_filters();
42605
42688
  init_ac_quote_validator();
42606
42689
  init_ac_structural_counterfactual();
42607
42690
  init_adversarial();
@@ -42629,6 +42712,13 @@ UNRESOLVED: <brief explanation of which findings conflicted and why they cannot
42629
42712
 
42630
42713
  Before emitting UNRESOLVED, confirm none of Exceptions 1\u2013${count} apply.
42631
42714
 
42715
+ **A missing-test or \`test-gap\` finding is never a false positive because a \`.nax/\` file exists.**
42716
+ \`.nax/\` is nax's own artifact directory; \`.nax-acceptance.test.ts\` is generated scaffolding for the
42717
+ acceptance gate \u2014 it is NOT source-tree test coverage. You may NOT cite any \`.nax/\`-resident file as
42718
+ evidence that an acceptance criterion is already tested, and you may NOT emit UNRESOLVED on that basis.
42719
+ The only valid response to a missing-test finding is to
42720
+ author a real test under the package's resolved test path.
42721
+
42632
42722
  ## Test-file edit exceptions
42633
42723
 
42634
42724
  The "do not modify test files" rule has ${countWord} narrow escape valves. Each requires a
@@ -53992,7 +54082,7 @@ ${stderr}`;
53992
54082
  errorExitCode = exitCode;
53993
54083
  allFailedACs.push("AC-ERROR");
53994
54084
  allFindings.push(acSentinelToFinding("AC-ERROR", output));
53995
- failedPackages.push({ testPath, packageDir, testFramework, commandOverride });
54085
+ failedPackages.push({ testPath, packageDir, testFramework, commandOverride, output, failedACs: ["AC-ERROR"] });
53996
54086
  continue;
53997
54087
  }
53998
54088
  for (const acId of actualFailures) {
@@ -54002,7 +54092,14 @@ ${stderr}`;
54002
54092
  }
54003
54093
  }
54004
54094
  if (actualFailures.length > 0) {
54005
- failedPackages.push({ testPath, packageDir, testFramework, commandOverride });
54095
+ failedPackages.push({
54096
+ testPath,
54097
+ packageDir,
54098
+ testFramework,
54099
+ commandOverride,
54100
+ output,
54101
+ failedACs: actualFailures
54102
+ });
54006
54103
  logger.error("acceptance", "Acceptance tests failed", {
54007
54104
  storyId: ctx.story.id,
54008
54105
  failedACs: actualFailures,
@@ -55441,6 +55538,7 @@ var init_types9 = __esm(() => {
55441
55538
  "test-writer",
55442
55539
  "greenfield-gate",
55443
55540
  "implementer",
55541
+ "test-presence-gate",
55444
55542
  "full-suite-gate",
55445
55543
  "verifier",
55446
55544
  "verify-scoped",
@@ -55453,6 +55551,7 @@ var init_types9 = __esm(() => {
55453
55551
  "test-writer": "testWriter",
55454
55552
  "greenfield-gate": "greenfieldGate",
55455
55553
  implementer: "implementer",
55554
+ "test-presence-gate": "testPresenceGate",
55456
55555
  "full-suite-gate": "fullSuiteGate",
55457
55556
  verifier: "verifier",
55458
55557
  "verify-scoped": "verifyScoped",
@@ -55595,6 +55694,8 @@ function collectOrderedPhases(state) {
55595
55694
  return [state.greenfieldGate];
55596
55695
  if (kind === "implementer" && state.implementer)
55597
55696
  return [state.implementer];
55697
+ if (kind === "test-presence-gate" && state.testPresenceGate)
55698
+ return [state.testPresenceGate];
55598
55699
  if (kind === "full-suite-gate" && state.fullSuiteGate)
55599
55700
  return [state.fullSuiteGate];
55600
55701
  if (kind === "verifier" && state.verifier)
@@ -56219,10 +56320,11 @@ class ExecutionPlan {
56219
56320
  }
56220
56321
  }
56221
56322
  }
56323
+ const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
56222
56324
  const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
56223
56325
  const advisoryOut = phaseOutputs["adversarial-review"];
56224
56326
  const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
56225
- if (advCfg && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
56327
+ if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
56226
56328
  await _storyOrchestratorDeps.runNonBlockingFix({
56227
56329
  workdir: this.ctx.packageDir,
56228
56330
  storyId: this.ctx.storyId,
@@ -56333,6 +56435,10 @@ class StoryOrchestratorBuilder {
56333
56435
  setPhase(this.state, "greenfield-gate", isSlot(value) ? value : { op: greenfieldGateOp, input: value });
56334
56436
  return this;
56335
56437
  }
56438
+ addTestPresenceGate(value) {
56439
+ setPhase(this.state, "test-presence-gate", isSlot(value) ? value : { op: testPresenceGateOp, input: value });
56440
+ return this;
56441
+ }
56336
56442
  addVerifier(value) {
56337
56443
  setPhase(this.state, "verifier", isSlot(value) ? value : { op: verifierOp, input: value });
56338
56444
  return this;
@@ -56418,6 +56524,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
56418
56524
  if (inputs.implementer) {
56419
56525
  builder.addImplementer(inputs.implementer);
56420
56526
  }
56527
+ if (!isThreeSession && inputs.testPresenceGate) {
56528
+ builder.addTestPresenceGate(inputs.testPresenceGate);
56529
+ }
56421
56530
  const regressionMode = config2.execution?.regressionGate?.mode ?? "deferred";
56422
56531
  if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
56423
56532
  builder.addFullSuiteGate(inputs.fullSuiteGate);
@@ -56491,7 +56600,12 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
56491
56600
  const nbStrategies = [];
56492
56601
  if (nbf?.enabled && inputs.adversarialReview) {
56493
56602
  const nbSink = makeDeclarationSink();
56494
- if (nbf.scope === "source") {
56603
+ if (!isThreeSession) {
56604
+ nbStrategies.push(makeAutofixImplementerStrategy(story, config2, nbSink, {
56605
+ includeAdversarialReview: true,
56606
+ promptSeverityFloor: "info"
56607
+ }));
56608
+ } else if (nbf.scope === "source") {
56495
56609
  nbStrategies.push(makeAutofixImplementerStrategy(story, config2, nbSink, {
56496
56610
  includeAdversarialReview: true,
56497
56611
  promptSeverityFloor: "info"
@@ -56637,6 +56751,7 @@ async function assemblePlanInputsFromCtx(ctx) {
56637
56751
  featureContextMarkdown: ctx.featureContextMarkdown,
56638
56752
  constitution: ctx.constitution?.content
56639
56753
  };
56754
+ const testPresenceGateInput = isSingleSessionTestOwningStrategy(ctx.routing.testStrategy) && resolvedTestPatterns ? { story, workdir: ctx.workdir, resolvedTestPatterns } : undefined;
56640
56755
  const _regressionMode = ctx.config.execution?.regressionGate?.mode;
56641
56756
  const fullSuiteGateInput = _isTdd || _regressionMode === "per-story" ? {
56642
56757
  story,
@@ -56741,6 +56856,7 @@ async function assemblePlanInputsFromCtx(ctx) {
56741
56856
  testWriter: testWriterInput,
56742
56857
  greenfieldGate: greenfieldGateInput,
56743
56858
  implementer: implementerInput,
56859
+ testPresenceGate: testPresenceGateInput,
56744
56860
  fullSuiteGate: fullSuiteGateInput,
56745
56861
  verifier: verifierInput,
56746
56862
  verifyScoped: verifyScopedInput,
@@ -56798,6 +56914,11 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason, failure
56798
56914
  case "review-incomplete":
56799
56915
  case "greenfield-no-tests":
56800
56916
  return { action: "escalate", reason: buildReason(failureCategory) };
56917
+ case "no-tests-authored":
56918
+ return {
56919
+ action: "escalate",
56920
+ reason: "No test files were authored for this story. You MUST write tests covering every acceptance criterion under the package's resolved test path before implementation is considered complete."
56921
+ };
56801
56922
  case "dependency-prep":
56802
56923
  return pauseFallback;
56803
56924
  default:
@@ -56902,6 +57023,10 @@ function deriveTddFailureCategory(phaseOutputs, unfixedFindings, gateRegressedDu
56902
57023
  if (greenfieldOutput?.success === false && greenfieldOutput?.pauseReason === "greenfield-no-tests") {
56903
57024
  return "greenfield-no-tests";
56904
57025
  }
57026
+ const testPresenceOutput = phaseOutputs[testPresenceGateOp.name];
57027
+ if (testPresenceOutput?.success === false && testPresenceOutput?.pauseReason === "no-tests-authored") {
57028
+ return "no-tests-authored";
57029
+ }
56905
57030
  const verifierOutput = phaseOutputs[verifierOp.name];
56906
57031
  if (verifierOutput?.success === false) {
56907
57032
  if (verifierOutput.failureCategory) {
@@ -57941,13 +58066,21 @@ var init_routing2 = __esm(() => {
57941
58066
  });
57942
58067
  const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, greenfieldScanDir, resolved?.globs);
57943
58068
  if (isGreenfield) {
57944
- logger.info("routing", "Greenfield detected \u2014 forcing test-after strategy", {
57945
- storyId: ctx.story.id,
57946
- originalStrategy: routing.testStrategy,
57947
- scanDir: greenfieldScanDir
57948
- });
57949
- routing.testStrategy = "test-after";
57950
- routing.reasoning = `${routing.reasoning} [GREENFIELD OVERRIDE: No test files exist, using test-after instead of TDD]`;
58069
+ if (isSecurityCriticalStory(ctx.story.title, ctx.story.tags)) {
58070
+ logger.info("routing", "Greenfield + security-critical \u2014 keeping three-session strategy", {
58071
+ storyId: ctx.story.id,
58072
+ strategy: routing.testStrategy,
58073
+ scanDir: greenfieldScanDir
58074
+ });
58075
+ } else {
58076
+ logger.info("routing", "Greenfield detected \u2014 forcing tdd-simple strategy", {
58077
+ storyId: ctx.story.id,
58078
+ originalStrategy: routing.testStrategy,
58079
+ scanDir: greenfieldScanDir
58080
+ });
58081
+ routing.testStrategy = "tdd-simple";
58082
+ routing.reasoning = `${routing.reasoning} [GREENFIELD OVERRIDE: No test files exist, using tdd-simple (test-first, single-session) instead of three-session TDD]`;
58083
+ }
57951
58084
  }
57952
58085
  }
57953
58086
  ctx.routing = routing;
@@ -61057,7 +61190,7 @@ var package_default;
61057
61190
  var init_package = __esm(() => {
61058
61191
  package_default = {
61059
61192
  name: "@nathapp/nax",
61060
- version: "0.70.6",
61193
+ version: "0.70.8",
61061
61194
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
61062
61195
  type: "module",
61063
61196
  bin: {
@@ -61157,8 +61290,8 @@ var init_version = __esm(() => {
61157
61290
  NAX_VERSION = package_default.version;
61158
61291
  NAX_COMMIT = (() => {
61159
61292
  try {
61160
- if (/^[0-9a-f]{6,10}$/.test("eb4e2b89"))
61161
- return "eb4e2b89";
61293
+ if (/^[0-9a-f]{6,10}$/.test("54e59fa6"))
61294
+ return "54e59fa6";
61162
61295
  } catch {}
61163
61296
  try {
61164
61297
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -61756,12 +61889,12 @@ __export(exports_acceptance_loop, {
61756
61889
  isTestLevelFailure: () => isTestLevelFailure,
61757
61890
  isStubTestFile: () => isStubTestFile,
61758
61891
  buildResult: () => buildResult,
61892
+ _runAcceptanceTestsOnceDeps: () => _runAcceptanceTestsOnceDeps,
61759
61893
  _regenerateDeps: () => _regenerateDeps,
61760
61894
  _acceptanceLoopDeps: () => _acceptanceLoopDeps,
61761
61895
  _acceptanceFixCycleDeps: () => _acceptanceFixCycleDeps
61762
61896
  });
61763
- function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackages, config2) {
61764
- const failedPackage = failedPackages?.[0];
61897
+ function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackage, config2) {
61765
61898
  const matchedEntry = failedPackage ? acceptanceTestPaths?.find((entry) => entry.testPath === failedPackage.testPath || entry.packageDir === failedPackage.packageDir) : undefined;
61766
61899
  const selectedPathEntry = matchedEntry ?? acceptanceTestPaths?.[0];
61767
61900
  return {
@@ -61792,11 +61925,11 @@ function findingsForDiagnosis(failedACs, testOutput, diagnosis) {
61792
61925
  { ...f, fixTarget: "test" }
61793
61926
  ]);
61794
61927
  }
61795
- function buildFixCycleCtx(ctx, runtime, storyId) {
61928
+ function buildFixCycleCtx(ctx, runtime, storyId, packageDir) {
61796
61929
  return {
61797
61930
  runtime,
61798
- packageView: runtime.packages.resolve(ctx.workdir),
61799
- packageDir: ctx.workdir,
61931
+ packageView: runtime.packages.resolve(packageDir),
61932
+ packageDir,
61800
61933
  storyId,
61801
61934
  featureName: ctx.feature,
61802
61935
  agentName: ctx.agentManager?.getDefault() ?? "claude"
@@ -61830,9 +61963,10 @@ function buildAcceptanceContext(ctx, prd) {
61830
61963
  abortSignal: ctx.abortSignal
61831
61964
  };
61832
61965
  }
61833
- async function runAcceptanceTestsOnce(ctx, prd) {
61834
- const acceptanceContext = buildAcceptanceContext(ctx, prd);
61835
- const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2));
61966
+ async function runAcceptanceTestsOnce(ctx, prd, packageFilter) {
61967
+ const baseCtx = packageFilter ? { ...ctx, acceptanceTestPaths: packageFilter } : ctx;
61968
+ const acceptanceContext = buildAcceptanceContext(baseCtx, prd);
61969
+ const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
61836
61970
  const result = await acceptanceStage2.execute(acceptanceContext);
61837
61971
  if (result.action !== "fail")
61838
61972
  return { passed: true, failedACs: [], testOutput: "" };
@@ -61846,7 +61980,7 @@ async function runAcceptanceTestsOnce(ctx, prd) {
61846
61980
  failedPackages: failures.failedPackages
61847
61981
  };
61848
61982
  }
61849
- async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, acceptanceTestPath, testCommand) {
61983
+ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, acceptanceTestPath, testCommand, fixTarget) {
61850
61984
  const runtime = ctx.runtime;
61851
61985
  if (!runtime) {
61852
61986
  return { iterations: [], finalFindings: [], exitReason: "no-strategy" };
@@ -61854,7 +61988,7 @@ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, accep
61854
61988
  let currentTestOutput = initialFailures.testOutput;
61855
61989
  let currentFailedACs = initialFailures.failedACs;
61856
61990
  const storyId = prd.userStories[0]?.id ?? "unknown";
61857
- const cycleCtx = buildFixCycleCtx(ctx, runtime, storyId);
61991
+ const cycleCtx = buildFixCycleCtx(ctx, runtime, storyId, fixTarget?.packageDir ?? ctx.workdir);
61858
61992
  const cycle = {
61859
61993
  findings: findingsForDiagnosis(initialFailures.failedACs, initialFailures.testOutput, diagnosis),
61860
61994
  iterations: [],
@@ -61892,7 +62026,8 @@ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, accep
61892
62026
  }
61893
62027
  ],
61894
62028
  validate: async (_ctx, _opts) => {
61895
- const result = await runAcceptanceTestsOnce(ctx, prd);
62029
+ const packageFilter = fixTarget ? ctx.acceptanceTestPaths?.filter((entry) => entry.packageDir === fixTarget.packageDir) : undefined;
62030
+ const result = await runAcceptanceTestsOnce(ctx, prd, packageFilter);
61896
62031
  if (result.passed)
61897
62032
  return [];
61898
62033
  currentTestOutput = result.testOutput;
@@ -61918,7 +62053,7 @@ async function runAcceptanceLoop(ctx) {
61918
62053
  const storiesCompleted = ctx.storiesCompleted;
61919
62054
  const prdDirty = false;
61920
62055
  logger?.info("acceptance", "All stories complete, running acceptance validation");
61921
- const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2));
62056
+ const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
61922
62057
  while (acceptanceRetries < maxRetries) {
61923
62058
  const firstStory = prd.userStories[0];
61924
62059
  const acceptanceContext = buildAcceptanceContext(ctx, prd);
@@ -61981,40 +62116,55 @@ async function runAcceptanceLoop(ctx) {
61981
62116
  logger?.error("acceptance", "Runtime not found for diagnosis", { storyId: firstStory?.id });
61982
62117
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty, failures.failedACs, acceptanceRetries);
61983
62118
  }
61984
- const { acceptanceTestPath, testCommand } = resolveAcceptanceFixTarget(ctx.acceptanceTestPaths, failures.failedPackages, ctx.config);
61985
- const testEntries = ctx.acceptanceTestPaths ? await loadAcceptanceTestContent(ctx.acceptanceTestPaths.map((p) => p.testPath)) : [];
61986
- const effectiveAcceptanceTestPath = acceptanceTestPath || testEntries[0]?.testPath || "";
61987
- const selectedTestEntry = testEntries.find((entry) => entry.testPath === effectiveAcceptanceTestPath);
61988
- const testFileContent = selectedTestEntry?.content ?? testEntries[0]?.content ?? "";
62119
+ const failedPkgs = failures.failedPackages && failures.failedPackages.length > 0 ? failures.failedPackages : [{ testPath: "", packageDir: ctx.workdir, output: failures.testOutput, failedACs: failures.failedACs }];
61989
62120
  const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
61990
- const diagnosis = await resolveAcceptanceDiagnosis({
61991
- ctx,
61992
- failures,
61993
- totalACs,
61994
- strategy,
61995
- semanticVerdicts,
61996
- diagnosisOpts: {
61997
- testOutput: failures.testOutput,
61998
- testFileContent,
61999
- acceptanceTestPath: effectiveAcceptanceTestPath,
62000
- workdir: ctx.workdir,
62001
- storyId: firstStory?.id
62002
- }
62003
- });
62004
- logger?.info("acceptance.diagnosis", "Diagnosis resolved", {
62005
- storyId: firstStory?.id,
62006
- verdict: diagnosis.verdict,
62007
- confidence: diagnosis.confidence,
62008
- attempt: acceptanceRetries
62009
- });
62010
- const cycleResult = await runAcceptanceFixCycle(ctx, prd, failures, diagnosis, effectiveAcceptanceTestPath, testCommand);
62011
- totalCost += cycleResult.costUsd ?? 0;
62012
- const success2 = cycleResult.exitReason === "resolved" || cycleResult.finalFindings.length === 0;
62013
- return buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty, success2 ? undefined : cycleResult.finalFindings.map((f) => f.message), acceptanceRetries + cycleResult.iterations.length);
62121
+ const testEntries = ctx.acceptanceTestPaths ? await _acceptanceLoopDeps.loadAcceptanceTestContent(ctx.acceptanceTestPaths.map((p) => p.testPath)) : [];
62122
+ const remainingFindings = [];
62123
+ let totalInternalIterations = 0;
62124
+ for (const pkg of failedPkgs) {
62125
+ const { acceptanceTestPath, testCommand } = resolveAcceptanceFixTarget(ctx.acceptanceTestPaths, pkg, ctx.config);
62126
+ const effectivePath = acceptanceTestPath || pkg.testPath || testEntries[0]?.testPath || "";
62127
+ const testFileContent = testEntries.find((entry) => entry.testPath === effectivePath)?.content ?? "";
62128
+ const pkgFailures = { failedACs: pkg.failedACs, testOutput: pkg.output };
62129
+ const diagnosis = await resolveAcceptanceDiagnosis({
62130
+ ctx,
62131
+ failures: pkgFailures,
62132
+ totalACs,
62133
+ strategy,
62134
+ semanticVerdicts,
62135
+ diagnosisOpts: {
62136
+ testOutput: pkg.output,
62137
+ testFileContent,
62138
+ acceptanceTestPath: effectivePath,
62139
+ workdir: pkg.packageDir,
62140
+ storyId: firstStory?.id
62141
+ }
62142
+ });
62143
+ logger?.info("acceptance.diagnosis", "Diagnosis resolved", {
62144
+ storyId: firstStory?.id,
62145
+ packageDir: pkg.packageDir,
62146
+ verdict: diagnosis.verdict,
62147
+ confidence: diagnosis.confidence,
62148
+ attempt: acceptanceRetries
62149
+ });
62150
+ const cycleResult = await runAcceptanceFixCycle(ctx, prd, pkgFailures, diagnosis, effectivePath, testCommand, {
62151
+ packageDir: pkg.packageDir,
62152
+ testPath: effectivePath
62153
+ });
62154
+ totalCost += cycleResult.costUsd ?? 0;
62155
+ totalInternalIterations += cycleResult.iterations.length;
62156
+ const pkgResolved = cycleResult.exitReason === "resolved" || cycleResult.finalFindings.length === 0;
62157
+ if (!pkgResolved)
62158
+ remainingFindings.push(...cycleResult.finalFindings);
62159
+ }
62160
+ const finalCheck = await runAcceptanceTestsOnce(ctx, prd);
62161
+ const success2 = finalCheck.passed && remainingFindings.length === 0;
62162
+ const failureMessages = !success2 ? finalCheck.failedACs.length > 0 ? finalCheck.failedACs : remainingFindings.length > 0 ? remainingFindings.map((f) => f.message) : ["acceptance validation failed (unknown cause)"] : undefined;
62163
+ return buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty, failureMessages, acceptanceRetries + totalInternalIterations);
62014
62164
  }
62015
62165
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
62016
62166
  }
62017
- var _acceptanceLoopDeps, _acceptanceFixCycleDeps, MAX_STUB_REGENS = 2;
62167
+ var _acceptanceLoopDeps, _acceptanceFixCycleDeps, _runAcceptanceTestsOnceDeps, MAX_STUB_REGENS = 2;
62018
62168
  var init_acceptance_loop = __esm(() => {
62019
62169
  init_acceptance2();
62020
62170
  init_findings();
@@ -62027,11 +62177,15 @@ var init_acceptance_loop = __esm(() => {
62027
62177
  init_acceptance_helpers();
62028
62178
  init_acceptance_helpers();
62029
62179
  _acceptanceLoopDeps = {
62030
- loadSemanticVerdicts
62180
+ loadSemanticVerdicts,
62181
+ loadAcceptanceTestContent
62031
62182
  };
62032
62183
  _acceptanceFixCycleDeps = {
62033
62184
  runFixCycle
62034
62185
  };
62186
+ _runAcceptanceTestsOnceDeps = {
62187
+ importAcceptanceStage: () => Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2))
62188
+ };
62035
62189
  });
62036
62190
 
62037
62191
  // src/session/scratch-purge.ts
@@ -62100,6 +62254,23 @@ var init_verification = __esm(() => {
62100
62254
  });
62101
62255
 
62102
62256
  // src/execution/lifecycle/run-regression.ts
62257
+ function buildRegressionFindings(summary, rawOutput) {
62258
+ const structured = testSummaryToFindings(summary);
62259
+ if (structured.length > 0)
62260
+ return structured;
62261
+ return [
62262
+ {
62263
+ source: "test-runner",
62264
+ severity: "error",
62265
+ category: "failed-test",
62266
+ rule: "regression-suite",
62267
+ message: `Full test suite is failing but no individual test failures could be parsed from the output. Diagnose and fix the underlying failure. Raw test output:
62268
+
62269
+ ${rawOutput.slice(0, SYNTHETIC_FINDING_OUTPUT_LIMIT)}`,
62270
+ fixTarget: "source"
62271
+ }
62272
+ ];
62273
+ }
62103
62274
  async function findResponsibleStory(testFile, workdir, passedStories) {
62104
62275
  const logger = getSafeLogger();
62105
62276
  for (let i = passedStories.length - 1;i >= 0; i--) {
@@ -62311,7 +62482,7 @@ async function runDeferredRegression(options) {
62311
62482
  maxRectificationAttempts
62312
62483
  });
62313
62484
  const storyStartMs = Date.now();
62314
- const initialFindings = testSummaryToFindings(_regressionDeps.parseTestOutput(currentTestOutput));
62485
+ const initialFindings = buildRegressionFindings(_regressionDeps.parseTestOutput(currentTestOutput), currentTestOutput);
62315
62486
  const packageView = runtime.packages.repo();
62316
62487
  const cycleCtx = {
62317
62488
  runtime,
@@ -62332,7 +62503,7 @@ async function runDeferredRegression(options) {
62332
62503
  if (verification.success)
62333
62504
  return [];
62334
62505
  if (verification.output)
62335
- return testSummaryToFindings(_regressionDeps.parseTestOutput(verification.output));
62506
+ return buildRegressionFindings(_regressionDeps.parseTestOutput(verification.output), verification.output);
62336
62507
  return initialFindings;
62337
62508
  }
62338
62509
  };
@@ -62406,7 +62577,7 @@ async function runDeferredRegression(options) {
62406
62577
  storyOutcomes: storyOutcomeAccum
62407
62578
  };
62408
62579
  }
62409
- var _regressionDeps;
62580
+ var SYNTHETIC_FINDING_OUTPUT_LIMIT = 2000, _regressionDeps;
62410
62581
  var init_run_regression = __esm(() => {
62411
62582
  init_findings();
62412
62583
  init_logger2();
@@ -64063,6 +64234,7 @@ function resolveMaxAttemptsOutcome(failureCategory) {
64063
64234
  case "isolation-violation":
64064
64235
  case "verifier-rejected":
64065
64236
  case "greenfield-no-tests":
64237
+ case "no-tests-authored":
64066
64238
  return "pause";
64067
64239
  case "runtime-crash":
64068
64240
  return "pause";
@@ -64098,7 +64270,7 @@ async function handleTierEscalation(ctx) {
64098
64270
  const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
64099
64271
  const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
64100
64272
  const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
64101
- const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
64273
+ const escalateRetryAsTddSimple = escalateFailureCategory === "greenfield-no-tests";
64102
64274
  const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
64103
64275
  if (!escalationResult || !ctx.config.autoMode.escalation.enabled) {
64104
64276
  return await handleNoTierAvailable(ctx, escalateFailureCategory);
@@ -64111,12 +64283,12 @@ async function handleTierEscalation(ctx) {
64111
64283
  const escalatedTier = escalationResult.tier;
64112
64284
  for (const s of storiesToEscalate) {
64113
64285
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
64114
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
64115
- if (shouldSwitchToTestAfter) {
64116
- logger?.warn("escalation", "Switching strategy to test-after (greenfield-no-tests fallback)", {
64286
+ const shouldSwitchToTddSimple = escalateRetryAsTddSimple && isThreeSessionStrategy(currentTestStrategy);
64287
+ if (shouldSwitchToTddSimple) {
64288
+ logger?.warn("escalation", "Switching strategy to tdd-simple (greenfield-no-tests fallback)", {
64117
64289
  storyId: s.id,
64118
64290
  fromStrategy: currentTestStrategy,
64119
- toStrategy: "test-after"
64291
+ toStrategy: "tdd-simple"
64120
64292
  });
64121
64293
  } else {
64122
64294
  logger?.warn("escalation", "Escalating story to next tier", {
@@ -64137,19 +64309,19 @@ async function handleTierEscalation(ctx) {
64137
64309
  if (!shouldEscalate)
64138
64310
  return s;
64139
64311
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
64140
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
64312
+ const shouldSwitchToTddSimple = escalateRetryAsTddSimple && isThreeSessionStrategy(currentTestStrategy);
64141
64313
  const baseRouting = s.routing ?? { ...ctx.routing };
64142
64314
  const updatedRouting = {
64143
64315
  ...baseRouting,
64144
- modelTier: shouldSwitchToTestAfter ? baseRouting.modelTier : escalatedTier,
64316
+ modelTier: shouldSwitchToTddSimple ? baseRouting.modelTier : escalatedTier,
64145
64317
  ...nextAgent !== undefined ? { agent: nextAgent } : {},
64146
64318
  ...escalateRetryAsLite ? { testStrategy: "three-session-tdd-lite" } : {},
64147
- ...shouldSwitchToTestAfter ? { testStrategy: "test-after" } : {}
64319
+ ...shouldSwitchToTddSimple ? { testStrategy: "tdd-simple" } : {}
64148
64320
  };
64149
64321
  const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
64150
64322
  const isChangingTier = currentStoryTier !== escalatedTier;
64151
- const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
64152
- const escalationRecord = isChangingTier || shouldSwitchToTestAfter ? buildEscalationRecord(currentStoryTier, shouldSwitchToTestAfter ? currentStoryTier : escalatedTier, ctx.pipelineResult.reason ?? "Escalated to next retry path", { fromAgent: s.routing?.agent, toAgent: nextAgent }) : undefined;
64323
+ const shouldResetAttempts = isChangingTier || shouldSwitchToTddSimple;
64324
+ const escalationRecord = isChangingTier || shouldSwitchToTddSimple ? buildEscalationRecord(currentStoryTier, shouldSwitchToTddSimple ? currentStoryTier : escalatedTier, ctx.pipelineResult.reason ?? "Escalated to next retry path", { fromAgent: s.routing?.agent, toAgent: nextAgent }) : undefined;
64153
64325
  const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost, verifiedPipelineReason, escalateFailureCategory);
64154
64326
  return {
64155
64327
  ...s,
@@ -64188,6 +64360,7 @@ async function handleTierEscalation(ctx) {
64188
64360
  var _tierEscalationDeps;
64189
64361
  var init_tier_escalation = __esm(() => {
64190
64362
  init_pipeline();
64363
+ init_config();
64191
64364
  init_hooks();
64192
64365
  init_logger2();
64193
64366
  init_prd();
@@ -97923,7 +98096,7 @@ var FIELD_DESCRIPTIONS = {
97923
98096
  "tdd.sessionTiers.verifier": "Model tier for verifier session",
97924
98097
  "tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
97925
98098
  "tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
97926
- "tdd.greenfieldDetection": "Force test-after on projects with no test files",
98099
+ "tdd.greenfieldDetection": "Force tdd-simple on projects with no test files",
97927
98100
  constitution: "Constitution settings (core rules and constraints)",
97928
98101
  "constitution.enabled": "Enable constitution loading and injection",
97929
98102
  "constitution.path": "Path to constitution file (relative to nax/ directory)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.70.6",
3
+ "version": "0.70.8",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {