@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.
- package/dist/nax.js +165 -81
- 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,
|
|
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
|
-
|
|
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(
|
|
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
|
|
22709
|
-
-
|
|
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 {
|
|
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, {
|
|
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
|
|
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(...
|
|
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
|
|
26669
|
+
const storyRefined = refinedByStory.get(story.id) ?? [];
|
|
26592
26670
|
const toPromote = [];
|
|
26593
26671
|
const toDiscard = [];
|
|
26594
|
-
for (const
|
|
26672
|
+
for (const refinedCriterion of storyRefined) {
|
|
26595
26673
|
acIndex++;
|
|
26596
26674
|
const acId = `AC-${acIndex}`;
|
|
26597
|
-
|
|
26598
|
-
|
|
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(
|
|
26679
|
+
toPromote.push(refinedCriterion.original);
|
|
26601
26680
|
}
|
|
26602
26681
|
}
|
|
26603
26682
|
if (toPromote.length > 0) {
|
|
26604
|
-
|
|
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
|
|
26958
|
-
|
|
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
|
|
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
|
|
27034
|
-
const
|
|
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 =
|
|
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
|
|
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
|
-
|
|
29403
|
-
|
|
29404
|
-
|
|
29405
|
-
|
|
29406
|
-
|
|
29407
|
-
|
|
29408
|
-
|
|
29409
|
-
|
|
29410
|
-
|
|
29411
|
-
|
|
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.
|
|
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("
|
|
36656
|
-
return "
|
|
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
|
|
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) {
|