@nathapp/nax 0.60.2 → 0.61.1

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 +178 -84
  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.
@@ -18063,6 +18070,7 @@ function isLegacyFlatModels(val) {
18063
18070
  var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewDialogueConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PromptAuditConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterPersonaEnum, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
18064
18071
  type: exports_external.enum(RESOLVER_TYPES).default(defaultType),
18065
18072
  agent: exports_external.string().min(1).optional(),
18073
+ model: exports_external.string().min(1).optional(),
18066
18074
  tieBreaker: exports_external.string().min(1).optional(),
18067
18075
  maxPromptTokens: exports_external.number().int().positive().optional()
18068
18076
  })), DebateStageConfigSchema = (defaults) => exports_external.preprocess(toObject, exports_external.object({
@@ -18308,7 +18316,8 @@ var init_schemas3 = __esm(() => {
18308
18316
  PlanConfigSchema = exports_external.object({
18309
18317
  model: ModelTierSchema,
18310
18318
  outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty"),
18311
- timeoutSeconds: exports_external.number().int().positive().default(600)
18319
+ timeoutSeconds: exports_external.number().int().positive().default(600),
18320
+ decomposeTimeoutSeconds: exports_external.number().int().min(30).max(1800).optional()
18312
18321
  });
18313
18322
  AcceptanceFixConfigSchema = exports_external.object({
18314
18323
  diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty").default("fast"),
@@ -19544,7 +19553,8 @@ class AcpAgentAdapter {
19544
19553
  workdir: options.workdir,
19545
19554
  featureName: options.featureName,
19546
19555
  storyId: options.storyId,
19547
- sessionRole: options.sessionRole ?? "decompose"
19556
+ sessionRole: options.sessionRole ?? "decompose",
19557
+ timeoutMs: (this.naxConfig?.plan?.decomposeTimeoutSeconds ?? this.naxConfig?.plan?.timeoutSeconds ?? 300) * 1000
19548
19558
  });
19549
19559
  output = completeResult.output;
19550
19560
  } catch (err) {
@@ -21315,14 +21325,16 @@ ${errors3.join(`
21315
21325
  }
21316
21326
  return result.data;
21317
21327
  }
21318
- async function loadConfigForWorkdir(rootConfigPath, packageDir) {
21328
+ async function loadConfigForWorkdir(rootConfigPath, packageDir, cliOverrides) {
21319
21329
  const logger = getLogger();
21320
21330
  const resolvedRootConfigPath = resolve5(rootConfigPath);
21321
21331
  const rootNaxDir = dirname2(resolvedRootConfigPath);
21322
- let rootConfigPromise = _rootConfigCache.get(resolvedRootConfigPath);
21332
+ const profileKey = cliOverrides?.profile ?? "";
21333
+ const cacheKey = profileKey ? `${resolvedRootConfigPath}:${profileKey}` : resolvedRootConfigPath;
21334
+ let rootConfigPromise = _rootConfigCache.get(cacheKey);
21323
21335
  if (!rootConfigPromise) {
21324
- rootConfigPromise = loadConfig(rootNaxDir);
21325
- _rootConfigCache.set(resolvedRootConfigPath, rootConfigPromise);
21336
+ rootConfigPromise = loadConfig(rootNaxDir, cliOverrides);
21337
+ _rootConfigCache.set(cacheKey, rootConfigPromise);
21326
21338
  }
21327
21339
  const rootConfig = await rootConfigPromise;
21328
21340
  if (!packageDir) {
@@ -21683,12 +21695,16 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21683
21695
  const adapter = _debateSessionDeps.getAgent(agentName, config2);
21684
21696
  if (adapter) {
21685
21697
  const synthesisSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "synthesis") : undefined;
21698
+ const resolverDebater = { agent: agentName, model: resolverConfig.model };
21699
+ const resolverTier = resolverConfig.model && MODEL_SHORTHAND_TIERS[resolverConfig.model.toLowerCase()] || modelTierFromDebater(resolverDebater);
21700
+ const resolverModelDef = resolveModelDefForDebater(resolverDebater, resolverTier, config2);
21686
21701
  const resolverResult = await synthesisResolver(proposalOutputs, critiqueOutputs, {
21687
21702
  adapter,
21688
21703
  promptSuffix,
21689
21704
  debaters,
21690
21705
  completeOptions: {
21691
- model: resolveDebaterModel({ agent: agentName }, config2),
21706
+ model: resolverModelDef.model,
21707
+ modelTier: resolverTier,
21692
21708
  config: config2,
21693
21709
  storyId,
21694
21710
  featureName,
@@ -21709,12 +21725,16 @@ async function resolveOutcome(proposalOutputs, critiqueOutputs, stageConfig, con
21709
21725
  if (resolverConfig.type === "custom") {
21710
21726
  const agentName = resolverConfig.agent ?? RESOLVER_FALLBACK_AGENT;
21711
21727
  const judgeSessionName = workdir !== undefined ? buildSessionName(workdir, featureName, storyId, "judge") : undefined;
21728
+ const resolverDebater = { agent: agentName, model: resolverConfig.model };
21729
+ const resolverTier = resolverConfig.model && MODEL_SHORTHAND_TIERS[resolverConfig.model.toLowerCase()] || modelTierFromDebater(resolverDebater);
21730
+ const resolverModelDef = resolveModelDefForDebater(resolverDebater, resolverTier, config2);
21712
21731
  const resolverResult = await judgeResolver(proposalOutputs, critiqueOutputs, resolverConfig, {
21713
21732
  getAgent: (name) => _debateSessionDeps.getAgent(name, config2),
21714
21733
  defaultAgentName: RESOLVER_FALLBACK_AGENT,
21715
21734
  debaters,
21716
21735
  completeOptions: {
21717
- model: resolveDebaterModel({ agent: agentName }, config2),
21736
+ model: resolverModelDef.model,
21737
+ modelTier: resolverTier,
21718
21738
  config: config2,
21719
21739
  storyId,
21720
21740
  featureName,
@@ -22705,9 +22725,11 @@ ${opts.specContent}
22705
22725
 
22706
22726
  The spec above is the authoritative source for acceptance criteria.
22707
22727
  - 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.
22710
- - Preserve the spec's AC wording. You may refine for clarity but must not change semantics.` : "";
22728
+ - 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.
22729
+ - \`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.
22730
+ - Never silently merge debater-invented criteria into \`acceptanceCriteria\`. The distinction matters: \`acceptanceCriteria\` drives automated testing; \`suggestedCriteria\` gates a hardening pass.
22731
+ - Preserve the spec's AC wording. You may refine for clarity but must not change semantics.
22732
+ - Preserve each story's \`routing\` object unchanged \u2014 especially \`routing.complexity\` and \`routing.testStrategy\`. These are required by the schema and must not be dropped or modified during synthesis.` : "";
22711
22733
  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
22734
  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));
22713
22735
  const winningOutput = outcome.output ?? successful[0].output;
@@ -25071,6 +25093,31 @@ function resolveAcceptanceTestCandidates(options) {
25071
25093
  return [];
25072
25094
  return [resolveAcceptanceFeatureTestPath(options.featureDir, options.testPathConfig, options.language)];
25073
25095
  }
25096
+ function groupStoriesByPackage(prd, workdir, featureName, testPathConfig, language) {
25097
+ const nonFixStories = prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed");
25098
+ const groupMap = new Map;
25099
+ for (const story of nonFixStories) {
25100
+ const wd = story.workdir ?? "";
25101
+ if (!groupMap.has(wd)) {
25102
+ groupMap.set(wd, { stories: [], criteria: [] });
25103
+ }
25104
+ const group = groupMap.get(wd);
25105
+ if (group) {
25106
+ group.stories.push(story);
25107
+ group.criteria.push(...story.acceptanceCriteria);
25108
+ }
25109
+ }
25110
+ if (groupMap.size === 0) {
25111
+ groupMap.set("", { stories: [], criteria: [] });
25112
+ }
25113
+ const groups = [];
25114
+ for (const [wd, { stories, criteria }] of groupMap) {
25115
+ const packageDir = wd ? path.join(workdir, wd) : workdir;
25116
+ const testPath = resolveAcceptancePackageFeatureTestPath(packageDir, featureName, testPathConfig, language);
25117
+ groups.push({ testPath, packageDir, stories, criteria });
25118
+ }
25119
+ return groups;
25120
+ }
25074
25121
  function suggestedTestFilename(language) {
25075
25122
  switch (language?.toLowerCase()) {
25076
25123
  case "go":
@@ -25142,6 +25189,9 @@ async function loadPRD(path2) {
25142
25189
  story.status = "passed";
25143
25190
  story.status = story.status ?? "pending";
25144
25191
  story.acceptanceCriteria = story.acceptanceCriteria ?? [];
25192
+ if (Array.isArray(story.suggestedCriteria) && story.suggestedCriteria.length === 0) {
25193
+ story.suggestedCriteria = undefined;
25194
+ }
25145
25195
  story.storyPoints = story.storyPoints ?? 1;
25146
25196
  }
25147
25197
  return prd;
@@ -25980,6 +26030,7 @@ Previous test failed because: ${options.previousFailure}` : "";
25980
26030
  featureName: options.featureName,
25981
26031
  sessionRole: "acceptance-gen"
25982
26032
  });
26033
+ const genCostUsd = typeof completeResult === "string" ? 0 : completeResult.costUsd ?? 0;
25983
26034
  const rawOutput = typeof completeResult === "string" ? completeResult : completeResult.output;
25984
26035
  let testCode = extractTestCode(rawOutput);
25985
26036
  logger.debug("acceptance", "Received raw output from LLM", {
@@ -26077,7 +26128,8 @@ Previous test failed because: ${options.previousFailure}` : "";
26077
26128
  }));
26078
26129
  return {
26079
26130
  testCode: generateSkeletonTests(options.featureName, skeletonCriteria, options.testFramework, options.language),
26080
- criteria: skeletonCriteria
26131
+ criteria: skeletonCriteria,
26132
+ costUsd: genCostUsd
26081
26133
  };
26082
26134
  }
26083
26135
  const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
@@ -26088,7 +26140,7 @@ Previous test failed because: ${options.previousFailure}` : "";
26088
26140
  storyId: c.storyId
26089
26141
  })), null, 2);
26090
26142
  await _generatorPRDDeps.writeFile(join16(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
26091
- return { testCode, criteria };
26143
+ return { testCode, criteria, costUsd: genCostUsd };
26092
26144
  }
26093
26145
  function parseAcceptanceCriteria(specContent) {
26094
26146
  const criteria = [];
@@ -26366,12 +26418,22 @@ function buildRefinementPrompt(criteria, codebaseContext, options) {
26366
26418
  `);
26367
26419
  const strategySection = buildStrategySection(options);
26368
26420
  const refinedExample = buildRefinedExample(options?.testStrategy);
26421
+ const storyLines = [];
26422
+ if (options?.storyTitle)
26423
+ storyLines.push(`Title: ${options.storyTitle}`);
26424
+ if (options?.storyDescription)
26425
+ storyLines.push(`Description: ${options.storyDescription}`);
26426
+ const storySection = storyLines.length > 0 ? `STORY CONTEXT:
26427
+ ${storyLines.join(`
26428
+ `)}
26429
+
26430
+ ` : "";
26369
26431
  const codebaseSection = codebaseContext ? `CODEBASE CONTEXT:
26370
26432
  ${codebaseContext}
26371
26433
  ` : "";
26372
26434
  const core2 = `You are an acceptance criteria refinement assistant. Your task is to convert raw acceptance criteria into concrete, machine-verifiable assertions.
26373
26435
 
26374
- ${codebaseSection}${strategySection}ACCEPTANCE CRITERIA TO REFINE:
26436
+ ${storySection}${codebaseSection}${strategySection}ACCEPTANCE CRITERIA TO REFINE:
26375
26437
  ${criteriaList}
26376
26438
 
26377
26439
  For each criterion, produce a refined version that is concrete and automatically testable where possible.
@@ -26455,13 +26517,28 @@ function parseRefinementResponse(response, criteria) {
26455
26517
  }
26456
26518
  async function refineAcceptanceCriteria(criteria, context) {
26457
26519
  if (criteria.length === 0) {
26458
- return [];
26520
+ return { criteria: [], costUsd: 0 };
26459
26521
  }
26460
- const { storyId, featureName, workdir, codebaseContext, config: config2, testStrategy, testFramework } = context;
26522
+ const {
26523
+ storyId,
26524
+ featureName,
26525
+ workdir,
26526
+ codebaseContext,
26527
+ config: config2,
26528
+ testStrategy,
26529
+ testFramework,
26530
+ storyTitle,
26531
+ storyDescription
26532
+ } = context;
26461
26533
  const logger = getLogger();
26462
26534
  const modelTier = config2.acceptance?.model ?? "fast";
26463
26535
  const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, modelTier, config2.autoMode.defaultAgent);
26464
- const prompt = buildRefinementPrompt(criteria, codebaseContext, { testStrategy, testFramework });
26536
+ const prompt = buildRefinementPrompt(criteria, codebaseContext, {
26537
+ testStrategy,
26538
+ testFramework,
26539
+ storyTitle,
26540
+ storyDescription
26541
+ });
26465
26542
  let response;
26466
26543
  try {
26467
26544
  const completeResult = await _refineDeps.adapter.complete(prompt, {
@@ -26475,20 +26552,21 @@ async function refineAcceptanceCriteria(criteria, context) {
26475
26552
  sessionRole: "refine",
26476
26553
  timeoutMs: config2.acceptance?.timeoutMs ?? 120000
26477
26554
  });
26555
+ const costUsd = typeof completeResult === "string" ? 0 : completeResult.costUsd ?? 0;
26478
26556
  response = typeof completeResult === "string" ? completeResult : completeResult.output;
26557
+ const parsed = parseRefinementResponse(response, criteria);
26558
+ return {
26559
+ criteria: parsed.map((item) => ({ ...item, storyId: item.storyId || storyId })),
26560
+ costUsd
26561
+ };
26479
26562
  } catch (error48) {
26480
26563
  const reason = errorMessage(error48);
26481
26564
  logger.warn("refinement", "adapter.complete() failed, falling back to original criteria", {
26482
26565
  storyId,
26483
26566
  error: reason
26484
26567
  });
26485
- return fallbackCriteria(criteria, storyId);
26568
+ return { criteria: fallbackCriteria(criteria, storyId), costUsd: 0 };
26486
26569
  }
26487
- const parsed = parseRefinementResponse(response, criteria);
26488
- return parsed.map((item) => ({
26489
- ...item,
26490
- storyId: item.storyId || storyId
26491
- }));
26492
26570
  }
26493
26571
  function fallbackCriteria(criteria, storyId = "") {
26494
26572
  return criteria.map((c) => ({
@@ -26540,14 +26618,17 @@ async function runHardeningPass(ctx) {
26540
26618
  const allRefined = [];
26541
26619
  for (const story of storiesWithSuggested) {
26542
26620
  const criteria = story.suggestedCriteria ?? [];
26543
- const refined = await _hardeningDeps.refine(criteria, {
26621
+ const refineResult = await _hardeningDeps.refine(criteria, {
26544
26622
  storyId: story.id,
26545
26623
  featureName: ctx.prd.feature,
26546
26624
  workdir: ctx.workdir,
26547
26625
  codebaseContext: "",
26548
- config: ctx.config
26626
+ config: ctx.config,
26627
+ storyTitle: story.title,
26628
+ storyDescription: story.description
26549
26629
  });
26550
- allRefined.push(...refined);
26630
+ allRefined.push(...refineResult.criteria);
26631
+ result.costUsd += refineResult.costUsd;
26551
26632
  }
26552
26633
  const language = ctx.config.project?.language;
26553
26634
  const suggestedTestPath = resolveSuggestedPackageFeatureTestPath(ctx.workdir, ctx.prd.feature, ctx.config.acceptance?.suggestedTestPath, language);
@@ -26568,6 +26649,7 @@ async function runHardeningPass(ctx) {
26568
26649
  language,
26569
26650
  targetTestFile: suggestedTestPath
26570
26651
  });
26652
+ result.costUsd += genResult.costUsd ?? 0;
26571
26653
  if (genResult.testCode) {
26572
26654
  await _hardeningDeps.writeFile(suggestedTestPath, genResult.testCode);
26573
26655
  }
@@ -26586,22 +26668,30 @@ async function runHardeningPass(ctx) {
26586
26668
  ${stderr}`;
26587
26669
  const failedACs = parseTestFailures(output);
26588
26670
  const failedSet = new Set(failedACs.map((ac) => ac.toUpperCase()));
26671
+ const refinedByStory = new Map;
26672
+ for (const r of allRefined) {
26673
+ const list = refinedByStory.get(r.storyId) ?? [];
26674
+ list.push(r);
26675
+ refinedByStory.set(r.storyId, list);
26676
+ }
26589
26677
  let acIndex = 0;
26590
26678
  for (const story of storiesWithSuggested) {
26591
- const suggested = story.suggestedCriteria ?? [];
26679
+ const storyRefined = refinedByStory.get(story.id) ?? [];
26592
26680
  const toPromote = [];
26593
26681
  const toDiscard = [];
26594
- for (const criterion of suggested) {
26682
+ for (const refinedCriterion of storyRefined) {
26595
26683
  acIndex++;
26596
26684
  const acId = `AC-${acIndex}`;
26597
- if (failedSet.has(acId) || exitCode !== 0 && failedACs.length === 0) {
26598
- toDiscard.push(criterion);
26685
+ const nonTestable = refinedCriterion.testable === false;
26686
+ if (nonTestable || failedSet.has(acId) || exitCode !== 0 && failedACs.length === 0) {
26687
+ toDiscard.push(refinedCriterion.original);
26599
26688
  } else {
26600
- toPromote.push(criterion);
26689
+ toPromote.push(refinedCriterion.original);
26601
26690
  }
26602
26691
  }
26603
26692
  if (toPromote.length > 0) {
26604
- story.acceptanceCriteria = [...story.acceptanceCriteria, ...toPromote];
26693
+ const existingACs = new Set(story.acceptanceCriteria);
26694
+ story.acceptanceCriteria = [...story.acceptanceCriteria, ...toPromote.filter((ac) => !existingACs.has(ac))];
26605
26695
  result.promoted.push(...toPromote);
26606
26696
  }
26607
26697
  result.discarded.push(...toDiscard);
@@ -26666,6 +26756,24 @@ function parseTestFailures(output) {
26666
26756
  }
26667
26757
  }
26668
26758
  }
26759
+ if (line.includes("--- FAIL:")) {
26760
+ const acMatch = line.match(/AC[-_]?(\d+)/i);
26761
+ if (acMatch) {
26762
+ const acId = `AC-${acMatch[1]}`;
26763
+ if (!failedACs.includes(acId)) {
26764
+ failedACs.push(acId);
26765
+ }
26766
+ }
26767
+ }
26768
+ if (/FAILED\s/.test(line)) {
26769
+ const acMatch = line.match(/AC[-_]?(\d+)/i);
26770
+ if (acMatch) {
26771
+ const acId = `AC-${acMatch[1]}`;
26772
+ if (!failedACs.includes(acId)) {
26773
+ failedACs.push(acId);
26774
+ }
26775
+ }
26776
+ }
26669
26777
  }
26670
26778
  return failedACs;
26671
26779
  }
@@ -26917,7 +27025,7 @@ ${stderr}` };
26917
27025
  },
26918
27026
  refine: async (_criteria, _context) => {
26919
27027
  const { refineAcceptanceCriteria: refineAcceptanceCriteria2 } = await Promise.resolve().then(() => (init_refinement(), exports_refinement));
26920
- return refineAcceptanceCriteria2(_criteria, _context);
27028
+ return (await refineAcceptanceCriteria2(_criteria, _context)).criteria;
26921
27029
  },
26922
27030
  generate: async (_stories, _refined, _options) => {
26923
27031
  const { generateFromPRD: generateFromPRD2 } = await Promise.resolve().then(() => (init_generator(), exports_generator));
@@ -26937,29 +27045,9 @@ ${stderr}` };
26937
27045
  const testPathConfig = ctx.config.acceptance.testPath;
26938
27046
  const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
26939
27047
  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
27048
  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
- }
27049
+ const groups = groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
27050
+ const nonFixStories = groups.flatMap((g) => g.stories);
26963
27051
  let totalCriteria = 0;
26964
27052
  let testableCount = 0;
26965
27053
  const fingerprint = computeACFingerprint(allCriteria);
@@ -26980,7 +27068,7 @@ ${stderr}` };
26980
27068
  storedFingerprint: meta3.acFingerprint
26981
27069
  });
26982
27070
  }
26983
- for (const { testPath } of testPaths) {
27071
+ for (const { testPath } of groups) {
26984
27072
  if (await _acceptanceSetupDeps.fileExists(testPath)) {
26985
27073
  await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
26986
27074
  await _acceptanceSetupDeps.deleteFile(testPath);
@@ -27030,9 +27118,8 @@ ${stderr}` };
27030
27118
  })));
27031
27119
  }
27032
27120
  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);
27121
+ for (const group of groups) {
27122
+ const { testPath, packageDir } = group;
27036
27123
  const groupStoryIds = new Set(group.stories.map((s) => s.id));
27037
27124
  const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
27038
27125
  let modelDef;
@@ -27067,13 +27154,13 @@ ${stderr}` };
27067
27154
  generator: "nax"
27068
27155
  });
27069
27156
  }
27070
- ctx.acceptanceTestPaths = testPaths;
27157
+ ctx.acceptanceTestPaths = groups.map((g) => ({ testPath: g.testPath, packageDir: g.packageDir }));
27071
27158
  if (ctx.config.acceptance.redGate === false) {
27072
27159
  ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
27073
27160
  return { action: "continue" };
27074
27161
  }
27075
27162
  let redFailCount = 0;
27076
- for (const { testPath, packageDir } of testPaths) {
27163
+ for (const { testPath, packageDir } of groups) {
27077
27164
  const runCmd = buildAcceptanceRunCommand(testPath, ctx.config.project?.testFramework, ctx.config.acceptance.command);
27078
27165
  getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
27079
27166
  cmd: runCmd.join(" "),
@@ -29152,7 +29239,6 @@ var CLARIFY_REGEX, autofixStage, _autofixDeps;
29152
29239
  var init_autofix = __esm(() => {
29153
29240
  init_registry();
29154
29241
  init_config();
29155
- init_loader();
29156
29242
  init_logger2();
29157
29243
  init_quality();
29158
29244
  init_event_bus();
@@ -29268,8 +29354,7 @@ var init_autofix = __esm(() => {
29268
29354
  getAgent: (name, config2) => createAgentRegistry(config2).getAgent(name),
29269
29355
  runQualityCommand,
29270
29356
  recheckReview,
29271
- runAgentRectification: (ctx, lintFixCmd, formatFixCmd, effectiveWorkdir) => runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWorkdir),
29272
- loadConfigForWorkdir
29357
+ runAgentRectification: (ctx, lintFixCmd, formatFixCmd, effectiveWorkdir) => runAgentRectification(ctx, lintFixCmd, formatFixCmd, effectiveWorkdir)
29273
29358
  };
29274
29359
  });
29275
29360
 
@@ -29398,17 +29483,17 @@ var init_completion = __esm(() => {
29398
29483
  logger.warn("completion", "Story marked for re-review", { storyId: completedStory.id });
29399
29484
  }
29400
29485
  }
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);
29486
+ const semanticCheck = ctx.reviewResult?.checks?.find((c) => c.check === "semantic");
29487
+ if (ctx.featureDir && semanticCheck) {
29488
+ const verdict = {
29489
+ storyId: completedStory.id,
29490
+ passed: semanticCheck.success,
29491
+ timestamp: new Date().toISOString(),
29492
+ acCount: completedStory.acceptanceCriteria?.length ?? 0,
29493
+ findings: semanticCheck.success ? [] : semanticCheck.findings ?? []
29494
+ };
29495
+ await _completionDeps.persistSemanticVerdict(ctx.featureDir, completedStory.id, verdict);
29496
+ }
29412
29497
  }
29413
29498
  await _completionDeps.savePRD(ctx.prd, prdPath);
29414
29499
  const updatedCounts = countStories(ctx.prd);
@@ -36572,7 +36657,7 @@ var package_default;
36572
36657
  var init_package = __esm(() => {
36573
36658
  package_default = {
36574
36659
  name: "@nathapp/nax",
36575
- version: "0.60.2",
36660
+ version: "0.61.1",
36576
36661
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
36577
36662
  type: "module",
36578
36663
  bin: {
@@ -36652,8 +36737,8 @@ var init_version = __esm(() => {
36652
36737
  NAX_VERSION = package_default.version;
36653
36738
  NAX_COMMIT = (() => {
36654
36739
  try {
36655
- if (/^[0-9a-f]{6,10}$/.test("db851936"))
36656
- return "db851936";
36740
+ if (/^[0-9a-f]{6,10}$/.test("a11d3b57"))
36741
+ return "a11d3b57";
36657
36742
  } catch {}
36658
36743
  try {
36659
36744
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -39060,7 +39145,8 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
39060
39145
  }
39061
39146
  }
39062
39147
  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;
39148
+ const profileOverride = ctx.config.profile && ctx.config.profile !== "default" ? { profile: ctx.config.profile } : undefined;
39149
+ const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join45(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
39064
39150
  const pipelineContext = {
39065
39151
  config: effectiveConfig,
39066
39152
  rootConfig: ctx.config,
@@ -39863,9 +39949,10 @@ async function runParallelBatch(options) {
39863
39949
  worktreePaths.set(story.id, path17.join(workdir, ".nax-wt", story.id));
39864
39950
  }
39865
39951
  const rootConfigPath = path17.join(workdir, ".nax", "config.json");
39952
+ const profileOverride = config2.profile && config2.profile !== "default" ? { profile: config2.profile } : undefined;
39866
39953
  const storyEffectiveConfigs = new Map;
39867
39954
  await Promise.all(stories.filter((story) => story.workdir).map(async (story) => {
39868
- const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir);
39955
+ const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir, profileOverride);
39869
39956
  storyEffectiveConfigs.set(story.id, effectiveConfig);
39870
39957
  }));
39871
39958
  const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs.size > 0 ? storyEffectiveConfigs : undefined);
@@ -73387,6 +73474,9 @@ ${packageDetailsSection}
73387
73474
  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
73475
  const workdirField = isMonorepo ? `
73389
73476
  "workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
73477
+ const specAnchorSection = specContent.trim() ? `
73478
+
73479
+ ${SPEC_ANCHOR_RULES}` : "";
73390
73480
  const taskContext = `You are a senior software architect generating a product requirements document (PRD) as JSON.
73391
73481
 
73392
73482
  ## Step 1: Understand the Spec
@@ -73427,7 +73517,7 @@ Based on your Step 2 analysis, create stories that produce CODE CHANGES.
73427
73517
 
73428
73518
  ${GROUPING_RULES}
73429
73519
 
73430
- ${getAcQualityRules(projectProfile)}
73520
+ ${getAcQualityRules(projectProfile)}${specAnchorSection}
73431
73521
 
73432
73522
  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
73523
 
@@ -73450,7 +73540,8 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
73450
73540
  "id": "string \u2014 e.g. US-001",
73451
73541
  "title": "string \u2014 concise story title",
73452
73542
  "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."],
73543
+ "acceptanceCriteria": ["string \u2014 behavioral, testable criteria. Format: 'When [X], then [Y]'. One assertion per AC. Never include quality gates."],${specContent.trim() ? `
73544
+ "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
73545
  "contextFiles": ["string \u2014 key source files the agent should read (max 5, relative paths)"],
73455
73546
  "tags": ["string \u2014 routing tags, e.g. feature, security, api"],
73456
73547
  "dependencies": ["string \u2014 story IDs this story depends on"],${workdirField}
@@ -76350,6 +76441,7 @@ init_version();
76350
76441
  init_crash_recovery();
76351
76442
 
76352
76443
  // src/execution/runner-completion.ts
76444
+ init_test_path();
76353
76445
  init_hooks();
76354
76446
  init_logger2();
76355
76447
  init_prd();
@@ -76382,6 +76474,7 @@ async function runCompletionPhase(options) {
76382
76474
  logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
76383
76475
  } else if (options.config.acceptance.enabled && isComplete(options.prd)) {
76384
76476
  options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
76477
+ 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
76478
  const acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
76386
76479
  config: options.config,
76387
76480
  prd: options.prd,
@@ -76397,7 +76490,8 @@ async function runCompletionPhase(options) {
76397
76490
  pluginRegistry: options.pluginRegistry,
76398
76491
  eventEmitter: options.eventEmitter,
76399
76492
  statusWriter: options.statusWriter,
76400
- agentGetFn: options.agentGetFn
76493
+ agentGetFn: options.agentGetFn,
76494
+ acceptanceTestPaths
76401
76495
  });
76402
76496
  const lastRunAt = new Date().toISOString();
76403
76497
  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.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {