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