@nathapp/nax 0.60.2 → 0.61.0

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 +165 -81
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -3238,7 +3238,14 @@ GOOD (write ACs like these):
3238
3238
  - "validatePostRunAction() returns false and logs warning when postRunAction.execute is not a function"
3239
3239
  - "cleanupRun() calls action.execute() only when action.shouldRun() resolves to true"
3240
3240
  - "When action.execute() throws, cleanupRun() logs at warn level and continues to the next action"
3241
- - "resolveRouting() short-circuits and returns story.routing values when both complexity and testStrategy are already set"`, LANGUAGE_PATTERNS, TYPE_PATTERNS, GROUPING_RULES = `## Story Rules
3241
+ - "resolveRouting() short-circuits and returns story.routing values when both complexity and testStrategy are already set"`, LANGUAGE_PATTERNS, TYPE_PATTERNS, SPEC_ANCHOR_RULES = `## Spec Fidelity Rules
3242
+
3243
+ When a spec is provided, these rules govern acceptance criteria generation:
3244
+
3245
+ 1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria\`, verbatim or lightly rephrased for testability. Never silently drop a spec AC.
3246
+ 2. **Do not invent spec ACs.** If you identify useful behavioral edge cases or negative paths that the spec did not explicitly list, place them in \`suggestedCriteria\` (a string array on the same story object) \u2014 never in \`acceptanceCriteria\`. These go through a separate hardening pass.
3247
+ 3. **Respect story scope.** Each story's criteria must only cover what the spec says for that story. Do not assign criteria that belong to a different story's scope (wrong feature area, wrong file, wrong dependency chain).
3248
+ 4. **\`suggestedCriteria\` format.** Each element must be a plain behavioral assertion \u2014 an observable output, return value, state change, or error condition that a test can assert. Never include implementation details (imports, internal structure), design suggestions, or vague descriptions.`, GROUPING_RULES = `## Story Rules
3242
3249
 
3243
3250
  - Every story must produce code changes verifiable by tests or review.
3244
3251
  - NEVER create stories for analysis, planning, documentation, or migration plans.
@@ -18308,7 +18315,8 @@ var init_schemas3 = __esm(() => {
18308
18315
  PlanConfigSchema = exports_external.object({
18309
18316
  model: ModelTierSchema,
18310
18317
  outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty"),
18311
- timeoutSeconds: exports_external.number().int().positive().default(600)
18318
+ timeoutSeconds: exports_external.number().int().positive().default(600),
18319
+ decomposeTimeoutSeconds: exports_external.number().int().min(30).max(1800).optional()
18312
18320
  });
18313
18321
  AcceptanceFixConfigSchema = exports_external.object({
18314
18322
  diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty").default("fast"),
@@ -19544,7 +19552,8 @@ class AcpAgentAdapter {
19544
19552
  workdir: options.workdir,
19545
19553
  featureName: options.featureName,
19546
19554
  storyId: options.storyId,
19547
- sessionRole: options.sessionRole ?? "decompose"
19555
+ sessionRole: options.sessionRole ?? "decompose",
19556
+ timeoutMs: (this.naxConfig?.plan?.decomposeTimeoutSeconds ?? this.naxConfig?.plan?.timeoutSeconds ?? 300) * 1000
19548
19557
  });
19549
19558
  output = completeResult.output;
19550
19559
  } catch (err) {
@@ -21315,14 +21324,16 @@ ${errors3.join(`
21315
21324
  }
21316
21325
  return result.data;
21317
21326
  }
21318
- async function loadConfigForWorkdir(rootConfigPath, packageDir) {
21327
+ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
21319
21328
  const logger = getLogger();
21320
21329
  const resolvedRootConfigPath = resolve5(rootConfigPath);
21321
21330
  const rootNaxDir = dirname2(resolvedRootConfigPath);
21322
- let rootConfigPromise = _rootConfigCache.get(resolvedRootConfigPath);
21331
+ const profileKey = cliOverrides?.profile ?? "";
21332
+ const cacheKey = profileKey ? `${resolvedRootConfigPath}:${profileKey}` : resolvedRootConfigPath;
21333
+ let rootConfigPromise = _rootConfigCache.get(cacheKey);
21323
21334
  if (!rootConfigPromise) {
21324
- rootConfigPromise = loadConfig(rootNaxDir);
21325
- _rootConfigCache.set(resolvedRootConfigPath, rootConfigPromise);
21335
+ rootConfigPromise = loadConfig(rootNaxDir, cliOverrides);
21336
+ _rootConfigCache.set(cacheKey, rootConfigPromise);
21326
21337
  }
21327
21338
  const rootConfig = await rootConfigPromise;
21328
21339
  if (!packageDir) {
@@ -22705,8 +22716,9 @@ ${opts.specContent}
22705
22716
 
22706
22717
  The spec above is the authoritative source for acceptance criteria.
22707
22718
  - Each story's \`acceptanceCriteria\` array MUST contain only criteria that are explicitly stated or directly implied by the spec.
22708
- - If a debater proposed criteria beyond the spec (edge cases, error handling, implementation details), place those in a separate \`suggestedCriteria\` array on the same story object. Each element of \`suggestedCriteria\` MUST be a plain string \u2014 never an object or structured value.
22709
- - Never silently merge debater-invented criteria into \`acceptanceCriteria\`. The distinction matters: \`acceptanceCriteria\` drives automated testing; \`suggestedCriteria\` is logged for human review.
22719
+ - If a debater proposed criteria beyond the spec (observable edge cases, error-path behaviors), place those in a separate \`suggestedCriteria\` array on the same story object. Each element of \`suggestedCriteria\` MUST be a plain string \u2014 never an object or structured value.
22720
+ - \`suggestedCriteria\` MUST contain only behavioral acceptance criteria \u2014 observable outputs, return values, state changes, or error conditions a test can assert. DO NOT include: implementation details (imports, internal structure), design suggestions ("consider X"), "not required" notes, or any criterion that cannot be expressed as a test assertion.
22721
+ - Never silently merge debater-invented criteria into \`acceptanceCriteria\`. The distinction matters: \`acceptanceCriteria\` drives automated testing; \`suggestedCriteria\` gates a hardening pass.
22710
22722
  - Preserve the spec's AC wording. You may refine for clarity but must not change semantics.` : "";
22711
22723
  const planSynthesisSuffix = `IMPORTANT: Your response must be a single valid JSON object in PRD format (with project, feature, branchName, userStories array, etc.). Do NOT wrap it in markdown fences. Output raw JSON only.${specAnchor}`;
22712
22724
  const outcome = await resolveOutcome(proposalOutputs, critiqueOutputs, ctx.stageConfig, ctx.config, ctx.storyId, resolverTimeoutMs, opts.workdir, opts.feature, undefined, undefined, planSynthesisSuffix, successful.map((p) => p.debater));
@@ -25071,6 +25083,31 @@ function resolveAcceptanceTestCandidates(options) {
25071
25083
  return [];
25072
25084
  return [resolveAcceptanceFeatureTestPath(options.featureDir, options.testPathConfig, options.language)];
25073
25085
  }
25086
+ function groupStoriesByPackage(prd, workdir, featureName, testPathConfig, language) {
25087
+ const nonFixStories = prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed");
25088
+ const groupMap = new Map;
25089
+ for (const story of nonFixStories) {
25090
+ const wd = story.workdir ?? "";
25091
+ if (!groupMap.has(wd)) {
25092
+ groupMap.set(wd, { stories: [], criteria: [] });
25093
+ }
25094
+ const group = groupMap.get(wd);
25095
+ if (group) {
25096
+ group.stories.push(story);
25097
+ group.criteria.push(...story.acceptanceCriteria);
25098
+ }
25099
+ }
25100
+ if (groupMap.size === 0) {
25101
+ groupMap.set("", { stories: [], criteria: [] });
25102
+ }
25103
+ const groups = [];
25104
+ for (const [wd, { stories, criteria }] of groupMap) {
25105
+ const packageDir = wd ? path.join(workdir, wd) : workdir;
25106
+ const testPath = resolveAcceptancePackageFeatureTestPath(packageDir, featureName, testPathConfig, language);
25107
+ groups.push({ testPath, packageDir, stories, criteria });
25108
+ }
25109
+ return groups;
25110
+ }
25074
25111
  function suggestedTestFilename(language) {
25075
25112
  switch (language?.toLowerCase()) {
25076
25113
  case "go":
@@ -25142,6 +25179,9 @@ async function loadPRD(path2) {
25142
25179
  story.status = "passed";
25143
25180
  story.status = story.status ?? "pending";
25144
25181
  story.acceptanceCriteria = story.acceptanceCriteria ?? [];
25182
+ if (Array.isArray(story.suggestedCriteria) && story.suggestedCriteria.length === 0) {
25183
+ story.suggestedCriteria = undefined;
25184
+ }
25145
25185
  story.storyPoints = story.storyPoints ?? 1;
25146
25186
  }
25147
25187
  return prd;
@@ -25980,6 +26020,7 @@ Previous test failed because: ${options.previousFailure}` : "";
25980
26020
  featureName: options.featureName,
25981
26021
  sessionRole: "acceptance-gen"
25982
26022
  });
26023
+ const genCostUsd = typeof completeResult === "string" ? 0 : completeResult.costUsd ?? 0;
25983
26024
  const rawOutput = typeof completeResult === "string" ? completeResult : completeResult.output;
25984
26025
  let testCode = extractTestCode(rawOutput);
25985
26026
  logger.debug("acceptance", "Received raw output from LLM", {
@@ -26077,7 +26118,8 @@ Previous test failed because: ${options.previousFailure}` : "";
26077
26118
  }));
26078
26119
  return {
26079
26120
  testCode: generateSkeletonTests(options.featureName, skeletonCriteria, options.testFramework, options.language),
26080
- criteria: skeletonCriteria
26121
+ criteria: skeletonCriteria,
26122
+ costUsd: genCostUsd
26081
26123
  };
26082
26124
  }
26083
26125
  const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
@@ -26088,7 +26130,7 @@ Previous test failed because: ${options.previousFailure}` : "";
26088
26130
  storyId: c.storyId
26089
26131
  })), null, 2);
26090
26132
  await _generatorPRDDeps.writeFile(join16(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
26091
- return { testCode, criteria };
26133
+ return { testCode, criteria, costUsd: genCostUsd };
26092
26134
  }
26093
26135
  function parseAcceptanceCriteria(specContent) {
26094
26136
  const criteria = [];
@@ -26366,12 +26408,22 @@ function buildRefinementPrompt(criteria, codebaseContext, options) {
26366
26408
  `);
26367
26409
  const strategySection = buildStrategySection(options);
26368
26410
  const refinedExample = buildRefinedExample(options?.testStrategy);
26411
+ const storyLines = [];
26412
+ if (options?.storyTitle)
26413
+ storyLines.push(`Title: ${options.storyTitle}`);
26414
+ if (options?.storyDescription)
26415
+ storyLines.push(`Description: ${options.storyDescription}`);
26416
+ const storySection = storyLines.length > 0 ? `STORY CONTEXT:
26417
+ ${storyLines.join(`
26418
+ `)}
26419
+
26420
+ ` : "";
26369
26421
  const codebaseSection = codebaseContext ? `CODEBASE CONTEXT:
26370
26422
  ${codebaseContext}
26371
26423
  ` : "";
26372
26424
  const core2 = `You are an acceptance criteria refinement assistant. Your task is to convert raw acceptance criteria into concrete, machine-verifiable assertions.
26373
26425
 
26374
- ${codebaseSection}${strategySection}ACCEPTANCE CRITERIA TO REFINE:
26426
+ ${storySection}${codebaseSection}${strategySection}ACCEPTANCE CRITERIA TO REFINE:
26375
26427
  ${criteriaList}
26376
26428
 
26377
26429
  For each criterion, produce a refined version that is concrete and automatically testable where possible.
@@ -26455,13 +26507,28 @@ function parseRefinementResponse(response, criteria) {
26455
26507
  }
26456
26508
  async function refineAcceptanceCriteria(criteria, context) {
26457
26509
  if (criteria.length === 0) {
26458
- return [];
26510
+ return { criteria: [], costUsd: 0 };
26459
26511
  }
26460
- const { storyId, featureName, workdir, codebaseContext, config: config2, testStrategy, testFramework } = context;
26512
+ const {
26513
+ storyId,
26514
+ featureName,
26515
+ workdir,
26516
+ codebaseContext,
26517
+ config: config2,
26518
+ testStrategy,
26519
+ testFramework,
26520
+ storyTitle,
26521
+ storyDescription
26522
+ } = context;
26461
26523
  const logger = getLogger();
26462
26524
  const modelTier = config2.acceptance?.model ?? "fast";
26463
26525
  const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, modelTier, config2.autoMode.defaultAgent);
26464
- const prompt = buildRefinementPrompt(criteria, codebaseContext, { testStrategy, testFramework });
26526
+ const prompt = buildRefinementPrompt(criteria, codebaseContext, {
26527
+ testStrategy,
26528
+ testFramework,
26529
+ storyTitle,
26530
+ storyDescription
26531
+ });
26465
26532
  let response;
26466
26533
  try {
26467
26534
  const completeResult = await _refineDeps.adapter.complete(prompt, {
@@ -26475,20 +26542,21 @@ async function refineAcceptanceCriteria(criteria, context) {
26475
26542
  sessionRole: "refine",
26476
26543
  timeoutMs: config2.acceptance?.timeoutMs ?? 120000
26477
26544
  });
26545
+ const costUsd = typeof completeResult === "string" ? 0 : completeResult.costUsd ?? 0;
26478
26546
  response = typeof completeResult === "string" ? completeResult : completeResult.output;
26547
+ const parsed = parseRefinementResponse(response, criteria);
26548
+ return {
26549
+ criteria: parsed.map((item) => ({ ...item, storyId: item.storyId || storyId })),
26550
+ costUsd
26551
+ };
26479
26552
  } catch (error48) {
26480
26553
  const reason = errorMessage(error48);
26481
26554
  logger.warn("refinement", "adapter.complete() failed, falling back to original criteria", {
26482
26555
  storyId,
26483
26556
  error: reason
26484
26557
  });
26485
- return fallbackCriteria(criteria, storyId);
26558
+ return { criteria: fallbackCriteria(criteria, storyId), costUsd: 0 };
26486
26559
  }
26487
- const parsed = parseRefinementResponse(response, criteria);
26488
- return parsed.map((item) => ({
26489
- ...item,
26490
- storyId: item.storyId || storyId
26491
- }));
26492
26560
  }
26493
26561
  function fallbackCriteria(criteria, storyId = "") {
26494
26562
  return criteria.map((c) => ({
@@ -26540,14 +26608,17 @@ async function runHardeningPass(ctx) {
26540
26608
  const allRefined = [];
26541
26609
  for (const story of storiesWithSuggested) {
26542
26610
  const criteria = story.suggestedCriteria ?? [];
26543
- const refined = await _hardeningDeps.refine(criteria, {
26611
+ const refineResult = await _hardeningDeps.refine(criteria, {
26544
26612
  storyId: story.id,
26545
26613
  featureName: ctx.prd.feature,
26546
26614
  workdir: ctx.workdir,
26547
26615
  codebaseContext: "",
26548
- config: ctx.config
26616
+ config: ctx.config,
26617
+ storyTitle: story.title,
26618
+ storyDescription: story.description
26549
26619
  });
26550
- allRefined.push(...refined);
26620
+ allRefined.push(...refineResult.criteria);
26621
+ result.costUsd += refineResult.costUsd;
26551
26622
  }
26552
26623
  const language = ctx.config.project?.language;
26553
26624
  const suggestedTestPath = resolveSuggestedPackageFeatureTestPath(ctx.workdir, ctx.prd.feature, ctx.config.acceptance?.suggestedTestPath, language);
@@ -26568,6 +26639,7 @@ async function runHardeningPass(ctx) {
26568
26639
  language,
26569
26640
  targetTestFile: suggestedTestPath
26570
26641
  });
26642
+ result.costUsd += genResult.costUsd ?? 0;
26571
26643
  if (genResult.testCode) {
26572
26644
  await _hardeningDeps.writeFile(suggestedTestPath, genResult.testCode);
26573
26645
  }
@@ -26586,22 +26658,30 @@ async function runHardeningPass(ctx) {
26586
26658
  ${stderr}`;
26587
26659
  const failedACs = parseTestFailures(output);
26588
26660
  const failedSet = new Set(failedACs.map((ac) => ac.toUpperCase()));
26661
+ const refinedByStory = new Map;
26662
+ for (const r of allRefined) {
26663
+ const list = refinedByStory.get(r.storyId) ?? [];
26664
+ list.push(r);
26665
+ refinedByStory.set(r.storyId, list);
26666
+ }
26589
26667
  let acIndex = 0;
26590
26668
  for (const story of storiesWithSuggested) {
26591
- const suggested = story.suggestedCriteria ?? [];
26669
+ const storyRefined = refinedByStory.get(story.id) ?? [];
26592
26670
  const toPromote = [];
26593
26671
  const toDiscard = [];
26594
- for (const criterion of suggested) {
26672
+ for (const refinedCriterion of storyRefined) {
26595
26673
  acIndex++;
26596
26674
  const acId = `AC-${acIndex}`;
26597
- if (failedSet.has(acId) || exitCode !== 0 && failedACs.length === 0) {
26598
- toDiscard.push(criterion);
26675
+ const nonTestable = refinedCriterion.testable === false;
26676
+ if (nonTestable || failedSet.has(acId) || exitCode !== 0 && failedACs.length === 0) {
26677
+ toDiscard.push(refinedCriterion.original);
26599
26678
  } else {
26600
- toPromote.push(criterion);
26679
+ toPromote.push(refinedCriterion.original);
26601
26680
  }
26602
26681
  }
26603
26682
  if (toPromote.length > 0) {
26604
- story.acceptanceCriteria = [...story.acceptanceCriteria, ...toPromote];
26683
+ const existingACs = new Set(story.acceptanceCriteria);
26684
+ story.acceptanceCriteria = [...story.acceptanceCriteria, ...toPromote.filter((ac) => !existingACs.has(ac))];
26605
26685
  result.promoted.push(...toPromote);
26606
26686
  }
26607
26687
  result.discarded.push(...toDiscard);
@@ -26666,6 +26746,24 @@ function parseTestFailures(output) {
26666
26746
  }
26667
26747
  }
26668
26748
  }
26749
+ if (line.includes("--- FAIL:")) {
26750
+ const acMatch = line.match(/AC[-_]?(\d+)/i);
26751
+ if (acMatch) {
26752
+ const acId = `AC-${acMatch[1]}`;
26753
+ if (!failedACs.includes(acId)) {
26754
+ failedACs.push(acId);
26755
+ }
26756
+ }
26757
+ }
26758
+ if (/FAILED\s/.test(line)) {
26759
+ const acMatch = line.match(/AC[-_]?(\d+)/i);
26760
+ if (acMatch) {
26761
+ const acId = `AC-${acMatch[1]}`;
26762
+ if (!failedACs.includes(acId)) {
26763
+ failedACs.push(acId);
26764
+ }
26765
+ }
26766
+ }
26669
26767
  }
26670
26768
  return failedACs;
26671
26769
  }
@@ -26917,7 +27015,7 @@ ${stderr}` };
26917
27015
  },
26918
27016
  refine: async (_criteria, _context) => {
26919
27017
  const { refineAcceptanceCriteria: refineAcceptanceCriteria2 } = await Promise.resolve().then(() => (init_refinement(), exports_refinement));
26920
- return refineAcceptanceCriteria2(_criteria, _context);
27018
+ return (await refineAcceptanceCriteria2(_criteria, _context)).criteria;
26921
27019
  },
26922
27020
  generate: async (_stories, _refined, _options) => {
26923
27021
  const { generateFromPRD: generateFromPRD2 } = await Promise.resolve().then(() => (init_generator(), exports_generator));
@@ -26937,29 +27035,9 @@ ${stderr}` };
26937
27035
  const testPathConfig = ctx.config.acceptance.testPath;
26938
27036
  const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
26939
27037
  const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
26940
- const nonFixStories = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed");
26941
- const workdirGroups = new Map;
26942
- for (const story of nonFixStories) {
26943
- const wd = story.workdir ?? "";
26944
- if (!workdirGroups.has(wd)) {
26945
- workdirGroups.set(wd, { stories: [], criteria: [] });
26946
- }
26947
- const group = workdirGroups.get(wd);
26948
- if (group) {
26949
- group.stories.push(story);
26950
- group.criteria.push(...story.acceptanceCriteria);
26951
- }
26952
- }
26953
- if (workdirGroups.size === 0) {
26954
- workdirGroups.set("", { stories: [], criteria: [] });
26955
- }
26956
27038
  const featureName = ctx.prd.feature ?? ctx.prd.featureName;
26957
- const testPaths = [];
26958
- for (const [workdir] of workdirGroups) {
26959
- const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
26960
- const testPath = resolveAcceptancePackageFeatureTestPath(packageDir, featureName, testPathConfig, language);
26961
- testPaths.push({ testPath, packageDir });
26962
- }
27039
+ const groups = groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
27040
+ const nonFixStories = groups.flatMap((g) => g.stories);
26963
27041
  let totalCriteria = 0;
26964
27042
  let testableCount = 0;
26965
27043
  const fingerprint = computeACFingerprint(allCriteria);
@@ -26980,7 +27058,7 @@ ${stderr}` };
26980
27058
  storedFingerprint: meta3.acFingerprint
26981
27059
  });
26982
27060
  }
26983
- for (const { testPath } of testPaths) {
27061
+ for (const { testPath } of groups) {
26984
27062
  if (await _acceptanceSetupDeps.fileExists(testPath)) {
26985
27063
  await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
26986
27064
  await _acceptanceSetupDeps.deleteFile(testPath);
@@ -27030,9 +27108,8 @@ ${stderr}` };
27030
27108
  })));
27031
27109
  }
27032
27110
  testableCount = allRefinedCriteria.filter((r) => r.testable).length;
27033
- for (const [workdir, group] of workdirGroups) {
27034
- const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
27035
- const testPath = resolveAcceptancePackageFeatureTestPath(packageDir, featureName, testPathConfig, language);
27111
+ for (const group of groups) {
27112
+ const { testPath, packageDir } = group;
27036
27113
  const groupStoryIds = new Set(group.stories.map((s) => s.id));
27037
27114
  const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
27038
27115
  let modelDef;
@@ -27067,13 +27144,13 @@ ${stderr}` };
27067
27144
  generator: "nax"
27068
27145
  });
27069
27146
  }
27070
- ctx.acceptanceTestPaths = testPaths;
27147
+ ctx.acceptanceTestPaths = groups.map((g) => ({ testPath: g.testPath, packageDir: g.packageDir }));
27071
27148
  if (ctx.config.acceptance.redGate === false) {
27072
27149
  ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
27073
27150
  return { action: "continue" };
27074
27151
  }
27075
27152
  let redFailCount = 0;
27076
- for (const { testPath, packageDir } of testPaths) {
27153
+ for (const { testPath, packageDir } of groups) {
27077
27154
  const runCmd = buildAcceptanceRunCommand(testPath, ctx.config.project?.testFramework, ctx.config.acceptance.command);
27078
27155
  getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
27079
27156
  cmd: runCmd.join(" "),
@@ -29152,7 +29229,6 @@ var CLARIFY_REGEX, autofixStage, _autofixDeps;
29152
29229
  var init_autofix = __esm(() => {
29153
29230
  init_registry();
29154
29231
  init_config();
29155
- init_loader();
29156
29232
  init_logger2();
29157
29233
  init_quality();
29158
29234
  init_event_bus();
@@ -29268,8 +29344,7 @@ var init_autofix = __esm(() => {
29268
29344
  getAgent: (name, config2) => createAgentRegistry(config2).getAgent(name),
29269
29345
  runQualityCommand,
29270
29346
  recheckReview,
29271
- runAgentRectification: (ctx, lintFixCmd, formatFixCmd, effectiveWorkdir) => runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWorkdir),
29272
- loadConfigForWorkdir
29347
+ runAgentRectification: (ctx, lintFixCmd, formatFixCmd, effectiveWorkdir) => runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWorkdir)
29273
29348
  };
29274
29349
  });
29275
29350
 
@@ -29398,17 +29473,17 @@ var init_completion = __esm(() => {
29398
29473
  logger.warn("completion", "Story marked for re-review", { storyId: completedStory.id });
29399
29474
  }
29400
29475
  }
29401
- }
29402
- const semanticCheck = ctx.reviewResult?.checks?.find((c) => c.check === "semantic");
29403
- if (ctx.featureDir && semanticCheck) {
29404
- const verdict = {
29405
- storyId: ctx.story.id,
29406
- passed: semanticCheck.success,
29407
- timestamp: new Date().toISOString(),
29408
- acCount: ctx.story.acceptanceCriteria?.length ?? 0,
29409
- findings: semanticCheck.success ? [] : semanticCheck.findings ?? []
29410
- };
29411
- await _completionDeps.persistSemanticVerdict(ctx.featureDir, ctx.story.id, verdict);
29476
+ const semanticCheck = ctx.reviewResult?.checks?.find((c) => c.check === "semantic");
29477
+ if (ctx.featureDir && semanticCheck) {
29478
+ const verdict = {
29479
+ storyId: completedStory.id,
29480
+ passed: semanticCheck.success,
29481
+ timestamp: new Date().toISOString(),
29482
+ acCount: completedStory.acceptanceCriteria?.length ?? 0,
29483
+ findings: semanticCheck.success ? [] : semanticCheck.findings ?? []
29484
+ };
29485
+ await _completionDeps.persistSemanticVerdict(ctx.featureDir, completedStory.id, verdict);
29486
+ }
29412
29487
  }
29413
29488
  await _completionDeps.savePRD(ctx.prd, prdPath);
29414
29489
  const updatedCounts = countStories(ctx.prd);
@@ -36572,7 +36647,7 @@ var package_default;
36572
36647
  var init_package = __esm(() => {
36573
36648
  package_default = {
36574
36649
  name: "@nathapp/nax",
36575
- version: "0.60.2",
36650
+ version: "0.61.0",
36576
36651
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
36577
36652
  type: "module",
36578
36653
  bin: {
@@ -36652,8 +36727,8 @@ var init_version = __esm(() => {
36652
36727
  NAX_VERSION = package_default.version;
36653
36728
  NAX_COMMIT = (() => {
36654
36729
  try {
36655
- if (/^[0-9a-f]{6,10}$/.test("db851936"))
36656
- return "db851936";
36730
+ if (/^[0-9a-f]{6,10}$/.test("de7efdbe"))
36731
+ return "de7efdbe";
36657
36732
  } catch {}
36658
36733
  try {
36659
36734
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -39060,7 +39135,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
39060
39135
  }
39061
39136
  }
39062
39137
  const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
39063
- const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join45(ctx.workdir, ".nax", "config.json"), story.workdir) : ctx.config;
39138
+ const profileOverride = ctx.config.profile && ctx.config.profile !== "default" ? { profile: ctx.config.profile } : undefined;
39139
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join45(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
39064
39140
  const pipelineContext = {
39065
39141
  config: effectiveConfig,
39066
39142
  rootConfig: ctx.config,
@@ -39863,9 +39939,10 @@ async function runParallelBatch(options) {
39863
39939
  worktreePaths.set(story.id, path17.join(workdir, ".nax-wt", story.id));
39864
39940
  }
39865
39941
  const rootConfigPath = path17.join(workdir, ".nax", "config.json");
39942
+ const profileOverride = config2.profile && config2.profile !== "default" ? { profile: config2.profile } : undefined;
39866
39943
  const storyEffectiveConfigs = new Map;
39867
39944
  await Promise.all(stories.filter((story) => story.workdir).map(async (story) => {
39868
- const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir);
39945
+ const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir, profileOverride);
39869
39946
  storyEffectiveConfigs.set(story.id, effectiveConfig);
39870
39947
  }));
39871
39948
  const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs.size > 0 ? storyEffectiveConfigs : undefined);
@@ -73387,6 +73464,9 @@ ${packageDetailsSection}
73387
73464
  For each user story, set the "workdir" field to the relevant package path (e.g. "packages/api"). Stories that span the root should omit "workdir".` : "";
73388
73465
  const workdirField = isMonorepo ? `
73389
73466
  "workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
73467
+ const specAnchorSection = specContent.trim() ? `
73468
+
73469
+ ${SPEC_ANCHOR_RULES}` : "";
73390
73470
  const taskContext = `You are a senior software architect generating a product requirements document (PRD) as JSON.
73391
73471
 
73392
73472
  ## Step 1: Understand the Spec
@@ -73427,7 +73507,7 @@ Based on your Step 2 analysis, create stories that produce CODE CHANGES.
73427
73507
 
73428
73508
  ${GROUPING_RULES}
73429
73509
 
73430
- ${getAcQualityRules(projectProfile)}
73510
+ ${getAcQualityRules(projectProfile)}${specAnchorSection}
73431
73511
 
73432
73512
  For each story, set "contextFiles" to the key source files the agent should read before implementing (max 5 per story). Use your Step 2 analysis to identify the most relevant files. Leave empty for greenfield stories with no existing files to reference.
73433
73513
 
@@ -73450,7 +73530,8 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
73450
73530
  "id": "string \u2014 e.g. US-001",
73451
73531
  "title": "string \u2014 concise story title",
73452
73532
  "description": "string \u2014 detailed description of the story",
73453
- "acceptanceCriteria": ["string \u2014 behavioral, testable criteria. Format: 'When [X], then [Y]'. One assertion per AC. Never include quality gates."],
73533
+ "acceptanceCriteria": ["string \u2014 behavioral, testable criteria. Format: 'When [X], then [Y]'. One assertion per AC. Never include quality gates."],${specContent.trim() ? `
73534
+ "suggestedCriteria": ["string \u2014 optional. Behavioral edge cases or negative paths you identified that are NOT in the spec. Plain assertions only \u2014 observable outputs, return values, state changes, or error conditions. No implementation details or vague descriptions. Omit this field if empty."],` : ""}
73454
73535
  "contextFiles": ["string \u2014 key source files the agent should read (max 5, relative paths)"],
73455
73536
  "tags": ["string \u2014 routing tags, e.g. feature, security, api"],
73456
73537
  "dependencies": ["string \u2014 story IDs this story depends on"],${workdirField}
@@ -76350,6 +76431,7 @@ init_version();
76350
76431
  init_crash_recovery();
76351
76432
 
76352
76433
  // src/execution/runner-completion.ts
76434
+ init_test_path();
76353
76435
  init_hooks();
76354
76436
  init_logger2();
76355
76437
  init_prd();
@@ -76382,6 +76464,7 @@ async function runCompletionPhase(options) {
76382
76464
  logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
76383
76465
  } else if (options.config.acceptance.enabled && isComplete(options.prd)) {
76384
76466
  options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
76467
+ const acceptanceTestPaths = options.featureDir ? groupStoriesByPackage(options.prd, options.workdir, options.feature, options.config.acceptance.testPath, options.config.project?.language).map((g) => ({ testPath: g.testPath, packageDir: g.packageDir })) : undefined;
76385
76468
  const acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
76386
76469
  config: options.config,
76387
76470
  prd: options.prd,
@@ -76397,7 +76480,8 @@ async function runCompletionPhase(options) {
76397
76480
  pluginRegistry: options.pluginRegistry,
76398
76481
  eventEmitter: options.eventEmitter,
76399
76482
  statusWriter: options.statusWriter,
76400
- agentGetFn: options.agentGetFn
76483
+ agentGetFn: options.agentGetFn,
76484
+ acceptanceTestPaths
76401
76485
  });
76402
76486
  const lastRunAt = new Date().toISOString();
76403
76487
  if (acceptanceResult.success) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.60.2",
3
+ "version": "0.61.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {