@nathapp/nax 0.70.5 → 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 +444 -257
  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;
@@ -60861,7 +60968,7 @@ var init_loader4 = __esm(() => {
60861
60968
  });
60862
60969
 
60863
60970
  // src/hooks/runner.ts
60864
- import { join as join77 } from "path";
60971
+ import { join as join78 } from "path";
60865
60972
  function createDrainDeadline2(deadlineMs) {
60866
60973
  let timeoutId;
60867
60974
  const promise2 = new Promise((resolve16) => {
@@ -60880,14 +60987,14 @@ async function loadHooksConfig(projectDir, globalDir) {
60880
60987
  let globalHooks = { hooks: {} };
60881
60988
  let projectHooks = { hooks: {} };
60882
60989
  let skipGlobal = false;
60883
- const projectPath = join77(projectDir, "hooks.json");
60990
+ const projectPath = join78(projectDir, "hooks.json");
60884
60991
  const projectData = await loadJsonFile(projectPath, "hooks");
60885
60992
  if (projectData) {
60886
60993
  projectHooks = projectData;
60887
60994
  skipGlobal = projectData.skipGlobal ?? false;
60888
60995
  }
60889
60996
  if (!skipGlobal && globalDir) {
60890
- const globalPath = join77(globalDir, "hooks.json");
60997
+ const globalPath = join78(globalDir, "hooks.json");
60891
60998
  const globalData = await loadJsonFile(globalPath, "hooks");
60892
60999
  if (globalData) {
60893
61000
  globalHooks = globalData;
@@ -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.5",
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("0e9abf26"))
61161
- return "0e9abf26";
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,24 +62151,28 @@ 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
62038
62166
  import { mkdir as mkdir12, rename, rm } from "fs/promises";
62039
- import { dirname as dirname12, join as join78 } from "path";
62167
+ import { dirname as dirname12, join as join79 } from "path";
62040
62168
  async function purgeStaleScratch(projectDir, featureName, retentionDays, archiveInsteadOfDelete = false) {
62041
- const sessionsDir = join78(projectDir, ".nax", "features", featureName, "sessions");
62169
+ const sessionsDir = join79(projectDir, ".nax", "features", featureName, "sessions");
62042
62170
  const sessionIds = await _scratchPurgeDeps.listSessionDirs(sessionsDir);
62043
62171
  const cutoffMs = _scratchPurgeDeps.now() - retentionDays * 86400000;
62044
62172
  let purged = 0;
62045
62173
  for (const sessionId of sessionIds) {
62046
- const sessionDir = join78(sessionsDir, sessionId);
62047
- const descriptorPath = join78(sessionDir, "descriptor.json");
62174
+ const sessionDir = join79(sessionsDir, sessionId);
62175
+ const descriptorPath = join79(sessionDir, "descriptor.json");
62048
62176
  if (!await _scratchPurgeDeps.fileExists(descriptorPath))
62049
62177
  continue;
62050
62178
  let lastActivityAt;
@@ -62060,7 +62188,7 @@ async function purgeStaleScratch(projectDir, featureName, retentionDays, archive
62060
62188
  if (new Date(lastActivityAt).getTime() >= cutoffMs)
62061
62189
  continue;
62062
62190
  if (archiveInsteadOfDelete) {
62063
- const archiveDest = join78(projectDir, ".nax", "features", featureName, "_archive", "sessions", sessionId);
62191
+ const archiveDest = join79(projectDir, ".nax", "features", featureName, "_archive", "sessions", sessionId);
62064
62192
  await _scratchPurgeDeps.move(sessionDir, archiveDest);
62065
62193
  } else {
62066
62194
  await _scratchPurgeDeps.remove(sessionDir);
@@ -62811,12 +62939,12 @@ var DEFAULT_MAX_BATCH_SIZE = 4;
62811
62939
 
62812
62940
  // src/pipeline/subscribers/events-writer.ts
62813
62941
  import { appendFile as appendFile4, mkdir as mkdir13 } from "fs/promises";
62814
- import { basename as basename13, join as join79 } from "path";
62942
+ import { basename as basename13, join as join80 } from "path";
62815
62943
  function wireEventsWriter(bus, feature, runId, workdir) {
62816
62944
  const logger = getSafeLogger();
62817
62945
  const project = basename13(workdir);
62818
- const eventsDir = join79(getEventsRootDir(), project);
62819
- const eventsFile = join79(eventsDir, "events.jsonl");
62946
+ const eventsDir = join80(getEventsRootDir(), project);
62947
+ const eventsFile = join80(eventsDir, "events.jsonl");
62820
62948
  let dirReady = false;
62821
62949
  const write = (line) => {
62822
62950
  return (async () => {
@@ -62997,12 +63125,12 @@ var init_interaction2 = __esm(() => {
62997
63125
 
62998
63126
  // src/pipeline/subscribers/registry.ts
62999
63127
  import { mkdir as mkdir14, writeFile as writeFile2 } from "fs/promises";
63000
- import { basename as basename14, join as join80 } from "path";
63128
+ import { basename as basename14, join as join81 } from "path";
63001
63129
  function wireRegistry(bus, feature, runId, workdir, outputDir) {
63002
63130
  const logger = getSafeLogger();
63003
63131
  const project = basename14(workdir);
63004
- const runDir = join80(getRunsDir(), `${project}-${feature}-${runId}`);
63005
- const metaFile = join80(runDir, "meta.json");
63132
+ const runDir = join81(getRunsDir(), `${project}-${feature}-${runId}`);
63133
+ const metaFile = join81(runDir, "meta.json");
63006
63134
  const unsub = bus.on("run:started", (_ev) => {
63007
63135
  return (async () => {
63008
63136
  try {
@@ -63012,8 +63140,8 @@ function wireRegistry(bus, feature, runId, workdir, outputDir) {
63012
63140
  project,
63013
63141
  feature,
63014
63142
  workdir,
63015
- statusPath: join80(outputDir, "features", feature, "status.json"),
63016
- eventsDir: join80(outputDir, "features", feature, "runs"),
63143
+ statusPath: join81(outputDir, "features", feature, "status.json"),
63144
+ eventsDir: join81(outputDir, "features", feature, "runs"),
63017
63145
  registeredAt: new Date().toISOString()
63018
63146
  };
63019
63147
  await writeFile2(metaFile, JSON.stringify(meta3, null, 2));
@@ -63258,8 +63386,8 @@ var init_types10 = __esm(() => {
63258
63386
  });
63259
63387
 
63260
63388
  // src/worktree/dependencies.ts
63261
- import { existsSync as existsSync31 } from "fs";
63262
- import { join as join81 } from "path";
63389
+ import { existsSync as existsSync32 } from "fs";
63390
+ import { join as join82 } from "path";
63263
63391
  async function prepareWorktreeDependencies(options) {
63264
63392
  const mode = options.config.execution.worktreeDependencies.mode;
63265
63393
  const resolvedCwd = resolveDependencyCwd(options);
@@ -63273,7 +63401,7 @@ async function prepareWorktreeDependencies(options) {
63273
63401
  }
63274
63402
  }
63275
63403
  function resolveDependencyCwd(options) {
63276
- return options.storyWorkdir ? join81(options.worktreeRoot, options.storyWorkdir) : options.worktreeRoot;
63404
+ return options.storyWorkdir ? join82(options.worktreeRoot, options.storyWorkdir) : options.worktreeRoot;
63277
63405
  }
63278
63406
  function resolveInheritedDependencies(options, resolvedCwd) {
63279
63407
  if (hasDependencyManifests(options.worktreeRoot, resolvedCwd)) {
@@ -63283,7 +63411,7 @@ function resolveInheritedDependencies(options, resolvedCwd) {
63283
63411
  }
63284
63412
  function hasDependencyManifests(worktreeRoot, resolvedCwd) {
63285
63413
  const directories = resolvedCwd === worktreeRoot ? [worktreeRoot] : [worktreeRoot, resolvedCwd];
63286
- return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(join81(directory, filename))));
63414
+ return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(join82(directory, filename))));
63287
63415
  }
63288
63416
  async function provisionDependencies(config2, worktreeRoot, resolvedCwd) {
63289
63417
  const setupCommand2 = config2.execution.worktreeDependencies.setupCommand;
@@ -63334,7 +63462,7 @@ var init_dependencies = __esm(() => {
63334
63462
  "build.gradle.kts"
63335
63463
  ];
63336
63464
  _worktreeDependencyDeps = {
63337
- existsSync: existsSync31,
63465
+ existsSync: existsSync32,
63338
63466
  spawn
63339
63467
  };
63340
63468
  });
@@ -63345,19 +63473,19 @@ __export(exports_manager, {
63345
63473
  _managerDeps: () => _managerDeps,
63346
63474
  WorktreeManager: () => WorktreeManager
63347
63475
  });
63348
- import { existsSync as existsSync32, symlinkSync } from "fs";
63476
+ import { existsSync as existsSync33, symlinkSync } from "fs";
63349
63477
  import { mkdir as mkdir15 } from "fs/promises";
63350
- import { join as join82 } from "path";
63478
+ import { join as join83 } from "path";
63351
63479
 
63352
63480
  class WorktreeManager {
63353
63481
  async ensureGitExcludes(projectRoot) {
63354
63482
  const logger = getSafeLogger();
63355
- const infoDir = join82(projectRoot, ".git", "info");
63356
- const excludePath = join82(infoDir, "exclude");
63483
+ const infoDir = join83(projectRoot, ".git", "info");
63484
+ const excludePath = join83(infoDir, "exclude");
63357
63485
  try {
63358
63486
  await mkdir15(infoDir, { recursive: true });
63359
63487
  let existing = "";
63360
- if (existsSync32(excludePath)) {
63488
+ if (existsSync33(excludePath)) {
63361
63489
  existing = await Bun.file(excludePath).text();
63362
63490
  }
63363
63491
  const missing = NAX_GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
@@ -63380,7 +63508,7 @@ ${missing.join(`
63380
63508
  }
63381
63509
  async create(projectRoot, storyId) {
63382
63510
  validateStoryId(storyId);
63383
- const worktreePath = join82(projectRoot, ".nax-wt", storyId);
63511
+ const worktreePath = join83(projectRoot, ".nax-wt", storyId);
63384
63512
  const branchName = `nax/${storyId}`;
63385
63513
  try {
63386
63514
  const pruneProc = _managerDeps.spawn(["git", "worktree", "prune"], {
@@ -63441,9 +63569,9 @@ ${missing.join(`
63441
63569
  projectRoot
63442
63570
  });
63443
63571
  }
63444
- const envSource = join82(projectRoot, ".env");
63445
- if (existsSync32(envSource)) {
63446
- const envTarget = join82(worktreePath, ".env");
63572
+ const envSource = join83(projectRoot, ".env");
63573
+ if (existsSync33(envSource)) {
63574
+ const envTarget = join83(worktreePath, ".env");
63447
63575
  try {
63448
63576
  symlinkSync(envSource, envTarget, "file");
63449
63577
  } catch (error48) {
@@ -63459,7 +63587,7 @@ ${missing.join(`
63459
63587
  }
63460
63588
  async remove(projectRoot, storyId) {
63461
63589
  validateStoryId(storyId);
63462
- const worktreePath = join82(projectRoot, ".nax-wt", storyId);
63590
+ const worktreePath = join83(projectRoot, ".nax-wt", storyId);
63463
63591
  const branchName = `nax/${storyId}`;
63464
63592
  try {
63465
63593
  const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
@@ -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();
@@ -64306,10 +64436,10 @@ var init_merge_conflict_rectify = __esm(() => {
64306
64436
  });
64307
64437
 
64308
64438
  // src/execution/pipeline-result-handler.ts
64309
- import { join as join83 } from "path";
64439
+ import { join as join84 } from "path";
64310
64440
  async function removeWorktreeDirectory(projectRoot, storyId) {
64311
64441
  const logger = getSafeLogger();
64312
- const worktreePath = join83(projectRoot, ".nax-wt", storyId);
64442
+ const worktreePath = join84(projectRoot, ".nax-wt", storyId);
64313
64443
  try {
64314
64444
  const proc = _resultHandlerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
64315
64445
  cwd: projectRoot,
@@ -64525,8 +64655,8 @@ var init_pipeline_result_handler = __esm(() => {
64525
64655
  });
64526
64656
 
64527
64657
  // src/execution/iteration-runner.ts
64528
- import { existsSync as existsSync33 } from "fs";
64529
- import { join as join84 } from "path";
64658
+ import { existsSync as existsSync34 } from "fs";
64659
+ import { join as join85 } from "path";
64530
64660
  async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
64531
64661
  const { story, storiesToExecute, routing, isBatchExecution } = selection;
64532
64662
  if (ctx.dryRun) {
@@ -64551,7 +64681,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
64551
64681
  const storyStartTime = Date.now();
64552
64682
  let effectiveWorkdir = ctx.workdir;
64553
64683
  if (ctx.config.execution.storyIsolation === "worktree") {
64554
- const worktreePath = join84(ctx.workdir, ".nax-wt", story.id);
64684
+ const worktreePath = join85(ctx.workdir, ".nax-wt", story.id);
64555
64685
  const worktreeExists = _iterationRunnerDeps.existsSync(worktreePath);
64556
64686
  if (!worktreeExists) {
64557
64687
  await _iterationRunnerDeps.worktreeManager.ensureGitExcludes(ctx.workdir);
@@ -64571,7 +64701,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
64571
64701
  }
64572
64702
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
64573
64703
  const profileOverride = profileOverrideFromConfig(ctx.config);
64574
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join84(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
64704
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join85(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
64575
64705
  let dependencyContext;
64576
64706
  if (ctx.config.execution.storyIsolation === "worktree") {
64577
64707
  try {
@@ -64598,7 +64728,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
64598
64728
  };
64599
64729
  }
64600
64730
  }
64601
- const resolvedWorkdir = dependencyContext?.cwd ? dependencyContext.cwd : ctx.config.execution.storyIsolation === "worktree" ? story.workdir ? join84(effectiveWorkdir, story.workdir) : effectiveWorkdir : story.workdir ? join84(ctx.workdir, story.workdir) : ctx.workdir;
64731
+ const resolvedWorkdir = dependencyContext?.cwd ? dependencyContext.cwd : ctx.config.execution.storyIsolation === "worktree" ? story.workdir ? join85(effectiveWorkdir, story.workdir) : effectiveWorkdir : story.workdir ? join85(ctx.workdir, story.workdir) : ctx.workdir;
64602
64732
  const pipelineContext = {
64603
64733
  config: effectiveConfig,
64604
64734
  rootConfig: ctx.config,
@@ -64731,7 +64861,7 @@ var init_iteration_runner = __esm(() => {
64731
64861
  loadConfigForWorkdir,
64732
64862
  prepareWorktreeDependencies,
64733
64863
  runPipeline,
64734
- existsSync: existsSync33,
64864
+ existsSync: existsSync34,
64735
64865
  worktreeManager: new WorktreeManager
64736
64866
  };
64737
64867
  });
@@ -64801,7 +64931,7 @@ __export(exports_parallel_worker, {
64801
64931
  buildWorktreePipelineContext: () => buildWorktreePipelineContext,
64802
64932
  _parallelWorkerDeps: () => _parallelWorkerDeps
64803
64933
  });
64804
- import { join as join85 } from "path";
64934
+ import { join as join86 } from "path";
64805
64935
  function buildWorktreePipelineContext(base, _story) {
64806
64936
  return { ...base, prd: structuredClone(base.prd) };
64807
64937
  }
@@ -64824,7 +64954,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
64824
64954
  story,
64825
64955
  stories: [story],
64826
64956
  projectDir: context.projectDir,
64827
- workdir: dependencyContext.cwd ?? (story.workdir ? join85(worktreePath, story.workdir) : worktreePath),
64957
+ workdir: dependencyContext.cwd ?? (story.workdir ? join86(worktreePath, story.workdir) : worktreePath),
64828
64958
  worktreeDependencyContext: dependencyContext,
64829
64959
  routing,
64830
64960
  storyGitRef: storyGitRef ?? undefined
@@ -65724,7 +65854,7 @@ async function writeStatusFile(filePath, status) {
65724
65854
  var init_status_file = () => {};
65725
65855
 
65726
65856
  // src/execution/status-writer.ts
65727
- import { join as join86 } from "path";
65857
+ import { join as join87 } from "path";
65728
65858
 
65729
65859
  class StatusWriter {
65730
65860
  statusFile;
@@ -65843,7 +65973,7 @@ class StatusWriter {
65843
65973
  if (!this._prd)
65844
65974
  return;
65845
65975
  const safeLogger = getSafeLogger();
65846
- const featureStatusPath = join86(featureDir, "status.json");
65976
+ const featureStatusPath = join87(featureDir, "status.json");
65847
65977
  const write = async () => {
65848
65978
  try {
65849
65979
  const base = this.getSnapshot(totalCost, iterations);
@@ -65874,11 +66004,11 @@ __export(exports_migrate, {
65874
66004
  migrateCommand: () => migrateCommand,
65875
66005
  detectGeneratedContent: () => detectGeneratedContent
65876
66006
  });
65877
- import { existsSync as existsSync34 } from "fs";
66007
+ import { existsSync as existsSync35 } from "fs";
65878
66008
  import { mkdir as mkdir16, readdir as readdir5, rename as rename3 } from "fs/promises";
65879
66009
  import path25 from "path";
65880
66010
  async function detectGeneratedContent(naxDir) {
65881
- if (!existsSync34(naxDir))
66011
+ if (!existsSync35(naxDir))
65882
66012
  return [];
65883
66013
  const candidates = [];
65884
66014
  let entries = [];
@@ -65893,7 +66023,7 @@ async function detectGeneratedContent(naxDir) {
65893
66023
  }
65894
66024
  }
65895
66025
  const featuresDir = path25.join(naxDir, "features");
65896
- if (existsSync34(featuresDir)) {
66026
+ if (existsSync35(featuresDir)) {
65897
66027
  let featureDirs = [];
65898
66028
  try {
65899
66029
  featureDirs = await readdir5(featuresDir);
@@ -65955,7 +66085,7 @@ async function migrateCommand(options) {
65955
66085
  });
65956
66086
  }
65957
66087
  const src = path25.join(globalConfigDir(), options.reclaim);
65958
- if (!existsSync34(src)) {
66088
+ if (!existsSync35(src)) {
65959
66089
  throw new NaxError(`Nothing to reclaim: ~/.nax/${options.reclaim} does not exist`, "MIGRATE_RECLAIM_NOT_FOUND", {
65960
66090
  stage: "migrate",
65961
66091
  name: options.reclaim
@@ -66001,7 +66131,7 @@ async function migrateCommand(options) {
66001
66131
  }
66002
66132
  const naxDir = path25.join(options.workdir, ".nax");
66003
66133
  const configPath = path25.join(naxDir, "config.json");
66004
- if (!existsSync34(configPath)) {
66134
+ if (!existsSync35(configPath)) {
66005
66135
  throw new NaxError("No .nax/config.json found \u2014 run nax init first", "MIGRATE_NO_CONFIG", {
66006
66136
  stage: "migrate",
66007
66137
  workdir: options.workdir
@@ -66036,7 +66166,7 @@ async function migrateCommand(options) {
66036
66166
  for (const candidate of candidates) {
66037
66167
  const dest = path25.join(destBase, candidate.name);
66038
66168
  await mkdir16(path25.dirname(dest), { recursive: true });
66039
- if (existsSync34(dest)) {
66169
+ if (existsSync35(dest)) {
66040
66170
  throw new NaxError(`Migration conflict: destination already exists.
66041
66171
  Source: ${candidate.srcPath}
66042
66172
  Destination: ${dest}
@@ -66287,7 +66417,7 @@ __export(exports_run_initialization, {
66287
66417
  initializeRun: () => initializeRun,
66288
66418
  _reconcileDeps: () => _reconcileDeps
66289
66419
  });
66290
- import { join as join87 } from "path";
66420
+ import { join as join88 } from "path";
66291
66421
  async function reconcileState(prd, prdPath, workdir, config2) {
66292
66422
  const logger = getSafeLogger();
66293
66423
  let reconciledCount = 0;
@@ -66304,7 +66434,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
66304
66434
  });
66305
66435
  continue;
66306
66436
  }
66307
- const effectiveWorkdir = story.workdir ? join87(workdir, story.workdir) : workdir;
66437
+ const effectiveWorkdir = story.workdir ? join88(workdir, story.workdir) : workdir;
66308
66438
  try {
66309
66439
  const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
66310
66440
  if (!reviewResult.success) {
@@ -96135,7 +96265,7 @@ __export(exports_curator, {
96135
96265
  });
96136
96266
  import { readdirSync as readdirSync9 } from "fs";
96137
96267
  import { unlink as unlink4 } from "fs/promises";
96138
- import { basename as basename15, join as join89 } from "path";
96268
+ import { basename as basename15, join as join90 } from "path";
96139
96269
  function getProjectKey(config2, projectDir) {
96140
96270
  return config2.name?.trim() || basename15(projectDir);
96141
96271
  }
@@ -96218,7 +96348,7 @@ async function curatorStatus(options) {
96218
96348
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
96219
96349
  const projectKey = getProjectKey(config2, resolved.projectDir);
96220
96350
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
96221
- const runsDir = join89(outputDir, "runs");
96351
+ const runsDir = join90(outputDir, "runs");
96222
96352
  const runIds = listRunIds(runsDir);
96223
96353
  let runId;
96224
96354
  if (options.run) {
@@ -96235,8 +96365,8 @@ async function curatorStatus(options) {
96235
96365
  runId = runIds[runIds.length - 1];
96236
96366
  }
96237
96367
  console.log(`Run: ${runId}`);
96238
- const runDir = join89(runsDir, runId);
96239
- const observationsPath = join89(runDir, "observations.jsonl");
96368
+ const runDir = join90(runsDir, runId);
96369
+ const observationsPath = join90(runDir, "observations.jsonl");
96240
96370
  const observations = await parseObservations(observationsPath);
96241
96371
  const counts = new Map;
96242
96372
  for (const obs of observations) {
@@ -96246,7 +96376,7 @@ async function curatorStatus(options) {
96246
96376
  for (const [kind, count] of counts.entries()) {
96247
96377
  console.log(` ${kind}: ${count}`);
96248
96378
  }
96249
- const proposalsPath = join89(runDir, "curator-proposals.md");
96379
+ const proposalsPath = join90(runDir, "curator-proposals.md");
96250
96380
  const proposalText = await _curatorCmdDeps.readFile(proposalsPath).catch(() => null);
96251
96381
  if (proposalText !== null) {
96252
96382
  console.log("");
@@ -96260,8 +96390,8 @@ async function curatorCommit(options) {
96260
96390
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
96261
96391
  const projectKey = getProjectKey(config2, resolved.projectDir);
96262
96392
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
96263
- const runDir = join89(outputDir, "runs", options.runId);
96264
- const proposalsPath = join89(runDir, "curator-proposals.md");
96393
+ const runDir = join90(outputDir, "runs", options.runId);
96394
+ const proposalsPath = join90(runDir, "curator-proposals.md");
96265
96395
  const proposalText = await _curatorCmdDeps.readFile(proposalsPath).catch(() => null);
96266
96396
  if (proposalText === null) {
96267
96397
  console.log(`curator-proposals.md not found for run ${options.runId}.`);
@@ -96277,7 +96407,7 @@ async function curatorCommit(options) {
96277
96407
  const dropFileState = new Map;
96278
96408
  const skippedDrops = new Set;
96279
96409
  for (const drop2 of drops) {
96280
- const targetPath = join89(resolved.projectDir, drop2.canonicalFile);
96410
+ const targetPath = join90(resolved.projectDir, drop2.canonicalFile);
96281
96411
  if (!dropFileState.has(targetPath)) {
96282
96412
  const fileExists2 = await Bun.file(targetPath).exists();
96283
96413
  const existing = fileExists2 ? await _curatorCmdDeps.readFile(targetPath).catch(() => "") : "";
@@ -96311,7 +96441,7 @@ async function curatorCommit(options) {
96311
96441
  if (skippedDrops.has(drop2)) {
96312
96442
  continue;
96313
96443
  }
96314
- const targetPath = join89(resolved.projectDir, drop2.canonicalFile);
96444
+ const targetPath = join90(resolved.projectDir, drop2.canonicalFile);
96315
96445
  const existing = await _curatorCmdDeps.readFile(targetPath).catch(() => "");
96316
96446
  const filtered = filterDropContent(existing, drop2.description);
96317
96447
  await _curatorCmdDeps.writeFile(targetPath, filtered);
@@ -96320,7 +96450,7 @@ async function curatorCommit(options) {
96320
96450
  }
96321
96451
  const adds = proposals.filter((p) => p.action === "add" || p.action === "advisory");
96322
96452
  for (const add2 of adds) {
96323
- const targetPath = join89(resolved.projectDir, add2.canonicalFile);
96453
+ const targetPath = join90(resolved.projectDir, add2.canonicalFile);
96324
96454
  const content = buildAddContent(add2);
96325
96455
  await _curatorCmdDeps.appendFile(targetPath, content);
96326
96456
  modifiedFiles.add(targetPath);
@@ -96357,7 +96487,7 @@ async function curatorDryrun(options) {
96357
96487
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
96358
96488
  const projectKey = getProjectKey(config2, resolved.projectDir);
96359
96489
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
96360
- const runsDir = join89(outputDir, "runs");
96490
+ const runsDir = join90(outputDir, "runs");
96361
96491
  const runIds = listRunIds(runsDir);
96362
96492
  if (runIds.length === 0) {
96363
96493
  console.log("No runs found.");
@@ -96368,7 +96498,7 @@ async function curatorDryrun(options) {
96368
96498
  console.log(`Run ${options.run} not found in ${runsDir}.`);
96369
96499
  return;
96370
96500
  }
96371
- const observationsPath = join89(runsDir, runId, "observations.jsonl");
96501
+ const observationsPath = join90(runsDir, runId, "observations.jsonl");
96372
96502
  const observations = await parseObservations(observationsPath);
96373
96503
  const thresholds = getThresholds(config2);
96374
96504
  const proposals = runHeuristics(observations, thresholds);
@@ -96409,12 +96539,12 @@ async function curatorGc(options) {
96409
96539
  await _curatorCmdDeps.writeFile(rollupPath, newContent);
96410
96540
  const projectKey = getProjectKey(config2, resolved.projectDir);
96411
96541
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
96412
- const perRunsDir = join89(outputDir, "runs");
96542
+ const perRunsDir = join90(outputDir, "runs");
96413
96543
  for (const runId of uniqueRunIds) {
96414
96544
  if (!keepSet.has(runId)) {
96415
- const runDir = join89(perRunsDir, runId);
96416
- await _curatorCmdDeps.removeFile(join89(runDir, "observations.jsonl"));
96417
- await _curatorCmdDeps.removeFile(join89(runDir, "curator-proposals.md"));
96545
+ const runDir = join90(perRunsDir, runId);
96546
+ await _curatorCmdDeps.removeFile(join90(runDir, "observations.jsonl"));
96547
+ await _curatorCmdDeps.removeFile(join90(runDir, "curator-proposals.md"));
96418
96548
  }
96419
96549
  }
96420
96550
  console.log(`[gc] Pruned rollup to ${keep} most recent runs (was ${uniqueRunIds.length}).`);
@@ -96457,9 +96587,9 @@ var init_curator2 = __esm(() => {
96457
96587
 
96458
96588
  // bin/nax.ts
96459
96589
  init_source();
96460
- import { existsSync as existsSync36, mkdirSync as mkdirSync7 } from "fs";
96590
+ import { existsSync as existsSync37, mkdirSync as mkdirSync7 } from "fs";
96461
96591
  import { homedir as homedir3 } from "os";
96462
- import { basename as basename16, join as join90 } from "path";
96592
+ import { basename as basename16, join as join91 } from "path";
96463
96593
 
96464
96594
  // node_modules/commander/esm.mjs
96465
96595
  var import__ = __toESM(require_commander(), 1);
@@ -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)",
@@ -98733,64 +98863,119 @@ async function resolveRunProfileOverride(opts) {
98733
98863
  }
98734
98864
  // src/cli/features-resolve.ts
98735
98865
  init_config();
98736
- import { existsSync as existsSync27, readdirSync as readdirSync6 } from "fs";
98866
+ import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
98867
+ import { join as join70, relative as relative15 } from "path";
98868
+
98869
+ // src/cli/features-acceptance.ts
98870
+ init_acceptance2();
98871
+ init_config();
98872
+ init_logger2();
98873
+ init_prd();
98874
+ import { existsSync as existsSync27 } from "fs";
98737
98875
  import { join as join69, relative as relative14 } from "path";
98876
+ async function resolveFeatureAcceptance(featureName, workdir) {
98877
+ let enabled = true;
98878
+ try {
98879
+ const naxDir = findProjectDir(workdir);
98880
+ if (!naxDir) {
98881
+ return { status: "no-prd", enabled, groups: [] };
98882
+ }
98883
+ const repoRoot = join69(naxDir, "..");
98884
+ const config2 = await loadConfig(workdir);
98885
+ enabled = config2.acceptance?.enabled ?? true;
98886
+ if (!enabled) {
98887
+ return { status: "disabled", enabled: false, groups: [] };
98888
+ }
98889
+ const prdPath = join69(naxDir, "features", featureName, "prd.json");
98890
+ if (!existsSync27(prdPath)) {
98891
+ return { status: "no-prd", enabled, groups: [] };
98892
+ }
98893
+ const prd = await loadPRD(prdPath);
98894
+ const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
98895
+ const groups = await Promise.all(testGroups.map(async (g) => {
98896
+ const packageDir = relative14(repoRoot, g.packageDir);
98897
+ const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
98898
+ return {
98899
+ packageDir,
98900
+ testPath: relative14(repoRoot, g.testPath),
98901
+ exists: await Bun.file(g.testPath).exists(),
98902
+ command,
98903
+ language: g.language
98904
+ };
98905
+ }));
98906
+ return { status: "ok", enabled, groups };
98907
+ } catch (err) {
98908
+ getSafeLogger()?.warn("acceptance", "Failed to resolve feature acceptance targets", {
98909
+ featureName,
98910
+ cause: errorMessage(err)
98911
+ });
98912
+ return { status: "no-prd", enabled, groups: [] };
98913
+ }
98914
+ }
98915
+ async function resolveGroupCommand(repoRoot, packageDir, rootCommand) {
98916
+ if (packageDir === "")
98917
+ return rootCommand;
98918
+ const override = await loadPackageOverride(repoRoot, packageDir);
98919
+ return override?.acceptance?.command ?? rootCommand;
98920
+ }
98921
+
98922
+ // src/cli/features-resolve.ts
98738
98923
  async function isNonEmptyFile(absolutePath) {
98739
- if (!existsSync27(absolutePath))
98924
+ if (!existsSync28(absolutePath))
98740
98925
  return false;
98741
98926
  const content = await Bun.file(absolutePath).text();
98742
98927
  return content.trim().length > 0;
98743
98928
  }
98744
98929
  async function searchSpecSource(naxDir, repoRoot, name) {
98745
98930
  const candidates = [
98746
- { abs: join69(naxDir, "features", name, "spec.md"), kind: "markdown" },
98747
- { abs: join69(naxDir, "specs", `${name}.md`), kind: "markdown" }
98931
+ { abs: join70(naxDir, "features", name, "spec.md"), kind: "markdown" },
98932
+ { abs: join70(naxDir, "specs", `${name}.md`), kind: "markdown" }
98748
98933
  ];
98749
- const docsSpecExact = join69(repoRoot, "docs", "specs", `SPEC-${name}.md`);
98934
+ const docsSpecExact = join70(repoRoot, "docs", "specs", `SPEC-${name}.md`);
98750
98935
  candidates.push({ abs: docsSpecExact, kind: "markdown" });
98751
- const checked = candidates.map((c) => relative14(repoRoot, c.abs));
98936
+ const checked = candidates.map((c) => relative15(repoRoot, c.abs));
98752
98937
  for (const { abs, kind } of candidates.slice(0, 2)) {
98753
98938
  if (kind === "markdown") {
98754
98939
  const nonEmpty = await isNonEmptyFile(abs);
98755
98940
  if (nonEmpty) {
98756
- return { source: { kind, path: relative14(repoRoot, abs) }, checked };
98941
+ return { source: { kind, path: relative15(repoRoot, abs) }, checked };
98757
98942
  }
98758
98943
  }
98759
98944
  }
98760
98945
  if (await isNonEmptyFile(docsSpecExact)) {
98761
- return { source: { kind: "markdown", path: relative14(repoRoot, docsSpecExact) }, checked };
98946
+ return { source: { kind: "markdown", path: relative15(repoRoot, docsSpecExact) }, checked };
98762
98947
  }
98763
- const docsSpecsDir = join69(repoRoot, "docs", "specs");
98764
- if (existsSync27(docsSpecsDir)) {
98948
+ const docsSpecsDir = join70(repoRoot, "docs", "specs");
98949
+ if (existsSync28(docsSpecsDir)) {
98765
98950
  const glob = new Bun.Glob(`*${name}*.md`);
98766
98951
  for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
98767
- const abs = join69(docsSpecsDir, match);
98952
+ const abs = join70(docsSpecsDir, match);
98768
98953
  if (await isNonEmptyFile(abs)) {
98769
- const relPath = relative14(repoRoot, abs);
98954
+ const relPath = relative15(repoRoot, abs);
98770
98955
  if (!checked.includes(relPath))
98771
98956
  checked.push(relPath);
98772
98957
  return { source: { kind: "markdown", path: relPath }, checked };
98773
98958
  }
98774
98959
  }
98775
98960
  }
98776
- const prdAbs = join69(naxDir, "features", name, "prd.json");
98777
- const prdRel = relative14(repoRoot, prdAbs);
98961
+ const prdAbs = join70(naxDir, "features", name, "prd.json");
98962
+ const prdRel = relative15(repoRoot, prdAbs);
98778
98963
  if (!checked.includes(prdRel))
98779
98964
  checked.push(prdRel);
98780
- if (existsSync27(prdAbs)) {
98965
+ if (existsSync28(prdAbs)) {
98781
98966
  return { source: { kind: "prd", path: prdRel }, checked };
98782
98967
  }
98783
98968
  return { source: null, checked };
98784
98969
  }
98785
98970
  function discoverCandidates(naxDir) {
98786
- const featuresDir = join69(naxDir, "features");
98787
- if (!existsSync27(featuresDir))
98971
+ const featuresDir = join70(naxDir, "features");
98972
+ if (!existsSync28(featuresDir))
98788
98973
  return [];
98789
98974
  return readdirSync6(featuresDir, { withFileTypes: true }).filter((e) => {
98790
98975
  if (!e.isDirectory())
98791
98976
  return false;
98792
- const dir = join69(featuresDir, e.name);
98793
- return existsSync27(join69(dir, "prd.json")) || existsSync27(join69(dir, "spec.md"));
98977
+ const dir = join70(featuresDir, e.name);
98978
+ return existsSync28(join70(dir, "prd.json")) || existsSync28(join70(dir, "spec.md"));
98794
98979
  }).map((e) => e.name).sort();
98795
98980
  }
98796
98981
  async function resolveFeatureSpec(name, workdir) {
@@ -98801,10 +98986,10 @@ async function resolveFeatureSpec(name, workdir) {
98801
98986
  message: `not a nax repo: no .nax/config.json found from ${workdir}`
98802
98987
  };
98803
98988
  }
98804
- const repoRoot = join69(naxDir, "..");
98989
+ const repoRoot = join70(naxDir, "..");
98805
98990
  if (name !== undefined && (name.startsWith("./") || name.startsWith("/") || name.endsWith(".md"))) {
98806
- const abs = name.startsWith("/") ? name : join69(workdir, name);
98807
- if (!existsSync27(abs)) {
98991
+ const abs = name.startsWith("/") ? name : join70(workdir, name);
98992
+ if (!existsSync28(abs)) {
98808
98993
  return {
98809
98994
  status: "missing",
98810
98995
  featureName: null,
@@ -98826,8 +99011,8 @@ async function resolveFeatureSpec(name, workdir) {
98826
99011
  return {
98827
99012
  status: "ok",
98828
99013
  featureName: null,
98829
- specSource: { kind: "markdown", path: relative14(repoRoot, abs) },
98830
- message: `resolved spec: ${relative14(repoRoot, abs)}`
99014
+ specSource: { kind: "markdown", path: relative15(repoRoot, abs) },
99015
+ message: `resolved spec: ${relative15(repoRoot, abs)}`
98831
99016
  };
98832
99017
  }
98833
99018
  if (name !== undefined && name.trim() !== "") {
@@ -98838,11 +99023,12 @@ async function resolveFeatureSpec(name, workdir) {
98838
99023
  status: "ok",
98839
99024
  featureName: name,
98840
99025
  specSource: source2,
99026
+ acceptance: await resolveFeatureAcceptance(name, workdir),
98841
99027
  message: `resolved spec: ${source2.path}`
98842
99028
  };
98843
99029
  }
98844
- const featureDir = join69(naxDir, "features", name);
98845
- if (existsSync27(featureDir)) {
99030
+ const featureDir = join70(naxDir, "features", name);
99031
+ if (existsSync28(featureDir)) {
98846
99032
  return {
98847
99033
  status: "missing",
98848
99034
  featureName: name,
@@ -98883,6 +99069,7 @@ async function resolveFeatureSpec(name, workdir) {
98883
99069
  status: "ok",
98884
99070
  featureName: onlyName,
98885
99071
  specSource: source,
99072
+ acceptance: await resolveFeatureAcceptance(onlyName, workdir),
98886
99073
  message: `resolved spec: ${source.path}`
98887
99074
  };
98888
99075
  }
@@ -98901,7 +99088,7 @@ init_logger2();
98901
99088
  init_detect2();
98902
99089
  init_workspace();
98903
99090
  init_common();
98904
- import { join as join70 } from "path";
99091
+ import { join as join71 } from "path";
98905
99092
  function resolveEffective(detected, configPatterns) {
98906
99093
  if (configPatterns !== undefined)
98907
99094
  return "config";
@@ -98986,7 +99173,7 @@ async function detectCommand(options) {
98986
99173
  const rootDetected = detectionMap[""] ?? { patterns: [], confidence: "empty", sources: [] };
98987
99174
  const pkgEntries = await Promise.all(packageDirs.map(async (dir) => {
98988
99175
  const det = detectionMap[dir] ?? { patterns: [], confidence: "empty", sources: [] };
98989
- const pkgConfigPath = join70(workdir, ".nax", "mono", dir, "config.json");
99176
+ const pkgConfigPath = join71(workdir, ".nax", "mono", dir, "config.json");
98990
99177
  const pkgRaw = await loadRawConfig(pkgConfigPath);
98991
99178
  const pkgPatterns = deepGet(pkgRaw, TEST_PATTERNS_KEY);
98992
99179
  const effective = Array.isArray(pkgPatterns) ? pkgPatterns : undefined;
@@ -99040,13 +99227,13 @@ async function detectCommand(options) {
99040
99227
  if (rootDetected.confidence === "empty") {
99041
99228
  console.log(source_default.yellow(" root: skipped (empty detection)"));
99042
99229
  } else {
99043
- const rootConfigPath = join70(workdir, ".nax", "config.json");
99230
+ const rootConfigPath = join71(workdir, ".nax", "config.json");
99044
99231
  try {
99045
99232
  const status = await applyToConfig(rootConfigPath, rootDetected.patterns, options.force ?? false);
99046
99233
  if (status === "skipped") {
99047
99234
  console.log(source_default.dim(" root: skipped (testFilePatterns already set; use --force to overwrite)"));
99048
99235
  } else {
99049
- console.log(source_default.green(` root: ${status} \u2192 ${join70(".nax", "config.json")}`));
99236
+ console.log(source_default.green(` root: ${status} \u2192 ${join71(".nax", "config.json")}`));
99050
99237
  }
99051
99238
  } catch (err) {
99052
99239
  console.error(source_default.red(` root: write failed \u2014 ${err.message}`));
@@ -99059,13 +99246,13 @@ async function detectCommand(options) {
99059
99246
  console.log(source_default.dim(` ${dir}: skipped (empty detection)`));
99060
99247
  continue;
99061
99248
  }
99062
- const pkgConfigPath = join70(workdir, ".nax", "mono", dir, "config.json");
99249
+ const pkgConfigPath = join71(workdir, ".nax", "mono", dir, "config.json");
99063
99250
  try {
99064
99251
  const status = await applyToConfig(pkgConfigPath, det.patterns, options.force ?? false);
99065
99252
  if (status === "skipped") {
99066
99253
  console.log(source_default.dim(` ${dir}: skipped (already set)`));
99067
99254
  } else {
99068
- console.log(source_default.green(` ${dir}: ${status} \u2192 ${join70(".nax", "mono", dir, "config.json")}`));
99255
+ console.log(source_default.green(` ${dir}: ${status} \u2192 ${join71(".nax", "mono", dir, "config.json")}`));
99069
99256
  }
99070
99257
  } catch (err) {
99071
99258
  console.error(source_default.red(` ${dir}: write failed \u2014 ${err.message}`));
@@ -99082,20 +99269,20 @@ async function detectCommand(options) {
99082
99269
 
99083
99270
  // src/commands/logs.ts
99084
99271
  init_common();
99085
- import { existsSync as existsSync29 } from "fs";
99086
- import { join as join73 } from "path";
99272
+ import { existsSync as existsSync30 } from "fs";
99273
+ import { join as join74 } from "path";
99087
99274
 
99088
99275
  // src/commands/logs-formatter.ts
99089
99276
  init_source();
99090
99277
  init_formatter();
99091
99278
  import { readdirSync as readdirSync8 } from "fs";
99092
- import { join as join72 } from "path";
99279
+ import { join as join73 } from "path";
99093
99280
 
99094
99281
  // src/commands/logs-reader.ts
99095
99282
  init_paths3();
99096
- import { existsSync as existsSync28, readdirSync as readdirSync7 } from "fs";
99283
+ import { existsSync as existsSync29, readdirSync as readdirSync7 } from "fs";
99097
99284
  import { readdir as readdir3 } from "fs/promises";
99098
- import { join as join71 } from "path";
99285
+ import { join as join72 } from "path";
99099
99286
  var _logsReaderDeps = {
99100
99287
  getRunsDir
99101
99288
  };
@@ -99109,7 +99296,7 @@ async function resolveRunFileFromRegistry(runId) {
99109
99296
  }
99110
99297
  let matched = null;
99111
99298
  for (const entry of entries) {
99112
- const metaPath = join71(runsDir, entry, "meta.json");
99299
+ const metaPath = join72(runsDir, entry, "meta.json");
99113
99300
  try {
99114
99301
  const meta3 = await Bun.file(metaPath).json();
99115
99302
  if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
@@ -99121,7 +99308,7 @@ async function resolveRunFileFromRegistry(runId) {
99121
99308
  if (!matched) {
99122
99309
  throw new Error(`Run not found in registry: ${runId}`);
99123
99310
  }
99124
- if (!existsSync28(matched.eventsDir)) {
99311
+ if (!existsSync29(matched.eventsDir)) {
99125
99312
  console.log(`Log directory unavailable for run: ${runId}`);
99126
99313
  return null;
99127
99314
  }
@@ -99131,14 +99318,14 @@ async function resolveRunFileFromRegistry(runId) {
99131
99318
  return null;
99132
99319
  }
99133
99320
  const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
99134
- return join71(matched.eventsDir, specificFile ?? files[0]);
99321
+ return join72(matched.eventsDir, specificFile ?? files[0]);
99135
99322
  }
99136
99323
  async function selectRunFile(runsDir) {
99137
99324
  const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
99138
99325
  if (files.length === 0) {
99139
99326
  return null;
99140
99327
  }
99141
- return join71(runsDir, files[0]);
99328
+ return join72(runsDir, files[0]);
99142
99329
  }
99143
99330
  async function extractRunSummary(filePath) {
99144
99331
  const file3 = Bun.file(filePath);
@@ -99224,7 +99411,7 @@ Runs:
99224
99411
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
99225
99412
  console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
99226
99413
  for (const file3 of files) {
99227
- const filePath = join72(runsDir, file3);
99414
+ const filePath = join73(runsDir, file3);
99228
99415
  const summary = await extractRunSummary(filePath);
99229
99416
  const timestamp = file3.replace(".jsonl", "");
99230
99417
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
@@ -99338,7 +99525,7 @@ async function logsCommand(options) {
99338
99525
  return;
99339
99526
  }
99340
99527
  const resolved = resolveProject({ dir: options.dir });
99341
- const naxDir = join73(resolved.projectDir, ".nax");
99528
+ const naxDir = join74(resolved.projectDir, ".nax");
99342
99529
  const configPath = resolved.configPath;
99343
99530
  const configFile = Bun.file(configPath);
99344
99531
  const config2 = await configFile.json();
@@ -99346,9 +99533,9 @@ async function logsCommand(options) {
99346
99533
  if (!featureName) {
99347
99534
  throw new Error("No feature specified in config.json");
99348
99535
  }
99349
- const featureDir = join73(naxDir, "features", featureName);
99350
- const runsDir = join73(featureDir, "runs");
99351
- if (!existsSync29(runsDir)) {
99536
+ const featureDir = join74(naxDir, "features", featureName);
99537
+ const runsDir = join74(featureDir, "runs");
99538
+ if (!existsSync30(runsDir)) {
99352
99539
  throw new Error(`No runs directory found for feature: ${featureName}`);
99353
99540
  }
99354
99541
  if (options.list) {
@@ -99372,8 +99559,8 @@ init_config();
99372
99559
  init_prd();
99373
99560
  init_precheck();
99374
99561
  init_common();
99375
- import { existsSync as existsSync30 } from "fs";
99376
- import { join as join74 } from "path";
99562
+ import { existsSync as existsSync31 } from "fs";
99563
+ import { join as join75 } from "path";
99377
99564
  async function precheckCommand(options) {
99378
99565
  const resolved = resolveProject({
99379
99566
  dir: options.dir,
@@ -99395,14 +99582,14 @@ async function precheckCommand(options) {
99395
99582
  process.exit(1);
99396
99583
  }
99397
99584
  }
99398
- const naxDir = join74(resolved.projectDir, ".nax");
99399
- const featureDir = join74(naxDir, "features", featureName);
99400
- const prdPath = join74(featureDir, "prd.json");
99401
- if (!existsSync30(featureDir)) {
99585
+ const naxDir = join75(resolved.projectDir, ".nax");
99586
+ const featureDir = join75(naxDir, "features", featureName);
99587
+ const prdPath = join75(featureDir, "prd.json");
99588
+ if (!existsSync31(featureDir)) {
99402
99589
  console.error(source_default.red(`Feature not found: ${featureName}`));
99403
99590
  process.exit(1);
99404
99591
  }
99405
- if (!existsSync30(prdPath)) {
99592
+ if (!existsSync31(prdPath)) {
99406
99593
  console.error(source_default.red(`Missing prd.json for feature: ${featureName}`));
99407
99594
  console.error(source_default.dim(`Run: nax plan -f ${featureName} --from spec.md --auto`));
99408
99595
  process.exit(EXIT_CODES.INVALID_PRD);
@@ -99420,7 +99607,7 @@ async function precheckCommand(options) {
99420
99607
  init_source();
99421
99608
  init_paths3();
99422
99609
  import { readdir as readdir4 } from "fs/promises";
99423
- import { join as join75 } from "path";
99610
+ import { join as join76 } from "path";
99424
99611
  var DEFAULT_LIMIT = 20;
99425
99612
  var _runsCmdDeps = {
99426
99613
  getRunsDir
@@ -99475,7 +99662,7 @@ async function runsCommand(options = {}) {
99475
99662
  }
99476
99663
  const rows = [];
99477
99664
  for (const entry of entries) {
99478
- const metaPath = join75(runsDir, entry, "meta.json");
99665
+ const metaPath = join76(runsDir, entry, "meta.json");
99479
99666
  let meta3;
99480
99667
  try {
99481
99668
  meta3 = await Bun.file(metaPath).json();
@@ -99552,7 +99739,7 @@ async function runsCommand(options = {}) {
99552
99739
 
99553
99740
  // src/commands/unlock.ts
99554
99741
  init_source();
99555
- import { join as join76 } from "path";
99742
+ import { join as join77 } from "path";
99556
99743
  function isProcessAlive2(pid) {
99557
99744
  try {
99558
99745
  process.kill(pid, 0);
@@ -99567,7 +99754,7 @@ function formatLockAge(ageMs) {
99567
99754
  }
99568
99755
  async function unlockCommand(options) {
99569
99756
  const workdir = options.dir ?? process.cwd();
99570
- const lockPath = join76(workdir, "nax.lock");
99757
+ const lockPath = join77(workdir, "nax.lock");
99571
99758
  const lockFile = Bun.file(lockPath);
99572
99759
  const exists = await lockFile.exists();
99573
99760
  if (!exists) {
@@ -108077,8 +108264,8 @@ Next: nax generate --package ${options.package}`));
108077
108264
  }
108078
108265
  return;
108079
108266
  }
108080
- const naxDir = join90(workdir, ".nax");
108081
- if (existsSync36(naxDir) && !options.force) {
108267
+ const naxDir = join91(workdir, ".nax");
108268
+ if (existsSync37(naxDir) && !options.force) {
108082
108269
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
108083
108270
  return;
108084
108271
  }
@@ -108106,11 +108293,11 @@ Next: nax generate --package ${options.package}`));
108106
108293
  }
108107
108294
  }
108108
108295
  }
108109
- mkdirSync7(join90(naxDir, "features"), { recursive: true });
108110
- mkdirSync7(join90(naxDir, "hooks"), { recursive: true });
108296
+ mkdirSync7(join91(naxDir, "features"), { recursive: true });
108297
+ mkdirSync7(join91(naxDir, "hooks"), { recursive: true });
108111
108298
  const initConfig = options.name ? { ...DEFAULT_CONFIG, name: options.name } : DEFAULT_CONFIG;
108112
- await Bun.write(join90(naxDir, "config.json"), JSON.stringify(initConfig, null, 2));
108113
- await Bun.write(join90(naxDir, "hooks.json"), JSON.stringify({
108299
+ await Bun.write(join91(naxDir, "config.json"), JSON.stringify(initConfig, null, 2));
108300
+ await Bun.write(join91(naxDir, "hooks.json"), JSON.stringify({
108114
108301
  hooks: {
108115
108302
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
108116
108303
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -108118,12 +108305,12 @@ Next: nax generate --package ${options.package}`));
108118
108305
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
108119
108306
  }
108120
108307
  }, null, 2));
108121
- await Bun.write(join90(naxDir, ".gitignore"), `# nax temp files
108308
+ await Bun.write(join91(naxDir, ".gitignore"), `# nax temp files
108122
108309
  *.tmp
108123
108310
  .paused.json
108124
108311
  .nax-verifier-verdict.json
108125
108312
  `);
108126
- await Bun.write(join90(naxDir, "context.md"), `# Project Context
108313
+ await Bun.write(join91(naxDir, "context.md"), `# Project Context
108127
108314
 
108128
108315
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
108129
108316
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -108238,7 +108425,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
108238
108425
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
108239
108426
  process.exit(1);
108240
108427
  }
108241
- if (options.from && !existsSync36(options.from)) {
108428
+ if (options.from && !existsSync37(options.from)) {
108242
108429
  console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
108243
108430
  process.exit(1);
108244
108431
  }
@@ -108265,7 +108452,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
108265
108452
  const cliOverrides = {};
108266
108453
  const cliProfiles = options.profile ?? [];
108267
108454
  const profileOverride = naxDir ? await resolveRunProfileOverride({
108268
- prdPath: join90(naxDir, "features", options.feature, "prd.json"),
108455
+ prdPath: join91(naxDir, "features", options.feature, "prd.json"),
108269
108456
  projectRoot: workdir,
108270
108457
  cliProfile: cliProfiles,
108271
108458
  envProfile: process.env.NAX_PROFILE
@@ -108278,10 +108465,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
108278
108465
  console.error(source_default.red("nax not initialized. Run: nax init"));
108279
108466
  process.exit(1);
108280
108467
  }
108281
- const featureDir = join90(naxDir, "features", options.feature);
108282
- const prdPath = join90(featureDir, "prd.json");
108468
+ const featureDir = join91(naxDir, "features", options.feature);
108469
+ const prdPath = join91(featureDir, "prd.json");
108283
108470
  if (options.plan && options.from) {
108284
- if (existsSync36(prdPath) && !options.force) {
108471
+ if (existsSync37(prdPath) && !options.force) {
108285
108472
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
108286
108473
  console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
108287
108474
  process.exit(1);
@@ -108301,10 +108488,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
108301
108488
  }
108302
108489
  }
108303
108490
  try {
108304
- const planLogDir = join90(featureDir, "plan");
108491
+ const planLogDir = join91(featureDir, "plan");
108305
108492
  mkdirSync7(planLogDir, { recursive: true });
108306
108493
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
108307
- const planLogPath = join90(planLogDir, `${planLogId}.jsonl`);
108494
+ const planLogPath = join91(planLogDir, `${planLogId}.jsonl`);
108308
108495
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
108309
108496
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
108310
108497
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -108343,17 +108530,17 @@ program2.command("run").description("Run the orchestration loop for a feature").
108343
108530
  process.exit(1);
108344
108531
  }
108345
108532
  }
108346
- if (!existsSync36(prdPath)) {
108533
+ if (!existsSync37(prdPath)) {
108347
108534
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
108348
108535
  process.exit(1);
108349
108536
  }
108350
108537
  resetLogger();
108351
108538
  const projectKey = config2.name?.trim() || basename16(workdir);
108352
108539
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
108353
- const runsDir = join90(outputDir, "features", options.feature, "runs");
108540
+ const runsDir = join91(outputDir, "features", options.feature, "runs");
108354
108541
  mkdirSync7(runsDir, { recursive: true });
108355
108542
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
108356
- const logFilePath = join90(runsDir, `${runId}.jsonl`);
108543
+ const logFilePath = join91(runsDir, `${runId}.jsonl`);
108357
108544
  const isTTY = process.stdout.isTTY ?? false;
108358
108545
  const headlessFlag = options.headless ?? false;
108359
108546
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -108371,7 +108558,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
108371
108558
  config2.agent.default = options.agent;
108372
108559
  }
108373
108560
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
108374
- const globalNaxDir = join90(homedir3(), ".nax");
108561
+ const globalNaxDir = join91(homedir3(), ".nax");
108375
108562
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
108376
108563
  const eventEmitter = new PipelineEventEmitter;
108377
108564
  const agentStreamEvents = useHeadless ? undefined : new AgentStreamEventBus;
@@ -108391,12 +108578,12 @@ program2.command("run").description("Run the orchestration loop for a feature").
108391
108578
  events: eventEmitter,
108392
108579
  ptyOptions: null,
108393
108580
  agentStreamEvents,
108394
- queueFilePath: join90(workdir, ".queue.txt")
108581
+ queueFilePath: join91(workdir, ".queue.txt")
108395
108582
  });
108396
108583
  } else {
108397
108584
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
108398
108585
  }
108399
- const statusFilePath = join90(outputDir, "status.json");
108586
+ const statusFilePath = join91(outputDir, "status.json");
108400
108587
  let parallel;
108401
108588
  if (options.parallel !== undefined) {
108402
108589
  parallel = Number.parseInt(options.parallel, 10);
@@ -108423,9 +108610,9 @@ program2.command("run").description("Run the orchestration loop for a feature").
108423
108610
  skipPrecheck: options.skipPrecheck ?? false,
108424
108611
  agentStreamEvents
108425
108612
  });
108426
- const latestSymlink = join90(runsDir, "latest.jsonl");
108613
+ const latestSymlink = join91(runsDir, "latest.jsonl");
108427
108614
  try {
108428
- if (existsSync36(latestSymlink)) {
108615
+ if (existsSync37(latestSymlink)) {
108429
108616
  Bun.spawnSync(["rm", latestSymlink]);
108430
108617
  }
108431
108618
  Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
@@ -108520,9 +108707,9 @@ features.command("create <name>").description("Create a new feature").option("-d
108520
108707
  console.error(source_default.red("nax not initialized. Run: nax init"));
108521
108708
  process.exit(1);
108522
108709
  }
108523
- const featureDir = join90(naxDir, "features", name);
108710
+ const featureDir = join91(naxDir, "features", name);
108524
108711
  mkdirSync7(featureDir, { recursive: true });
108525
- await Bun.write(join90(featureDir, "spec.md"), `# Feature: ${name}
108712
+ await Bun.write(join91(featureDir, "spec.md"), `# Feature: ${name}
108526
108713
 
108527
108714
  ## Overview
108528
108715
 
@@ -108555,7 +108742,7 @@ features.command("create <name>").description("Create a new feature").option("-d
108555
108742
 
108556
108743
  <!-- What this feature explicitly does NOT cover. -->
108557
108744
  `);
108558
- await Bun.write(join90(featureDir, "progress.txt"), `# Progress: ${name}
108745
+ await Bun.write(join91(featureDir, "progress.txt"), `# Progress: ${name}
108559
108746
 
108560
108747
  Created: ${new Date().toISOString()}
108561
108748
 
@@ -108581,8 +108768,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
108581
108768
  console.error(source_default.red("nax not initialized."));
108582
108769
  process.exit(1);
108583
108770
  }
108584
- const featuresDir = join90(naxDir, "features");
108585
- if (!existsSync36(featuresDir)) {
108771
+ const featuresDir = join91(naxDir, "features");
108772
+ if (!existsSync37(featuresDir)) {
108586
108773
  console.log(source_default.dim("No features yet."));
108587
108774
  return;
108588
108775
  }
@@ -108596,8 +108783,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
108596
108783
  Features:
108597
108784
  `));
108598
108785
  for (const name of entries) {
108599
- const prdPath = join90(featuresDir, name, "prd.json");
108600
- if (existsSync36(prdPath)) {
108786
+ const prdPath = join91(featuresDir, name, "prd.json");
108787
+ if (existsSync37(prdPath)) {
108601
108788
  const prd = await loadPRD(prdPath);
108602
108789
  const c = countStories(prd);
108603
108790
  console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);
@@ -108669,10 +108856,10 @@ Use: nax plan -f <feature> --from <spec>`));
108669
108856
  cliOverrides.profile = cliProfiles;
108670
108857
  }
108671
108858
  const config2 = await loadConfig(workdir, cliOverrides);
108672
- const featureLogDir = join90(naxDir, "features", options.feature, "plan");
108859
+ const featureLogDir = join91(naxDir, "features", options.feature, "plan");
108673
108860
  mkdirSync7(featureLogDir, { recursive: true });
108674
108861
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
108675
- const planLogPath = join90(featureLogDir, `${planLogId}.jsonl`);
108862
+ const planLogPath = join91(featureLogDir, `${planLogId}.jsonl`);
108676
108863
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
108677
108864
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
108678
108865
  try {