@nathapp/nax 0.60.0-canary.1 → 0.60.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 +501 -290
- package/package.json +1 -1
package/dist/nax.js
CHANGED
|
@@ -18334,7 +18334,11 @@ var init_schemas3 = __esm(() => {
|
|
|
18334
18334
|
fixModel: "balanced",
|
|
18335
18335
|
strategy: "diagnose-first",
|
|
18336
18336
|
maxRetries: 2
|
|
18337
|
-
})
|
|
18337
|
+
}),
|
|
18338
|
+
suggestedTestPath: exports_external.string().min(1).optional(),
|
|
18339
|
+
hardening: exports_external.object({
|
|
18340
|
+
enabled: exports_external.boolean().default(true)
|
|
18341
|
+
}).optional().default({ enabled: true })
|
|
18338
18342
|
});
|
|
18339
18343
|
TestCoverageConfigSchema = exports_external.object({
|
|
18340
18344
|
enabled: exports_external.boolean().default(true),
|
|
@@ -18663,7 +18667,8 @@ var init_schemas3 = __esm(() => {
|
|
|
18663
18667
|
fixModel: "balanced",
|
|
18664
18668
|
strategy: "diagnose-first",
|
|
18665
18669
|
maxRetries: 2
|
|
18666
|
-
}
|
|
18670
|
+
},
|
|
18671
|
+
hardening: { enabled: true }
|
|
18667
18672
|
}),
|
|
18668
18673
|
context: ContextConfigSchema.default({
|
|
18669
18674
|
fileInjection: "disabled",
|
|
@@ -21929,6 +21934,130 @@ ${c.output}`).join(`
|
|
|
21929
21934
|
buildDebaterLabel(debater) {
|
|
21930
21935
|
return debater.persona ? `${debater.agent} (${debater.persona})` : debater.agent;
|
|
21931
21936
|
}
|
|
21937
|
+
buildReviewPrompt(diff, story) {
|
|
21938
|
+
const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
|
|
21939
|
+
`);
|
|
21940
|
+
return [
|
|
21941
|
+
`Review the following code diff for story ${story.id}: ${story.title}`,
|
|
21942
|
+
"",
|
|
21943
|
+
"## Acceptance Criteria",
|
|
21944
|
+
criteria,
|
|
21945
|
+
"",
|
|
21946
|
+
"## Diff",
|
|
21947
|
+
diff,
|
|
21948
|
+
"",
|
|
21949
|
+
"Also flag any changes in the diff not required by the acceptance criteria above as out-of-scope findings.",
|
|
21950
|
+
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
|
|
21951
|
+
].join(`
|
|
21952
|
+
`);
|
|
21953
|
+
}
|
|
21954
|
+
buildReReviewPrompt(updatedDiff, previousFindings) {
|
|
21955
|
+
const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
|
|
21956
|
+
`) : "(none)";
|
|
21957
|
+
return [
|
|
21958
|
+
"This is a follow-up re-review. Please review the updated diff below.",
|
|
21959
|
+
"",
|
|
21960
|
+
"## Previous Findings",
|
|
21961
|
+
findingsList,
|
|
21962
|
+
"",
|
|
21963
|
+
"## Updated Diff",
|
|
21964
|
+
updatedDiff,
|
|
21965
|
+
"",
|
|
21966
|
+
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string }, deltaSummary: string }",
|
|
21967
|
+
"deltaSummary should describe which previous findings are resolved vs still present."
|
|
21968
|
+
].join(`
|
|
21969
|
+
`);
|
|
21970
|
+
}
|
|
21971
|
+
buildResolverPrompt(proposals, critiques, diff, story, resolverContext) {
|
|
21972
|
+
const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
|
|
21973
|
+
`);
|
|
21974
|
+
const framing = this.buildResolverFraming(resolverContext);
|
|
21975
|
+
const voteTally = this.buildVoteTallyLine(resolverContext);
|
|
21976
|
+
const proposalsSection = this.buildLabeledProposalsSection(proposals);
|
|
21977
|
+
const critiquesSection = this.buildLabeledCritiquesSection(critiques);
|
|
21978
|
+
return [
|
|
21979
|
+
framing,
|
|
21980
|
+
"",
|
|
21981
|
+
`## Story ${story.id}: ${story.title}`,
|
|
21982
|
+
"",
|
|
21983
|
+
"## Acceptance Criteria",
|
|
21984
|
+
criteria,
|
|
21985
|
+
"",
|
|
21986
|
+
"## Debater Proposals",
|
|
21987
|
+
proposalsSection,
|
|
21988
|
+
critiquesSection,
|
|
21989
|
+
"",
|
|
21990
|
+
"## Diff",
|
|
21991
|
+
diff,
|
|
21992
|
+
voteTally,
|
|
21993
|
+
"",
|
|
21994
|
+
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
|
|
21995
|
+
].filter((line) => line !== undefined).join(`
|
|
21996
|
+
`);
|
|
21997
|
+
}
|
|
21998
|
+
buildReResolverPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext) {
|
|
21999
|
+
const framing = this.buildResolverFraming(resolverContext);
|
|
22000
|
+
const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
|
|
22001
|
+
`) : "(none)";
|
|
22002
|
+
const proposalsSection = this.buildLabeledProposalsSection(proposals);
|
|
22003
|
+
const critiquesSection = this.buildLabeledCritiquesSection(critiques);
|
|
22004
|
+
return [
|
|
22005
|
+
`${framing} This is a re-review after implementer changes.`,
|
|
22006
|
+
"",
|
|
22007
|
+
"## Previous Findings",
|
|
22008
|
+
findingsList,
|
|
22009
|
+
"",
|
|
22010
|
+
"## Updated Debater Proposals",
|
|
22011
|
+
proposalsSection,
|
|
22012
|
+
critiquesSection,
|
|
22013
|
+
"",
|
|
22014
|
+
"## Updated Diff",
|
|
22015
|
+
updatedDiff,
|
|
22016
|
+
"",
|
|
22017
|
+
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string }, deltaSummary: string }",
|
|
22018
|
+
"deltaSummary should describe which previous findings are resolved vs still present."
|
|
22019
|
+
].filter((line) => line !== undefined).join(`
|
|
22020
|
+
`);
|
|
22021
|
+
}
|
|
22022
|
+
buildResolverFraming(ctx) {
|
|
22023
|
+
switch (ctx.resolverType) {
|
|
22024
|
+
case "majority-fail-closed":
|
|
22025
|
+
case "majority-fail-open":
|
|
22026
|
+
return "You are the authoritative reviewer resolving a debate. A preliminary vote was taken \u2014 see tally below. Verify disputed findings using tools (READ files, GREP for usage) and give your final verdict.";
|
|
22027
|
+
case "synthesis":
|
|
22028
|
+
return "You are a synthesis reviewer. Synthesize the debater proposals into a single, coherent, tool-verified verdict. Use READ and GREP to verify claims before ruling.";
|
|
22029
|
+
case "custom":
|
|
22030
|
+
return "You are the judge. Evaluate the debater proposals independently. Verify claims with tools (READ, GREP) and give your final authoritative verdict.";
|
|
22031
|
+
default:
|
|
22032
|
+
return "You are the reviewer. Evaluate the debater proposals and give your final authoritative verdict.";
|
|
22033
|
+
}
|
|
22034
|
+
}
|
|
22035
|
+
buildVoteTallyLine(ctx) {
|
|
22036
|
+
if (!ctx.majorityVote)
|
|
22037
|
+
return "";
|
|
22038
|
+
const { passCount, failCount } = ctx.majorityVote;
|
|
22039
|
+
const failOpenNote = ctx.resolverType === "majority-fail-open" ? " (unparseable proposals count as pass)" : " (unparseable proposals count as fail)";
|
|
22040
|
+
return `
|
|
22041
|
+
|
|
22042
|
+
The preliminary majority vote is: **${passCount} passed, ${failCount} failed**${failOpenNote}. Verify the failing findings with tools before giving your authoritative verdict.`;
|
|
22043
|
+
}
|
|
22044
|
+
buildLabeledProposalsSection(proposals) {
|
|
22045
|
+
return proposals.map((p) => `### ${p.debater}
|
|
22046
|
+
${p.output}`).join(`
|
|
22047
|
+
|
|
22048
|
+
`);
|
|
22049
|
+
}
|
|
22050
|
+
buildLabeledCritiquesSection(critiques) {
|
|
22051
|
+
if (critiques.length === 0)
|
|
22052
|
+
return "";
|
|
22053
|
+
return `
|
|
22054
|
+
|
|
22055
|
+
## Critiques
|
|
22056
|
+
${critiques.map((c, i) => `### Critique ${i + 1}
|
|
22057
|
+
${c}`).join(`
|
|
22058
|
+
|
|
22059
|
+
`)}`;
|
|
22060
|
+
}
|
|
21932
22061
|
}
|
|
21933
22062
|
var init_prompt_builder = __esm(() => {
|
|
21934
22063
|
init_personas();
|
|
@@ -22566,7 +22695,20 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
|
|
|
22566
22695
|
logger?.warn("debate", "hybrid mode requires sessionMode: stateful for plan \u2014 running as panel");
|
|
22567
22696
|
}
|
|
22568
22697
|
const resolverTimeoutMs = (ctx.stageConfig.timeoutSeconds ?? 600) * 1000;
|
|
22569
|
-
const
|
|
22698
|
+
const specAnchor = opts.specContent ? `
|
|
22699
|
+
|
|
22700
|
+
## Original Spec
|
|
22701
|
+
|
|
22702
|
+
${opts.specContent}
|
|
22703
|
+
|
|
22704
|
+
## Synthesis Rules \u2014 Acceptance Criteria
|
|
22705
|
+
|
|
22706
|
+
The spec above is the authoritative source for acceptance criteria.
|
|
22707
|
+
- 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.
|
|
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.` : "";
|
|
22711
|
+
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}`;
|
|
22570
22712
|
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));
|
|
22571
22713
|
const winningOutput = outcome.output ?? successful[0].output;
|
|
22572
22714
|
const proposals = successful.map((p) => ({ debater: p.debater, output: p.output }));
|
|
@@ -24929,6 +25071,24 @@ function resolveAcceptanceTestCandidates(options) {
|
|
|
24929
25071
|
return [];
|
|
24930
25072
|
return [resolveAcceptanceFeatureTestPath(options.featureDir, options.testPathConfig, options.language)];
|
|
24931
25073
|
}
|
|
25074
|
+
function suggestedTestFilename(language) {
|
|
25075
|
+
switch (language?.toLowerCase()) {
|
|
25076
|
+
case "go":
|
|
25077
|
+
return ".nax-suggested_test.go";
|
|
25078
|
+
case "python":
|
|
25079
|
+
return ".nax-suggested.test.py";
|
|
25080
|
+
case "rust":
|
|
25081
|
+
return ".nax-suggested.rs";
|
|
25082
|
+
default:
|
|
25083
|
+
return ".nax-suggested.test.ts";
|
|
25084
|
+
}
|
|
25085
|
+
}
|
|
25086
|
+
function resolveSuggestedTestFile(language, testPathConfig) {
|
|
25087
|
+
return testPathConfig ?? suggestedTestFilename(language);
|
|
25088
|
+
}
|
|
25089
|
+
function resolveSuggestedPackageFeatureTestPath(packageDir, featureName, testPathConfig, language) {
|
|
25090
|
+
return path.join(packageDir, ".nax", "features", featureName, resolveSuggestedTestFile(language, testPathConfig));
|
|
25091
|
+
}
|
|
24932
25092
|
async function findExistingAcceptanceTestPath(options) {
|
|
24933
25093
|
const candidates = resolveAcceptanceTestCandidates(options);
|
|
24934
25094
|
for (const testPath of candidates) {
|
|
@@ -25795,7 +25955,7 @@ Rules:
|
|
|
25795
25955
|
- Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
|
|
25796
25956
|
- **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
|
|
25797
25957
|
- **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
|
|
25798
|
-
- **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join16(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile2(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).
|
|
25958
|
+
- **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${options.targetTestFile ?? join16(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile2(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).
|
|
25799
25959
|
- **Process cwd**: When spawning child processes to invoke a CLI or binary, set the working directory to the **package root** (\`join(import.meta.dir, "../../..")\`) as your default \u2014 unless your Step 2 exploration reveals the CLI uses a different working directory convention (e.g. reads config from \`~/.config/\`, or resolves paths relative to a flag value). Always check how the CLI resolves file paths before assuming.`;
|
|
25800
25960
|
const implementationSection = options.implementationContext && options.implementationContext.length > 0 ? `
|
|
25801
25961
|
|
|
@@ -25828,7 +25988,7 @@ Previous test failed because: ${options.previousFailure}` : "";
|
|
|
25828
25988
|
outputPreview: rawOutput.slice(0, 300)
|
|
25829
25989
|
});
|
|
25830
25990
|
if (!testCode) {
|
|
25831
|
-
const targetPath = join16(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile2(options.language, options.config?.acceptance?.testPath));
|
|
25991
|
+
const targetPath = options.targetTestFile ?? join16(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile2(options.language, options.config?.acceptance?.testPath));
|
|
25832
25992
|
const backupPath = `${targetPath}.llm-recovery.bak`;
|
|
25833
25993
|
let recoveryFailed = false;
|
|
25834
25994
|
logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", {
|
|
@@ -26193,156 +26353,6 @@ function logTestOutput(logger, stage, output, opts = {}) {
|
|
|
26193
26353
|
});
|
|
26194
26354
|
}
|
|
26195
26355
|
|
|
26196
|
-
// src/pipeline/stages/acceptance.ts
|
|
26197
|
-
var exports_acceptance = {};
|
|
26198
|
-
__export(exports_acceptance, {
|
|
26199
|
-
acceptanceStage: () => acceptanceStage
|
|
26200
|
-
});
|
|
26201
|
-
function parseTestFailures(output) {
|
|
26202
|
-
const failedACs = [];
|
|
26203
|
-
const lines = output.split(`
|
|
26204
|
-
`);
|
|
26205
|
-
for (const line of lines) {
|
|
26206
|
-
if (line.includes("(fail)")) {
|
|
26207
|
-
const acMatch = line.match(/(AC-\d+):/i);
|
|
26208
|
-
if (acMatch) {
|
|
26209
|
-
const acId = acMatch[1].toUpperCase();
|
|
26210
|
-
if (!failedACs.includes(acId)) {
|
|
26211
|
-
failedACs.push(acId);
|
|
26212
|
-
}
|
|
26213
|
-
}
|
|
26214
|
-
}
|
|
26215
|
-
}
|
|
26216
|
-
return failedACs;
|
|
26217
|
-
}
|
|
26218
|
-
function areAllStoriesComplete(ctx) {
|
|
26219
|
-
const counts = countStories(ctx.prd);
|
|
26220
|
-
const totalComplete = counts.passed + counts.failed + counts.skipped;
|
|
26221
|
-
return totalComplete === counts.total;
|
|
26222
|
-
}
|
|
26223
|
-
var acceptanceStage;
|
|
26224
|
-
var init_acceptance = __esm(() => {
|
|
26225
|
-
init_generator();
|
|
26226
|
-
init_test_path();
|
|
26227
|
-
init_logger2();
|
|
26228
|
-
init_prd();
|
|
26229
|
-
acceptanceStage = {
|
|
26230
|
-
name: "acceptance",
|
|
26231
|
-
enabled(ctx) {
|
|
26232
|
-
if (!ctx.config.acceptance.enabled) {
|
|
26233
|
-
return false;
|
|
26234
|
-
}
|
|
26235
|
-
if (!areAllStoriesComplete(ctx)) {
|
|
26236
|
-
return false;
|
|
26237
|
-
}
|
|
26238
|
-
return true;
|
|
26239
|
-
},
|
|
26240
|
-
async execute(ctx) {
|
|
26241
|
-
const logger = getLogger();
|
|
26242
|
-
logger.info("acceptance", "Running acceptance tests", { storyId: ctx.story.id });
|
|
26243
|
-
if (!ctx.featureDir) {
|
|
26244
|
-
logger.warn("acceptance", "No feature directory \u2014 skipping acceptance tests", { storyId: ctx.story.id });
|
|
26245
|
-
return { action: "continue" };
|
|
26246
|
-
}
|
|
26247
|
-
const testGroups = ctx.acceptanceTestPaths ?? [
|
|
26248
|
-
{
|
|
26249
|
-
testPath: resolveAcceptanceFeatureTestPath(ctx.featureDir, ctx.config.acceptance.testPath, ctx.config.project?.language),
|
|
26250
|
-
packageDir: ctx.workdir
|
|
26251
|
-
}
|
|
26252
|
-
];
|
|
26253
|
-
const allFailedACs = [];
|
|
26254
|
-
const allOutputParts = [];
|
|
26255
|
-
let anyError = false;
|
|
26256
|
-
let errorExitCode = 0;
|
|
26257
|
-
for (const { testPath, packageDir } of testGroups) {
|
|
26258
|
-
const testFile = Bun.file(testPath);
|
|
26259
|
-
const exists = await testFile.exists();
|
|
26260
|
-
if (!exists) {
|
|
26261
|
-
logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", { storyId: ctx.story.id, testPath });
|
|
26262
|
-
continue;
|
|
26263
|
-
}
|
|
26264
|
-
const testCmdParts = buildAcceptanceRunCommand(testPath, ctx.config.project?.testFramework, ctx.config.acceptance.command);
|
|
26265
|
-
logger.info("acceptance", "Running acceptance command", {
|
|
26266
|
-
storyId: ctx.story.id,
|
|
26267
|
-
cmd: testCmdParts.join(" "),
|
|
26268
|
-
packageDir
|
|
26269
|
-
});
|
|
26270
|
-
const proc = Bun.spawn(testCmdParts, {
|
|
26271
|
-
cwd: packageDir,
|
|
26272
|
-
stdout: "pipe",
|
|
26273
|
-
stderr: "pipe"
|
|
26274
|
-
});
|
|
26275
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
26276
|
-
proc.exited,
|
|
26277
|
-
new Response(proc.stdout).text(),
|
|
26278
|
-
new Response(proc.stderr).text()
|
|
26279
|
-
]);
|
|
26280
|
-
const output = `${stdout}
|
|
26281
|
-
${stderr}`;
|
|
26282
|
-
allOutputParts.push(output);
|
|
26283
|
-
const failedACs = parseTestFailures(output);
|
|
26284
|
-
const overrides = ctx.prd.acceptanceOverrides ?? {};
|
|
26285
|
-
const actualFailures = failedACs.filter((acId) => !overrides[acId]);
|
|
26286
|
-
const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
|
|
26287
|
-
if (overriddenFailures.length > 0) {
|
|
26288
|
-
logger.warn("acceptance", "Skipped failures (overridden)", {
|
|
26289
|
-
storyId: ctx.story.id,
|
|
26290
|
-
overriddenFailures,
|
|
26291
|
-
overrides: overriddenFailures.map((acId) => ({ acId, reason: overrides[acId] }))
|
|
26292
|
-
});
|
|
26293
|
-
}
|
|
26294
|
-
if (failedACs.length === 0 && exitCode !== 0) {
|
|
26295
|
-
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
26296
|
-
storyId: ctx.story.id,
|
|
26297
|
-
exitCode,
|
|
26298
|
-
packageDir
|
|
26299
|
-
});
|
|
26300
|
-
logTestOutput(logger, "acceptance", output);
|
|
26301
|
-
anyError = true;
|
|
26302
|
-
errorExitCode = exitCode;
|
|
26303
|
-
allFailedACs.push("AC-ERROR");
|
|
26304
|
-
continue;
|
|
26305
|
-
}
|
|
26306
|
-
for (const acId of actualFailures) {
|
|
26307
|
-
if (!allFailedACs.includes(acId)) {
|
|
26308
|
-
allFailedACs.push(acId);
|
|
26309
|
-
}
|
|
26310
|
-
}
|
|
26311
|
-
if (actualFailures.length > 0) {
|
|
26312
|
-
logger.error("acceptance", "Acceptance tests failed", {
|
|
26313
|
-
storyId: ctx.story.id,
|
|
26314
|
-
failedACs: actualFailures,
|
|
26315
|
-
packageDir
|
|
26316
|
-
});
|
|
26317
|
-
logTestOutput(logger, "acceptance", output);
|
|
26318
|
-
} else if (exitCode === 0) {
|
|
26319
|
-
logger.info("acceptance", "Package acceptance tests passed", { storyId: ctx.story.id, packageDir });
|
|
26320
|
-
}
|
|
26321
|
-
}
|
|
26322
|
-
const combinedOutput = allOutputParts.join(`
|
|
26323
|
-
`);
|
|
26324
|
-
if (allFailedACs.length === 0) {
|
|
26325
|
-
logger.info("acceptance", "All acceptance tests passed", { storyId: ctx.story.id });
|
|
26326
|
-
return { action: "continue" };
|
|
26327
|
-
}
|
|
26328
|
-
ctx.acceptanceFailures = {
|
|
26329
|
-
failedACs: allFailedACs,
|
|
26330
|
-
testOutput: combinedOutput
|
|
26331
|
-
};
|
|
26332
|
-
if (anyError) {
|
|
26333
|
-
return {
|
|
26334
|
-
action: "fail",
|
|
26335
|
-
reason: `Acceptance tests errored (exit code ${errorExitCode}): syntax error, import failure, or unhandled exception`
|
|
26336
|
-
};
|
|
26337
|
-
}
|
|
26338
|
-
return {
|
|
26339
|
-
action: "fail",
|
|
26340
|
-
reason: `Acceptance tests failed: ${allFailedACs.join(", ")}`
|
|
26341
|
-
};
|
|
26342
|
-
}
|
|
26343
|
-
};
|
|
26344
|
-
});
|
|
26345
|
-
|
|
26346
26356
|
// src/acceptance/refinement.ts
|
|
26347
26357
|
var exports_refinement = {};
|
|
26348
26358
|
__export(exports_refinement, {
|
|
@@ -26509,6 +26519,315 @@ var init_refinement = __esm(() => {
|
|
|
26509
26519
|
};
|
|
26510
26520
|
});
|
|
26511
26521
|
|
|
26522
|
+
// src/acceptance/hardening.ts
|
|
26523
|
+
var exports_hardening = {};
|
|
26524
|
+
__export(exports_hardening, {
|
|
26525
|
+
runHardeningPass: () => runHardeningPass,
|
|
26526
|
+
_hardeningDeps: () => _hardeningDeps
|
|
26527
|
+
});
|
|
26528
|
+
async function runHardeningPass(ctx) {
|
|
26529
|
+
const logger = getSafeLogger();
|
|
26530
|
+
const result = { promoted: [], discarded: [], costUsd: 0 };
|
|
26531
|
+
const storiesWithSuggested = ctx.prd.userStories.filter((s) => s.suggestedCriteria && s.suggestedCriteria.length > 0);
|
|
26532
|
+
if (storiesWithSuggested.length === 0)
|
|
26533
|
+
return result;
|
|
26534
|
+
logger?.info("acceptance", "Starting hardening pass", {
|
|
26535
|
+
storyId: storiesWithSuggested[0].id,
|
|
26536
|
+
storiesWithSuggested: storiesWithSuggested.length,
|
|
26537
|
+
totalSuggestedACs: storiesWithSuggested.reduce((n, s) => n + (s.suggestedCriteria?.length ?? 0), 0)
|
|
26538
|
+
});
|
|
26539
|
+
try {
|
|
26540
|
+
const allRefined = [];
|
|
26541
|
+
for (const story of storiesWithSuggested) {
|
|
26542
|
+
const criteria = story.suggestedCriteria ?? [];
|
|
26543
|
+
const refined = await _hardeningDeps.refine(criteria, {
|
|
26544
|
+
storyId: story.id,
|
|
26545
|
+
featureName: ctx.prd.feature,
|
|
26546
|
+
workdir: ctx.workdir,
|
|
26547
|
+
codebaseContext: "",
|
|
26548
|
+
config: ctx.config
|
|
26549
|
+
});
|
|
26550
|
+
allRefined.push(...refined);
|
|
26551
|
+
}
|
|
26552
|
+
const language = ctx.config.project?.language;
|
|
26553
|
+
const suggestedTestPath = resolveSuggestedPackageFeatureTestPath(ctx.workdir, ctx.prd.feature, ctx.config.acceptance?.suggestedTestPath, language);
|
|
26554
|
+
let modelDef;
|
|
26555
|
+
try {
|
|
26556
|
+
modelDef = resolveModelForAgent(ctx.config.models, ctx.config.autoMode?.defaultAgent ?? "claude", ctx.config.acceptance?.model ?? "fast", ctx.config.autoMode?.defaultAgent ?? "claude");
|
|
26557
|
+
} catch {
|
|
26558
|
+
modelDef = { provider: "anthropic", model: "claude-haiku-4-5-20251001" };
|
|
26559
|
+
}
|
|
26560
|
+
const genResult = await _hardeningDeps.generate(storiesWithSuggested, allRefined, {
|
|
26561
|
+
featureName: ctx.prd.feature,
|
|
26562
|
+
workdir: ctx.workdir,
|
|
26563
|
+
featureDir: ctx.featureDir,
|
|
26564
|
+
codebaseContext: "",
|
|
26565
|
+
modelTier: ctx.config.acceptance?.model ?? "fast",
|
|
26566
|
+
modelDef,
|
|
26567
|
+
config: ctx.config,
|
|
26568
|
+
language,
|
|
26569
|
+
targetTestFile: suggestedTestPath
|
|
26570
|
+
});
|
|
26571
|
+
if (genResult.testCode) {
|
|
26572
|
+
await _hardeningDeps.writeFile(suggestedTestPath, genResult.testCode);
|
|
26573
|
+
}
|
|
26574
|
+
const testCmd = buildAcceptanceRunCommand(suggestedTestPath, ctx.config.project?.testFramework, ctx.config.acceptance?.command);
|
|
26575
|
+
const proc = _hardeningDeps.spawn(testCmd, {
|
|
26576
|
+
cwd: ctx.workdir,
|
|
26577
|
+
stdout: "pipe",
|
|
26578
|
+
stderr: "pipe"
|
|
26579
|
+
});
|
|
26580
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
26581
|
+
proc.exited,
|
|
26582
|
+
new Response(proc.stdout).text(),
|
|
26583
|
+
new Response(proc.stderr).text()
|
|
26584
|
+
]);
|
|
26585
|
+
const output = `${stdout}
|
|
26586
|
+
${stderr}`;
|
|
26587
|
+
const failedACs = parseTestFailures(output);
|
|
26588
|
+
const failedSet = new Set(failedACs.map((ac) => ac.toUpperCase()));
|
|
26589
|
+
let acIndex = 0;
|
|
26590
|
+
for (const story of storiesWithSuggested) {
|
|
26591
|
+
const suggested = story.suggestedCriteria ?? [];
|
|
26592
|
+
const toPromote = [];
|
|
26593
|
+
const toDiscard = [];
|
|
26594
|
+
for (const criterion of suggested) {
|
|
26595
|
+
acIndex++;
|
|
26596
|
+
const acId = `AC-${acIndex}`;
|
|
26597
|
+
if (failedSet.has(acId) || exitCode !== 0 && failedACs.length === 0) {
|
|
26598
|
+
toDiscard.push(criterion);
|
|
26599
|
+
} else {
|
|
26600
|
+
toPromote.push(criterion);
|
|
26601
|
+
}
|
|
26602
|
+
}
|
|
26603
|
+
if (toPromote.length > 0) {
|
|
26604
|
+
story.acceptanceCriteria = [...story.acceptanceCriteria, ...toPromote];
|
|
26605
|
+
result.promoted.push(...toPromote);
|
|
26606
|
+
}
|
|
26607
|
+
result.discarded.push(...toDiscard);
|
|
26608
|
+
story.suggestedCriteria = toDiscard.length > 0 ? toDiscard : undefined;
|
|
26609
|
+
}
|
|
26610
|
+
if (result.promoted.length > 0) {
|
|
26611
|
+
await _hardeningDeps.savePRD(ctx.prd, ctx.prdPath);
|
|
26612
|
+
}
|
|
26613
|
+
logger?.info("acceptance", "Hardening pass complete", {
|
|
26614
|
+
storyId: storiesWithSuggested[0].id,
|
|
26615
|
+
promoted: result.promoted.length,
|
|
26616
|
+
discarded: result.discarded.length,
|
|
26617
|
+
costUsd: result.costUsd
|
|
26618
|
+
});
|
|
26619
|
+
} catch (err) {
|
|
26620
|
+
logger?.warn("acceptance", "Hardening pass failed (non-blocking)", {
|
|
26621
|
+
storyId: storiesWithSuggested[0].id,
|
|
26622
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26623
|
+
});
|
|
26624
|
+
}
|
|
26625
|
+
return result;
|
|
26626
|
+
}
|
|
26627
|
+
var _hardeningDeps;
|
|
26628
|
+
var init_hardening = __esm(() => {
|
|
26629
|
+
init_config();
|
|
26630
|
+
init_logger2();
|
|
26631
|
+
init_acceptance();
|
|
26632
|
+
init_prd();
|
|
26633
|
+
init_generator();
|
|
26634
|
+
init_generator();
|
|
26635
|
+
init_refinement();
|
|
26636
|
+
init_test_path();
|
|
26637
|
+
_hardeningDeps = {
|
|
26638
|
+
refine: refineAcceptanceCriteria,
|
|
26639
|
+
generate: generateFromPRD,
|
|
26640
|
+
savePRD,
|
|
26641
|
+
spawn: Bun.spawn,
|
|
26642
|
+
writeFile: async (p, c) => {
|
|
26643
|
+
await Bun.write(p, c);
|
|
26644
|
+
}
|
|
26645
|
+
};
|
|
26646
|
+
});
|
|
26647
|
+
|
|
26648
|
+
// src/pipeline/stages/acceptance.ts
|
|
26649
|
+
var exports_acceptance = {};
|
|
26650
|
+
__export(exports_acceptance, {
|
|
26651
|
+
parseTestFailures: () => parseTestFailures,
|
|
26652
|
+
acceptanceStage: () => acceptanceStage,
|
|
26653
|
+
_acceptanceStageDeps: () => _acceptanceStageDeps
|
|
26654
|
+
});
|
|
26655
|
+
function parseTestFailures(output) {
|
|
26656
|
+
const failedACs = [];
|
|
26657
|
+
const lines = output.split(`
|
|
26658
|
+
`);
|
|
26659
|
+
for (const line of lines) {
|
|
26660
|
+
if (line.includes("(fail)")) {
|
|
26661
|
+
const acMatch = line.match(/(AC-\d+):/i);
|
|
26662
|
+
if (acMatch) {
|
|
26663
|
+
const acId = acMatch[1].toUpperCase();
|
|
26664
|
+
if (!failedACs.includes(acId)) {
|
|
26665
|
+
failedACs.push(acId);
|
|
26666
|
+
}
|
|
26667
|
+
}
|
|
26668
|
+
}
|
|
26669
|
+
}
|
|
26670
|
+
return failedACs;
|
|
26671
|
+
}
|
|
26672
|
+
function areAllStoriesComplete(ctx) {
|
|
26673
|
+
const counts = countStories(ctx.prd);
|
|
26674
|
+
const totalComplete = counts.passed + counts.failed + counts.skipped;
|
|
26675
|
+
return totalComplete === counts.total;
|
|
26676
|
+
}
|
|
26677
|
+
var _acceptanceStageDeps, acceptanceStage;
|
|
26678
|
+
var init_acceptance = __esm(() => {
|
|
26679
|
+
init_generator();
|
|
26680
|
+
init_test_path();
|
|
26681
|
+
init_logger2();
|
|
26682
|
+
init_prd();
|
|
26683
|
+
_acceptanceStageDeps = {
|
|
26684
|
+
runHardeningPass: async (ctx) => {
|
|
26685
|
+
const { runHardeningPass: runHardeningPass2 } = await Promise.resolve().then(() => (init_hardening(), exports_hardening));
|
|
26686
|
+
return runHardeningPass2(ctx);
|
|
26687
|
+
}
|
|
26688
|
+
};
|
|
26689
|
+
acceptanceStage = {
|
|
26690
|
+
name: "acceptance",
|
|
26691
|
+
enabled(ctx) {
|
|
26692
|
+
if (!ctx.config.acceptance.enabled) {
|
|
26693
|
+
return false;
|
|
26694
|
+
}
|
|
26695
|
+
if (!areAllStoriesComplete(ctx)) {
|
|
26696
|
+
return false;
|
|
26697
|
+
}
|
|
26698
|
+
return true;
|
|
26699
|
+
},
|
|
26700
|
+
async execute(ctx) {
|
|
26701
|
+
const logger = getLogger();
|
|
26702
|
+
logger.info("acceptance", "Running acceptance tests", { storyId: ctx.story.id });
|
|
26703
|
+
if (!ctx.featureDir) {
|
|
26704
|
+
logger.warn("acceptance", "No feature directory \u2014 skipping acceptance tests", { storyId: ctx.story.id });
|
|
26705
|
+
return { action: "continue" };
|
|
26706
|
+
}
|
|
26707
|
+
const testGroups = ctx.acceptanceTestPaths ?? [
|
|
26708
|
+
{
|
|
26709
|
+
testPath: resolveAcceptanceFeatureTestPath(ctx.featureDir, ctx.config.acceptance.testPath, ctx.config.project?.language),
|
|
26710
|
+
packageDir: ctx.workdir
|
|
26711
|
+
}
|
|
26712
|
+
];
|
|
26713
|
+
const allFailedACs = [];
|
|
26714
|
+
const allOutputParts = [];
|
|
26715
|
+
let anyError = false;
|
|
26716
|
+
let errorExitCode = 0;
|
|
26717
|
+
for (const { testPath, packageDir } of testGroups) {
|
|
26718
|
+
const testFile = Bun.file(testPath);
|
|
26719
|
+
const exists = await testFile.exists();
|
|
26720
|
+
if (!exists) {
|
|
26721
|
+
logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", { storyId: ctx.story.id, testPath });
|
|
26722
|
+
continue;
|
|
26723
|
+
}
|
|
26724
|
+
const testCmdParts = buildAcceptanceRunCommand(testPath, ctx.config.project?.testFramework, ctx.config.acceptance.command);
|
|
26725
|
+
logger.info("acceptance", "Running acceptance command", {
|
|
26726
|
+
storyId: ctx.story.id,
|
|
26727
|
+
cmd: testCmdParts.join(" "),
|
|
26728
|
+
packageDir
|
|
26729
|
+
});
|
|
26730
|
+
const proc = Bun.spawn(testCmdParts, {
|
|
26731
|
+
cwd: packageDir,
|
|
26732
|
+
stdout: "pipe",
|
|
26733
|
+
stderr: "pipe"
|
|
26734
|
+
});
|
|
26735
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
26736
|
+
proc.exited,
|
|
26737
|
+
new Response(proc.stdout).text(),
|
|
26738
|
+
new Response(proc.stderr).text()
|
|
26739
|
+
]);
|
|
26740
|
+
const output = `${stdout}
|
|
26741
|
+
${stderr}`;
|
|
26742
|
+
allOutputParts.push(output);
|
|
26743
|
+
const failedACs = parseTestFailures(output);
|
|
26744
|
+
const overrides = ctx.prd.acceptanceOverrides ?? {};
|
|
26745
|
+
const actualFailures = failedACs.filter((acId) => !overrides[acId]);
|
|
26746
|
+
const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
|
|
26747
|
+
if (overriddenFailures.length > 0) {
|
|
26748
|
+
logger.warn("acceptance", "Skipped failures (overridden)", {
|
|
26749
|
+
storyId: ctx.story.id,
|
|
26750
|
+
overriddenFailures,
|
|
26751
|
+
overrides: overriddenFailures.map((acId) => ({ acId, reason: overrides[acId] }))
|
|
26752
|
+
});
|
|
26753
|
+
}
|
|
26754
|
+
if (failedACs.length === 0 && exitCode !== 0) {
|
|
26755
|
+
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
26756
|
+
storyId: ctx.story.id,
|
|
26757
|
+
exitCode,
|
|
26758
|
+
packageDir
|
|
26759
|
+
});
|
|
26760
|
+
logTestOutput(logger, "acceptance", output);
|
|
26761
|
+
anyError = true;
|
|
26762
|
+
errorExitCode = exitCode;
|
|
26763
|
+
allFailedACs.push("AC-ERROR");
|
|
26764
|
+
continue;
|
|
26765
|
+
}
|
|
26766
|
+
for (const acId of actualFailures) {
|
|
26767
|
+
if (!allFailedACs.includes(acId)) {
|
|
26768
|
+
allFailedACs.push(acId);
|
|
26769
|
+
}
|
|
26770
|
+
}
|
|
26771
|
+
if (actualFailures.length > 0) {
|
|
26772
|
+
logger.error("acceptance", "Acceptance tests failed", {
|
|
26773
|
+
storyId: ctx.story.id,
|
|
26774
|
+
failedACs: actualFailures,
|
|
26775
|
+
packageDir
|
|
26776
|
+
});
|
|
26777
|
+
logTestOutput(logger, "acceptance", output);
|
|
26778
|
+
} else if (exitCode === 0) {
|
|
26779
|
+
logger.info("acceptance", "Package acceptance tests passed", { storyId: ctx.story.id, packageDir });
|
|
26780
|
+
}
|
|
26781
|
+
}
|
|
26782
|
+
const combinedOutput = allOutputParts.join(`
|
|
26783
|
+
`);
|
|
26784
|
+
if (allFailedACs.length === 0) {
|
|
26785
|
+
logger.info("acceptance", "All acceptance tests passed", { storyId: ctx.story.id });
|
|
26786
|
+
const hardeningEnabled = ctx.config.acceptance?.hardening?.enabled !== false;
|
|
26787
|
+
const hasAnySuggested = ctx.prd.userStories.some((s) => s.suggestedCriteria && s.suggestedCriteria.length > 0);
|
|
26788
|
+
if (hardeningEnabled && hasAnySuggested && ctx.featureDir) {
|
|
26789
|
+
try {
|
|
26790
|
+
const prdPath = ctx.prdPath ?? `${ctx.featureDir}/prd.json`;
|
|
26791
|
+
const result = await _acceptanceStageDeps.runHardeningPass({
|
|
26792
|
+
prd: ctx.prd,
|
|
26793
|
+
prdPath,
|
|
26794
|
+
featureDir: ctx.featureDir,
|
|
26795
|
+
workdir: ctx.workdir,
|
|
26796
|
+
config: ctx.config,
|
|
26797
|
+
agentGetFn: ctx.agentGetFn
|
|
26798
|
+
});
|
|
26799
|
+
logger.info("acceptance", "Hardening pass complete", {
|
|
26800
|
+
storyId: ctx.story.id,
|
|
26801
|
+
promoted: result.promoted.length,
|
|
26802
|
+
discarded: result.discarded.length
|
|
26803
|
+
});
|
|
26804
|
+
} catch (err) {
|
|
26805
|
+
logger.warn("acceptance", "Hardening pass failed (non-blocking)", {
|
|
26806
|
+
storyId: ctx.story.id,
|
|
26807
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26808
|
+
});
|
|
26809
|
+
}
|
|
26810
|
+
}
|
|
26811
|
+
return { action: "continue" };
|
|
26812
|
+
}
|
|
26813
|
+
ctx.acceptanceFailures = {
|
|
26814
|
+
failedACs: allFailedACs,
|
|
26815
|
+
testOutput: combinedOutput
|
|
26816
|
+
};
|
|
26817
|
+
if (anyError) {
|
|
26818
|
+
return {
|
|
26819
|
+
action: "fail",
|
|
26820
|
+
reason: `Acceptance tests errored (exit code ${errorExitCode}): syntax error, import failure, or unhandled exception`
|
|
26821
|
+
};
|
|
26822
|
+
}
|
|
26823
|
+
return {
|
|
26824
|
+
action: "fail",
|
|
26825
|
+
reason: `Acceptance tests failed: ${allFailedACs.join(", ")}`
|
|
26826
|
+
};
|
|
26827
|
+
}
|
|
26828
|
+
};
|
|
26829
|
+
});
|
|
26830
|
+
|
|
26512
26831
|
// src/pipeline/stages/acceptance-setup.ts
|
|
26513
26832
|
var exports_acceptance_setup = {};
|
|
26514
26833
|
__export(exports_acceptance_setup, {
|
|
@@ -27132,132 +27451,6 @@ var init_agents = __esm(() => {
|
|
|
27132
27451
|
init_errors();
|
|
27133
27452
|
});
|
|
27134
27453
|
|
|
27135
|
-
// src/review/dialogue-prompts.ts
|
|
27136
|
-
function buildReviewPrompt(diff, story, _semanticConfig) {
|
|
27137
|
-
const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
|
|
27138
|
-
`);
|
|
27139
|
-
return [
|
|
27140
|
-
`Review the following code diff for story ${story.id}: ${story.title}`,
|
|
27141
|
-
"",
|
|
27142
|
-
"## Acceptance Criteria",
|
|
27143
|
-
criteria,
|
|
27144
|
-
"",
|
|
27145
|
-
"## Diff",
|
|
27146
|
-
diff,
|
|
27147
|
-
"",
|
|
27148
|
-
"Also flag any changes in the diff not required by the acceptance criteria above as out-of-scope findings.",
|
|
27149
|
-
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
|
|
27150
|
-
].join(`
|
|
27151
|
-
`);
|
|
27152
|
-
}
|
|
27153
|
-
function buildReReviewPrompt(updatedDiff, previousFindings) {
|
|
27154
|
-
const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
|
|
27155
|
-
`) : "(none)";
|
|
27156
|
-
return [
|
|
27157
|
-
"This is a follow-up re-review. Please review the updated diff below.",
|
|
27158
|
-
"",
|
|
27159
|
-
"## Previous Findings",
|
|
27160
|
-
findingsList,
|
|
27161
|
-
"",
|
|
27162
|
-
"## Updated Diff",
|
|
27163
|
-
updatedDiff,
|
|
27164
|
-
"",
|
|
27165
|
-
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string }, deltaSummary: string }",
|
|
27166
|
-
"deltaSummary should describe which previous findings are resolved vs still present."
|
|
27167
|
-
].join(`
|
|
27168
|
-
`);
|
|
27169
|
-
}
|
|
27170
|
-
function buildProposalsSection2(proposals) {
|
|
27171
|
-
return proposals.map((p) => `### ${p.debater}
|
|
27172
|
-
${p.output}`).join(`
|
|
27173
|
-
|
|
27174
|
-
`);
|
|
27175
|
-
}
|
|
27176
|
-
function buildCritiquesSection(critiques) {
|
|
27177
|
-
if (critiques.length === 0)
|
|
27178
|
-
return "";
|
|
27179
|
-
return `
|
|
27180
|
-
|
|
27181
|
-
## Critiques
|
|
27182
|
-
${critiques.map((c, i) => `### Critique ${i + 1}
|
|
27183
|
-
${c}`).join(`
|
|
27184
|
-
|
|
27185
|
-
`)}`;
|
|
27186
|
-
}
|
|
27187
|
-
function buildVoteTallyLine(ctx) {
|
|
27188
|
-
if (!ctx.majorityVote)
|
|
27189
|
-
return "";
|
|
27190
|
-
const { passCount, failCount } = ctx.majorityVote;
|
|
27191
|
-
const failOpenNote = ctx.resolverType === "majority-fail-open" ? " (unparseable proposals count as pass)" : " (unparseable proposals count as fail)";
|
|
27192
|
-
return `
|
|
27193
|
-
|
|
27194
|
-
The preliminary majority vote is: **${passCount} passed, ${failCount} failed**${failOpenNote}. Verify the failing findings with tools before giving your authoritative verdict.`;
|
|
27195
|
-
}
|
|
27196
|
-
function buildResolverFraming(ctx) {
|
|
27197
|
-
switch (ctx.resolverType) {
|
|
27198
|
-
case "majority-fail-closed":
|
|
27199
|
-
case "majority-fail-open":
|
|
27200
|
-
return "You are the authoritative reviewer resolving a debate. A preliminary vote was taken \u2014 see tally below. Verify disputed findings using tools (READ files, GREP for usage) and give your final verdict.";
|
|
27201
|
-
case "synthesis":
|
|
27202
|
-
return "You are a synthesis reviewer. Synthesize the debater proposals into a single, coherent, tool-verified verdict. Use READ and GREP to verify claims before ruling.";
|
|
27203
|
-
case "custom":
|
|
27204
|
-
return "You are the judge. Evaluate the debater proposals independently. Verify claims with tools (READ, GREP) and give your final authoritative verdict.";
|
|
27205
|
-
default:
|
|
27206
|
-
return "You are the reviewer. Evaluate the debater proposals and give your final authoritative verdict.";
|
|
27207
|
-
}
|
|
27208
|
-
}
|
|
27209
|
-
function buildDebateResolverPrompt(proposals, critiques, diff, story, _semanticConfig, resolverContext) {
|
|
27210
|
-
const criteria = story.acceptanceCriteria.map((c) => `- ${c}`).join(`
|
|
27211
|
-
`);
|
|
27212
|
-
const framing = buildResolverFraming(resolverContext);
|
|
27213
|
-
const voteTally = buildVoteTallyLine(resolverContext);
|
|
27214
|
-
const proposalsSection = buildProposalsSection2(proposals);
|
|
27215
|
-
const critiquesSection = buildCritiquesSection(critiques);
|
|
27216
|
-
return [
|
|
27217
|
-
framing,
|
|
27218
|
-
"",
|
|
27219
|
-
`## Story ${story.id}: ${story.title}`,
|
|
27220
|
-
"",
|
|
27221
|
-
"## Acceptance Criteria",
|
|
27222
|
-
criteria,
|
|
27223
|
-
"",
|
|
27224
|
-
"## Debater Proposals",
|
|
27225
|
-
proposalsSection,
|
|
27226
|
-
critiquesSection,
|
|
27227
|
-
"",
|
|
27228
|
-
"## Diff",
|
|
27229
|
-
diff,
|
|
27230
|
-
voteTally,
|
|
27231
|
-
"",
|
|
27232
|
-
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string } }"
|
|
27233
|
-
].filter((line) => line !== undefined).join(`
|
|
27234
|
-
`);
|
|
27235
|
-
}
|
|
27236
|
-
function buildDebateReReviewPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext) {
|
|
27237
|
-
const framing = buildResolverFraming(resolverContext);
|
|
27238
|
-
const findingsList = previousFindings.length > 0 ? previousFindings.map((f) => `- ${f.ruleId}: ${f.message}`).join(`
|
|
27239
|
-
`) : "(none)";
|
|
27240
|
-
const proposalsSection = buildProposalsSection2(proposals);
|
|
27241
|
-
const critiquesSection = buildCritiquesSection(critiques);
|
|
27242
|
-
return [
|
|
27243
|
-
`${framing} This is a re-review after implementer changes.`,
|
|
27244
|
-
"",
|
|
27245
|
-
"## Previous Findings",
|
|
27246
|
-
findingsList,
|
|
27247
|
-
"",
|
|
27248
|
-
"## Updated Debater Proposals",
|
|
27249
|
-
proposalsSection,
|
|
27250
|
-
critiquesSection,
|
|
27251
|
-
"",
|
|
27252
|
-
"## Updated Diff",
|
|
27253
|
-
updatedDiff,
|
|
27254
|
-
"",
|
|
27255
|
-
"Respond with JSON: { passed: boolean, findings: [...], findingReasoning: { [id]: string }, deltaSummary: string }",
|
|
27256
|
-
"deltaSummary should describe which previous findings are resolved vs still present."
|
|
27257
|
-
].filter((line) => line !== undefined).join(`
|
|
27258
|
-
`);
|
|
27259
|
-
}
|
|
27260
|
-
|
|
27261
27454
|
// src/review/dialogue.ts
|
|
27262
27455
|
function extractDeltaSummary(rawOutput, previousFindings, newFindings) {
|
|
27263
27456
|
const parsed = tryParseLLMJson(rawOutput);
|
|
@@ -27333,6 +27526,7 @@ function createReviewerSession(agent, storyId, workdir, featureName, _config) {
|
|
|
27333
27526
|
generation: 1,
|
|
27334
27527
|
pendingCompactionContext: null
|
|
27335
27528
|
};
|
|
27529
|
+
const promptBuilder = new DebatePromptBuilder({ taskContext: "", outputFormat: "", stage: "review" }, { debaters: [], sessionMode: "stateful" });
|
|
27336
27530
|
function resolveRunParams(semanticConfig) {
|
|
27337
27531
|
const modelTier = semanticConfig.modelTier;
|
|
27338
27532
|
const defaultAgent = _config.autoMode?.defaultAgent ?? "claude";
|
|
@@ -27367,7 +27561,7 @@ ${prompt}`,
|
|
|
27367
27561
|
if (!active) {
|
|
27368
27562
|
throw new NaxError(`[dialogue] ReviewerSession for story ${storyId} has been destroyed`, "REVIEWER_SESSION_DESTROYED", { stage: "review", storyId, featureName });
|
|
27369
27563
|
}
|
|
27370
|
-
const prompt = buildReviewPrompt(diff, story
|
|
27564
|
+
const prompt = promptBuilder.buildReviewPrompt(diff, story);
|
|
27371
27565
|
const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(semanticConfig);
|
|
27372
27566
|
const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
|
|
27373
27567
|
const result = await agent.run({
|
|
@@ -27405,7 +27599,7 @@ ${prompt}`,
|
|
|
27405
27599
|
});
|
|
27406
27600
|
}
|
|
27407
27601
|
const previousFindings = lastCheckResult.checkResult.findings;
|
|
27408
|
-
const prompt = buildReReviewPrompt(updatedDiff, previousFindings);
|
|
27602
|
+
const prompt = promptBuilder.buildReReviewPrompt(updatedDiff, previousFindings);
|
|
27409
27603
|
const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(lastSemanticConfig);
|
|
27410
27604
|
const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
|
|
27411
27605
|
const result = await agent.run({
|
|
@@ -27465,7 +27659,7 @@ ${prompt}`,
|
|
|
27465
27659
|
if (!active) {
|
|
27466
27660
|
throw new NaxError(`[dialogue] ReviewerSession for story ${storyId} has been destroyed`, "REVIEWER_SESSION_DESTROYED", { stage: "review", storyId, featureName });
|
|
27467
27661
|
}
|
|
27468
|
-
const prompt =
|
|
27662
|
+
const prompt = promptBuilder.buildResolverPrompt(proposals, critiques, diff, story, resolverContext);
|
|
27469
27663
|
const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(semanticConfig);
|
|
27470
27664
|
const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
|
|
27471
27665
|
const result = await agent.run({
|
|
@@ -27500,7 +27694,7 @@ ${prompt}`,
|
|
|
27500
27694
|
throw new NaxError(`[dialogue] reReviewDebate() called before any resolveDebate() on story ${storyId}`, "NO_REVIEW_RESULT", { stage: "review", storyId });
|
|
27501
27695
|
}
|
|
27502
27696
|
const previousFindings = lastCheckResult.checkResult.findings;
|
|
27503
|
-
const prompt =
|
|
27697
|
+
const prompt = promptBuilder.buildReResolverPrompt(proposals, critiques, updatedDiff, previousFindings, resolverContext);
|
|
27504
27698
|
const { modelTier, modelDef, timeoutSeconds } = resolveRunParams(lastSemanticConfig);
|
|
27505
27699
|
const { effectivePrompt, acpSessionName } = buildEffectiveRunArgs(prompt);
|
|
27506
27700
|
const result = await agent.run({
|
|
@@ -27552,6 +27746,7 @@ ${prompt}`,
|
|
|
27552
27746
|
};
|
|
27553
27747
|
}
|
|
27554
27748
|
var init_dialogue = __esm(() => {
|
|
27749
|
+
init_prompt_builder();
|
|
27555
27750
|
init_errors();
|
|
27556
27751
|
});
|
|
27557
27752
|
|
|
@@ -36376,7 +36571,7 @@ var package_default;
|
|
|
36376
36571
|
var init_package = __esm(() => {
|
|
36377
36572
|
package_default = {
|
|
36378
36573
|
name: "@nathapp/nax",
|
|
36379
|
-
version: "0.60.0
|
|
36574
|
+
version: "0.60.0",
|
|
36380
36575
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
36381
36576
|
type: "module",
|
|
36382
36577
|
bin: {
|
|
@@ -36456,8 +36651,8 @@ var init_version = __esm(() => {
|
|
|
36456
36651
|
NAX_VERSION = package_default.version;
|
|
36457
36652
|
NAX_COMMIT = (() => {
|
|
36458
36653
|
try {
|
|
36459
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
36460
|
-
return "
|
|
36654
|
+
if (/^[0-9a-f]{6,10}$/.test("73c9c082"))
|
|
36655
|
+
return "73c9c082";
|
|
36461
36656
|
} catch {}
|
|
36462
36657
|
try {
|
|
36463
36658
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -73029,6 +73224,20 @@ function validateStory(raw, index, allIds) {
|
|
|
73029
73224
|
throw new Error(`[schema] story[${index}].acceptanceCriteria[${i}] must be a string`);
|
|
73030
73225
|
}
|
|
73031
73226
|
}
|
|
73227
|
+
let suggestedCriteria;
|
|
73228
|
+
if (s.suggestedCriteria !== undefined && s.suggestedCriteria !== null) {
|
|
73229
|
+
if (!Array.isArray(s.suggestedCriteria)) {
|
|
73230
|
+
throw new Error(`[schema] story[${index}].suggestedCriteria must be an array when present`);
|
|
73231
|
+
}
|
|
73232
|
+
if (s.suggestedCriteria.length > 0) {
|
|
73233
|
+
for (let i = 0;i < s.suggestedCriteria.length; i++) {
|
|
73234
|
+
if (typeof s.suggestedCriteria[i] !== "string") {
|
|
73235
|
+
throw new Error(`[schema] story[${index}].suggestedCriteria[${i}] must be a string`);
|
|
73236
|
+
}
|
|
73237
|
+
}
|
|
73238
|
+
suggestedCriteria = s.suggestedCriteria;
|
|
73239
|
+
}
|
|
73240
|
+
}
|
|
73032
73241
|
const routing = typeof s.routing === "object" && s.routing !== null ? s.routing : {};
|
|
73033
73242
|
const rawComplexity = routing.complexity ?? s.complexity;
|
|
73034
73243
|
if (rawComplexity === undefined || rawComplexity === null) {
|
|
@@ -73096,7 +73305,8 @@ function validateStory(raw, index, allIds) {
|
|
|
73096
73305
|
...noTestJustification !== undefined ? { noTestJustification } : {}
|
|
73097
73306
|
},
|
|
73098
73307
|
...workdir !== undefined ? { workdir } : {},
|
|
73099
|
-
...contextFiles.length > 0 ? { contextFiles } : {}
|
|
73308
|
+
...contextFiles.length > 0 ? { contextFiles } : {},
|
|
73309
|
+
...suggestedCriteria !== undefined ? { suggestedCriteria } : {}
|
|
73100
73310
|
};
|
|
73101
73311
|
}
|
|
73102
73312
|
function sanitizeInvalidEscapes(text) {
|
|
@@ -73234,7 +73444,8 @@ async function planCommand(workdir, config2, options) {
|
|
|
73234
73444
|
outputDir,
|
|
73235
73445
|
timeoutSeconds,
|
|
73236
73446
|
dangerouslySkipPermissions: resolvedPerm.skipPermissions,
|
|
73237
|
-
maxInteractionTurns: config2?.agent?.maxInteractionTurns
|
|
73447
|
+
maxInteractionTurns: config2?.agent?.maxInteractionTurns,
|
|
73448
|
+
specContent
|
|
73238
73449
|
});
|
|
73239
73450
|
if (debateResult.outcome !== "failed" && debateResult.output) {
|
|
73240
73451
|
rawResponse = debateResult.output;
|