@nathapp/nax 0.70.6 → 0.70.7

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 +229 -99
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -24860,9 +24860,10 @@ var init_parser = __esm(() => {
24860
24860
 
24861
24861
  // src/test-runners/ac-parser.ts
24862
24862
  function parseTestFailures(output) {
24863
- const framework = detectFramework(output);
24863
+ const clean = output.replace(ANSI_ESCAPE_PATTERN, "");
24864
+ const framework = detectFramework(clean);
24864
24865
  const failedACs = [];
24865
- const lines = output.split(`
24866
+ const lines = clean.split(`
24866
24867
  `);
24867
24868
  for (const line of lines) {
24868
24869
  if (framework === "bun" || framework === "unknown") {
@@ -24896,7 +24897,7 @@ function parseTestFailures(output) {
24896
24897
  }
24897
24898
  }
24898
24899
  if (framework === "jest" || framework === "vitest" || framework === "unknown") {
24899
- if (/[\u25CF\u00D7\u2715]/.test(line)) {
24900
+ if (/[\u25CF\u00D7\u2715]/.test(line) || /^\s*FAIL\s/.test(line)) {
24900
24901
  const acMatch = line.match(/AC[-_]?(\d+)/i);
24901
24902
  if (acMatch) {
24902
24903
  const acId = `AC-${acMatch[1]}`;
@@ -24913,8 +24914,10 @@ function parseTestFailures(output) {
24913
24914
  }
24914
24915
  return failedACs;
24915
24916
  }
24917
+ var ANSI_ESCAPE_PATTERN;
24916
24918
  var init_ac_parser = __esm(() => {
24917
24919
  init_detector2();
24920
+ ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
24918
24921
  });
24919
24922
 
24920
24923
  // src/utils/git.ts
@@ -30246,11 +30249,7 @@ async function gitLsFiles2(workdir) {
30246
30249
  return null;
30247
30250
  }
30248
30251
  }
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
- }
30252
+ async function hasTestFilesOnDisk(workdir, patterns) {
30254
30253
  for (const pattern of patterns) {
30255
30254
  const g = new Bun.Glob(pattern);
30256
30255
  for await (const path3 of g.scan({ cwd: workdir, onlyFiles: true })) {
@@ -30261,6 +30260,13 @@ async function hasTestFiles(workdir, patterns) {
30261
30260
  }
30262
30261
  return false;
30263
30262
  }
30263
+ async function hasTestFiles(workdir, patterns) {
30264
+ const files = await gitLsFiles2(workdir);
30265
+ if (files !== null) {
30266
+ return files.some((f) => isTestFileByPatterns(f, patterns));
30267
+ }
30268
+ return hasTestFilesOnDisk(workdir, patterns);
30269
+ }
30264
30270
  async function isGreenfieldStory(_story, workdir, patterns) {
30265
30271
  try {
30266
30272
  return !await hasTestFiles(workdir, patterns ?? DEFAULT_TEST_FILE_PATTERNS);
@@ -30291,6 +30297,7 @@ var init_greenfield = __esm(() => {
30291
30297
  "out",
30292
30298
  "tmp",
30293
30299
  "temp",
30300
+ ".nax",
30294
30301
  ".git"
30295
30302
  ]);
30296
30303
  });
@@ -32865,13 +32872,13 @@ var init_semantic_helpers = __esm(() => {
32865
32872
 
32866
32873
  // src/review/semantic-evidence.ts
32867
32874
  import { isAbsolute as isAbsolute8 } from "path";
32868
- async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error") {
32875
+ async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error", repoRoot) {
32869
32876
  if (diffMode !== "ref")
32870
32877
  return findings;
32871
32878
  return Promise.all(findings.map(async (finding) => {
32872
32879
  if (!isBlockingSeverity(finding.severity, blockingThreshold))
32873
32880
  return finding;
32874
- const evidence = await checkFindingEvidence({ finding, workdir });
32881
+ const evidence = await checkFindingEvidence({ finding, workdir, repoRoot });
32875
32882
  if (evidence.status !== "unmatched")
32876
32883
  return finding;
32877
32884
  return downgradeUnsubstantiatedFinding({ finding, storyId, ...evidence });
@@ -32883,7 +32890,8 @@ async function checkFindingEvidence(opts) {
32883
32890
  const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
32884
32891
  if (!observed)
32885
32892
  return { status: "missing-observed", file: file3, line };
32886
- const contents = await readSafeFile(opts.workdir, file3);
32893
+ const roots = opts.repoRoot && opts.repoRoot !== opts.workdir ? [opts.repoRoot, opts.workdir] : [opts.workdir];
32894
+ const contents = await readSafeFile(roots, file3);
32887
32895
  if (contents === null)
32888
32896
  return { status: "unreadable", file: file3, line, observed };
32889
32897
  return matchesEvidence(contents, observed, line) ? { status: "matched", file: file3, line, observed } : { status: "unmatched", file: file3, line, observed };
@@ -32912,13 +32920,13 @@ function downgradeUnsubstantiatedFinding(opts) {
32912
32920
  });
32913
32921
  return { ...opts.finding, severity: "unverifiable" };
32914
32922
  }
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;
32923
+ async function readSafeFile(roots, file3) {
32924
+ for (const root of roots) {
32925
+ const validated = validateModulePath(file3, [root]);
32926
+ if (validated.valid && validated.absolutePath) {
32927
+ try {
32928
+ return await Bun.file(validated.absolutePath).text();
32929
+ } catch {}
32922
32930
  }
32923
32931
  }
32924
32932
  if (isAbsolute8(file3)) {
@@ -32963,11 +32971,11 @@ function hasInspectionTrail(raw) {
32963
32971
  return Array.isArray(files) && files.some((f) => typeof f === "string" && f.trim().length > 0);
32964
32972
  }
32965
32973
  async function substantiateAdversarialFindings(opts) {
32966
- const { findings, workdir, storyId, blockingThreshold } = opts;
32974
+ const { findings, workdir, storyId, blockingThreshold, repoRoot } = opts;
32967
32975
  return Promise.all(findings.map(async (finding) => {
32968
32976
  if (!isBlockingSeverity(finding.severity, blockingThreshold))
32969
32977
  return finding;
32970
- const evidence = await checkFindingEvidence({ finding, workdir });
32978
+ const evidence = await checkFindingEvidence({ finding, workdir, repoRoot });
32971
32979
  if (evidence.status !== "unmatched" && evidence.status !== "missing-observed")
32972
32980
  return finding;
32973
32981
  return downgradeUnsubstantiatedFinding({
@@ -33072,7 +33080,11 @@ async function requoteBlockingAdversarialFindings(findings, ctx) {
33072
33080
  for (const [index, finding] of next.entries()) {
33073
33081
  if (!isBlockingSeverity(finding.severity, threshold))
33074
33082
  continue;
33075
- const initialEvidence = await checkFindingEvidence({ finding, workdir: ctx.input.workdir });
33083
+ const initialEvidence = await checkFindingEvidence({
33084
+ finding,
33085
+ workdir: ctx.input.workdir,
33086
+ repoRoot: ctx.input.repoRoot
33087
+ });
33076
33088
  if (initialEvidence.status !== "unmatched")
33077
33089
  continue;
33078
33090
  if (used >= maxRequotes)
@@ -33101,7 +33113,8 @@ async function requoteBlockingAdversarialFindings(findings, ctx) {
33101
33113
  };
33102
33114
  const requotedEvidence = await checkFindingEvidence({
33103
33115
  finding: updatedFinding,
33104
- workdir: ctx.input.workdir
33116
+ workdir: ctx.input.workdir,
33117
+ repoRoot: ctx.input.repoRoot
33105
33118
  });
33106
33119
  if (requotedEvidence.status === "matched") {
33107
33120
  getSafeLogger()?.info("review", "Recovered adversarial finding via same-session requote", {
@@ -33329,6 +33342,7 @@ var init_adversarial_review = __esm(() => {
33329
33342
  const substantiated = await substantiateAdversarialFindings({
33330
33343
  findings,
33331
33344
  workdir: input.workdir,
33345
+ repoRoot: input.repoRoot,
33332
33346
  storyId: input.story.id,
33333
33347
  blockingThreshold: threshold
33334
33348
  });
@@ -34026,6 +34040,7 @@ async function runAdversarialReview(opts) {
34026
34040
  try {
34027
34041
  opResult = await _adversarialDeps.callOp(callCtx, adversarialReviewOp, {
34028
34042
  workdir,
34043
+ repoRoot: projectDir ?? workdir,
34029
34044
  story,
34030
34045
  adversarialConfig,
34031
34046
  mode: diffMode,
@@ -35548,6 +35563,10 @@ var init_decompose2 = __esm(() => {
35548
35563
  });
35549
35564
 
35550
35565
  // src/routing/classify.ts
35566
+ function isSecurityCriticalStory(title, tags = []) {
35567
+ const text = [title, ...tags ?? []].join(" ").toLowerCase();
35568
+ return SECURITY_KEYWORDS.some((kw) => text.includes(kw)) || PUBLIC_API_KEYWORDS.some((kw) => text.includes(kw));
35569
+ }
35551
35570
  function classifyComplexity(title, _description, acceptanceCriteria, tags = []) {
35552
35571
  const text = [title, ...acceptanceCriteria ?? [], ...tags ?? []].join(" ").toLowerCase();
35553
35572
  if (EXPERT_KEYWORDS.some((kw) => text.includes(kw)))
@@ -35565,10 +35584,7 @@ function determineTestStrategy(complexity, title, _description, tags = [], tddSt
35565
35584
  return "tdd-simple";
35566
35585
  if (tddStrategy === "off")
35567
35586
  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)
35587
+ if (isSecurityCriticalStory(title, tags))
35572
35588
  return "three-session-tdd";
35573
35589
  if (complexity === "expert")
35574
35590
  return "three-session-tdd";
@@ -35956,6 +35972,7 @@ __export(exports_routing, {
35956
35972
  routeTask: () => routeTask,
35957
35973
  routeStory: () => routeStory,
35958
35974
  resolveRouting: () => resolveRouting,
35975
+ isSecurityCriticalStory: () => isSecurityCriticalStory,
35959
35976
  determineTestStrategy: () => determineTestStrategy,
35960
35977
  complexityToModelTier: () => complexityToModelTier,
35961
35978
  clearCache: () => clearCache,
@@ -36801,7 +36818,11 @@ async function requoteBlockingFindings(findings, ctx) {
36801
36818
  for (const [index, finding] of next.entries()) {
36802
36819
  if (!isBlockingSeverity(finding.severity, threshold))
36803
36820
  continue;
36804
- const initialEvidence = await checkFindingEvidence({ finding, workdir: ctx.input.workdir });
36821
+ const initialEvidence = await checkFindingEvidence({
36822
+ finding,
36823
+ workdir: ctx.input.workdir,
36824
+ repoRoot: ctx.input.repoRoot
36825
+ });
36805
36826
  if (initialEvidence.status !== "unmatched")
36806
36827
  continue;
36807
36828
  if (used >= maxRequotes)
@@ -36831,7 +36852,8 @@ async function requoteBlockingFindings(findings, ctx) {
36831
36852
  };
36832
36853
  const requotedEvidence = await checkFindingEvidence({
36833
36854
  finding: updatedFinding,
36834
- workdir: ctx.input.workdir
36855
+ workdir: ctx.input.workdir,
36856
+ repoRoot: ctx.input.repoRoot
36835
36857
  });
36836
36858
  if (requotedEvidence.status === "matched") {
36837
36859
  getSafeLogger()?.info("review", "Recovered semantic finding via same-session requote", {
@@ -36967,7 +36989,7 @@ var init_semantic_review = __esm(() => {
36967
36989
  const threshold = input.blockingThreshold ?? "error";
36968
36990
  const findings = parsed.findings;
36969
36991
  const sanitized = sanitizeRefModeFindings(findings, input.mode, threshold);
36970
- const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold);
36992
+ const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold, input.repoRoot);
36971
36993
  const { accepted, dropped } = filterByAcGroundingMinimal(substantiated, input.story.acceptanceCriteria);
36972
36994
  const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
36973
36995
  const passed = parsed.passed && blocking.length === 0;
@@ -38975,8 +38997,13 @@ var init_greenfield_gate = __esm(() => {
38975
38997
  config: greenfieldGateConfigSelector,
38976
38998
  async execute(input, _ctx) {
38977
38999
  const globs = input.resolvedTestPatterns.globs;
38978
- const isGreenfield = await isGreenfieldStory(input.story, input.workdir, globs);
38979
- if (isGreenfield) {
39000
+ let hasTests;
39001
+ try {
39002
+ hasTests = await hasTestFilesOnDisk(input.workdir, globs);
39003
+ } catch {
39004
+ return { success: true, hasPreExistingTests: true };
39005
+ }
39006
+ if (!hasTests) {
38980
39007
  return { success: false, hasPreExistingTests: false, pauseReason: "greenfield-no-tests" };
38981
39008
  }
38982
39009
  return { success: true, hasPreExistingTests: true };
@@ -38984,6 +39011,33 @@ var init_greenfield_gate = __esm(() => {
38984
39011
  };
38985
39012
  });
38986
39013
 
39014
+ // src/operations/test-presence-gate.ts
39015
+ var testPresenceGateConfigSelector, testPresenceGateOp;
39016
+ var init_test_presence_gate = __esm(() => {
39017
+ init_config();
39018
+ init_greenfield();
39019
+ testPresenceGateConfigSelector = pickSelector("test-presence-gate", "execution");
39020
+ testPresenceGateOp = {
39021
+ kind: "deterministic",
39022
+ name: "test-presence-gate",
39023
+ stage: "verify",
39024
+ config: testPresenceGateConfigSelector,
39025
+ async execute(input, _ctx) {
39026
+ const globs = input.resolvedTestPatterns.globs;
39027
+ let hasTests;
39028
+ try {
39029
+ hasTests = await hasTestFilesOnDisk(input.workdir, globs);
39030
+ } catch {
39031
+ return { success: true, hasTests: true };
39032
+ }
39033
+ if (!hasTests) {
39034
+ return { success: false, hasTests: false, pauseReason: "no-tests-authored" };
39035
+ }
39036
+ return { success: true, hasTests: true };
39037
+ }
39038
+ };
39039
+ });
39040
+
38987
39041
  // src/utils/command-argv.ts
38988
39042
  function parseCommandToArgv(command) {
38989
39043
  const safeEnv = buildAllowedEnv();
@@ -40667,6 +40721,7 @@ var init_operations = __esm(() => {
40667
40721
  init_plan_critic_llm();
40668
40722
  init_execution_gates();
40669
40723
  init_greenfield_gate();
40724
+ init_test_presence_gate();
40670
40725
  init_full_suite_gate();
40671
40726
  init_full_suite_rectify();
40672
40727
  init_full_suite_rectify_op();
@@ -42048,6 +42103,7 @@ async function runSemanticReview(opts) {
42048
42103
  try {
42049
42104
  opResult = await _semanticDeps.callOp(callCtx, semanticReviewOp, {
42050
42105
  workdir,
42106
+ repoRoot: projectDir ?? workdir,
42051
42107
  story,
42052
42108
  semanticConfig,
42053
42109
  mode: diffMode,
@@ -42602,6 +42658,7 @@ var init_runner2 = __esm(() => {
42602
42658
  var init_review = __esm(() => {
42603
42659
  init_semantic_helpers();
42604
42660
  init_category_fix_target();
42661
+ init_finding_filters();
42605
42662
  init_ac_quote_validator();
42606
42663
  init_ac_structural_counterfactual();
42607
42664
  init_adversarial();
@@ -42629,6 +42686,13 @@ UNRESOLVED: <brief explanation of which findings conflicted and why they cannot
42629
42686
 
42630
42687
  Before emitting UNRESOLVED, confirm none of Exceptions 1\u2013${count} apply.
42631
42688
 
42689
+ **A missing-test or \`test-gap\` finding is never a false positive because a \`.nax/\` file exists.**
42690
+ \`.nax/\` is nax's own artifact directory; \`.nax-acceptance.test.ts\` is generated scaffolding for the
42691
+ acceptance gate \u2014 it is NOT source-tree test coverage. You may NOT cite any \`.nax/\`-resident file as
42692
+ evidence that an acceptance criterion is already tested, and you may NOT emit UNRESOLVED on that basis.
42693
+ The only valid response to a missing-test finding is to
42694
+ author a real test under the package's resolved test path.
42695
+
42632
42696
  ## Test-file edit exceptions
42633
42697
 
42634
42698
  The "do not modify test files" rule has ${countWord} narrow escape valves. Each requires a
@@ -53992,7 +54056,7 @@ ${stderr}`;
53992
54056
  errorExitCode = exitCode;
53993
54057
  allFailedACs.push("AC-ERROR");
53994
54058
  allFindings.push(acSentinelToFinding("AC-ERROR", output));
53995
- failedPackages.push({ testPath, packageDir, testFramework, commandOverride });
54059
+ failedPackages.push({ testPath, packageDir, testFramework, commandOverride, output, failedACs: ["AC-ERROR"] });
53996
54060
  continue;
53997
54061
  }
53998
54062
  for (const acId of actualFailures) {
@@ -54002,7 +54066,14 @@ ${stderr}`;
54002
54066
  }
54003
54067
  }
54004
54068
  if (actualFailures.length > 0) {
54005
- failedPackages.push({ testPath, packageDir, testFramework, commandOverride });
54069
+ failedPackages.push({
54070
+ testPath,
54071
+ packageDir,
54072
+ testFramework,
54073
+ commandOverride,
54074
+ output,
54075
+ failedACs: actualFailures
54076
+ });
54006
54077
  logger.error("acceptance", "Acceptance tests failed", {
54007
54078
  storyId: ctx.story.id,
54008
54079
  failedACs: actualFailures,
@@ -55441,6 +55512,7 @@ var init_types9 = __esm(() => {
55441
55512
  "test-writer",
55442
55513
  "greenfield-gate",
55443
55514
  "implementer",
55515
+ "test-presence-gate",
55444
55516
  "full-suite-gate",
55445
55517
  "verifier",
55446
55518
  "verify-scoped",
@@ -55453,6 +55525,7 @@ var init_types9 = __esm(() => {
55453
55525
  "test-writer": "testWriter",
55454
55526
  "greenfield-gate": "greenfieldGate",
55455
55527
  implementer: "implementer",
55528
+ "test-presence-gate": "testPresenceGate",
55456
55529
  "full-suite-gate": "fullSuiteGate",
55457
55530
  verifier: "verifier",
55458
55531
  "verify-scoped": "verifyScoped",
@@ -55595,6 +55668,8 @@ function collectOrderedPhases(state) {
55595
55668
  return [state.greenfieldGate];
55596
55669
  if (kind === "implementer" && state.implementer)
55597
55670
  return [state.implementer];
55671
+ if (kind === "test-presence-gate" && state.testPresenceGate)
55672
+ return [state.testPresenceGate];
55598
55673
  if (kind === "full-suite-gate" && state.fullSuiteGate)
55599
55674
  return [state.fullSuiteGate];
55600
55675
  if (kind === "verifier" && state.verifier)
@@ -56219,10 +56294,11 @@ class ExecutionPlan {
56219
56294
  }
56220
56295
  }
56221
56296
  }
56297
+ const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
56222
56298
  const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
56223
56299
  const advisoryOut = phaseOutputs["adversarial-review"];
56224
56300
  const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
56225
- if (advCfg && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
56301
+ if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
56226
56302
  await _storyOrchestratorDeps.runNonBlockingFix({
56227
56303
  workdir: this.ctx.packageDir,
56228
56304
  storyId: this.ctx.storyId,
@@ -56333,6 +56409,10 @@ class StoryOrchestratorBuilder {
56333
56409
  setPhase(this.state, "greenfield-gate", isSlot(value) ? value : { op: greenfieldGateOp, input: value });
56334
56410
  return this;
56335
56411
  }
56412
+ addTestPresenceGate(value) {
56413
+ setPhase(this.state, "test-presence-gate", isSlot(value) ? value : { op: testPresenceGateOp, input: value });
56414
+ return this;
56415
+ }
56336
56416
  addVerifier(value) {
56337
56417
  setPhase(this.state, "verifier", isSlot(value) ? value : { op: verifierOp, input: value });
56338
56418
  return this;
@@ -56418,6 +56498,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
56418
56498
  if (inputs.implementer) {
56419
56499
  builder.addImplementer(inputs.implementer);
56420
56500
  }
56501
+ if (!isThreeSession && inputs.testPresenceGate) {
56502
+ builder.addTestPresenceGate(inputs.testPresenceGate);
56503
+ }
56421
56504
  const regressionMode = config2.execution?.regressionGate?.mode ?? "deferred";
56422
56505
  if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
56423
56506
  builder.addFullSuiteGate(inputs.fullSuiteGate);
@@ -56491,7 +56574,12 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
56491
56574
  const nbStrategies = [];
56492
56575
  if (nbf?.enabled && inputs.adversarialReview) {
56493
56576
  const nbSink = makeDeclarationSink();
56494
- if (nbf.scope === "source") {
56577
+ if (!isThreeSession) {
56578
+ nbStrategies.push(makeAutofixImplementerStrategy(story, config2, nbSink, {
56579
+ includeAdversarialReview: true,
56580
+ promptSeverityFloor: "info"
56581
+ }));
56582
+ } else if (nbf.scope === "source") {
56495
56583
  nbStrategies.push(makeAutofixImplementerStrategy(story, config2, nbSink, {
56496
56584
  includeAdversarialReview: true,
56497
56585
  promptSeverityFloor: "info"
@@ -56637,6 +56725,7 @@ async function assemblePlanInputsFromCtx(ctx) {
56637
56725
  featureContextMarkdown: ctx.featureContextMarkdown,
56638
56726
  constitution: ctx.constitution?.content
56639
56727
  };
56728
+ const testPresenceGateInput = isSingleSessionTestOwningStrategy(ctx.routing.testStrategy) && resolvedTestPatterns ? { story, workdir: ctx.workdir, resolvedTestPatterns } : undefined;
56640
56729
  const _regressionMode = ctx.config.execution?.regressionGate?.mode;
56641
56730
  const fullSuiteGateInput = _isTdd || _regressionMode === "per-story" ? {
56642
56731
  story,
@@ -56741,6 +56830,7 @@ async function assemblePlanInputsFromCtx(ctx) {
56741
56830
  testWriter: testWriterInput,
56742
56831
  greenfieldGate: greenfieldGateInput,
56743
56832
  implementer: implementerInput,
56833
+ testPresenceGate: testPresenceGateInput,
56744
56834
  fullSuiteGate: fullSuiteGateInput,
56745
56835
  verifier: verifierInput,
56746
56836
  verifyScoped: verifyScopedInput,
@@ -56798,6 +56888,11 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason, failure
56798
56888
  case "review-incomplete":
56799
56889
  case "greenfield-no-tests":
56800
56890
  return { action: "escalate", reason: buildReason(failureCategory) };
56891
+ case "no-tests-authored":
56892
+ return {
56893
+ action: "escalate",
56894
+ 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."
56895
+ };
56801
56896
  case "dependency-prep":
56802
56897
  return pauseFallback;
56803
56898
  default:
@@ -56902,6 +56997,10 @@ function deriveTddFailureCategory(phaseOutputs, unfixedFindings, gateRegressedDu
56902
56997
  if (greenfieldOutput?.success === false && greenfieldOutput?.pauseReason === "greenfield-no-tests") {
56903
56998
  return "greenfield-no-tests";
56904
56999
  }
57000
+ const testPresenceOutput = phaseOutputs[testPresenceGateOp.name];
57001
+ if (testPresenceOutput?.success === false && testPresenceOutput?.pauseReason === "no-tests-authored") {
57002
+ return "no-tests-authored";
57003
+ }
56905
57004
  const verifierOutput = phaseOutputs[verifierOp.name];
56906
57005
  if (verifierOutput?.success === false) {
56907
57006
  if (verifierOutput.failureCategory) {
@@ -57941,13 +58040,21 @@ var init_routing2 = __esm(() => {
57941
58040
  });
57942
58041
  const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, greenfieldScanDir, resolved?.globs);
57943
58042
  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]`;
58043
+ if (isSecurityCriticalStory(ctx.story.title, ctx.story.tags)) {
58044
+ logger.info("routing", "Greenfield + security-critical \u2014 keeping three-session strategy", {
58045
+ storyId: ctx.story.id,
58046
+ strategy: routing.testStrategy,
58047
+ scanDir: greenfieldScanDir
58048
+ });
58049
+ } else {
58050
+ logger.info("routing", "Greenfield detected \u2014 forcing tdd-simple strategy", {
58051
+ storyId: ctx.story.id,
58052
+ originalStrategy: routing.testStrategy,
58053
+ scanDir: greenfieldScanDir
58054
+ });
58055
+ routing.testStrategy = "tdd-simple";
58056
+ routing.reasoning = `${routing.reasoning} [GREENFIELD OVERRIDE: No test files exist, using tdd-simple (test-first, single-session) instead of three-session TDD]`;
58057
+ }
57951
58058
  }
57952
58059
  }
57953
58060
  ctx.routing = routing;
@@ -61057,7 +61164,7 @@ var package_default;
61057
61164
  var init_package = __esm(() => {
61058
61165
  package_default = {
61059
61166
  name: "@nathapp/nax",
61060
- version: "0.70.6",
61167
+ version: "0.70.7",
61061
61168
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
61062
61169
  type: "module",
61063
61170
  bin: {
@@ -61157,8 +61264,8 @@ var init_version = __esm(() => {
61157
61264
  NAX_VERSION = package_default.version;
61158
61265
  NAX_COMMIT = (() => {
61159
61266
  try {
61160
- if (/^[0-9a-f]{6,10}$/.test("eb4e2b89"))
61161
- return "eb4e2b89";
61267
+ if (/^[0-9a-f]{6,10}$/.test("20a6d5ac"))
61268
+ return "20a6d5ac";
61162
61269
  } catch {}
61163
61270
  try {
61164
61271
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -61756,12 +61863,12 @@ __export(exports_acceptance_loop, {
61756
61863
  isTestLevelFailure: () => isTestLevelFailure,
61757
61864
  isStubTestFile: () => isStubTestFile,
61758
61865
  buildResult: () => buildResult,
61866
+ _runAcceptanceTestsOnceDeps: () => _runAcceptanceTestsOnceDeps,
61759
61867
  _regenerateDeps: () => _regenerateDeps,
61760
61868
  _acceptanceLoopDeps: () => _acceptanceLoopDeps,
61761
61869
  _acceptanceFixCycleDeps: () => _acceptanceFixCycleDeps
61762
61870
  });
61763
- function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackages, config2) {
61764
- const failedPackage = failedPackages?.[0];
61871
+ function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackage, config2) {
61765
61872
  const matchedEntry = failedPackage ? acceptanceTestPaths?.find((entry) => entry.testPath === failedPackage.testPath || entry.packageDir === failedPackage.packageDir) : undefined;
61766
61873
  const selectedPathEntry = matchedEntry ?? acceptanceTestPaths?.[0];
61767
61874
  return {
@@ -61792,11 +61899,11 @@ function findingsForDiagnosis(failedACs, testOutput, diagnosis) {
61792
61899
  { ...f, fixTarget: "test" }
61793
61900
  ]);
61794
61901
  }
61795
- function buildFixCycleCtx(ctx, runtime, storyId) {
61902
+ function buildFixCycleCtx(ctx, runtime, storyId, packageDir) {
61796
61903
  return {
61797
61904
  runtime,
61798
- packageView: runtime.packages.resolve(ctx.workdir),
61799
- packageDir: ctx.workdir,
61905
+ packageView: runtime.packages.resolve(packageDir),
61906
+ packageDir,
61800
61907
  storyId,
61801
61908
  featureName: ctx.feature,
61802
61909
  agentName: ctx.agentManager?.getDefault() ?? "claude"
@@ -61830,9 +61937,10 @@ function buildAcceptanceContext(ctx, prd) {
61830
61937
  abortSignal: ctx.abortSignal
61831
61938
  };
61832
61939
  }
61833
- async function runAcceptanceTestsOnce(ctx, prd) {
61834
- const acceptanceContext = buildAcceptanceContext(ctx, prd);
61835
- const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2));
61940
+ async function runAcceptanceTestsOnce(ctx, prd, packageFilter) {
61941
+ const baseCtx = packageFilter ? { ...ctx, acceptanceTestPaths: packageFilter } : ctx;
61942
+ const acceptanceContext = buildAcceptanceContext(baseCtx, prd);
61943
+ const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
61836
61944
  const result = await acceptanceStage2.execute(acceptanceContext);
61837
61945
  if (result.action !== "fail")
61838
61946
  return { passed: true, failedACs: [], testOutput: "" };
@@ -61846,7 +61954,7 @@ async function runAcceptanceTestsOnce(ctx, prd) {
61846
61954
  failedPackages: failures.failedPackages
61847
61955
  };
61848
61956
  }
61849
- async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, acceptanceTestPath, testCommand) {
61957
+ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, acceptanceTestPath, testCommand, fixTarget) {
61850
61958
  const runtime = ctx.runtime;
61851
61959
  if (!runtime) {
61852
61960
  return { iterations: [], finalFindings: [], exitReason: "no-strategy" };
@@ -61854,7 +61962,7 @@ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, accep
61854
61962
  let currentTestOutput = initialFailures.testOutput;
61855
61963
  let currentFailedACs = initialFailures.failedACs;
61856
61964
  const storyId = prd.userStories[0]?.id ?? "unknown";
61857
- const cycleCtx = buildFixCycleCtx(ctx, runtime, storyId);
61965
+ const cycleCtx = buildFixCycleCtx(ctx, runtime, storyId, fixTarget?.packageDir ?? ctx.workdir);
61858
61966
  const cycle = {
61859
61967
  findings: findingsForDiagnosis(initialFailures.failedACs, initialFailures.testOutput, diagnosis),
61860
61968
  iterations: [],
@@ -61892,7 +62000,8 @@ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, accep
61892
62000
  }
61893
62001
  ],
61894
62002
  validate: async (_ctx, _opts) => {
61895
- const result = await runAcceptanceTestsOnce(ctx, prd);
62003
+ const packageFilter = fixTarget ? ctx.acceptanceTestPaths?.filter((entry) => entry.packageDir === fixTarget.packageDir) : undefined;
62004
+ const result = await runAcceptanceTestsOnce(ctx, prd, packageFilter);
61896
62005
  if (result.passed)
61897
62006
  return [];
61898
62007
  currentTestOutput = result.testOutput;
@@ -61918,7 +62027,7 @@ async function runAcceptanceLoop(ctx) {
61918
62027
  const storiesCompleted = ctx.storiesCompleted;
61919
62028
  const prdDirty = false;
61920
62029
  logger?.info("acceptance", "All stories complete, running acceptance validation");
61921
- const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2));
62030
+ const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
61922
62031
  while (acceptanceRetries < maxRetries) {
61923
62032
  const firstStory = prd.userStories[0];
61924
62033
  const acceptanceContext = buildAcceptanceContext(ctx, prd);
@@ -61981,40 +62090,55 @@ async function runAcceptanceLoop(ctx) {
61981
62090
  logger?.error("acceptance", "Runtime not found for diagnosis", { storyId: firstStory?.id });
61982
62091
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty, failures.failedACs, acceptanceRetries);
61983
62092
  }
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 ?? "";
62093
+ const failedPkgs = failures.failedPackages && failures.failedPackages.length > 0 ? failures.failedPackages : [{ testPath: "", packageDir: ctx.workdir, output: failures.testOutput, failedACs: failures.failedACs }];
61989
62094
  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);
62095
+ const testEntries = ctx.acceptanceTestPaths ? await _acceptanceLoopDeps.loadAcceptanceTestContent(ctx.acceptanceTestPaths.map((p) => p.testPath)) : [];
62096
+ const remainingFindings = [];
62097
+ let totalInternalIterations = 0;
62098
+ for (const pkg of failedPkgs) {
62099
+ const { acceptanceTestPath, testCommand } = resolveAcceptanceFixTarget(ctx.acceptanceTestPaths, pkg, ctx.config);
62100
+ const effectivePath = acceptanceTestPath || pkg.testPath || testEntries[0]?.testPath || "";
62101
+ const testFileContent = testEntries.find((entry) => entry.testPath === effectivePath)?.content ?? "";
62102
+ const pkgFailures = { failedACs: pkg.failedACs, testOutput: pkg.output };
62103
+ const diagnosis = await resolveAcceptanceDiagnosis({
62104
+ ctx,
62105
+ failures: pkgFailures,
62106
+ totalACs,
62107
+ strategy,
62108
+ semanticVerdicts,
62109
+ diagnosisOpts: {
62110
+ testOutput: pkg.output,
62111
+ testFileContent,
62112
+ acceptanceTestPath: effectivePath,
62113
+ workdir: pkg.packageDir,
62114
+ storyId: firstStory?.id
62115
+ }
62116
+ });
62117
+ logger?.info("acceptance.diagnosis", "Diagnosis resolved", {
62118
+ storyId: firstStory?.id,
62119
+ packageDir: pkg.packageDir,
62120
+ verdict: diagnosis.verdict,
62121
+ confidence: diagnosis.confidence,
62122
+ attempt: acceptanceRetries
62123
+ });
62124
+ const cycleResult = await runAcceptanceFixCycle(ctx, prd, pkgFailures, diagnosis, effectivePath, testCommand, {
62125
+ packageDir: pkg.packageDir,
62126
+ testPath: effectivePath
62127
+ });
62128
+ totalCost += cycleResult.costUsd ?? 0;
62129
+ totalInternalIterations += cycleResult.iterations.length;
62130
+ const pkgResolved = cycleResult.exitReason === "resolved" || cycleResult.finalFindings.length === 0;
62131
+ if (!pkgResolved)
62132
+ remainingFindings.push(...cycleResult.finalFindings);
62133
+ }
62134
+ const finalCheck = await runAcceptanceTestsOnce(ctx, prd);
62135
+ const success2 = finalCheck.passed && remainingFindings.length === 0;
62136
+ const failureMessages = !success2 ? finalCheck.failedACs.length > 0 ? finalCheck.failedACs : remainingFindings.length > 0 ? remainingFindings.map((f) => f.message) : ["acceptance validation failed (unknown cause)"] : undefined;
62137
+ return buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty, failureMessages, acceptanceRetries + totalInternalIterations);
62014
62138
  }
62015
62139
  return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
62016
62140
  }
62017
- var _acceptanceLoopDeps, _acceptanceFixCycleDeps, MAX_STUB_REGENS = 2;
62141
+ var _acceptanceLoopDeps, _acceptanceFixCycleDeps, _runAcceptanceTestsOnceDeps, MAX_STUB_REGENS = 2;
62018
62142
  var init_acceptance_loop = __esm(() => {
62019
62143
  init_acceptance2();
62020
62144
  init_findings();
@@ -62027,11 +62151,15 @@ var init_acceptance_loop = __esm(() => {
62027
62151
  init_acceptance_helpers();
62028
62152
  init_acceptance_helpers();
62029
62153
  _acceptanceLoopDeps = {
62030
- loadSemanticVerdicts
62154
+ loadSemanticVerdicts,
62155
+ loadAcceptanceTestContent
62031
62156
  };
62032
62157
  _acceptanceFixCycleDeps = {
62033
62158
  runFixCycle
62034
62159
  };
62160
+ _runAcceptanceTestsOnceDeps = {
62161
+ importAcceptanceStage: () => Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2))
62162
+ };
62035
62163
  });
62036
62164
 
62037
62165
  // src/session/scratch-purge.ts
@@ -64063,6 +64191,7 @@ function resolveMaxAttemptsOutcome(failureCategory) {
64063
64191
  case "isolation-violation":
64064
64192
  case "verifier-rejected":
64065
64193
  case "greenfield-no-tests":
64194
+ case "no-tests-authored":
64066
64195
  return "pause";
64067
64196
  case "runtime-crash":
64068
64197
  return "pause";
@@ -64098,7 +64227,7 @@ async function handleTierEscalation(ctx) {
64098
64227
  const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
64099
64228
  const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
64100
64229
  const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
64101
- const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
64230
+ const escalateRetryAsTddSimple = escalateFailureCategory === "greenfield-no-tests";
64102
64231
  const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
64103
64232
  if (!escalationResult || !ctx.config.autoMode.escalation.enabled) {
64104
64233
  return await handleNoTierAvailable(ctx, escalateFailureCategory);
@@ -64111,12 +64240,12 @@ async function handleTierEscalation(ctx) {
64111
64240
  const escalatedTier = escalationResult.tier;
64112
64241
  for (const s of storiesToEscalate) {
64113
64242
  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)", {
64243
+ const shouldSwitchToTddSimple = escalateRetryAsTddSimple && isThreeSessionStrategy(currentTestStrategy);
64244
+ if (shouldSwitchToTddSimple) {
64245
+ logger?.warn("escalation", "Switching strategy to tdd-simple (greenfield-no-tests fallback)", {
64117
64246
  storyId: s.id,
64118
64247
  fromStrategy: currentTestStrategy,
64119
- toStrategy: "test-after"
64248
+ toStrategy: "tdd-simple"
64120
64249
  });
64121
64250
  } else {
64122
64251
  logger?.warn("escalation", "Escalating story to next tier", {
@@ -64137,19 +64266,19 @@ async function handleTierEscalation(ctx) {
64137
64266
  if (!shouldEscalate)
64138
64267
  return s;
64139
64268
  const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
64140
- const shouldSwitchToTestAfter = escalateRetryAsTestAfter && currentTestStrategy !== "test-after" && currentTestStrategy !== "no-test";
64269
+ const shouldSwitchToTddSimple = escalateRetryAsTddSimple && isThreeSessionStrategy(currentTestStrategy);
64141
64270
  const baseRouting = s.routing ?? { ...ctx.routing };
64142
64271
  const updatedRouting = {
64143
64272
  ...baseRouting,
64144
- modelTier: shouldSwitchToTestAfter ? baseRouting.modelTier : escalatedTier,
64273
+ modelTier: shouldSwitchToTddSimple ? baseRouting.modelTier : escalatedTier,
64145
64274
  ...nextAgent !== undefined ? { agent: nextAgent } : {},
64146
64275
  ...escalateRetryAsLite ? { testStrategy: "three-session-tdd-lite" } : {},
64147
- ...shouldSwitchToTestAfter ? { testStrategy: "test-after" } : {}
64276
+ ...shouldSwitchToTddSimple ? { testStrategy: "tdd-simple" } : {}
64148
64277
  };
64149
64278
  const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
64150
64279
  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;
64280
+ const shouldResetAttempts = isChangingTier || shouldSwitchToTddSimple;
64281
+ 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
64282
  const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost, verifiedPipelineReason, escalateFailureCategory);
64154
64283
  return {
64155
64284
  ...s,
@@ -64188,6 +64317,7 @@ async function handleTierEscalation(ctx) {
64188
64317
  var _tierEscalationDeps;
64189
64318
  var init_tier_escalation = __esm(() => {
64190
64319
  init_pipeline();
64320
+ init_config();
64191
64321
  init_hooks();
64192
64322
  init_logger2();
64193
64323
  init_prd();
@@ -97923,7 +98053,7 @@ var FIELD_DESCRIPTIONS = {
97923
98053
  "tdd.sessionTiers.verifier": "Model tier for verifier session",
97924
98054
  "tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
97925
98055
  "tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
97926
- "tdd.greenfieldDetection": "Force test-after on projects with no test files",
98056
+ "tdd.greenfieldDetection": "Force tdd-simple on projects with no test files",
97927
98057
  constitution: "Constitution settings (core rules and constraints)",
97928
98058
  "constitution.enabled": "Enable constitution loading and injection",
97929
98059
  "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.7",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {