@nathapp/nax 0.57.2 → 0.57.3
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 +517 -281
- package/package.json +1 -1
package/dist/nax.js
CHANGED
|
@@ -3044,16 +3044,6 @@ var init_logger = __esm(() => {
|
|
|
3044
3044
|
});
|
|
3045
3045
|
|
|
3046
3046
|
// src/logger/index.ts
|
|
3047
|
-
var exports_logger = {};
|
|
3048
|
-
__export(exports_logger, {
|
|
3049
|
-
resetLogger: () => resetLogger,
|
|
3050
|
-
initLogger: () => initLogger,
|
|
3051
|
-
getSafeLogger: () => getSafeLogger,
|
|
3052
|
-
getLogger: () => getLogger,
|
|
3053
|
-
formatJsonl: () => formatJsonl,
|
|
3054
|
-
formatConsole: () => formatConsole,
|
|
3055
|
-
Logger: () => Logger
|
|
3056
|
-
});
|
|
3057
3047
|
var init_logger2 = __esm(() => {
|
|
3058
3048
|
init_logger();
|
|
3059
3049
|
init_formatters();
|
|
@@ -3304,6 +3294,55 @@ var init_test_strategy = __esm(() => {
|
|
|
3304
3294
|
|
|
3305
3295
|
// src/agents/shared/decompose.ts
|
|
3306
3296
|
function buildDecomposePrompt(options) {
|
|
3297
|
+
if (options.targetStory) {
|
|
3298
|
+
return buildPlanModeDecomposePrompt(options);
|
|
3299
|
+
}
|
|
3300
|
+
return buildSpecDecomposePrompt(options);
|
|
3301
|
+
}
|
|
3302
|
+
function buildPlanModeDecomposePrompt(options) {
|
|
3303
|
+
const targetStory = options.targetStory;
|
|
3304
|
+
const siblings = options.siblings ?? [];
|
|
3305
|
+
const siblingsSummary = siblings.length > 0 ? `
|
|
3306
|
+
## Sibling Stories
|
|
3307
|
+
|
|
3308
|
+
${siblings.map((s) => `- ${s.id}: ${s.title}`).join(`
|
|
3309
|
+
`)}
|
|
3310
|
+
` : "";
|
|
3311
|
+
return `You are a senior software architect decomposing a complex user story into smaller, implementable sub-stories.
|
|
3312
|
+
|
|
3313
|
+
## Target Story
|
|
3314
|
+
|
|
3315
|
+
${JSON.stringify(targetStory, null, 2)}${siblingsSummary}
|
|
3316
|
+
## Codebase Context
|
|
3317
|
+
|
|
3318
|
+
${options.codebaseContext}
|
|
3319
|
+
|
|
3320
|
+
${COMPLEXITY_GUIDE}
|
|
3321
|
+
|
|
3322
|
+
${TEST_STRATEGY_GUIDE}
|
|
3323
|
+
|
|
3324
|
+
${GROUPING_RULES}
|
|
3325
|
+
|
|
3326
|
+
## Output
|
|
3327
|
+
|
|
3328
|
+
Return a JSON array of sub-stories (no markdown code fences, no explanation \u2014 JSON array only):
|
|
3329
|
+
|
|
3330
|
+
[{
|
|
3331
|
+
"id": "string \u2014 e.g. ${targetStory.id}-A",
|
|
3332
|
+
"title": "string",
|
|
3333
|
+
"description": "string",
|
|
3334
|
+
"acceptanceCriteria": ["string \u2014 behavioral, testable criteria"],
|
|
3335
|
+
"contextFiles": ["string \u2014 required, non-empty list of key source files"],
|
|
3336
|
+
"tags": ["string"],
|
|
3337
|
+
"dependencies": ["string"],
|
|
3338
|
+
"complexity": "simple | medium | complex | expert",
|
|
3339
|
+
"reasoning": "string",
|
|
3340
|
+
"estimatedLOC": 0,
|
|
3341
|
+
"risks": ["string"],
|
|
3342
|
+
"testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after"
|
|
3343
|
+
}]`;
|
|
3344
|
+
}
|
|
3345
|
+
function buildSpecDecomposePrompt(options) {
|
|
3307
3346
|
return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
|
|
3308
3347
|
|
|
3309
3348
|
CODEBASE CONTEXT:
|
|
@@ -3614,9 +3653,11 @@ class SpawnAcpSession {
|
|
|
3614
3653
|
try {
|
|
3615
3654
|
proc.stdin?.write(text);
|
|
3616
3655
|
proc.stdin?.end();
|
|
3617
|
-
const exitCode = await
|
|
3618
|
-
|
|
3619
|
-
|
|
3656
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
3657
|
+
proc.exited,
|
|
3658
|
+
new Response(proc.stdout).text(),
|
|
3659
|
+
new Response(proc.stderr).text()
|
|
3660
|
+
]);
|
|
3620
3661
|
if (exitCode !== 0) {
|
|
3621
3662
|
getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
|
|
3622
3663
|
exitCode,
|
|
@@ -3646,6 +3687,21 @@ class SpawnAcpSession {
|
|
|
3646
3687
|
await this.pidRegistry?.unregister(processPid);
|
|
3647
3688
|
}
|
|
3648
3689
|
}
|
|
3690
|
+
async trackedSpawn(cmd, opts) {
|
|
3691
|
+
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe", ...opts });
|
|
3692
|
+
const pid = proc.pid;
|
|
3693
|
+
await this.pidRegistry?.register(pid);
|
|
3694
|
+
try {
|
|
3695
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
3696
|
+
proc.exited,
|
|
3697
|
+
new Response(proc.stdout).text(),
|
|
3698
|
+
new Response(proc.stderr).text()
|
|
3699
|
+
]);
|
|
3700
|
+
return { exitCode, stdout, stderr };
|
|
3701
|
+
} finally {
|
|
3702
|
+
await this.pidRegistry?.unregister(pid);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3649
3705
|
async close(options) {
|
|
3650
3706
|
if (this.activeProc) {
|
|
3651
3707
|
try {
|
|
@@ -3656,10 +3712,8 @@ class SpawnAcpSession {
|
|
|
3656
3712
|
}
|
|
3657
3713
|
const cmd = ["acpx", "--cwd", this.cwd, this.agentName, "sessions", "close", this.sessionName];
|
|
3658
3714
|
getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
|
|
3659
|
-
const
|
|
3660
|
-
const exitCode = await proc.exited;
|
|
3715
|
+
const { exitCode, stderr } = await this.trackedSpawn(cmd);
|
|
3661
3716
|
if (exitCode !== 0) {
|
|
3662
|
-
const stderr = await new Response(proc.stderr).text();
|
|
3663
3717
|
getSafeLogger()?.warn("acp-adapter", "Failed to close session", {
|
|
3664
3718
|
sessionName: this.sessionName,
|
|
3665
3719
|
stderr: stderr.slice(0, 200)
|
|
@@ -3667,8 +3721,7 @@ class SpawnAcpSession {
|
|
|
3667
3721
|
}
|
|
3668
3722
|
if (options?.forceTerminate) {
|
|
3669
3723
|
try {
|
|
3670
|
-
|
|
3671
|
-
await stopProc.exited;
|
|
3724
|
+
await this.trackedSpawn(["acpx", this.agentName, "stop"]);
|
|
3672
3725
|
} catch (err) {
|
|
3673
3726
|
getSafeLogger()?.debug("acp-adapter", "acpx stop failed (swallowed)", { cause: String(err) });
|
|
3674
3727
|
}
|
|
@@ -3683,8 +3736,7 @@ class SpawnAcpSession {
|
|
|
3683
3736
|
}
|
|
3684
3737
|
const cmd = ["acpx", this.agentName, "cancel"];
|
|
3685
3738
|
getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
|
|
3686
|
-
|
|
3687
|
-
await proc.exited;
|
|
3739
|
+
await this.trackedSpawn(cmd);
|
|
3688
3740
|
}
|
|
3689
3741
|
}
|
|
3690
3742
|
|
|
@@ -3710,14 +3762,27 @@ class SpawnAcpClient {
|
|
|
3710
3762
|
this.pidRegistry = pidRegistry;
|
|
3711
3763
|
}
|
|
3712
3764
|
async start() {}
|
|
3765
|
+
async trackedSpawn(cmd) {
|
|
3766
|
+
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
3767
|
+
const pid = proc.pid;
|
|
3768
|
+
await this.pidRegistry?.register(pid);
|
|
3769
|
+
try {
|
|
3770
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
3771
|
+
proc.exited,
|
|
3772
|
+
new Response(proc.stdout).text(),
|
|
3773
|
+
new Response(proc.stderr).text()
|
|
3774
|
+
]);
|
|
3775
|
+
return { exitCode, stdout, stderr };
|
|
3776
|
+
} finally {
|
|
3777
|
+
await this.pidRegistry?.unregister(pid);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3713
3780
|
async createSession(opts) {
|
|
3714
3781
|
const sessionName = opts.sessionName || `nax-${Date.now()}`;
|
|
3715
3782
|
const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "ensure", "--name", sessionName];
|
|
3716
3783
|
getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
|
|
3717
|
-
const
|
|
3718
|
-
const exitCode = await proc.exited;
|
|
3784
|
+
const { exitCode, stderr } = await this.trackedSpawn(cmd);
|
|
3719
3785
|
if (exitCode !== 0) {
|
|
3720
|
-
const stderr = await new Response(proc.stderr).text();
|
|
3721
3786
|
throw new Error(`[acp-adapter] Failed to create session: ${stderr || `exit code ${exitCode}`}`);
|
|
3722
3787
|
}
|
|
3723
3788
|
return new SpawnAcpSession({
|
|
@@ -3733,8 +3798,7 @@ class SpawnAcpClient {
|
|
|
3733
3798
|
}
|
|
3734
3799
|
async loadSession(sessionName, agentName, permissionMode) {
|
|
3735
3800
|
const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
|
|
3736
|
-
const
|
|
3737
|
-
const exitCode = await proc.exited;
|
|
3801
|
+
const { exitCode } = await this.trackedSpawn(cmd);
|
|
3738
3802
|
if (exitCode !== 0) {
|
|
3739
3803
|
return null;
|
|
3740
3804
|
}
|
|
@@ -18082,6 +18146,7 @@ var init_schemas3 = __esm(() => {
|
|
|
18082
18146
|
command: exports_external.string().optional(),
|
|
18083
18147
|
model: exports_external.enum(["fast", "balanced", "powerful"]).default("fast"),
|
|
18084
18148
|
refinement: exports_external.boolean().default(true),
|
|
18149
|
+
refinementConcurrency: exports_external.number().int().min(1).max(10).default(3),
|
|
18085
18150
|
redGate: exports_external.boolean().default(true),
|
|
18086
18151
|
testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
|
|
18087
18152
|
testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional(),
|
|
@@ -18285,8 +18350,6 @@ var init_schemas3 = __esm(() => {
|
|
|
18285
18350
|
maxRectificationAttempts: 3
|
|
18286
18351
|
},
|
|
18287
18352
|
contextProviderTokenBudget: 2000,
|
|
18288
|
-
lintCommand: null,
|
|
18289
|
-
typecheckCommand: null,
|
|
18290
18353
|
dangerouslySkipPermissions: true,
|
|
18291
18354
|
permissionProfile: "unrestricted",
|
|
18292
18355
|
smartTestRunner: true
|
|
@@ -18392,6 +18455,7 @@ var init_schemas3 = __esm(() => {
|
|
|
18392
18455
|
testPath: ".nax-acceptance.test.ts",
|
|
18393
18456
|
model: "fast",
|
|
18394
18457
|
refinement: true,
|
|
18458
|
+
refinementConcurrency: 3,
|
|
18395
18459
|
redGate: true,
|
|
18396
18460
|
timeoutMs: 1800000,
|
|
18397
18461
|
fix: {
|
|
@@ -18641,7 +18705,7 @@ async function readAcpSession(workdir, featureName, storyId) {
|
|
|
18641
18705
|
return null;
|
|
18642
18706
|
}
|
|
18643
18707
|
}
|
|
18644
|
-
async function sweepFeatureSessions(workdir, featureName) {
|
|
18708
|
+
async function sweepFeatureSessions(workdir, featureName, pidRegistry) {
|
|
18645
18709
|
const path = acpSessionsPath(workdir, featureName);
|
|
18646
18710
|
let sessions;
|
|
18647
18711
|
try {
|
|
@@ -18665,7 +18729,7 @@ async function sweepFeatureSessions(workdir, featureName) {
|
|
|
18665
18729
|
}
|
|
18666
18730
|
for (const [agentName, sessionNames] of byAgent) {
|
|
18667
18731
|
const cmdStr = `acpx ${agentName}`;
|
|
18668
|
-
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
18732
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir, undefined, pidRegistry);
|
|
18669
18733
|
try {
|
|
18670
18734
|
await client.start();
|
|
18671
18735
|
for (const sessionName of sessionNames) {
|
|
@@ -18690,7 +18754,7 @@ async function sweepFeatureSessions(workdir, featureName) {
|
|
|
18690
18754
|
logger?.warn("acp-adapter", "[sweep] Failed to clear sidecar after sweep", { error: String(err) });
|
|
18691
18755
|
}
|
|
18692
18756
|
}
|
|
18693
|
-
async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS) {
|
|
18757
|
+
async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS, pidRegistry) {
|
|
18694
18758
|
const path = acpSessionsPath(workdir, featureName);
|
|
18695
18759
|
const file3 = Bun.file(path);
|
|
18696
18760
|
if (!await file3.exists())
|
|
@@ -18702,7 +18766,7 @@ async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SE
|
|
|
18702
18766
|
featureName,
|
|
18703
18767
|
ageMs
|
|
18704
18768
|
});
|
|
18705
|
-
await sweepFeatureSessions(workdir, featureName);
|
|
18769
|
+
await sweepFeatureSessions(workdir, featureName, pidRegistry);
|
|
18706
18770
|
}
|
|
18707
18771
|
function extractOutput(response) {
|
|
18708
18772
|
if (!response)
|
|
@@ -19202,7 +19266,9 @@ class AcpAgentAdapter {
|
|
|
19202
19266
|
jsonMode: true,
|
|
19203
19267
|
config: options.config,
|
|
19204
19268
|
workdir: options.workdir,
|
|
19205
|
-
|
|
19269
|
+
featureName: options.featureName,
|
|
19270
|
+
storyId: options.storyId,
|
|
19271
|
+
sessionRole: options.sessionRole ?? "decompose"
|
|
19206
19272
|
});
|
|
19207
19273
|
output = completeResult.output;
|
|
19208
19274
|
} catch (err) {
|
|
@@ -20040,17 +20106,27 @@ class ClaudeCodeAdapter {
|
|
|
20040
20106
|
cmd.splice(cmd.length - 2, 0, "--dangerously-skip-permissions");
|
|
20041
20107
|
}
|
|
20042
20108
|
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
20109
|
+
const env2 = this.buildAllowedEnv({
|
|
20110
|
+
workdir: options.workdir,
|
|
20111
|
+
modelDef,
|
|
20112
|
+
prompt: "",
|
|
20113
|
+
modelTier: options.modelTier || "balanced",
|
|
20114
|
+
timeoutSeconds: 600
|
|
20115
|
+
});
|
|
20116
|
+
if (options.featureName) {
|
|
20117
|
+
env2.NAX_FEATURE_NAME = options.featureName;
|
|
20118
|
+
}
|
|
20119
|
+
if (options.storyId) {
|
|
20120
|
+
env2.NAX_STORY_ID = options.storyId;
|
|
20121
|
+
}
|
|
20122
|
+
if (options.sessionRole) {
|
|
20123
|
+
env2.NAX_SESSION_ROLE = options.sessionRole;
|
|
20124
|
+
}
|
|
20043
20125
|
const proc = _decomposeDeps.spawn(cmd, {
|
|
20044
20126
|
cwd: options.workdir,
|
|
20045
20127
|
stdout: "pipe",
|
|
20046
20128
|
stderr: "inherit",
|
|
20047
|
-
env:
|
|
20048
|
-
workdir: options.workdir,
|
|
20049
|
-
modelDef,
|
|
20050
|
-
prompt: "",
|
|
20051
|
-
modelTier: options.modelTier || "balanced",
|
|
20052
|
-
timeoutSeconds: 600
|
|
20053
|
-
})
|
|
20129
|
+
env: env2
|
|
20054
20130
|
});
|
|
20055
20131
|
await pidRegistry.register(proc.pid);
|
|
20056
20132
|
const DECOMPOSE_TIMEOUT_MS = 300000;
|
|
@@ -21188,6 +21264,9 @@ function skeletonImportLine(testFramework) {
|
|
|
21188
21264
|
}
|
|
21189
21265
|
return `import { describe, test, expect } from "bun:test";`;
|
|
21190
21266
|
}
|
|
21267
|
+
function hasLikelyTestContent(content) {
|
|
21268
|
+
return /\b(?:describe|test|it|expect)\s*\(/.test(content) || /func\s+Test\w+\s*\(/.test(content) || /def\s+test_\w+/.test(content) || /#\[test\]/.test(content);
|
|
21269
|
+
}
|
|
21191
21270
|
function acceptanceTestFilename(language) {
|
|
21192
21271
|
switch (language?.toLowerCase()) {
|
|
21193
21272
|
case "go":
|
|
@@ -21291,36 +21370,80 @@ Rules:
|
|
|
21291
21370
|
});
|
|
21292
21371
|
if (!testCode) {
|
|
21293
21372
|
const targetPath = join6(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
|
|
21373
|
+
const backupPath = `${targetPath}.llm-recovery.bak`;
|
|
21294
21374
|
let recoveryFailed = false;
|
|
21295
|
-
logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", {
|
|
21375
|
+
logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", {
|
|
21376
|
+
targetPath,
|
|
21377
|
+
backupPath,
|
|
21378
|
+
featureName: options.featureName,
|
|
21379
|
+
workdir: options.workdir
|
|
21380
|
+
});
|
|
21296
21381
|
try {
|
|
21297
21382
|
const existing = await Bun.file(targetPath).text();
|
|
21298
21383
|
const recovered = extractTestCode(existing);
|
|
21384
|
+
const likelyTestContent = hasLikelyTestContent(existing);
|
|
21299
21385
|
logger.debug("acceptance", "BUG-076 recovery: file check result", {
|
|
21300
21386
|
fileSize: existing.length,
|
|
21301
21387
|
extractedCode: recovered !== null,
|
|
21388
|
+
likelyTestContent,
|
|
21302
21389
|
filePreview: existing.slice(0, 300)
|
|
21303
21390
|
});
|
|
21304
21391
|
if (recovered) {
|
|
21305
21392
|
logger.info("acceptance", "Acceptance test written directly by agent \u2014 using existing file", { targetPath });
|
|
21306
21393
|
testCode = recovered;
|
|
21394
|
+
} else if (existing.trim().length > 0 && likelyTestContent) {
|
|
21395
|
+
let backupCreated = false;
|
|
21396
|
+
try {
|
|
21397
|
+
await _generatorPRDDeps.backupFile(backupPath, existing);
|
|
21398
|
+
backupCreated = true;
|
|
21399
|
+
} catch (backupError) {
|
|
21400
|
+
logger.warn("acceptance", "BUG-076: failed to create recovery backup; preserving file anyway", {
|
|
21401
|
+
targetPath,
|
|
21402
|
+
backupPath,
|
|
21403
|
+
backupError: backupError instanceof Error ? backupError.message : String(backupError)
|
|
21404
|
+
});
|
|
21405
|
+
}
|
|
21406
|
+
logger.warn("acceptance", "BUG-076: preserving agent-written file with backup (heuristic recovery)", {
|
|
21407
|
+
targetPath,
|
|
21408
|
+
backupPath,
|
|
21409
|
+
backupCreated,
|
|
21410
|
+
reason: "extractTestCode returned null"
|
|
21411
|
+
});
|
|
21412
|
+
testCode = existing;
|
|
21307
21413
|
} else {
|
|
21414
|
+
if (existing.trim().length > 0) {
|
|
21415
|
+
try {
|
|
21416
|
+
await _generatorPRDDeps.backupFile(backupPath, existing);
|
|
21417
|
+
} catch (backupError) {
|
|
21418
|
+
logger.warn("acceptance", "BUG-076: failed to create fallback backup for unrecognized file", {
|
|
21419
|
+
targetPath,
|
|
21420
|
+
backupPath,
|
|
21421
|
+
backupError: backupError instanceof Error ? backupError.message : String(backupError)
|
|
21422
|
+
});
|
|
21423
|
+
}
|
|
21424
|
+
}
|
|
21308
21425
|
recoveryFailed = true;
|
|
21309
|
-
logger.error("acceptance", "BUG-076:
|
|
21426
|
+
logger.error("acceptance", "BUG-076: agent-written file not recognized as test code \u2014 falling back to skeleton", {
|
|
21310
21427
|
targetPath,
|
|
21428
|
+
backupPath,
|
|
21429
|
+
fileSize: existing.length,
|
|
21311
21430
|
filePreview: existing.slice(0, 300)
|
|
21312
21431
|
});
|
|
21313
21432
|
}
|
|
21314
|
-
} catch {
|
|
21433
|
+
} catch (error48) {
|
|
21315
21434
|
recoveryFailed = true;
|
|
21316
|
-
logger.debug("acceptance", "BUG-076 recovery:
|
|
21435
|
+
logger.debug("acceptance", "BUG-076 recovery: failed to read agent-written file, falling back to skeleton", {
|
|
21317
21436
|
targetPath,
|
|
21437
|
+
backupPath,
|
|
21438
|
+
error: error48 instanceof Error ? error48.message : String(error48),
|
|
21318
21439
|
rawOutputPreview: rawOutput.slice(0, 500)
|
|
21319
21440
|
});
|
|
21320
21441
|
}
|
|
21321
21442
|
if (recoveryFailed) {
|
|
21322
|
-
logger.error("acceptance", "BUG-076: LLM returned non-code output and
|
|
21323
|
-
rawOutputPreview: rawOutput.slice(0, 500)
|
|
21443
|
+
logger.error("acceptance", "BUG-076: LLM returned non-code output and recovery could not produce runnable tests \u2014 falling back to skeleton", {
|
|
21444
|
+
rawOutputPreview: rawOutput.slice(0, 500),
|
|
21445
|
+
targetPath,
|
|
21446
|
+
backupPath
|
|
21324
21447
|
});
|
|
21325
21448
|
}
|
|
21326
21449
|
}
|
|
@@ -21586,6 +21709,9 @@ var init_generator = __esm(() => {
|
|
|
21586
21709
|
},
|
|
21587
21710
|
writeFile: async (path, content) => {
|
|
21588
21711
|
await Bun.write(path, content);
|
|
21712
|
+
},
|
|
21713
|
+
backupFile: async (path, content) => {
|
|
21714
|
+
await Bun.write(path, content);
|
|
21589
21715
|
}
|
|
21590
21716
|
};
|
|
21591
21717
|
});
|
|
@@ -22342,7 +22468,7 @@ var package_default;
|
|
|
22342
22468
|
var init_package = __esm(() => {
|
|
22343
22469
|
package_default = {
|
|
22344
22470
|
name: "@nathapp/nax",
|
|
22345
|
-
version: "0.57.
|
|
22471
|
+
version: "0.57.3",
|
|
22346
22472
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22347
22473
|
type: "module",
|
|
22348
22474
|
bin: {
|
|
@@ -22421,8 +22547,8 @@ var init_version = __esm(() => {
|
|
|
22421
22547
|
NAX_VERSION = package_default.version;
|
|
22422
22548
|
NAX_COMMIT = (() => {
|
|
22423
22549
|
try {
|
|
22424
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22425
|
-
return "
|
|
22550
|
+
if (/^[0-9a-f]{6,10}$/.test("166deae0"))
|
|
22551
|
+
return "166deae0";
|
|
22426
22552
|
} catch {}
|
|
22427
22553
|
try {
|
|
22428
22554
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -25043,7 +25169,7 @@ var init_version_detection = __esm(() => {
|
|
|
25043
25169
|
// src/precheck/checks-agents.ts
|
|
25044
25170
|
async function checkMultiAgentHealth() {
|
|
25045
25171
|
try {
|
|
25046
|
-
const versions2 = await getAgentVersions();
|
|
25172
|
+
const versions2 = await _checkAgentsDeps.getAgentVersions();
|
|
25047
25173
|
const installed = versions2.filter((v) => v.installed);
|
|
25048
25174
|
const notInstalled = versions2.filter((v) => !v.installed);
|
|
25049
25175
|
const lines = [];
|
|
@@ -25080,8 +25206,12 @@ Available but not installed (${notInstalled.length}):`);
|
|
|
25080
25206
|
};
|
|
25081
25207
|
}
|
|
25082
25208
|
}
|
|
25209
|
+
var _checkAgentsDeps;
|
|
25083
25210
|
var init_checks_agents = __esm(() => {
|
|
25084
25211
|
init_version_detection();
|
|
25212
|
+
_checkAgentsDeps = {
|
|
25213
|
+
getAgentVersions
|
|
25214
|
+
};
|
|
25085
25215
|
});
|
|
25086
25216
|
|
|
25087
25217
|
// src/precheck/checks.ts
|
|
@@ -26252,9 +26382,12 @@ ${stderr}` };
|
|
|
26252
26382
|
const agent = (ctx.agentGetFn ?? _acceptanceSetupDeps.getAgent)(ctx.config.autoMode.defaultAgent);
|
|
26253
26383
|
let allRefinedCriteria;
|
|
26254
26384
|
if (ctx.config.acceptance.refinement) {
|
|
26255
|
-
|
|
26256
|
-
|
|
26257
|
-
|
|
26385
|
+
const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
|
|
26386
|
+
const results = new Array(nonFixStories.length);
|
|
26387
|
+
const executing = new Set;
|
|
26388
|
+
for (let i = 0;i < nonFixStories.length; i++) {
|
|
26389
|
+
const story = nonFixStories[i];
|
|
26390
|
+
const task = _acceptanceSetupDeps.refine(story.acceptanceCriteria, {
|
|
26258
26391
|
storyId: story.id,
|
|
26259
26392
|
featureName: ctx.prd.feature,
|
|
26260
26393
|
workdir: ctx.workdir,
|
|
@@ -26262,9 +26395,18 @@ ${stderr}` };
|
|
|
26262
26395
|
config: ctx.config,
|
|
26263
26396
|
testStrategy: ctx.config.acceptance.testStrategy,
|
|
26264
26397
|
testFramework: ctx.config.acceptance.testFramework
|
|
26398
|
+
}).then((refined) => {
|
|
26399
|
+
results[i] = refined;
|
|
26400
|
+
}).finally(() => {
|
|
26401
|
+
executing.delete(task);
|
|
26265
26402
|
});
|
|
26266
|
-
|
|
26403
|
+
executing.add(task);
|
|
26404
|
+
if (executing.size >= maxConcurrency) {
|
|
26405
|
+
await Promise.race(executing);
|
|
26406
|
+
}
|
|
26267
26407
|
}
|
|
26408
|
+
await Promise.all(executing);
|
|
26409
|
+
allRefinedCriteria = results.flat();
|
|
26268
26410
|
} else {
|
|
26269
26411
|
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
26270
26412
|
original: c,
|
|
@@ -26593,6 +26735,93 @@ var init_event_bus = __esm(() => {
|
|
|
26593
26735
|
pipelineEventBus = new PipelineEventBus;
|
|
26594
26736
|
});
|
|
26595
26737
|
|
|
26738
|
+
// src/pipeline/stages/autofix-prompts.ts
|
|
26739
|
+
function formatCheckErrors(checks3) {
|
|
26740
|
+
return checks3.map((c) => `## ${c.check} errors (exit code ${c.exitCode})
|
|
26741
|
+
\`\`\`
|
|
26742
|
+
${c.output}
|
|
26743
|
+
\`\`\``).join(`
|
|
26744
|
+
|
|
26745
|
+
`);
|
|
26746
|
+
}
|
|
26747
|
+
function buildSemanticRectificationPrompt(semanticChecks, story, scopeConstraint) {
|
|
26748
|
+
const errors3 = formatCheckErrors(semanticChecks);
|
|
26749
|
+
const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
|
|
26750
|
+
`);
|
|
26751
|
+
return `You are fixing acceptance criteria compliance issues found during semantic review.
|
|
26752
|
+
|
|
26753
|
+
Story: ${story.title} (${story.id})
|
|
26754
|
+
|
|
26755
|
+
### Acceptance Criteria
|
|
26756
|
+
${acList}
|
|
26757
|
+
|
|
26758
|
+
### Semantic Review Findings
|
|
26759
|
+
${errors3}
|
|
26760
|
+
|
|
26761
|
+
**Important:** The semantic reviewer only analyzed the git diff and may have flagged false positives (e.g., claiming a key or function is "missing" when it already exists in the codebase). Before making any changes:
|
|
26762
|
+
1. Read the relevant files to verify each finding is a real issue
|
|
26763
|
+
2. Only fix findings that are actually valid problems
|
|
26764
|
+
3. Do NOT add keys, functions, or imports that already exist \u2014 check first
|
|
26765
|
+
|
|
26766
|
+
Do NOT change test files or test behavior.
|
|
26767
|
+
Do NOT add new features \u2014 only fix valid issues.
|
|
26768
|
+
Commit your fixes when done.${scopeConstraint}`;
|
|
26769
|
+
}
|
|
26770
|
+
function buildMechanicalRectificationPrompt(mechanicalChecks, story, scopeConstraint) {
|
|
26771
|
+
const errors3 = formatCheckErrors(mechanicalChecks);
|
|
26772
|
+
return `You are fixing lint/typecheck errors from a code review.
|
|
26773
|
+
|
|
26774
|
+
Story: ${story.title} (${story.id})
|
|
26775
|
+
|
|
26776
|
+
The following quality checks failed after implementation:
|
|
26777
|
+
|
|
26778
|
+
${errors3}
|
|
26779
|
+
|
|
26780
|
+
Fix ALL errors listed above. Do NOT change test files or test behavior.
|
|
26781
|
+
Do NOT add new features \u2014 only fix the quality check errors.
|
|
26782
|
+
Commit your fixes when done.${scopeConstraint}`;
|
|
26783
|
+
}
|
|
26784
|
+
function buildReviewRectificationPrompt(failedChecks, story) {
|
|
26785
|
+
const scopeConstraint = story.workdir ? `
|
|
26786
|
+
|
|
26787
|
+
IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
|
|
26788
|
+
const semanticChecks = failedChecks.filter((c) => c.check === "semantic");
|
|
26789
|
+
const mechanicalChecks = failedChecks.filter((c) => c.check !== "semantic");
|
|
26790
|
+
if (semanticChecks.length > 0 && mechanicalChecks.length === 0) {
|
|
26791
|
+
return buildSemanticRectificationPrompt(semanticChecks, story, scopeConstraint);
|
|
26792
|
+
}
|
|
26793
|
+
if (mechanicalChecks.length > 0 && semanticChecks.length === 0) {
|
|
26794
|
+
return buildMechanicalRectificationPrompt(mechanicalChecks, story, scopeConstraint);
|
|
26795
|
+
}
|
|
26796
|
+
const mechanicalSection = formatCheckErrors(mechanicalChecks);
|
|
26797
|
+
const semanticSection = formatCheckErrors(semanticChecks);
|
|
26798
|
+
const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
|
|
26799
|
+
`);
|
|
26800
|
+
return `You are fixing issues from a code review.
|
|
26801
|
+
|
|
26802
|
+
Story: ${story.title} (${story.id})
|
|
26803
|
+
|
|
26804
|
+
## Lint/Typecheck Errors
|
|
26805
|
+
|
|
26806
|
+
${mechanicalSection}
|
|
26807
|
+
|
|
26808
|
+
Fix ALL lint/typecheck errors listed above.
|
|
26809
|
+
|
|
26810
|
+
## Semantic Review Findings (AC Compliance)
|
|
26811
|
+
|
|
26812
|
+
### Acceptance Criteria
|
|
26813
|
+
${acList}
|
|
26814
|
+
|
|
26815
|
+
### Findings
|
|
26816
|
+
${semanticSection}
|
|
26817
|
+
|
|
26818
|
+
**Important:** The semantic reviewer may have flagged false positives. Before making changes for semantic findings, read the relevant files to verify each finding is a real issue. Do NOT add keys, functions, or imports that already exist.
|
|
26819
|
+
|
|
26820
|
+
Do NOT change test files or test behavior.
|
|
26821
|
+
Do NOT add new features \u2014 only fix the identified issues.
|
|
26822
|
+
Commit your fixes when done.${scopeConstraint}`;
|
|
26823
|
+
}
|
|
26824
|
+
|
|
26596
26825
|
// src/utils/git.ts
|
|
26597
26826
|
async function gitWithTimeout(args, workdir) {
|
|
26598
26827
|
const proc = _gitDeps.spawn(["git", ...args], {
|
|
@@ -26853,7 +27082,7 @@ ${stat}
|
|
|
26853
27082
|
return `${statPreamble}${truncated}
|
|
26854
27083
|
... (truncated at ${DIFF_CAP_BYTES} bytes, showing ${visibleFiles}/${totalFiles} files)`;
|
|
26855
27084
|
}
|
|
26856
|
-
function buildPrompt(story, semanticConfig, diff) {
|
|
27085
|
+
function buildPrompt(story, semanticConfig, diff, stat) {
|
|
26857
27086
|
const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
|
|
26858
27087
|
`);
|
|
26859
27088
|
const customRulesSection = semanticConfig.rules.length > 0 ? `
|
|
@@ -26861,7 +27090,7 @@ function buildPrompt(story, semanticConfig, diff) {
|
|
|
26861
27090
|
${semanticConfig.rules.map((r, i) => `${i + 1}. ${r}`).join(`
|
|
26862
27091
|
`)}
|
|
26863
27092
|
` : "";
|
|
26864
|
-
return `You are a semantic code reviewer. Your job is to verify that the implementation satisfies the story's acceptance criteria (ACs). You are NOT a linter or style checker \u2014 lint, typecheck, and convention checks are handled separately.
|
|
27093
|
+
return `You are a semantic code reviewer with access to the repository files. Your job is to verify that the implementation satisfies the story's acceptance criteria (ACs). You are NOT a linter or style checker \u2014 lint, typecheck, and convention checks are handled separately.
|
|
26865
27094
|
|
|
26866
27095
|
## Story: ${story.title}
|
|
26867
27096
|
|
|
@@ -26878,20 +27107,28 @@ ${diff}\`\`\`
|
|
|
26878
27107
|
|
|
26879
27108
|
## Instructions
|
|
26880
27109
|
|
|
26881
|
-
For each acceptance criterion, verify the diff implements it correctly.
|
|
26882
|
-
|
|
27110
|
+
For each acceptance criterion, verify the diff implements it correctly.
|
|
27111
|
+
|
|
27112
|
+
**Before reporting any finding as "error", you MUST verify it using your tools:**
|
|
27113
|
+
- If you suspect a key, function, import, or variable is missing, READ the relevant file to confirm before flagging.
|
|
27114
|
+
- If you suspect a code path is not wired in, GREP for its usage to confirm.
|
|
27115
|
+
- Do NOT flag something as missing based solely on its absence from the diff \u2014 it may already exist in the codebase. Check the actual file first.
|
|
27116
|
+
- If you cannot verify a claim even after checking, use "unverifiable" severity instead of "error".
|
|
27117
|
+
|
|
27118
|
+
Flag issues only when you have confirmed:
|
|
27119
|
+
1. An AC is not implemented or partially implemented (verified by reading the actual files)
|
|
26883
27120
|
2. The implementation contradicts what the AC specifies
|
|
26884
27121
|
3. New code has dead paths that will never execute (stubs, noops, unreachable branches)
|
|
26885
|
-
4. New code is not wired into callers/exports (
|
|
27122
|
+
4. New code is not wired into callers/exports (verified by grepping for usage)
|
|
26886
27123
|
|
|
26887
27124
|
Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.
|
|
26888
27125
|
|
|
26889
|
-
Respond
|
|
27126
|
+
Respond with JSON only \u2014 no explanation text before or after:
|
|
26890
27127
|
{
|
|
26891
27128
|
"passed": boolean,
|
|
26892
27129
|
"findings": [
|
|
26893
27130
|
{
|
|
26894
|
-
"severity": "error" | "warn" | "info",
|
|
27131
|
+
"severity": "error" | "warn" | "info" | "unverifiable",
|
|
26895
27132
|
"file": "path/to/file",
|
|
26896
27133
|
"line": 42,
|
|
26897
27134
|
"issue": "description of the issue",
|
|
@@ -26929,10 +27166,15 @@ function formatFindings(findings) {
|
|
|
26929
27166
|
function normalizeSeverity(sev) {
|
|
26930
27167
|
if (sev === "warn")
|
|
26931
27168
|
return "warning";
|
|
27169
|
+
if (sev === "unverifiable")
|
|
27170
|
+
return "info";
|
|
26932
27171
|
if (sev === "critical" || sev === "error" || sev === "warning" || sev === "info" || sev === "low")
|
|
26933
27172
|
return sev;
|
|
26934
27173
|
return "info";
|
|
26935
27174
|
}
|
|
27175
|
+
function isBlockingSeverity(sev) {
|
|
27176
|
+
return sev !== "unverifiable";
|
|
27177
|
+
}
|
|
26936
27178
|
function toReviewFindings(findings) {
|
|
26937
27179
|
return findings.map((f) => ({
|
|
26938
27180
|
ruleId: "semantic",
|
|
@@ -26975,10 +27217,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
26975
27217
|
modelTier: semanticConfig.modelTier,
|
|
26976
27218
|
configProvided: !!naxConfig
|
|
26977
27219
|
});
|
|
26978
|
-
const rawDiff = await
|
|
26979
|
-
|
|
26980
|
-
|
|
26981
|
-
|
|
27220
|
+
const [rawDiff, stat] = await Promise.all([
|
|
27221
|
+
collectDiff(workdir, effectiveRef, semanticConfig.excludePatterns),
|
|
27222
|
+
collectDiffStat(workdir, effectiveRef)
|
|
27223
|
+
]);
|
|
27224
|
+
const diff = truncateDiff(rawDiff, rawDiff.length > DIFF_CAP_BYTES ? stat : undefined);
|
|
26982
27225
|
if (!diff) {
|
|
26983
27226
|
return {
|
|
26984
27227
|
check: "semantic",
|
|
@@ -27003,7 +27246,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
27003
27246
|
durationMs: Date.now() - startTime
|
|
27004
27247
|
};
|
|
27005
27248
|
}
|
|
27006
|
-
const prompt = buildPrompt(story, semanticConfig, diff);
|
|
27249
|
+
const prompt = buildPrompt(story, semanticConfig, diff, stat || undefined);
|
|
27007
27250
|
const reviewDebateEnabled = naxConfig?.debate?.enabled && naxConfig?.debate?.stages?.review?.enabled;
|
|
27008
27251
|
if (reviewDebateEnabled) {
|
|
27009
27252
|
const reviewStageConfig = naxConfig?.debate?.stages.review;
|
|
@@ -27042,10 +27285,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
27042
27285
|
deduped.push(f);
|
|
27043
27286
|
}
|
|
27044
27287
|
}
|
|
27288
|
+
const debateBlocking = deduped.filter((f) => isBlockingSeverity(f.severity));
|
|
27045
27289
|
const durationMs2 = Date.now() - startTime;
|
|
27046
27290
|
if (!majorityPassed) {
|
|
27047
|
-
if (
|
|
27048
|
-
logger?.warn("review", `Semantic review failed (debate): ${
|
|
27291
|
+
if (debateBlocking.length > 0) {
|
|
27292
|
+
logger?.warn("review", `Semantic review failed (debate): ${debateBlocking.length} findings`, {
|
|
27049
27293
|
storyId: story.id,
|
|
27050
27294
|
durationMs: durationMs2
|
|
27051
27295
|
});
|
|
@@ -27056,17 +27300,21 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
27056
27300
|
exitCode: 1,
|
|
27057
27301
|
output: `Semantic review failed:
|
|
27058
27302
|
|
|
27059
|
-
${formatFindings(
|
|
27303
|
+
${formatFindings(debateBlocking)}`,
|
|
27060
27304
|
durationMs: durationMs2,
|
|
27061
|
-
findings: toReviewFindings(
|
|
27305
|
+
findings: toReviewFindings(debateBlocking)
|
|
27062
27306
|
};
|
|
27063
27307
|
}
|
|
27308
|
+
logger?.info("review", "Semantic review passed (debate, all findings non-blocking)", {
|
|
27309
|
+
storyId: story.id,
|
|
27310
|
+
durationMs: durationMs2
|
|
27311
|
+
});
|
|
27064
27312
|
return {
|
|
27065
27313
|
check: "semantic",
|
|
27066
|
-
success:
|
|
27314
|
+
success: true,
|
|
27067
27315
|
command: "",
|
|
27068
|
-
exitCode:
|
|
27069
|
-
output: "Semantic review
|
|
27316
|
+
exitCode: 0,
|
|
27317
|
+
output: "Semantic review passed (debate, all findings were unverifiable or informational)",
|
|
27070
27318
|
durationMs: durationMs2
|
|
27071
27319
|
};
|
|
27072
27320
|
}
|
|
@@ -27127,15 +27375,23 @@ ${formatFindings(deduped)}`,
|
|
|
27127
27375
|
durationMs: Date.now() - startTime
|
|
27128
27376
|
};
|
|
27129
27377
|
}
|
|
27130
|
-
|
|
27378
|
+
const blockingFindings = parsed.findings.filter((f) => isBlockingSeverity(f.severity));
|
|
27379
|
+
const nonBlockingFindings = parsed.findings.filter((f) => !isBlockingSeverity(f.severity));
|
|
27380
|
+
if (nonBlockingFindings.length > 0) {
|
|
27381
|
+
logger?.debug("review", `Semantic review: ${nonBlockingFindings.length} non-blocking findings (unverifiable/info)`, {
|
|
27382
|
+
storyId: story.id,
|
|
27383
|
+
findings: nonBlockingFindings.map((f) => ({ severity: f.severity, file: f.file, issue: f.issue }))
|
|
27384
|
+
});
|
|
27385
|
+
}
|
|
27386
|
+
if (!parsed.passed && blockingFindings.length > 0) {
|
|
27131
27387
|
const durationMs2 = Date.now() - startTime;
|
|
27132
|
-
logger?.warn("review", `Semantic review failed: ${
|
|
27388
|
+
logger?.warn("review", `Semantic review failed: ${blockingFindings.length} findings`, {
|
|
27133
27389
|
storyId: story.id,
|
|
27134
27390
|
durationMs: durationMs2
|
|
27135
27391
|
});
|
|
27136
27392
|
logger?.debug("review", "Semantic review findings", {
|
|
27137
27393
|
storyId: story.id,
|
|
27138
|
-
findings:
|
|
27394
|
+
findings: blockingFindings.map((f) => ({
|
|
27139
27395
|
severity: f.severity,
|
|
27140
27396
|
file: f.file,
|
|
27141
27397
|
line: f.line,
|
|
@@ -27145,7 +27401,7 @@ ${formatFindings(deduped)}`,
|
|
|
27145
27401
|
});
|
|
27146
27402
|
const output = `Semantic review failed:
|
|
27147
27403
|
|
|
27148
|
-
${formatFindings(
|
|
27404
|
+
${formatFindings(blockingFindings)}`;
|
|
27149
27405
|
return {
|
|
27150
27406
|
check: "semantic",
|
|
27151
27407
|
success: false,
|
|
@@ -27153,7 +27409,19 @@ ${formatFindings(parsed.findings)}`;
|
|
|
27153
27409
|
exitCode: 1,
|
|
27154
27410
|
output,
|
|
27155
27411
|
durationMs: durationMs2,
|
|
27156
|
-
findings: toReviewFindings(
|
|
27412
|
+
findings: toReviewFindings(blockingFindings)
|
|
27413
|
+
};
|
|
27414
|
+
}
|
|
27415
|
+
if (!parsed.passed && blockingFindings.length === 0) {
|
|
27416
|
+
const durationMs2 = Date.now() - startTime;
|
|
27417
|
+
logger?.info("review", "Semantic review passed (all findings non-blocking)", { storyId: story.id, durationMs: durationMs2 });
|
|
27418
|
+
return {
|
|
27419
|
+
check: "semantic",
|
|
27420
|
+
success: true,
|
|
27421
|
+
command: "",
|
|
27422
|
+
exitCode: 0,
|
|
27423
|
+
output: "Semantic review passed (all findings were unverifiable or informational)",
|
|
27424
|
+
durationMs: durationMs2
|
|
27157
27425
|
};
|
|
27158
27426
|
}
|
|
27159
27427
|
const durationMs = Date.now() - startTime;
|
|
@@ -27560,28 +27828,6 @@ async function recheckReview(ctx) {
|
|
|
27560
27828
|
function collectFailedChecks(ctx) {
|
|
27561
27829
|
return (ctx.reviewResult?.checks ?? []).filter((c) => !c.success);
|
|
27562
27830
|
}
|
|
27563
|
-
function buildReviewRectificationPrompt(failedChecks, story) {
|
|
27564
|
-
const errors3 = failedChecks.map((c) => `## ${c.check} errors (exit code ${c.exitCode})
|
|
27565
|
-
\`\`\`
|
|
27566
|
-
${c.output}
|
|
27567
|
-
\`\`\``).join(`
|
|
27568
|
-
|
|
27569
|
-
`);
|
|
27570
|
-
const scopeConstraint = story.workdir ? `
|
|
27571
|
-
|
|
27572
|
-
IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
|
|
27573
|
-
return `You are fixing lint/typecheck errors from a code review.
|
|
27574
|
-
|
|
27575
|
-
Story: ${story.title} (${story.id})
|
|
27576
|
-
|
|
27577
|
-
The following quality checks failed after implementation:
|
|
27578
|
-
|
|
27579
|
-
${errors3}
|
|
27580
|
-
|
|
27581
|
-
Fix ALL errors listed above. Do NOT change test files or test behavior.
|
|
27582
|
-
Do NOT add new features \u2014 only fix the quality check errors.
|
|
27583
|
-
Commit your fixes when done.${scopeConstraint}`;
|
|
27584
|
-
}
|
|
27585
27831
|
function buildAutofixEscalationPreamble(attempt, maxAttempts, rethinkAtAttempt, urgencyAtAttempt) {
|
|
27586
27832
|
return buildProgressivePromptPreamble({
|
|
27587
27833
|
attempt,
|
|
@@ -29164,35 +29410,12 @@ async function isGreenfieldStory(story, workdir, testPattern = "**/*.{test,spec}
|
|
|
29164
29410
|
}
|
|
29165
29411
|
var init_greenfield = () => {};
|
|
29166
29412
|
// src/verification/executor.ts
|
|
29167
|
-
|
|
29168
|
-
const
|
|
29169
|
-
const
|
|
29170
|
-
|
|
29171
|
-
|
|
29172
|
-
|
|
29173
|
-
});
|
|
29174
|
-
return Promise.race([p, timeoutPromise]).finally(() => clearTimeout(timerId));
|
|
29175
|
-
};
|
|
29176
|
-
let out = "";
|
|
29177
|
-
try {
|
|
29178
|
-
const stdout = race(new Response(proc.stdout).text());
|
|
29179
|
-
const stderr = race(new Response(proc.stderr).text());
|
|
29180
|
-
const [o, e] = await Promise.all([stdout, stderr]);
|
|
29181
|
-
if (o !== EMPTY)
|
|
29182
|
-
out += o;
|
|
29183
|
-
if (e !== EMPTY)
|
|
29184
|
-
out += (out ? `
|
|
29185
|
-
` : "") + e;
|
|
29186
|
-
} catch (error48) {
|
|
29187
|
-
const isExpectedStreamError = error48 instanceof TypeError || error48 instanceof Error && /abort|cancel|close|destroy|locked/i.test(error48.message);
|
|
29188
|
-
if (!isExpectedStreamError) {
|
|
29189
|
-
const { getSafeLogger: getSafeLogger4 } = await Promise.resolve().then(() => (init_logger2(), exports_logger));
|
|
29190
|
-
getSafeLogger4()?.debug("executor", "Unexpected error draining process output", {
|
|
29191
|
-
error: errorMessage(error48)
|
|
29192
|
-
});
|
|
29193
|
-
}
|
|
29194
|
-
}
|
|
29195
|
-
return out;
|
|
29413
|
+
function raceWithDeadline(p, deadlineMs) {
|
|
29414
|
+
const timer = { id: undefined };
|
|
29415
|
+
const timeoutP = new Promise((r) => {
|
|
29416
|
+
timer.id = setTimeout(() => r(DRAIN_TIMEOUT), deadlineMs);
|
|
29417
|
+
});
|
|
29418
|
+
return Promise.race([p, timeoutP]).finally(() => clearTimeout(timer.id));
|
|
29196
29419
|
}
|
|
29197
29420
|
function normalizeEnvironment(env2, stripVars) {
|
|
29198
29421
|
const normalized = { ...env2 };
|
|
@@ -29212,6 +29435,8 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
|
|
|
29212
29435
|
env: env2 || normalizeEnvironment(process.env),
|
|
29213
29436
|
cwd: options?.cwd
|
|
29214
29437
|
});
|
|
29438
|
+
const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
|
|
29439
|
+
const stderrPromise = new Response(proc.stderr).text().catch(() => "");
|
|
29215
29440
|
const timeoutMs = timeoutSeconds * 1000;
|
|
29216
29441
|
let timedOut = false;
|
|
29217
29442
|
const timer = { id: undefined };
|
|
@@ -29229,20 +29454,25 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
|
|
|
29229
29454
|
killProcessGroup(pid, "SIGTERM");
|
|
29230
29455
|
await Bun.sleep(gracePeriodMs);
|
|
29231
29456
|
killProcessGroup(pid, "SIGKILL");
|
|
29232
|
-
const
|
|
29457
|
+
const [out, err] = await Promise.all([
|
|
29458
|
+
raceWithDeadline(stdoutPromise, drainTimeoutMs),
|
|
29459
|
+
raceWithDeadline(stderrPromise, drainTimeoutMs)
|
|
29460
|
+
]);
|
|
29461
|
+
const parts = [out !== DRAIN_TIMEOUT ? out : "", err !== DRAIN_TIMEOUT ? err : ""].filter(Boolean);
|
|
29462
|
+
const partialOutput = parts.join(`
|
|
29463
|
+
`) || undefined;
|
|
29233
29464
|
return {
|
|
29234
29465
|
success: false,
|
|
29235
29466
|
timeout: true,
|
|
29236
29467
|
killed: true,
|
|
29237
29468
|
childProcessesKilled: true,
|
|
29238
|
-
output: partialOutput
|
|
29469
|
+
output: partialOutput,
|
|
29239
29470
|
error: `EXECUTION_TIMEOUT: Verification process exceeded ${timeoutSeconds}s. Process group (PID ${pid}) killed.`,
|
|
29240
29471
|
countsTowardEscalation: false
|
|
29241
29472
|
};
|
|
29242
29473
|
}
|
|
29243
|
-
const exitCode = raceResult;
|
|
29244
|
-
const stdout = await
|
|
29245
|
-
const stderr = await new Response(proc.stderr).text();
|
|
29474
|
+
const exitCode = typeof raceResult === "number" ? raceResult : 0;
|
|
29475
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
29246
29476
|
const output = `${stdout}
|
|
29247
29477
|
${stderr}`;
|
|
29248
29478
|
return {
|
|
@@ -29288,10 +29518,11 @@ function buildTestCommand(baseCommand, options) {
|
|
|
29288
29518
|
}
|
|
29289
29519
|
return command;
|
|
29290
29520
|
}
|
|
29291
|
-
var _executorDeps, DEFAULT_STRIP_ENV_VARS;
|
|
29521
|
+
var _executorDeps, DRAIN_TIMEOUT, DEFAULT_STRIP_ENV_VARS;
|
|
29292
29522
|
var init_executor = __esm(() => {
|
|
29293
29523
|
init_bun_deps();
|
|
29294
29524
|
_executorDeps = { spawn };
|
|
29525
|
+
DRAIN_TIMEOUT = Symbol("drain-timeout");
|
|
29295
29526
|
DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
|
|
29296
29527
|
});
|
|
29297
29528
|
|
|
@@ -34435,12 +34666,12 @@ function createSignalHandler(ctx) {
|
|
|
34435
34666
|
hardDeadline.unref();
|
|
34436
34667
|
const logger = getSafeLogger();
|
|
34437
34668
|
logger?.error("crash-recovery", `Received ${signal}, shutting down...`, { signal });
|
|
34438
|
-
if (ctx.pidRegistry) {
|
|
34439
|
-
await ctx.pidRegistry.killAll();
|
|
34440
|
-
}
|
|
34441
34669
|
if (ctx.onShutdown) {
|
|
34442
34670
|
await ctx.onShutdown().catch(() => {});
|
|
34443
34671
|
}
|
|
34672
|
+
if (ctx.pidRegistry) {
|
|
34673
|
+
await ctx.pidRegistry.killAll();
|
|
34674
|
+
}
|
|
34444
34675
|
ctx.emitError?.(signal.toLowerCase());
|
|
34445
34676
|
await writeFatalLog(ctx.jsonlFilePath, signal);
|
|
34446
34677
|
await writeRunComplete(ctx, signal.toLowerCase());
|
|
@@ -34460,12 +34691,12 @@ ${error48.stack ?? ""}
|
|
|
34460
34691
|
error: error48.message,
|
|
34461
34692
|
stack: error48.stack
|
|
34462
34693
|
});
|
|
34463
|
-
if (ctx.pidRegistry) {
|
|
34464
|
-
await ctx.pidRegistry.killAll();
|
|
34465
|
-
}
|
|
34466
34694
|
if (ctx.onShutdown) {
|
|
34467
34695
|
await ctx.onShutdown().catch(() => {});
|
|
34468
34696
|
}
|
|
34697
|
+
if (ctx.pidRegistry) {
|
|
34698
|
+
await ctx.pidRegistry.killAll();
|
|
34699
|
+
}
|
|
34469
34700
|
ctx.emitError?.("uncaughtException");
|
|
34470
34701
|
await writeFatalLog(ctx.jsonlFilePath, "uncaughtException", error48);
|
|
34471
34702
|
await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "uncaughtException", ctx.featureDir);
|
|
@@ -34484,12 +34715,12 @@ ${error48.stack ?? ""}
|
|
|
34484
34715
|
error: error48.message,
|
|
34485
34716
|
stack: error48.stack
|
|
34486
34717
|
});
|
|
34487
|
-
if (ctx.pidRegistry) {
|
|
34488
|
-
await ctx.pidRegistry.killAll();
|
|
34489
|
-
}
|
|
34490
34718
|
if (ctx.onShutdown) {
|
|
34491
34719
|
await ctx.onShutdown().catch(() => {});
|
|
34492
34720
|
}
|
|
34721
|
+
if (ctx.pidRegistry) {
|
|
34722
|
+
await ctx.pidRegistry.killAll();
|
|
34723
|
+
}
|
|
34493
34724
|
ctx.emitError?.("unhandledRejection");
|
|
34494
34725
|
await writeFatalLog(ctx.jsonlFilePath, "unhandledRejection", error48);
|
|
34495
34726
|
await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "unhandledRejection", ctx.featureDir);
|
|
@@ -38446,7 +38677,7 @@ async function setupRun(options) {
|
|
|
38446
38677
|
},
|
|
38447
38678
|
onShutdown: async () => {
|
|
38448
38679
|
const { sweepFeatureSessions: sweepFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
|
|
38449
|
-
await sweepFeatureSessions2(workdir, feature).catch(() => {});
|
|
38680
|
+
await sweepFeatureSessions2(workdir, feature, pidRegistry).catch(() => {});
|
|
38450
38681
|
}
|
|
38451
38682
|
});
|
|
38452
38683
|
let prd = await loadPRD(prdPath);
|
|
@@ -38469,7 +38700,7 @@ async function setupRun(options) {
|
|
|
38469
38700
|
logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
|
|
38470
38701
|
}
|
|
38471
38702
|
const { sweepStaleFeatureSessions: sweepStaleFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
|
|
38472
|
-
await sweepStaleFeatureSessions2(workdir, feature).catch(() => {});
|
|
38703
|
+
await sweepStaleFeatureSessions2(workdir, feature, undefined, pidRegistry).catch(() => {});
|
|
38473
38704
|
const lockAcquired = await acquireLock(workdir);
|
|
38474
38705
|
if (!lockAcquired) {
|
|
38475
38706
|
logger?.error("execution", "Another nax process is already running in this directory");
|
|
@@ -69988,6 +70219,7 @@ async function generateAcceptanceTestsForFeature(specContent, featureName, featu
|
|
|
69988
70219
|
}
|
|
69989
70220
|
// src/cli/plan.ts
|
|
69990
70221
|
init_registry();
|
|
70222
|
+
init_decompose();
|
|
69991
70223
|
import { existsSync as existsSync15 } from "fs";
|
|
69992
70224
|
import { join as join13 } from "path";
|
|
69993
70225
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -70534,6 +70766,48 @@ init_bridge_builder();
|
|
|
70534
70766
|
init_init();
|
|
70535
70767
|
init_logger2();
|
|
70536
70768
|
|
|
70769
|
+
// src/prd/decompose-mapper.ts
|
|
70770
|
+
init_errors();
|
|
70771
|
+
function mapDecomposedStoriesToUserStories(stories, parentStoryId) {
|
|
70772
|
+
return stories.map((story, entryIndex) => {
|
|
70773
|
+
if (!story.id) {
|
|
70774
|
+
throw new NaxError(`Entry at index ${entryIndex} is missing required field: id`, "DECOMPOSE_VALIDATION_FAILED", {
|
|
70775
|
+
stage: "decompose-mapper",
|
|
70776
|
+
entryIndex,
|
|
70777
|
+
parentStoryId
|
|
70778
|
+
});
|
|
70779
|
+
}
|
|
70780
|
+
if (!story.contextFiles || story.contextFiles.length === 0) {
|
|
70781
|
+
throw new NaxError(`Entry ${entryIndex} (${story.id}) has empty contextFiles`, "DECOMPOSE_VALIDATION_FAILED", {
|
|
70782
|
+
stage: "decompose-mapper",
|
|
70783
|
+
entryIndex,
|
|
70784
|
+
storyId: story.id,
|
|
70785
|
+
parentStoryId
|
|
70786
|
+
});
|
|
70787
|
+
}
|
|
70788
|
+
return {
|
|
70789
|
+
id: story.id,
|
|
70790
|
+
title: story.title,
|
|
70791
|
+
description: story.description,
|
|
70792
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
70793
|
+
tags: story.tags,
|
|
70794
|
+
dependencies: story.dependencies,
|
|
70795
|
+
contextFiles: story.contextFiles,
|
|
70796
|
+
status: "pending",
|
|
70797
|
+
passes: false,
|
|
70798
|
+
escalations: [],
|
|
70799
|
+
attempts: 0,
|
|
70800
|
+
parentStoryId,
|
|
70801
|
+
routing: {
|
|
70802
|
+
complexity: story.complexity,
|
|
70803
|
+
testStrategy: story.testStrategy ?? "test-after",
|
|
70804
|
+
reasoning: story.reasoning,
|
|
70805
|
+
modelTier: "balanced"
|
|
70806
|
+
}
|
|
70807
|
+
};
|
|
70808
|
+
});
|
|
70809
|
+
}
|
|
70810
|
+
|
|
70537
70811
|
// src/prd/schema.ts
|
|
70538
70812
|
init_test_strategy();
|
|
70539
70813
|
var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
|
|
@@ -70825,26 +71099,45 @@ async function planCommand(workdir, config2, options) {
|
|
|
70825
71099
|
timeoutSeconds
|
|
70826
71100
|
});
|
|
70827
71101
|
const pidRegistry = new PidRegistry(workdir);
|
|
71102
|
+
let planError = null;
|
|
70828
71103
|
try {
|
|
70829
|
-
|
|
70830
|
-
|
|
70831
|
-
|
|
70832
|
-
|
|
70833
|
-
|
|
70834
|
-
|
|
70835
|
-
|
|
70836
|
-
|
|
70837
|
-
|
|
70838
|
-
|
|
70839
|
-
|
|
70840
|
-
|
|
70841
|
-
|
|
71104
|
+
try {
|
|
71105
|
+
await adapter.plan({
|
|
71106
|
+
prompt,
|
|
71107
|
+
workdir,
|
|
71108
|
+
interactive: false,
|
|
71109
|
+
timeoutSeconds,
|
|
71110
|
+
config: config2,
|
|
71111
|
+
modelTier: config2?.plan?.model ?? "balanced",
|
|
71112
|
+
dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
|
|
71113
|
+
maxInteractionTurns: config2?.agent?.maxInteractionTurns,
|
|
71114
|
+
featureName: options.feature,
|
|
71115
|
+
pidRegistry,
|
|
71116
|
+
sessionRole: "plan"
|
|
71117
|
+
});
|
|
71118
|
+
} catch (err) {
|
|
71119
|
+
planError = err instanceof Error ? err : new Error(String(err));
|
|
71120
|
+
logger?.warn("plan", "ACP auto planning did not complete cleanly; checking for written PRD", {
|
|
71121
|
+
error: planError.message,
|
|
71122
|
+
outputPath
|
|
71123
|
+
});
|
|
71124
|
+
}
|
|
70842
71125
|
} finally {
|
|
70843
71126
|
await pidRegistry.killAll().catch(() => {});
|
|
70844
71127
|
}
|
|
70845
71128
|
if (!_planDeps.existsSync(outputPath)) {
|
|
71129
|
+
if (planError) {
|
|
71130
|
+
throw new Error(`[plan] ACP planning failed and no PRD was written: ${planError.message}`, {
|
|
71131
|
+
cause: planError
|
|
71132
|
+
});
|
|
71133
|
+
}
|
|
70846
71134
|
throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
|
|
70847
71135
|
}
|
|
71136
|
+
if (planError) {
|
|
71137
|
+
logger?.warn("plan", "Proceeding with PRD written by ACP despite incomplete terminal response", {
|
|
71138
|
+
outputPath
|
|
71139
|
+
});
|
|
71140
|
+
}
|
|
70848
71141
|
rawResponse = await _planDeps.readFile(outputPath);
|
|
70849
71142
|
} else {
|
|
70850
71143
|
const timeoutMs = (config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2) * 1000;
|
|
@@ -70893,20 +71186,30 @@ async function planCommand(workdir, config2, options) {
|
|
|
70893
71186
|
timeoutSeconds
|
|
70894
71187
|
});
|
|
70895
71188
|
const planStartTime = Date.now();
|
|
71189
|
+
let planError = null;
|
|
70896
71190
|
try {
|
|
70897
|
-
|
|
70898
|
-
|
|
70899
|
-
|
|
70900
|
-
|
|
70901
|
-
|
|
70902
|
-
|
|
70903
|
-
|
|
70904
|
-
|
|
70905
|
-
|
|
70906
|
-
|
|
70907
|
-
|
|
70908
|
-
|
|
70909
|
-
|
|
71191
|
+
try {
|
|
71192
|
+
await adapter.plan({
|
|
71193
|
+
prompt,
|
|
71194
|
+
workdir,
|
|
71195
|
+
interactive: true,
|
|
71196
|
+
timeoutSeconds,
|
|
71197
|
+
interactionBridge,
|
|
71198
|
+
config: config2,
|
|
71199
|
+
modelTier: resolvedModel,
|
|
71200
|
+
dangerouslySkipPermissions: resolvedPerm.skipPermissions,
|
|
71201
|
+
maxInteractionTurns: config2?.agent?.maxInteractionTurns,
|
|
71202
|
+
featureName: options.feature,
|
|
71203
|
+
pidRegistry,
|
|
71204
|
+
sessionRole: "plan"
|
|
71205
|
+
});
|
|
71206
|
+
} catch (err) {
|
|
71207
|
+
planError = err instanceof Error ? err : new Error(String(err));
|
|
71208
|
+
logger?.warn("plan", "Interactive planning did not complete cleanly; checking for written PRD", {
|
|
71209
|
+
error: planError.message,
|
|
71210
|
+
outputPath
|
|
71211
|
+
});
|
|
71212
|
+
}
|
|
70910
71213
|
} finally {
|
|
70911
71214
|
await pidRegistry.killAll().catch(() => {});
|
|
70912
71215
|
if (interactionChain)
|
|
@@ -70914,8 +71217,16 @@ async function planCommand(workdir, config2, options) {
|
|
|
70914
71217
|
logger?.info("plan", "Interactive session ended", { durationMs: Date.now() - planStartTime });
|
|
70915
71218
|
}
|
|
70916
71219
|
if (!_planDeps.existsSync(outputPath)) {
|
|
71220
|
+
if (planError) {
|
|
71221
|
+
throw new Error(`[plan] Planning failed and no PRD was written: ${planError.message}`, { cause: planError });
|
|
71222
|
+
}
|
|
70917
71223
|
throw new Error(`[plan] Agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
|
|
70918
71224
|
}
|
|
71225
|
+
if (planError) {
|
|
71226
|
+
logger?.warn("plan", "Proceeding with PRD written by agent despite incomplete terminal response", {
|
|
71227
|
+
outputPath
|
|
71228
|
+
});
|
|
71229
|
+
}
|
|
70919
71230
|
return _planDeps.readFile(outputPath);
|
|
70920
71231
|
}
|
|
70921
71232
|
const finalPrd = validatePlanOutput(rawResponse, options.feature, branchName);
|
|
@@ -71142,58 +71453,6 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
71142
71453
|
${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
|
|
71143
71454
|
Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.` : "Output ONLY the JSON object. Do not wrap in markdown code blocks."}`;
|
|
71144
71455
|
}
|
|
71145
|
-
function buildDecomposePrompt2(targetStory, siblings, codebaseContext) {
|
|
71146
|
-
const siblingsSummary = siblings.length > 0 ? `
|
|
71147
|
-
## Sibling Stories
|
|
71148
|
-
|
|
71149
|
-
${siblings.map((s) => `- ${s.id}: ${s.title}`).join(`
|
|
71150
|
-
`)}
|
|
71151
|
-
` : "";
|
|
71152
|
-
return `You are a senior software architect decomposing a complex user story into smaller, implementable sub-stories.
|
|
71153
|
-
|
|
71154
|
-
## Target Story
|
|
71155
|
-
|
|
71156
|
-
${JSON.stringify(targetStory, null, 2)}${siblingsSummary}
|
|
71157
|
-
## Codebase Context
|
|
71158
|
-
|
|
71159
|
-
${codebaseContext}
|
|
71160
|
-
|
|
71161
|
-
${COMPLEXITY_GUIDE}
|
|
71162
|
-
|
|
71163
|
-
${TEST_STRATEGY_GUIDE}
|
|
71164
|
-
|
|
71165
|
-
${GROUPING_RULES}
|
|
71166
|
-
|
|
71167
|
-
${getAcQualityRules()}
|
|
71168
|
-
|
|
71169
|
-
## Output
|
|
71170
|
-
|
|
71171
|
-
Return JSON with this exact structure (no markdown, no explanation \u2014 JSON only):
|
|
71172
|
-
|
|
71173
|
-
{
|
|
71174
|
-
"subStories": [
|
|
71175
|
-
{
|
|
71176
|
-
"id": "string \u2014 e.g. ${targetStory.id}-A",
|
|
71177
|
-
"title": "string",
|
|
71178
|
-
"description": "string",
|
|
71179
|
-
"acceptanceCriteria": ["string \u2014 behavioral, testable criteria"],
|
|
71180
|
-
"contextFiles": ["string \u2014 required, non-empty list of key source files"],
|
|
71181
|
-
"tags": ["string"],
|
|
71182
|
-
"dependencies": ["string"],
|
|
71183
|
-
"status": "pending",
|
|
71184
|
-
"passes": false,
|
|
71185
|
-
"routing": {
|
|
71186
|
-
"complexity": "simple | medium | complex | expert",
|
|
71187
|
-
"testStrategy": "no-test | tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
|
|
71188
|
-
"modelTier": "fast | balanced | powerful",
|
|
71189
|
-
"reasoning": "string"
|
|
71190
|
-
},
|
|
71191
|
-
"escalations": [],
|
|
71192
|
-
"attempts": 0
|
|
71193
|
-
}
|
|
71194
|
-
]
|
|
71195
|
-
}`;
|
|
71196
|
-
}
|
|
71197
71456
|
async function planDecomposeCommand(workdir, config2, options) {
|
|
71198
71457
|
const prdPath = join13(workdir, ".nax", "features", options.feature, "prd.json");
|
|
71199
71458
|
if (!_planDeps.existsSync(prdPath)) {
|
|
@@ -71220,31 +71479,34 @@ async function planDecomposeCommand(workdir, config2, options) {
|
|
|
71220
71479
|
const scan = await _planDeps.scanCodebase(workdir);
|
|
71221
71480
|
const codebaseContext = buildCodebaseContext2(scan);
|
|
71222
71481
|
const siblings = prd.userStories.filter((s) => s.id !== options.storyId);
|
|
71223
|
-
const prompt = buildDecomposePrompt2(targetStory, siblings, codebaseContext);
|
|
71224
71482
|
const agentName = config2?.autoMode?.defaultAgent ?? "claude";
|
|
71225
71483
|
const adapter = _planDeps.getAgent(agentName, config2);
|
|
71226
71484
|
if (!adapter)
|
|
71227
71485
|
throw new Error(`[decompose] No agent adapter found for '${agentName}'`);
|
|
71228
|
-
let decomposeModel;
|
|
71229
|
-
try {
|
|
71230
|
-
const planTier = config2?.plan?.model ?? "balanced";
|
|
71231
|
-
const { resolveModelForAgent: resolveModelForAgent2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
71232
|
-
if (config2?.models) {
|
|
71233
|
-
const defaultAgent = config2.autoMode?.defaultAgent ?? "claude";
|
|
71234
|
-
decomposeModel = resolveModelForAgent2(config2.models, defaultAgent, planTier, defaultAgent).model;
|
|
71235
|
-
}
|
|
71236
|
-
} catch {}
|
|
71237
|
-
const stages = config2?.debate?.stages;
|
|
71238
|
-
const debateEnabled = config2?.debate?.enabled && stages?.decompose?.enabled;
|
|
71239
|
-
let rawResponse;
|
|
71240
71486
|
const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
|
|
71241
|
-
const
|
|
71242
|
-
if (
|
|
71243
|
-
|
|
71487
|
+
const maxAcCount = config2?.precheck?.storySizeGate?.maxAcCount ?? Number.POSITIVE_INFINITY;
|
|
71488
|
+
if (typeof adapter.decompose !== "function") {
|
|
71489
|
+
throw new NaxError(`Agent "${agentName}" does not support decompose() required by plan --decompose`, "DECOMPOSE_NOT_SUPPORTED", { stage: "decompose", agent: agentName, storyId: options.storyId });
|
|
71490
|
+
}
|
|
71491
|
+
const debateStages = config2?.debate?.stages;
|
|
71492
|
+
const debateDecompEnabled = config2?.debate?.enabled && debateStages?.decompose?.enabled;
|
|
71493
|
+
let decompStories;
|
|
71494
|
+
if (debateDecompEnabled) {
|
|
71495
|
+
const decomposeStageConfig = debateStages.decompose;
|
|
71496
|
+
const prompt = buildDecomposePrompt({
|
|
71497
|
+
specContent: "",
|
|
71498
|
+
codebaseContext,
|
|
71499
|
+
workdir,
|
|
71500
|
+
targetStory,
|
|
71501
|
+
siblings,
|
|
71502
|
+
featureName: options.feature,
|
|
71503
|
+
storyId: options.storyId,
|
|
71504
|
+
config: config2
|
|
71505
|
+
});
|
|
71244
71506
|
const debateSession = _planDeps.createDebateSession({
|
|
71245
71507
|
storyId: options.storyId,
|
|
71246
71508
|
stage: "decompose",
|
|
71247
|
-
stageConfig,
|
|
71509
|
+
stageConfig: decomposeStageConfig,
|
|
71248
71510
|
config: config2,
|
|
71249
71511
|
workdir,
|
|
71250
71512
|
featureName: options.feature,
|
|
@@ -71252,52 +71514,24 @@ async function planDecomposeCommand(workdir, config2, options) {
|
|
|
71252
71514
|
});
|
|
71253
71515
|
const debateResult = await debateSession.run(prompt);
|
|
71254
71516
|
if (debateResult.outcome !== "failed" && debateResult.output) {
|
|
71255
|
-
|
|
71256
|
-
} else {
|
|
71257
|
-
const completeResult = await adapter.complete(prompt, {
|
|
71258
|
-
model: decomposeModel,
|
|
71259
|
-
jsonMode: true,
|
|
71260
|
-
workdir,
|
|
71261
|
-
sessionRole: "decompose",
|
|
71262
|
-
featureName: options.feature,
|
|
71263
|
-
storyId: options.storyId,
|
|
71264
|
-
timeoutMs
|
|
71265
|
-
});
|
|
71266
|
-
rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
|
|
71517
|
+
decompStories = parseDecomposeOutput(debateResult.output);
|
|
71267
71518
|
}
|
|
71268
|
-
}
|
|
71269
|
-
|
|
71270
|
-
|
|
71271
|
-
|
|
71519
|
+
}
|
|
71520
|
+
if (!decompStories) {
|
|
71521
|
+
const result = await adapter.decompose({
|
|
71522
|
+
specContent: "",
|
|
71523
|
+
codebaseContext,
|
|
71272
71524
|
workdir,
|
|
71273
|
-
|
|
71525
|
+
targetStory,
|
|
71526
|
+
siblings,
|
|
71274
71527
|
featureName: options.feature,
|
|
71275
71528
|
storyId: options.storyId,
|
|
71276
|
-
|
|
71529
|
+
config: config2
|
|
71277
71530
|
});
|
|
71278
|
-
|
|
71279
|
-
}
|
|
71280
|
-
const jsonMatch = rawResponse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
71281
|
-
const cleanedResponse = jsonMatch ? jsonMatch[1] : rawResponse;
|
|
71282
|
-
let parsed;
|
|
71283
|
-
try {
|
|
71284
|
-
parsed = JSON.parse(cleanedResponse.trim());
|
|
71285
|
-
} catch (err) {
|
|
71286
|
-
throw new NaxError(`Failed to parse decompose response as JSON: ${err.message}
|
|
71287
|
-
|
|
71288
|
-
Response (first 500 chars):
|
|
71289
|
-
${rawResponse.slice(0, 500)}`, "DECOMPOSE_PARSE_FAILED", { stage: "decompose", storyId: options.storyId });
|
|
71531
|
+
decompStories = result.stories;
|
|
71290
71532
|
}
|
|
71291
|
-
const
|
|
71292
|
-
|
|
71293
|
-
for (const sub of subStories) {
|
|
71294
|
-
if (!sub.contextFiles || sub.contextFiles.length === 0) {
|
|
71295
|
-
throw new NaxError(`Sub-story "${sub.id}" has empty contextFiles`, "DECOMPOSE_VALIDATION_FAILED", {
|
|
71296
|
-
stage: "decompose",
|
|
71297
|
-
storyId: sub.id
|
|
71298
|
-
});
|
|
71299
|
-
}
|
|
71300
|
-
if (!sub.routing?.complexity || !sub.routing?.testStrategy || !sub.routing?.modelTier) {
|
|
71533
|
+
for (const sub of decompStories) {
|
|
71534
|
+
if (!sub.complexity || !sub.testStrategy) {
|
|
71301
71535
|
throw new NaxError(`Sub-story "${sub.id}" is missing required routing fields`, "DECOMPOSE_VALIDATION_FAILED", {
|
|
71302
71536
|
stage: "decompose",
|
|
71303
71537
|
storyId: sub.id
|
|
@@ -71307,8 +71541,8 @@ ${rawResponse.slice(0, 500)}`, "DECOMPOSE_PARSE_FAILED", { stage: "decompose", s
|
|
|
71307
71541
|
throw new NaxError(`Sub-story "${sub.id}" has ${sub.acceptanceCriteria.length} ACs, exceeds maxAcCount of ${maxAcCount}`, "DECOMPOSE_VALIDATION_FAILED", { stage: "decompose", storyId: sub.id });
|
|
71308
71542
|
}
|
|
71309
71543
|
}
|
|
71544
|
+
const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId);
|
|
71310
71545
|
const updatedStories = prd.userStories.map((s) => s.id === options.storyId ? { ...s, status: "decomposed" } : s);
|
|
71311
|
-
const subStoriesWithParent = subStories.map((s) => ({ ...s, parentStoryId: options.storyId }));
|
|
71312
71546
|
const originalIndex = updatedStories.findIndex((s) => s.id === options.storyId);
|
|
71313
71547
|
const finalStories = [
|
|
71314
71548
|
...updatedStories.slice(0, originalIndex + 1),
|
|
@@ -72987,6 +73221,8 @@ var FIELD_DESCRIPTIONS = {
|
|
|
72987
73221
|
"acceptance.generateTests": "Generate acceptance tests during analyze",
|
|
72988
73222
|
"acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
|
|
72989
73223
|
"acceptance.command": "Override command to run acceptance tests. Use {{FILE}} as placeholder for the test file path (default: 'bun test {{FILE}} --timeout=60000')",
|
|
73224
|
+
"acceptance.model": "Model tier for acceptance generation/refinement LLM calls (fast | balanced | powerful). Default: fast.",
|
|
73225
|
+
"acceptance.refinement": "Enable acceptance criteria refinement step before execution (default: true). Disable to skip refinement and use generated criteria as-is.",
|
|
72990
73226
|
"acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
|
|
72991
73227
|
context: "Context injection configuration",
|
|
72992
73228
|
"context.fileInjection": "Mode: 'disabled' (default, MCP-aware agents pull context on-demand) | 'keyword' (legacy git-grep injection for non-MCP agents). Set context.fileInjection in config.",
|