@nathapp/nax 0.49.3 → 0.50.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/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/dist/nax.js +485 -202
- package/package.json +1 -1
- package/src/acceptance/generator.ts +48 -7
- package/src/agents/acp/adapter.ts +53 -23
- package/src/agents/acp/spawn-client.ts +0 -2
- package/src/agents/claude/execution.ts +14 -0
- package/src/agents/types.ts +7 -0
- package/src/cli/plan.ts +46 -13
- package/src/cli/prompts-main.ts +4 -59
- package/src/cli/prompts-shared.ts +70 -0
- package/src/cli/prompts-tdd.ts +1 -1
- package/src/config/merge.ts +18 -0
- package/src/config/test-strategy.ts +17 -16
- package/src/context/builder.ts +25 -0
- package/src/context/parent-context.ts +39 -0
- package/src/decompose/apply.ts +5 -1
- package/src/execution/escalation/tier-escalation.ts +1 -1
- package/src/execution/escalation/tier-outcome.ts +2 -2
- package/src/execution/lifecycle/run-initialization.ts +47 -13
- package/src/execution/parallel-coordinator.ts +3 -3
- package/src/execution/pipeline-result-handler.ts +30 -1
- package/src/interaction/plugins/webhook.ts +44 -25
- package/src/pipeline/stages/autofix.ts +10 -2
- package/src/prd/index.ts +9 -1
- package/src/prd/types.ts +6 -0
- package/src/routing/router.ts +1 -1
- package/src/tdd/cleanup.ts +15 -6
- package/src/tdd/isolation.ts +9 -2
- package/src/tdd/rectification-gate.ts +41 -10
- package/src/tdd/session-runner.ts +71 -38
- package/src/utils/git.ts +23 -0
- package/src/verification/executor.ts +4 -1
- package/src/verification/strategies/acceptance.ts +4 -1
package/dist/nax.js
CHANGED
|
@@ -3256,29 +3256,30 @@ function resolveTestStrategy(raw) {
|
|
|
3256
3256
|
}
|
|
3257
3257
|
var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guide
|
|
3258
3258
|
|
|
3259
|
-
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192
|
|
3260
|
-
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-
|
|
3259
|
+
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 tdd-simple
|
|
3260
|
+
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 three-session-tdd-lite
|
|
3261
3261
|
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
3262
|
-
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd
|
|
3262
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd
|
|
3263
3263
|
|
|
3264
3264
|
### Security Override
|
|
3265
3265
|
|
|
3266
3266
|
Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
|
|
3267
|
-
password hashing, access control) must
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
-
|
|
3271
|
-
- tdd
|
|
3272
|
-
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3267
|
+
password hashing, access control) must use three-session-tdd regardless of complexity.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
|
|
3268
|
+
|
|
3269
|
+
- tdd-simple: Simple stories (\u226450 LOC). Write failing tests first, then implement to pass them \u2014 all in one session.
|
|
3270
|
+
- three-session-tdd-lite: Medium stories, or complex stories involving UI/CLI/integration. 3 sessions: (1) test-writer writes failing tests and may create minimal src/ stubs for imports, (2) implementer makes tests pass and may replace stubs, (3) verifier confirms correctness.
|
|
3271
|
+
- three-session-tdd: Complex/expert stories or security-critical code. 3 sessions with strict isolation: (1) test-writer writes failing tests \u2014 no src/ changes allowed, (2) implementer makes them pass without modifying test files, (3) verifier confirms correctness.
|
|
3272
|
+
- test-after: Only when explicitly configured (tddStrategy: "off"). Write tests after implementation. Not auto-assigned.`, GROUPING_RULES = `## Story Rules
|
|
3273
|
+
|
|
3274
|
+
- Every story must produce code changes verifiable by tests or review.
|
|
3275
|
+
- NEVER create stories for analysis, planning, documentation, or migration plans.
|
|
3276
|
+
Your analysis belongs in the "analysis" field, not in a story.
|
|
3277
|
+
- NEVER create stories whose primary purpose is writing tests, achieving coverage
|
|
3278
|
+
targets, or running validation/regression suites. Each story's testStrategy
|
|
3279
|
+
handles test creation as part of implementation. Testing is a built-in pipeline
|
|
3280
|
+
stage, not a user story. No exceptions.
|
|
3275
3281
|
- Combine small, related tasks into a single "simple" or "medium" story.
|
|
3276
|
-
|
|
3277
|
-
- Do NOT create standalone stories purely for test coverage or testing.
|
|
3278
|
-
Each story's testStrategy already handles testing (tdd-simple writes tests first,
|
|
3279
|
-
three-session-tdd uses separate test-writer session, test-after writes tests after).
|
|
3280
|
-
Only create a dedicated test story for unique integration/E2E test logic that spans
|
|
3281
|
-
multiple stories and cannot be covered by individual story test strategies.
|
|
3282
|
+
Do NOT create separate stories for every single file or function unless complex.
|
|
3282
3283
|
- Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
|
|
3283
3284
|
var init_test_strategy = __esm(() => {
|
|
3284
3285
|
VALID_TEST_STRATEGIES = [
|
|
@@ -3667,6 +3668,16 @@ function buildAllowedEnv(options) {
|
|
|
3667
3668
|
async function executeOnce(binary, options, pidRegistry) {
|
|
3668
3669
|
const cmd = _runOnceDeps.buildCmd(binary, options);
|
|
3669
3670
|
const startTime = Date.now();
|
|
3671
|
+
if (options.sessionRole || options.acpSessionName || options.keepSessionOpen) {
|
|
3672
|
+
const logger2 = getLogger();
|
|
3673
|
+
logger2.debug("agent", "CLI mode: session options received (unused)", {
|
|
3674
|
+
sessionRole: options.sessionRole,
|
|
3675
|
+
acpSessionName: options.acpSessionName,
|
|
3676
|
+
keepSessionOpen: options.keepSessionOpen,
|
|
3677
|
+
featureName: options.featureName,
|
|
3678
|
+
storyId: options.storyId
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3670
3681
|
const proc = Bun.spawn(cmd, {
|
|
3671
3682
|
cwd: options.workdir,
|
|
3672
3683
|
stdout: "pipe",
|
|
@@ -18734,6 +18745,17 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
|
|
|
18734
18745
|
config: options.config
|
|
18735
18746
|
});
|
|
18736
18747
|
const testCode = extractTestCode(rawOutput);
|
|
18748
|
+
if (!testCode) {
|
|
18749
|
+
logger.warn("acceptance", "LLM returned non-code output for acceptance tests \u2014 falling back to skeleton", {
|
|
18750
|
+
outputPreview: rawOutput.slice(0, 200)
|
|
18751
|
+
});
|
|
18752
|
+
const skeletonCriteria = refinedCriteria.map((c, i) => ({
|
|
18753
|
+
id: `AC-${i + 1}`,
|
|
18754
|
+
text: c.refined,
|
|
18755
|
+
lineNumber: i + 1
|
|
18756
|
+
}));
|
|
18757
|
+
return { testCode: generateSkeletonTests(options.featureName, skeletonCriteria), criteria: skeletonCriteria };
|
|
18758
|
+
}
|
|
18737
18759
|
const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
|
|
18738
18760
|
acId: `AC-${i + 1}`,
|
|
18739
18761
|
original: c.original,
|
|
@@ -18860,6 +18882,15 @@ async function generateAcceptanceTests(adapter, options) {
|
|
|
18860
18882
|
config: options.config
|
|
18861
18883
|
});
|
|
18862
18884
|
const testCode = extractTestCode(output);
|
|
18885
|
+
if (!testCode) {
|
|
18886
|
+
logger.warn("acceptance", "LLM returned non-code output for acceptance tests \u2014 falling back to skeleton", {
|
|
18887
|
+
outputPreview: output.slice(0, 200)
|
|
18888
|
+
});
|
|
18889
|
+
return {
|
|
18890
|
+
testCode: generateSkeletonTests(options.featureName, criteria),
|
|
18891
|
+
criteria
|
|
18892
|
+
};
|
|
18893
|
+
}
|
|
18863
18894
|
return {
|
|
18864
18895
|
testCode,
|
|
18865
18896
|
criteria
|
|
@@ -18873,15 +18904,30 @@ async function generateAcceptanceTests(adapter, options) {
|
|
|
18873
18904
|
}
|
|
18874
18905
|
}
|
|
18875
18906
|
function extractTestCode(output) {
|
|
18907
|
+
let code;
|
|
18876
18908
|
const fenceMatch = output.match(/```(?:typescript|ts)?\s*([\s\S]*?)\s*```/);
|
|
18877
18909
|
if (fenceMatch) {
|
|
18878
|
-
|
|
18910
|
+
code = fenceMatch[1].trim();
|
|
18911
|
+
}
|
|
18912
|
+
if (!code) {
|
|
18913
|
+
const importMatch = output.match(/import\s+{[\s\S]+/);
|
|
18914
|
+
if (importMatch) {
|
|
18915
|
+
code = importMatch[0].trim();
|
|
18916
|
+
}
|
|
18879
18917
|
}
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18918
|
+
if (!code) {
|
|
18919
|
+
const describeMatch = output.match(/describe\s*\([\s\S]+/);
|
|
18920
|
+
if (describeMatch) {
|
|
18921
|
+
code = describeMatch[0].trim();
|
|
18922
|
+
}
|
|
18923
|
+
}
|
|
18924
|
+
if (!code)
|
|
18925
|
+
return null;
|
|
18926
|
+
const hasTestKeyword = /\b(?:describe|test|it|expect)\s*\(/.test(code);
|
|
18927
|
+
if (!hasTestKeyword) {
|
|
18928
|
+
return null;
|
|
18883
18929
|
}
|
|
18884
|
-
return
|
|
18930
|
+
return code;
|
|
18885
18931
|
}
|
|
18886
18932
|
function generateSkeletonTests(featureName, criteria) {
|
|
18887
18933
|
const tests = criteria.map((ac) => {
|
|
@@ -19276,7 +19322,6 @@ class SpawnAcpClient {
|
|
|
19276
19322
|
model;
|
|
19277
19323
|
cwd;
|
|
19278
19324
|
timeoutSeconds;
|
|
19279
|
-
permissionMode;
|
|
19280
19325
|
env;
|
|
19281
19326
|
pidRegistry;
|
|
19282
19327
|
constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
|
|
@@ -19290,7 +19335,6 @@ class SpawnAcpClient {
|
|
|
19290
19335
|
this.agentName = lastToken;
|
|
19291
19336
|
this.cwd = cwd || process.cwd();
|
|
19292
19337
|
this.timeoutSeconds = timeoutSeconds || 1800;
|
|
19293
|
-
this.permissionMode = "approve-reads";
|
|
19294
19338
|
this.env = buildAllowedEnv2();
|
|
19295
19339
|
this.pidRegistry = pidRegistry;
|
|
19296
19340
|
}
|
|
@@ -19432,7 +19476,13 @@ async function closeAcpSession(session) {
|
|
|
19432
19476
|
function acpSessionsPath(workdir, featureName) {
|
|
19433
19477
|
return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
|
|
19434
19478
|
}
|
|
19435
|
-
|
|
19479
|
+
function sidecarSessionName(entry) {
|
|
19480
|
+
return typeof entry === "string" ? entry : entry.sessionName;
|
|
19481
|
+
}
|
|
19482
|
+
function sidecarAgentName(entry) {
|
|
19483
|
+
return typeof entry === "string" ? "claude" : entry.agentName;
|
|
19484
|
+
}
|
|
19485
|
+
async function saveAcpSession(workdir, featureName, storyId, sessionName, agentName = "claude") {
|
|
19436
19486
|
try {
|
|
19437
19487
|
const path = acpSessionsPath(workdir, featureName);
|
|
19438
19488
|
let data = {};
|
|
@@ -19440,7 +19490,7 @@ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
|
|
|
19440
19490
|
const existing = await Bun.file(path).text();
|
|
19441
19491
|
data = JSON.parse(existing);
|
|
19442
19492
|
} catch {}
|
|
19443
|
-
data[storyId] = sessionName;
|
|
19493
|
+
data[storyId] = { sessionName, agentName };
|
|
19444
19494
|
await Bun.write(path, JSON.stringify(data, null, 2));
|
|
19445
19495
|
} catch (err) {
|
|
19446
19496
|
getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
|
|
@@ -19467,7 +19517,8 @@ async function readAcpSession(workdir, featureName, storyId) {
|
|
|
19467
19517
|
const path = acpSessionsPath(workdir, featureName);
|
|
19468
19518
|
const existing = await Bun.file(path).text();
|
|
19469
19519
|
const data = JSON.parse(existing);
|
|
19470
|
-
|
|
19520
|
+
const entry = data[storyId];
|
|
19521
|
+
return entry ? sidecarSessionName(entry) : null;
|
|
19471
19522
|
} catch {
|
|
19472
19523
|
return null;
|
|
19473
19524
|
}
|
|
@@ -19486,24 +19537,34 @@ async function sweepFeatureSessions(workdir, featureName) {
|
|
|
19486
19537
|
return;
|
|
19487
19538
|
const logger = getSafeLogger();
|
|
19488
19539
|
logger?.info("acp-adapter", `[sweep] Closing ${entries.length} open sessions for feature: ${featureName}`);
|
|
19489
|
-
const
|
|
19490
|
-
const
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19540
|
+
const byAgent = new Map;
|
|
19541
|
+
for (const [, entry] of entries) {
|
|
19542
|
+
const agent = sidecarAgentName(entry);
|
|
19543
|
+
const name = sidecarSessionName(entry);
|
|
19544
|
+
if (!byAgent.has(agent))
|
|
19545
|
+
byAgent.set(agent, []);
|
|
19546
|
+
byAgent.get(agent)?.push(name);
|
|
19547
|
+
}
|
|
19548
|
+
for (const [agentName, sessionNames] of byAgent) {
|
|
19549
|
+
const cmdStr = `acpx ${agentName}`;
|
|
19550
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
19551
|
+
try {
|
|
19552
|
+
await client.start();
|
|
19553
|
+
for (const sessionName of sessionNames) {
|
|
19554
|
+
try {
|
|
19555
|
+
if (client.loadSession) {
|
|
19556
|
+
const session = await client.loadSession(sessionName, agentName, "approve-reads");
|
|
19557
|
+
if (session) {
|
|
19558
|
+
await session.close().catch(() => {});
|
|
19559
|
+
}
|
|
19499
19560
|
}
|
|
19561
|
+
} catch (err) {
|
|
19562
|
+
logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
|
|
19500
19563
|
}
|
|
19501
|
-
} catch (err) {
|
|
19502
|
-
logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
|
|
19503
19564
|
}
|
|
19565
|
+
} finally {
|
|
19566
|
+
await client.close().catch(() => {});
|
|
19504
19567
|
}
|
|
19505
|
-
} finally {
|
|
19506
|
-
await client.close().catch(() => {});
|
|
19507
19568
|
}
|
|
19508
19569
|
try {
|
|
19509
19570
|
await Bun.write(path, JSON.stringify({}, null, 2));
|
|
@@ -19644,7 +19705,7 @@ class AcpAgentAdapter {
|
|
|
19644
19705
|
});
|
|
19645
19706
|
const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
|
|
19646
19707
|
if (options.featureName && options.storyId) {
|
|
19647
|
-
await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
|
|
19708
|
+
await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName, this.name);
|
|
19648
19709
|
}
|
|
19649
19710
|
let lastResponse = null;
|
|
19650
19711
|
let timedOut = false;
|
|
@@ -19702,13 +19763,15 @@ class AcpAgentAdapter {
|
|
|
19702
19763
|
}
|
|
19703
19764
|
runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
|
|
19704
19765
|
} finally {
|
|
19705
|
-
if (runState.succeeded) {
|
|
19766
|
+
if (runState.succeeded && !options.keepSessionOpen) {
|
|
19706
19767
|
await closeAcpSession(session);
|
|
19707
19768
|
if (options.featureName && options.storyId) {
|
|
19708
19769
|
await clearAcpSession(options.workdir, options.featureName, options.storyId);
|
|
19709
19770
|
}
|
|
19710
|
-
} else {
|
|
19771
|
+
} else if (!runState.succeeded) {
|
|
19711
19772
|
getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
|
|
19773
|
+
} else {
|
|
19774
|
+
getSafeLogger()?.debug("acp-adapter", "Keeping session open (keepSessionOpen=true)", { sessionName });
|
|
19712
19775
|
}
|
|
19713
19776
|
await client.close().catch(() => {});
|
|
19714
19777
|
}
|
|
@@ -20336,7 +20399,8 @@ function applyDecomposition(prd, result) {
|
|
|
20336
20399
|
const originalIndex = prd.userStories.findIndex((s) => s.id === parentStoryId);
|
|
20337
20400
|
if (originalIndex === -1)
|
|
20338
20401
|
return;
|
|
20339
|
-
prd.userStories[originalIndex]
|
|
20402
|
+
const parentStory = prd.userStories[originalIndex];
|
|
20403
|
+
parentStory.status = "decomposed";
|
|
20340
20404
|
const newStories = subStories.map((sub) => ({
|
|
20341
20405
|
id: sub.id,
|
|
20342
20406
|
title: sub.title,
|
|
@@ -20348,7 +20412,8 @@ function applyDecomposition(prd, result) {
|
|
|
20348
20412
|
passes: false,
|
|
20349
20413
|
escalations: [],
|
|
20350
20414
|
attempts: 0,
|
|
20351
|
-
parentStoryId: sub.parentStoryId
|
|
20415
|
+
parentStoryId: sub.parentStoryId,
|
|
20416
|
+
...parentStory.workdir !== undefined && { workdir: parentStory.workdir }
|
|
20352
20417
|
}));
|
|
20353
20418
|
prd.userStories.splice(originalIndex + 1, 0, ...newStories);
|
|
20354
20419
|
}
|
|
@@ -20726,6 +20791,18 @@ function mergePackageConfig(root, packageOverride) {
|
|
|
20726
20791
|
...packageOverride.review,
|
|
20727
20792
|
commands: {
|
|
20728
20793
|
...root.review.commands,
|
|
20794
|
+
...packageOverride.quality?.commands?.lint !== undefined && {
|
|
20795
|
+
lint: packageOverride.quality.commands.lint
|
|
20796
|
+
},
|
|
20797
|
+
...packageOverride.quality?.commands?.lintFix !== undefined && {
|
|
20798
|
+
lintFix: packageOverride.quality.commands.lintFix
|
|
20799
|
+
},
|
|
20800
|
+
...packageOverride.quality?.commands?.typecheck !== undefined && {
|
|
20801
|
+
typecheck: packageOverride.quality.commands.typecheck
|
|
20802
|
+
},
|
|
20803
|
+
...packageOverride.quality?.commands?.test !== undefined && {
|
|
20804
|
+
test: packageOverride.quality.commands.test
|
|
20805
|
+
},
|
|
20729
20806
|
...packageOverride.review?.commands
|
|
20730
20807
|
}
|
|
20731
20808
|
},
|
|
@@ -22216,7 +22293,7 @@ function markStoryPassed(prd, storyId) {
|
|
|
22216
22293
|
story.status = "passed";
|
|
22217
22294
|
}
|
|
22218
22295
|
}
|
|
22219
|
-
function markStoryFailed(prd, storyId, failureCategory) {
|
|
22296
|
+
function markStoryFailed(prd, storyId, failureCategory, failureStage) {
|
|
22220
22297
|
const story = prd.userStories.find((s) => s.id === storyId);
|
|
22221
22298
|
if (story) {
|
|
22222
22299
|
story.status = "failed";
|
|
@@ -22224,6 +22301,9 @@ function markStoryFailed(prd, storyId, failureCategory) {
|
|
|
22224
22301
|
if (failureCategory !== undefined) {
|
|
22225
22302
|
story.failureCategory = failureCategory;
|
|
22226
22303
|
}
|
|
22304
|
+
if (failureStage !== undefined) {
|
|
22305
|
+
story.failureStage = failureStage;
|
|
22306
|
+
}
|
|
22227
22307
|
}
|
|
22228
22308
|
}
|
|
22229
22309
|
function markStorySkipped(prd, storyId) {
|
|
@@ -22250,7 +22330,7 @@ var package_default;
|
|
|
22250
22330
|
var init_package = __esm(() => {
|
|
22251
22331
|
package_default = {
|
|
22252
22332
|
name: "@nathapp/nax",
|
|
22253
|
-
version: "0.
|
|
22333
|
+
version: "0.50.0",
|
|
22254
22334
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22255
22335
|
type: "module",
|
|
22256
22336
|
bin: {
|
|
@@ -22323,8 +22403,8 @@ var init_version = __esm(() => {
|
|
|
22323
22403
|
NAX_VERSION = package_default.version;
|
|
22324
22404
|
NAX_COMMIT = (() => {
|
|
22325
22405
|
try {
|
|
22326
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22327
|
-
return "
|
|
22406
|
+
if (/^[0-9a-f]{6,10}$/.test("0eeefb4"))
|
|
22407
|
+
return "0eeefb4";
|
|
22328
22408
|
} catch {}
|
|
22329
22409
|
try {
|
|
22330
22410
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -23309,6 +23389,7 @@ class WebhookInteractionPlugin {
|
|
|
23309
23389
|
server = null;
|
|
23310
23390
|
serverStartPromise = null;
|
|
23311
23391
|
pendingResponses = new Map;
|
|
23392
|
+
receiveCallbacks = new Map;
|
|
23312
23393
|
async init(config2) {
|
|
23313
23394
|
const cfg = WebhookConfigSchema.parse(config2);
|
|
23314
23395
|
this.config = {
|
|
@@ -23358,27 +23439,39 @@ class WebhookInteractionPlugin {
|
|
|
23358
23439
|
}
|
|
23359
23440
|
async receive(requestId, timeout = 60000) {
|
|
23360
23441
|
await this.startServer();
|
|
23361
|
-
const
|
|
23362
|
-
|
|
23363
|
-
|
|
23364
|
-
|
|
23365
|
-
const response = this.pendingResponses.get(requestId);
|
|
23366
|
-
if (response) {
|
|
23367
|
-
this.pendingResponses.delete(requestId);
|
|
23368
|
-
return response;
|
|
23369
|
-
}
|
|
23370
|
-
await _webhookPluginDeps.sleep(backoffMs);
|
|
23371
|
-
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
23442
|
+
const early = this.pendingResponses.get(requestId);
|
|
23443
|
+
if (early) {
|
|
23444
|
+
this.pendingResponses.delete(requestId);
|
|
23445
|
+
return early;
|
|
23372
23446
|
}
|
|
23373
|
-
return {
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23447
|
+
return new Promise((resolve7) => {
|
|
23448
|
+
const timer = setTimeout(() => {
|
|
23449
|
+
this.receiveCallbacks.delete(requestId);
|
|
23450
|
+
resolve7({
|
|
23451
|
+
requestId,
|
|
23452
|
+
action: "skip",
|
|
23453
|
+
respondedBy: "timeout",
|
|
23454
|
+
respondedAt: Date.now()
|
|
23455
|
+
});
|
|
23456
|
+
}, timeout);
|
|
23457
|
+
this.receiveCallbacks.set(requestId, (response) => {
|
|
23458
|
+
clearTimeout(timer);
|
|
23459
|
+
this.receiveCallbacks.delete(requestId);
|
|
23460
|
+
resolve7(response);
|
|
23461
|
+
});
|
|
23462
|
+
});
|
|
23379
23463
|
}
|
|
23380
23464
|
async cancel(requestId) {
|
|
23381
23465
|
this.pendingResponses.delete(requestId);
|
|
23466
|
+
this.receiveCallbacks.delete(requestId);
|
|
23467
|
+
}
|
|
23468
|
+
deliverResponse(requestId, response) {
|
|
23469
|
+
const cb = this.receiveCallbacks.get(requestId);
|
|
23470
|
+
if (cb) {
|
|
23471
|
+
cb(response);
|
|
23472
|
+
} else {
|
|
23473
|
+
this.pendingResponses.set(requestId, response);
|
|
23474
|
+
}
|
|
23382
23475
|
}
|
|
23383
23476
|
async startServer() {
|
|
23384
23477
|
if (this.server)
|
|
@@ -23431,7 +23524,7 @@ class WebhookInteractionPlugin {
|
|
|
23431
23524
|
try {
|
|
23432
23525
|
const parsed = JSON.parse(body);
|
|
23433
23526
|
const response = InteractionResponseSchema.parse(parsed);
|
|
23434
|
-
this.
|
|
23527
|
+
this.deliverResponse(requestId, response);
|
|
23435
23528
|
} catch {
|
|
23436
23529
|
return new Response("Bad Request: Invalid response format", { status: 400 });
|
|
23437
23530
|
}
|
|
@@ -23439,7 +23532,7 @@ class WebhookInteractionPlugin {
|
|
|
23439
23532
|
try {
|
|
23440
23533
|
const parsed = await req.json();
|
|
23441
23534
|
const response = InteractionResponseSchema.parse(parsed);
|
|
23442
|
-
this.
|
|
23535
|
+
this.deliverResponse(requestId, response);
|
|
23443
23536
|
} catch {
|
|
23444
23537
|
return new Response("Bad Request: Invalid response format", { status: 400 });
|
|
23445
23538
|
}
|
|
@@ -23466,12 +23559,9 @@ class WebhookInteractionPlugin {
|
|
|
23466
23559
|
}
|
|
23467
23560
|
}
|
|
23468
23561
|
}
|
|
23469
|
-
var
|
|
23562
|
+
var WebhookConfigSchema, InteractionResponseSchema;
|
|
23470
23563
|
var init_webhook = __esm(() => {
|
|
23471
23564
|
init_zod();
|
|
23472
|
-
_webhookPluginDeps = {
|
|
23473
|
-
sleep: (ms) => Bun.sleep(ms)
|
|
23474
|
-
};
|
|
23475
23565
|
WebhookConfigSchema = exports_external.object({
|
|
23476
23566
|
url: exports_external.string().url().optional(),
|
|
23477
23567
|
callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
|
|
@@ -24706,6 +24796,9 @@ ${c.output}
|
|
|
24706
24796
|
\`\`\``).join(`
|
|
24707
24797
|
|
|
24708
24798
|
`);
|
|
24799
|
+
const scopeConstraint = story.workdir ? `
|
|
24800
|
+
|
|
24801
|
+
IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
|
|
24709
24802
|
return `You are fixing lint/typecheck errors from a code review.
|
|
24710
24803
|
|
|
24711
24804
|
Story: ${story.title} (${story.id})
|
|
@@ -24716,7 +24809,7 @@ ${errors3}
|
|
|
24716
24809
|
|
|
24717
24810
|
Fix ALL errors listed above. Do NOT change test files or test behavior.
|
|
24718
24811
|
Do NOT add new features \u2014 only fix the quality check errors.
|
|
24719
|
-
Commit your fixes when done
|
|
24812
|
+
Commit your fixes when done.${scopeConstraint}`;
|
|
24720
24813
|
}
|
|
24721
24814
|
async function runAgentRectification(ctx) {
|
|
24722
24815
|
const logger = getLogger();
|
|
@@ -24743,9 +24836,10 @@ async function runAgentRectification(ctx) {
|
|
|
24743
24836
|
const prompt = buildReviewRectificationPrompt(failedChecks, ctx.story);
|
|
24744
24837
|
const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
|
|
24745
24838
|
const modelDef = resolveModel(ctx.config.models[modelTier]);
|
|
24839
|
+
const rectificationWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
|
|
24746
24840
|
await agent.run({
|
|
24747
24841
|
prompt,
|
|
24748
|
-
workdir:
|
|
24842
|
+
workdir: rectificationWorkdir,
|
|
24749
24843
|
modelTier,
|
|
24750
24844
|
modelDef,
|
|
24751
24845
|
timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
|
|
@@ -25299,6 +25393,32 @@ var init_elements = __esm(() => {
|
|
|
25299
25393
|
init_logger2();
|
|
25300
25394
|
});
|
|
25301
25395
|
|
|
25396
|
+
// src/context/parent-context.ts
|
|
25397
|
+
function getParentOutputFiles(story, allStories) {
|
|
25398
|
+
if (!story.dependencies || story.dependencies.length === 0)
|
|
25399
|
+
return [];
|
|
25400
|
+
const parentFiles = [];
|
|
25401
|
+
for (const depId of story.dependencies) {
|
|
25402
|
+
const parent = allStories.find((s) => s.id === depId);
|
|
25403
|
+
if (parent?.outputFiles) {
|
|
25404
|
+
parentFiles.push(...parent.outputFiles);
|
|
25405
|
+
}
|
|
25406
|
+
}
|
|
25407
|
+
const unique = [...new Set(parentFiles)];
|
|
25408
|
+
return unique.filter((f) => !NOISE_PATTERNS.some((p) => p.test(f))).slice(0, MAX_PARENT_FILES);
|
|
25409
|
+
}
|
|
25410
|
+
var MAX_PARENT_FILES = 10, NOISE_PATTERNS;
|
|
25411
|
+
var init_parent_context = __esm(() => {
|
|
25412
|
+
NOISE_PATTERNS = [
|
|
25413
|
+
/\.test\.(ts|js|tsx|jsx)$/,
|
|
25414
|
+
/\.spec\.(ts|js|tsx|jsx)$/,
|
|
25415
|
+
/package-lock\.json$/,
|
|
25416
|
+
/bun\.lockb?$/,
|
|
25417
|
+
/\.gitignore$/,
|
|
25418
|
+
/^nax\//
|
|
25419
|
+
];
|
|
25420
|
+
});
|
|
25421
|
+
|
|
25302
25422
|
// src/context/test-scanner.ts
|
|
25303
25423
|
import path6 from "path";
|
|
25304
25424
|
var {Glob } = globalThis.Bun;
|
|
@@ -25646,6 +25766,18 @@ async function buildContext(storyContext, budget) {
|
|
|
25646
25766
|
}
|
|
25647
25767
|
}
|
|
25648
25768
|
elements.push(createStoryContext(currentStory, 80));
|
|
25769
|
+
if (prd.analysis) {
|
|
25770
|
+
const analysisContent = `The following analysis was performed during the planning phase. Use it to understand the codebase context before implementing:
|
|
25771
|
+
|
|
25772
|
+
${prd.analysis}`;
|
|
25773
|
+
elements.push({
|
|
25774
|
+
type: "planning-analysis",
|
|
25775
|
+
label: "Planning Analysis",
|
|
25776
|
+
content: analysisContent,
|
|
25777
|
+
priority: 88,
|
|
25778
|
+
tokens: estimateTokens(analysisContent)
|
|
25779
|
+
});
|
|
25780
|
+
}
|
|
25649
25781
|
addDependencyElements(elements, currentStory, prd);
|
|
25650
25782
|
await addTestCoverageElement(elements, storyContext, currentStory);
|
|
25651
25783
|
await addFileElements(elements, storyContext, currentStory);
|
|
@@ -25706,6 +25838,15 @@ async function addFileElements(elements, storyContext, story) {
|
|
|
25706
25838
|
if (fileInjection !== "keyword")
|
|
25707
25839
|
return;
|
|
25708
25840
|
let contextFiles = getContextFiles(story);
|
|
25841
|
+
const parentFiles = getParentOutputFiles(story, storyContext.prd?.userStories ?? []);
|
|
25842
|
+
if (parentFiles.length > 0) {
|
|
25843
|
+
const logger = getLogger();
|
|
25844
|
+
logger.info("context", "Injecting parent output files for context chaining", {
|
|
25845
|
+
storyId: story.id,
|
|
25846
|
+
parentFiles
|
|
25847
|
+
});
|
|
25848
|
+
contextFiles = [...new Set([...contextFiles, ...parentFiles])];
|
|
25849
|
+
}
|
|
25709
25850
|
if (contextFiles.length === 0 && storyContext.config?.context?.autoDetect?.enabled !== false && storyContext.workdir) {
|
|
25710
25851
|
const autoDetectConfig = storyContext.config?.context?.autoDetect;
|
|
25711
25852
|
try {
|
|
@@ -25773,6 +25914,7 @@ var init_builder3 = __esm(() => {
|
|
|
25773
25914
|
init_prd();
|
|
25774
25915
|
init_auto_detect();
|
|
25775
25916
|
init_elements();
|
|
25917
|
+
init_parent_context();
|
|
25776
25918
|
init_test_scanner();
|
|
25777
25919
|
init_elements();
|
|
25778
25920
|
_deps5 = {
|
|
@@ -26037,13 +26179,13 @@ function isSourceFile(filePath) {
|
|
|
26037
26179
|
return SRC_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
26038
26180
|
}
|
|
26039
26181
|
async function getChangedFiles2(workdir, fromRef = "HEAD") {
|
|
26040
|
-
const proc =
|
|
26182
|
+
const proc = _isolationDeps.spawn(["git", "diff", "--name-only", fromRef], {
|
|
26041
26183
|
cwd: workdir,
|
|
26042
26184
|
stdout: "pipe",
|
|
26043
26185
|
stderr: "pipe"
|
|
26044
26186
|
});
|
|
26187
|
+
const output = await Bun.readableStreamToText(proc.stdout);
|
|
26045
26188
|
await proc.exited;
|
|
26046
|
-
const output = await new Response(proc.stdout).text();
|
|
26047
26189
|
return output.trim().split(`
|
|
26048
26190
|
`).filter(Boolean);
|
|
26049
26191
|
}
|
|
@@ -26090,8 +26232,9 @@ async function verifyImplementerIsolation(workdir, beforeRef) {
|
|
|
26090
26232
|
description: "Implementer should not modify test files"
|
|
26091
26233
|
};
|
|
26092
26234
|
}
|
|
26093
|
-
var TEST_PATTERNS, SRC_PATTERNS;
|
|
26235
|
+
var _isolationDeps, TEST_PATTERNS, SRC_PATTERNS;
|
|
26094
26236
|
var init_isolation = __esm(() => {
|
|
26237
|
+
_isolationDeps = { spawn: Bun.spawn };
|
|
26095
26238
|
TEST_PATTERNS = [/^test\//, /^tests\//, /^__tests__\//, /\.spec\.\w+$/, /\.test\.\w+$/, /\.e2e-spec\.\w+$/];
|
|
26096
26239
|
SRC_PATTERNS = [/^src\//, /^lib\//, /^packages\//];
|
|
26097
26240
|
});
|
|
@@ -26241,6 +26384,22 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
|
26241
26384
|
await commitProc.exited;
|
|
26242
26385
|
} catch {}
|
|
26243
26386
|
}
|
|
26387
|
+
async function captureOutputFiles(workdir, baseRef, scopePrefix) {
|
|
26388
|
+
if (!baseRef)
|
|
26389
|
+
return [];
|
|
26390
|
+
try {
|
|
26391
|
+
const args = ["diff", "--name-only", `${baseRef}..HEAD`];
|
|
26392
|
+
if (scopePrefix)
|
|
26393
|
+
args.push("--", `${scopePrefix}/`);
|
|
26394
|
+
const proc = _gitDeps.spawn(["git", ...args], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
26395
|
+
const output = await new Response(proc.stdout).text();
|
|
26396
|
+
await proc.exited;
|
|
26397
|
+
return output.trim().split(`
|
|
26398
|
+
`).filter(Boolean);
|
|
26399
|
+
} catch {
|
|
26400
|
+
return [];
|
|
26401
|
+
}
|
|
26402
|
+
}
|
|
26244
26403
|
var _gitDeps, GIT_TIMEOUT_MS = 1e4;
|
|
26245
26404
|
var init_git = __esm(() => {
|
|
26246
26405
|
init_logger2();
|
|
@@ -26289,7 +26448,7 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
|
|
|
26289
26448
|
const shell = options?.shell ?? "/bin/sh";
|
|
26290
26449
|
const gracePeriodMs = options?.gracePeriodMs ?? 5000;
|
|
26291
26450
|
const drainTimeoutMs = options?.drainTimeoutMs ?? 2000;
|
|
26292
|
-
const proc =
|
|
26451
|
+
const proc = _executorDeps.spawn([shell, "-c", command], {
|
|
26293
26452
|
stdout: "pipe",
|
|
26294
26453
|
stderr: "pipe",
|
|
26295
26454
|
env: env2 || normalizeEnvironment(process.env),
|
|
@@ -26383,8 +26542,9 @@ function buildTestCommand(baseCommand, options) {
|
|
|
26383
26542
|
}
|
|
26384
26543
|
return command;
|
|
26385
26544
|
}
|
|
26386
|
-
var DEFAULT_STRIP_ENV_VARS;
|
|
26545
|
+
var _executorDeps, DEFAULT_STRIP_ENV_VARS;
|
|
26387
26546
|
var init_executor = __esm(() => {
|
|
26547
|
+
_executorDeps = { spawn: Bun.spawn };
|
|
26388
26548
|
DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
|
|
26389
26549
|
});
|
|
26390
26550
|
|
|
@@ -26684,15 +26844,15 @@ var init_verification = __esm(() => {
|
|
|
26684
26844
|
// src/tdd/cleanup.ts
|
|
26685
26845
|
async function getPgid(pid) {
|
|
26686
26846
|
try {
|
|
26687
|
-
const proc =
|
|
26847
|
+
const proc = _cleanupDeps.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
|
|
26688
26848
|
stdout: "pipe",
|
|
26689
26849
|
stderr: "pipe"
|
|
26690
26850
|
});
|
|
26851
|
+
const output = await Bun.readableStreamToText(proc.stdout);
|
|
26691
26852
|
const exitCode = await proc.exited;
|
|
26692
26853
|
if (exitCode !== 0) {
|
|
26693
26854
|
return null;
|
|
26694
26855
|
}
|
|
26695
|
-
const output = await new Response(proc.stdout).text();
|
|
26696
26856
|
const pgid = Number.parseInt(output.trim(), 10);
|
|
26697
26857
|
return Number.isNaN(pgid) ? null : pgid;
|
|
26698
26858
|
} catch {
|
|
@@ -26706,7 +26866,7 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
|
|
|
26706
26866
|
return;
|
|
26707
26867
|
}
|
|
26708
26868
|
try {
|
|
26709
|
-
|
|
26869
|
+
_cleanupDeps.kill(-pgid, "SIGTERM");
|
|
26710
26870
|
} catch (error48) {
|
|
26711
26871
|
const err = error48;
|
|
26712
26872
|
if (err.code !== "ESRCH") {
|
|
@@ -26714,11 +26874,11 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
|
|
|
26714
26874
|
}
|
|
26715
26875
|
return;
|
|
26716
26876
|
}
|
|
26717
|
-
await
|
|
26877
|
+
await _cleanupDeps.sleep(gracePeriodMs);
|
|
26718
26878
|
const pgidAfterWait = await getPgid(pid);
|
|
26719
26879
|
if (pgidAfterWait && pgidAfterWait === pgid) {
|
|
26720
26880
|
try {
|
|
26721
|
-
|
|
26881
|
+
_cleanupDeps.kill(-pgid, "SIGKILL");
|
|
26722
26882
|
} catch {}
|
|
26723
26883
|
}
|
|
26724
26884
|
} catch (error48) {
|
|
@@ -26729,8 +26889,14 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
|
|
|
26729
26889
|
});
|
|
26730
26890
|
}
|
|
26731
26891
|
}
|
|
26892
|
+
var _cleanupDeps;
|
|
26732
26893
|
var init_cleanup = __esm(() => {
|
|
26733
26894
|
init_logger2();
|
|
26895
|
+
_cleanupDeps = {
|
|
26896
|
+
spawn: Bun.spawn,
|
|
26897
|
+
sleep: Bun.sleep,
|
|
26898
|
+
kill: process.kill.bind(process)
|
|
26899
|
+
};
|
|
26734
26900
|
});
|
|
26735
26901
|
|
|
26736
26902
|
// src/tdd/prompts.ts
|
|
@@ -26753,10 +26919,12 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
|
|
|
26753
26919
|
storyId: story.id,
|
|
26754
26920
|
timeout: fullSuiteTimeout
|
|
26755
26921
|
});
|
|
26756
|
-
const fullSuiteResult = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26922
|
+
const fullSuiteResult = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
26923
|
+
cwd: workdir
|
|
26924
|
+
});
|
|
26757
26925
|
const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
|
|
26758
26926
|
if (!fullSuitePassed && fullSuiteResult.output) {
|
|
26759
|
-
const testSummary = parseBunTestOutput(fullSuiteResult.output);
|
|
26927
|
+
const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
|
|
26760
26928
|
if (testSummary.failed > 0) {
|
|
26761
26929
|
return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
|
|
26762
26930
|
}
|
|
@@ -26797,8 +26965,14 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26797
26965
|
failedTests: testSummary.failed,
|
|
26798
26966
|
passedTests: testSummary.passed
|
|
26799
26967
|
});
|
|
26800
|
-
|
|
26968
|
+
const rectificationSessionName = buildSessionName(workdir, featureName, story.id, "implementer");
|
|
26969
|
+
logger.debug("tdd", "Rectification session name (shared across all attempts)", {
|
|
26970
|
+
storyId: story.id,
|
|
26971
|
+
sessionName: rectificationSessionName
|
|
26972
|
+
});
|
|
26973
|
+
while (_rectificationGateDeps.shouldRetryRectification(rectificationState, rectificationConfig)) {
|
|
26801
26974
|
rectificationState.attempt++;
|
|
26975
|
+
const isLastAttempt = rectificationState.attempt >= rectificationConfig.maxRetries;
|
|
26802
26976
|
logger.info("tdd", `-> Implementer rectification attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, { storyId: story.id, currentFailures: rectificationState.currentFailures });
|
|
26803
26977
|
const rectificationPrompt = buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig);
|
|
26804
26978
|
const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
|
|
@@ -26814,7 +26988,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26814
26988
|
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
26815
26989
|
featureName,
|
|
26816
26990
|
storyId: story.id,
|
|
26817
|
-
sessionRole: "implementer"
|
|
26991
|
+
sessionRole: "implementer",
|
|
26992
|
+
acpSessionName: rectificationSessionName,
|
|
26993
|
+
keepSessionOpen: !isLastAttempt
|
|
26818
26994
|
});
|
|
26819
26995
|
if (!rectifyResult.success && rectifyResult.pid) {
|
|
26820
26996
|
await cleanupProcessTree(rectifyResult.pid);
|
|
@@ -26842,7 +27018,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26842
27018
|
});
|
|
26843
27019
|
break;
|
|
26844
27020
|
}
|
|
26845
|
-
const retryFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
27021
|
+
const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
27022
|
+
cwd: workdir
|
|
27023
|
+
});
|
|
26846
27024
|
const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
|
|
26847
27025
|
if (retrySuitePassed) {
|
|
26848
27026
|
logger.info("tdd", "Full suite gate passed after rectification!", {
|
|
@@ -26852,7 +27030,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26852
27030
|
return true;
|
|
26853
27031
|
}
|
|
26854
27032
|
if (retryFullSuite.output) {
|
|
26855
|
-
const newTestSummary = parseBunTestOutput(retryFullSuite.output);
|
|
27033
|
+
const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
|
|
26856
27034
|
rectificationState.currentFailures = newTestSummary.failed;
|
|
26857
27035
|
testSummary.failures = newTestSummary.failures;
|
|
26858
27036
|
testSummary.failed = newTestSummary.failed;
|
|
@@ -26864,7 +27042,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26864
27042
|
remainingFailures: rectificationState.currentFailures
|
|
26865
27043
|
});
|
|
26866
27044
|
}
|
|
26867
|
-
const finalFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
27045
|
+
const finalFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
|
|
27046
|
+
cwd: workdir
|
|
27047
|
+
});
|
|
26868
27048
|
const finalSuitePassed = finalFullSuite.success && finalFullSuite.exitCode === 0;
|
|
26869
27049
|
if (!finalSuitePassed) {
|
|
26870
27050
|
logger.warn("tdd", "[WARN] Full suite gate failed after rectification exhausted", {
|
|
@@ -26877,13 +27057,20 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26877
27057
|
logger.info("tdd", "Full suite gate passed", { storyId: story.id });
|
|
26878
27058
|
return true;
|
|
26879
27059
|
}
|
|
27060
|
+
var _rectificationGateDeps;
|
|
26880
27061
|
var init_rectification_gate = __esm(() => {
|
|
27062
|
+
init_adapter2();
|
|
26881
27063
|
init_config();
|
|
26882
27064
|
init_git();
|
|
26883
27065
|
init_verification();
|
|
26884
27066
|
init_cleanup();
|
|
26885
27067
|
init_isolation();
|
|
26886
27068
|
init_prompts();
|
|
27069
|
+
_rectificationGateDeps = {
|
|
27070
|
+
executeWithTimeout,
|
|
27071
|
+
parseBunTestOutput,
|
|
27072
|
+
shouldRetryRectification
|
|
27073
|
+
};
|
|
26887
27074
|
});
|
|
26888
27075
|
|
|
26889
27076
|
// src/prompts/sections/conventions.ts
|
|
@@ -27373,7 +27560,7 @@ ${tail}`;
|
|
|
27373
27560
|
async function rollbackToRef(workdir, ref) {
|
|
27374
27561
|
const logger = getLogger();
|
|
27375
27562
|
logger.warn("tdd", "Rolling back git changes", { ref });
|
|
27376
|
-
const resetProc =
|
|
27563
|
+
const resetProc = _sessionRunnerDeps.spawn(["git", "reset", "--hard", ref], {
|
|
27377
27564
|
cwd: workdir,
|
|
27378
27565
|
stdout: "pipe",
|
|
27379
27566
|
stderr: "pipe"
|
|
@@ -27384,7 +27571,7 @@ async function rollbackToRef(workdir, ref) {
|
|
|
27384
27571
|
logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
|
|
27385
27572
|
throw new Error(`Git rollback failed: ${stderr}`);
|
|
27386
27573
|
}
|
|
27387
|
-
const cleanProc =
|
|
27574
|
+
const cleanProc = _sessionRunnerDeps.spawn(["git", "clean", "-fd"], {
|
|
27388
27575
|
cwd: workdir,
|
|
27389
27576
|
stdout: "pipe",
|
|
27390
27577
|
stderr: "pipe"
|
|
@@ -27399,19 +27586,24 @@ async function rollbackToRef(workdir, ref) {
|
|
|
27399
27586
|
async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
|
|
27400
27587
|
const startTime = Date.now();
|
|
27401
27588
|
let prompt;
|
|
27402
|
-
|
|
27403
|
-
|
|
27404
|
-
|
|
27405
|
-
|
|
27406
|
-
|
|
27407
|
-
|
|
27408
|
-
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
27589
|
+
if (_sessionRunnerDeps.buildPrompt) {
|
|
27590
|
+
prompt = await _sessionRunnerDeps.buildPrompt(role, config2, story, workdir, contextMarkdown, lite, constitution);
|
|
27591
|
+
} else {
|
|
27592
|
+
switch (role) {
|
|
27593
|
+
case "test-writer":
|
|
27594
|
+
prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
|
|
27595
|
+
break;
|
|
27596
|
+
case "implementer":
|
|
27597
|
+
prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
|
|
27598
|
+
break;
|
|
27599
|
+
case "verifier":
|
|
27600
|
+
prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
|
|
27601
|
+
break;
|
|
27602
|
+
}
|
|
27412
27603
|
}
|
|
27413
27604
|
const logger = getLogger();
|
|
27414
27605
|
logger.info("tdd", `-> Session: ${role}`, { role, storyId: story.id, lite });
|
|
27606
|
+
const keepSessionOpen = role === "implementer" && (config2.execution.rectification?.enabled ?? false);
|
|
27415
27607
|
const result = await agent.run({
|
|
27416
27608
|
prompt,
|
|
27417
27609
|
workdir,
|
|
@@ -27424,10 +27616,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
27424
27616
|
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
27425
27617
|
featureName,
|
|
27426
27618
|
storyId: story.id,
|
|
27427
|
-
sessionRole: role
|
|
27619
|
+
sessionRole: role,
|
|
27620
|
+
keepSessionOpen
|
|
27428
27621
|
});
|
|
27429
27622
|
if (!result.success && result.pid) {
|
|
27430
|
-
await cleanupProcessTree(result.pid);
|
|
27623
|
+
await _sessionRunnerDeps.cleanupProcessTree(result.pid);
|
|
27431
27624
|
}
|
|
27432
27625
|
if (result.success) {
|
|
27433
27626
|
logger.info("tdd", `Session complete: ${role}`, {
|
|
@@ -27449,12 +27642,12 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
27449
27642
|
if (!skipIsolation) {
|
|
27450
27643
|
if (role === "test-writer") {
|
|
27451
27644
|
const allowedPaths = config2.tdd.testWriterAllowedPaths ?? ["src/index.ts", "src/**/index.ts"];
|
|
27452
|
-
isolation = await verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
|
|
27645
|
+
isolation = await _sessionRunnerDeps.verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
|
|
27453
27646
|
} else if (role === "implementer" || role === "verifier") {
|
|
27454
|
-
isolation = await verifyImplementerIsolation(workdir, beforeRef);
|
|
27647
|
+
isolation = await _sessionRunnerDeps.verifyImplementerIsolation(workdir, beforeRef);
|
|
27455
27648
|
}
|
|
27456
27649
|
}
|
|
27457
|
-
const filesChanged = await
|
|
27650
|
+
const filesChanged = await _sessionRunnerDeps.getChangedFiles(workdir, beforeRef);
|
|
27458
27651
|
const durationMs = Date.now() - startTime;
|
|
27459
27652
|
if (isolation && !isolation.passed) {
|
|
27460
27653
|
logger.error("tdd", "Isolation violated", {
|
|
@@ -27497,10 +27690,18 @@ var init_session_runner = __esm(() => {
|
|
|
27497
27690
|
init_logger2();
|
|
27498
27691
|
init_prompts2();
|
|
27499
27692
|
init_git();
|
|
27693
|
+
init_git();
|
|
27500
27694
|
init_cleanup();
|
|
27501
27695
|
init_isolation();
|
|
27502
27696
|
_sessionRunnerDeps = {
|
|
27503
|
-
autoCommitIfDirty
|
|
27697
|
+
autoCommitIfDirty,
|
|
27698
|
+
spawn: Bun.spawn,
|
|
27699
|
+
getChangedFiles: getChangedFiles2,
|
|
27700
|
+
verifyTestWriterIsolation,
|
|
27701
|
+
verifyImplementerIsolation,
|
|
27702
|
+
captureGitRef,
|
|
27703
|
+
cleanupProcessTree,
|
|
27704
|
+
buildPrompt: null
|
|
27504
27705
|
};
|
|
27505
27706
|
});
|
|
27506
27707
|
|
|
@@ -28981,7 +29182,7 @@ class AcceptanceStrategy {
|
|
|
28981
29182
|
}
|
|
28982
29183
|
const start = Date.now();
|
|
28983
29184
|
const timeoutMs = ctx.timeoutSeconds * 1000;
|
|
28984
|
-
const proc =
|
|
29185
|
+
const proc = _acceptanceDeps.spawn(["bun", "test", testPath], {
|
|
28985
29186
|
cwd: ctx.workdir,
|
|
28986
29187
|
stdout: "pipe",
|
|
28987
29188
|
stderr: "pipe"
|
|
@@ -29049,8 +29250,10 @@ ${stderr}`;
|
|
|
29049
29250
|
});
|
|
29050
29251
|
}
|
|
29051
29252
|
}
|
|
29253
|
+
var _acceptanceDeps;
|
|
29052
29254
|
var init_acceptance3 = __esm(() => {
|
|
29053
29255
|
init_logger2();
|
|
29256
|
+
_acceptanceDeps = { spawn: Bun.spawn };
|
|
29054
29257
|
});
|
|
29055
29258
|
|
|
29056
29259
|
// src/verification/strategies/regression.ts
|
|
@@ -33047,7 +33250,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
|
|
|
33047
33250
|
worktreePath
|
|
33048
33251
|
});
|
|
33049
33252
|
} catch (error48) {
|
|
33050
|
-
markStoryFailed(currentPrd, story.id);
|
|
33253
|
+
markStoryFailed(currentPrd, story.id, undefined, undefined);
|
|
33051
33254
|
logger?.error("parallel", "Failed to create worktree", {
|
|
33052
33255
|
storyId: story.id,
|
|
33053
33256
|
error: errorMessage(error48)
|
|
@@ -33075,7 +33278,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
|
|
|
33075
33278
|
retryCount: mergeResult.retryCount
|
|
33076
33279
|
});
|
|
33077
33280
|
} else {
|
|
33078
|
-
markStoryFailed(currentPrd, mergeResult.storyId);
|
|
33281
|
+
markStoryFailed(currentPrd, mergeResult.storyId, undefined, undefined);
|
|
33079
33282
|
batchResult.mergeConflicts.push({
|
|
33080
33283
|
storyId: mergeResult.storyId,
|
|
33081
33284
|
conflictFiles: mergeResult.conflictFiles || [],
|
|
@@ -33093,7 +33296,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
|
|
|
33093
33296
|
}
|
|
33094
33297
|
}
|
|
33095
33298
|
for (const { story, error: error48 } of batchResult.failed) {
|
|
33096
|
-
markStoryFailed(currentPrd, story.id);
|
|
33299
|
+
markStoryFailed(currentPrd, story.id, undefined, undefined);
|
|
33097
33300
|
logger?.error("parallel", "Cleaning up failed story worktree", {
|
|
33098
33301
|
storyId: story.id,
|
|
33099
33302
|
error: error48
|
|
@@ -34030,7 +34233,7 @@ async function handleNoTierAvailable(ctx, failureCategory) {
|
|
|
34030
34233
|
return { outcome: "paused", prdDirty: true, prd: pausedPrd };
|
|
34031
34234
|
}
|
|
34032
34235
|
const failedPrd = { ...ctx.prd };
|
|
34033
|
-
markStoryFailed(failedPrd, ctx.story.id, failureCategory);
|
|
34236
|
+
markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
|
|
34034
34237
|
await savePRD(failedPrd, ctx.prdPath);
|
|
34035
34238
|
logger?.error("execution", "Story failed - execution failed", {
|
|
34036
34239
|
storyId: ctx.story.id
|
|
@@ -34070,7 +34273,7 @@ async function handleMaxAttemptsReached(ctx, failureCategory) {
|
|
|
34070
34273
|
return { outcome: "paused", prdDirty: true, prd: pausedPrd };
|
|
34071
34274
|
}
|
|
34072
34275
|
const failedPrd = { ...ctx.prd };
|
|
34073
|
-
markStoryFailed(failedPrd, ctx.story.id, failureCategory);
|
|
34276
|
+
markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
|
|
34074
34277
|
await savePRD(failedPrd, ctx.prdPath);
|
|
34075
34278
|
logger?.error("execution", "Story failed - max attempts reached", {
|
|
34076
34279
|
storyId: ctx.story.id,
|
|
@@ -34235,6 +34438,17 @@ var init_escalation = __esm(() => {
|
|
|
34235
34438
|
});
|
|
34236
34439
|
|
|
34237
34440
|
// src/execution/pipeline-result-handler.ts
|
|
34441
|
+
function filterOutputFiles(files) {
|
|
34442
|
+
const NOISE = [
|
|
34443
|
+
/\.test\.(ts|js|tsx|jsx)$/,
|
|
34444
|
+
/\.spec\.(ts|js|tsx|jsx)$/,
|
|
34445
|
+
/package-lock\.json$/,
|
|
34446
|
+
/bun\.lock(b?)$/,
|
|
34447
|
+
/\.gitignore$/,
|
|
34448
|
+
/^nax\//
|
|
34449
|
+
];
|
|
34450
|
+
return files.filter((f) => !NOISE.some((p) => p.test(f))).slice(0, 15);
|
|
34451
|
+
}
|
|
34238
34452
|
async function handlePipelineSuccess(ctx, pipelineResult) {
|
|
34239
34453
|
const logger = getSafeLogger();
|
|
34240
34454
|
const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
|
|
@@ -34263,6 +34477,17 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
|
|
|
34263
34477
|
testStrategy: ctx.routing.testStrategy
|
|
34264
34478
|
});
|
|
34265
34479
|
}
|
|
34480
|
+
if (ctx.storyGitRef) {
|
|
34481
|
+
for (const completedStory of ctx.storiesToExecute) {
|
|
34482
|
+
try {
|
|
34483
|
+
const rawFiles = await captureOutputFiles(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
|
|
34484
|
+
const filtered = filterOutputFiles(rawFiles);
|
|
34485
|
+
if (filtered.length > 0) {
|
|
34486
|
+
completedStory.outputFiles = filtered;
|
|
34487
|
+
}
|
|
34488
|
+
} catch {}
|
|
34489
|
+
}
|
|
34490
|
+
}
|
|
34266
34491
|
const updatedCounts = countStories(prd);
|
|
34267
34492
|
logger?.info("progress", "Progress update", {
|
|
34268
34493
|
totalStories: updatedCounts.total,
|
|
@@ -34299,7 +34524,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
|
|
|
34299
34524
|
prdDirty = true;
|
|
34300
34525
|
break;
|
|
34301
34526
|
case "fail":
|
|
34302
|
-
markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory);
|
|
34527
|
+
markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory, pipelineResult.stoppedAtStage);
|
|
34303
34528
|
await savePRD(prd, ctx.prdPath);
|
|
34304
34529
|
prdDirty = true;
|
|
34305
34530
|
logger?.error("pipeline", "Story failed", { storyId: ctx.story.id, reason: pipelineResult.reason });
|
|
@@ -34353,6 +34578,7 @@ var init_pipeline_result_handler = __esm(() => {
|
|
|
34353
34578
|
init_logger2();
|
|
34354
34579
|
init_event_bus();
|
|
34355
34580
|
init_prd();
|
|
34581
|
+
init_git();
|
|
34356
34582
|
init_escalation();
|
|
34357
34583
|
init_progress();
|
|
34358
34584
|
});
|
|
@@ -35007,25 +35233,44 @@ var init_precheck_runner = __esm(() => {
|
|
|
35007
35233
|
var exports_run_initialization = {};
|
|
35008
35234
|
__export(exports_run_initialization, {
|
|
35009
35235
|
logActiveProtocol: () => logActiveProtocol,
|
|
35010
|
-
initializeRun: () => initializeRun
|
|
35236
|
+
initializeRun: () => initializeRun,
|
|
35237
|
+
_reconcileDeps: () => _reconcileDeps
|
|
35011
35238
|
});
|
|
35012
|
-
|
|
35239
|
+
import { join as join51 } from "path";
|
|
35240
|
+
async function reconcileState(prd, prdPath, workdir, config2) {
|
|
35013
35241
|
const logger = getSafeLogger();
|
|
35014
35242
|
let reconciledCount = 0;
|
|
35015
35243
|
let modified = false;
|
|
35016
35244
|
for (const story of prd.userStories) {
|
|
35017
|
-
if (story.status
|
|
35018
|
-
|
|
35019
|
-
|
|
35020
|
-
|
|
35021
|
-
|
|
35022
|
-
|
|
35023
|
-
|
|
35024
|
-
|
|
35025
|
-
|
|
35026
|
-
|
|
35245
|
+
if (story.status !== "failed")
|
|
35246
|
+
continue;
|
|
35247
|
+
const hasCommits = await _reconcileDeps.hasCommitsForStory(workdir, story.id);
|
|
35248
|
+
if (!hasCommits)
|
|
35249
|
+
continue;
|
|
35250
|
+
if (story.failureStage === "review" || story.failureStage === "autofix") {
|
|
35251
|
+
const effectiveWorkdir = story.workdir ? join51(workdir, story.workdir) : workdir;
|
|
35252
|
+
try {
|
|
35253
|
+
const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
|
|
35254
|
+
if (!reviewResult.success) {
|
|
35255
|
+
logger?.warn("reconciliation", "Review still fails \u2014 not reconciling story", {
|
|
35256
|
+
storyId: story.id,
|
|
35257
|
+
failureReason: reviewResult.failureReason
|
|
35258
|
+
});
|
|
35259
|
+
continue;
|
|
35260
|
+
}
|
|
35261
|
+
logger?.info("reconciliation", "Review now passes \u2014 reconciling story", { storyId: story.id });
|
|
35262
|
+
} catch {
|
|
35263
|
+
logger?.warn("reconciliation", "Review check errored \u2014 not reconciling story", { storyId: story.id });
|
|
35264
|
+
continue;
|
|
35027
35265
|
}
|
|
35028
35266
|
}
|
|
35267
|
+
logger?.warn("reconciliation", "Failed story has commits in git history, marking as passed", {
|
|
35268
|
+
storyId: story.id,
|
|
35269
|
+
title: story.title
|
|
35270
|
+
});
|
|
35271
|
+
markStoryPassed(prd, story.id);
|
|
35272
|
+
reconciledCount++;
|
|
35273
|
+
modified = true;
|
|
35029
35274
|
}
|
|
35030
35275
|
if (reconciledCount > 0) {
|
|
35031
35276
|
logger?.info("reconciliation", `Reconciled ${reconciledCount} failed stories from git history`);
|
|
@@ -35075,7 +35320,7 @@ async function initializeRun(ctx) {
|
|
|
35075
35320
|
const logger = getSafeLogger();
|
|
35076
35321
|
await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
|
|
35077
35322
|
let prd = await loadPRD(ctx.prdPath);
|
|
35078
|
-
prd = await reconcileState(prd, ctx.prdPath, ctx.workdir);
|
|
35323
|
+
prd = await reconcileState(prd, ctx.prdPath, ctx.workdir, ctx.config);
|
|
35079
35324
|
const counts = countStories(prd);
|
|
35080
35325
|
validateStoryCount(counts, ctx.config);
|
|
35081
35326
|
logger?.info("execution", "Run initialization complete", {
|
|
@@ -35085,11 +35330,17 @@ async function initializeRun(ctx) {
|
|
|
35085
35330
|
});
|
|
35086
35331
|
return { prd, storyCounts: counts };
|
|
35087
35332
|
}
|
|
35333
|
+
var _reconcileDeps;
|
|
35088
35334
|
var init_run_initialization = __esm(() => {
|
|
35089
35335
|
init_errors3();
|
|
35090
35336
|
init_logger2();
|
|
35091
35337
|
init_prd();
|
|
35338
|
+
init_runner2();
|
|
35092
35339
|
init_git();
|
|
35340
|
+
_reconcileDeps = {
|
|
35341
|
+
hasCommitsForStory: (workdir, storyId) => hasCommitsForStory(workdir, storyId),
|
|
35342
|
+
runReview: (reviewConfig, workdir, executionConfig) => runReview(reviewConfig, workdir, executionConfig)
|
|
35343
|
+
};
|
|
35093
35344
|
});
|
|
35094
35345
|
|
|
35095
35346
|
// src/execution/lifecycle/run-setup.ts
|
|
@@ -66131,7 +66382,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
|
|
|
66131
66382
|
init_source();
|
|
66132
66383
|
import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
|
|
66133
66384
|
import { homedir as homedir10 } from "os";
|
|
66134
|
-
import { join as
|
|
66385
|
+
import { join as join52 } from "path";
|
|
66135
66386
|
|
|
66136
66387
|
// node_modules/commander/esm.mjs
|
|
66137
66388
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -67557,14 +67808,48 @@ For each user story, set the "workdir" field to the relevant package path (e.g.
|
|
|
67557
67808
|
"workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
|
|
67558
67809
|
return `You are a senior software architect generating a product requirements document (PRD) as JSON.
|
|
67559
67810
|
|
|
67811
|
+
## Step 1: Understand the Spec
|
|
67812
|
+
|
|
67813
|
+
Read the spec carefully. Identify the goal, scope, constraints, and what "done" looks like.
|
|
67814
|
+
|
|
67560
67815
|
## Spec
|
|
67561
67816
|
|
|
67562
67817
|
${specContent}
|
|
67563
67818
|
|
|
67819
|
+
## Step 2: Analyze
|
|
67820
|
+
|
|
67821
|
+
Examine the codebase context below.
|
|
67822
|
+
|
|
67823
|
+
If the codebase has existing code (refactoring, enhancement, bug fix):
|
|
67824
|
+
- Which existing files need modification?
|
|
67825
|
+
- Which files import from or depend on them?
|
|
67826
|
+
- What tests cover the affected code?
|
|
67827
|
+
- What are the risks (breaking changes, backward compatibility)?
|
|
67828
|
+
- What is the migration path?
|
|
67829
|
+
|
|
67830
|
+
If this is a greenfield project (empty or minimal codebase):
|
|
67831
|
+
- What is the target architecture?
|
|
67832
|
+
- What are the key technical decisions (framework, patterns, conventions)?
|
|
67833
|
+
- What should be built first (dependency order)?
|
|
67834
|
+
|
|
67835
|
+
Record ALL findings in the "analysis" field of the output JSON. This analysis is provided to every implementation agent as context \u2014 be thorough.
|
|
67836
|
+
|
|
67564
67837
|
## Codebase Context
|
|
67565
67838
|
|
|
67566
67839
|
${codebaseContext}${monorepoHint}
|
|
67567
67840
|
|
|
67841
|
+
## Step 3: Generate Implementation Stories
|
|
67842
|
+
|
|
67843
|
+
Based on your Step 2 analysis, create stories that produce CODE CHANGES.
|
|
67844
|
+
|
|
67845
|
+
${GROUPING_RULES}
|
|
67846
|
+
|
|
67847
|
+
For each story, set "contextFiles" to the key source files the agent should read before implementing (max 5 per story). Use your Step 2 analysis to identify the most relevant files. Leave empty for greenfield stories with no existing files to reference.
|
|
67848
|
+
|
|
67849
|
+
${COMPLEXITY_GUIDE}
|
|
67850
|
+
|
|
67851
|
+
${TEST_STRATEGY_GUIDE}
|
|
67852
|
+
|
|
67568
67853
|
## Output Schema
|
|
67569
67854
|
|
|
67570
67855
|
Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
|
|
@@ -67572,6 +67857,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
67572
67857
|
{
|
|
67573
67858
|
"project": "string \u2014 project name",
|
|
67574
67859
|
"feature": "string \u2014 feature name",
|
|
67860
|
+
"analysis": "string \u2014 your Step 2 analysis: key files, impact areas, risks, architecture decisions, migration notes. All implementation agents will receive this.",
|
|
67575
67861
|
"branchName": "string \u2014 git branch (e.g. feat/my-feature)",
|
|
67576
67862
|
"createdAt": "ISO 8601 timestamp",
|
|
67577
67863
|
"updatedAt": "ISO 8601 timestamp",
|
|
@@ -67581,13 +67867,14 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
67581
67867
|
"title": "string \u2014 concise story title",
|
|
67582
67868
|
"description": "string \u2014 detailed description of the story",
|
|
67583
67869
|
"acceptanceCriteria": ["string \u2014 each AC line"],
|
|
67870
|
+
"contextFiles": ["string \u2014 key source files the agent should read (max 5, relative paths)"],
|
|
67584
67871
|
"tags": ["string \u2014 routing tags, e.g. feature, security, api"],
|
|
67585
67872
|
"dependencies": ["string \u2014 story IDs this story depends on"],${workdirField}
|
|
67586
67873
|
"status": "pending",
|
|
67587
67874
|
"passes": false,
|
|
67588
67875
|
"routing": {
|
|
67589
67876
|
"complexity": "simple | medium | complex | expert",
|
|
67590
|
-
"testStrategy": "
|
|
67877
|
+
"testStrategy": "tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
|
|
67591
67878
|
"reasoning": "string \u2014 brief classification rationale"
|
|
67592
67879
|
},
|
|
67593
67880
|
"escalations": [],
|
|
@@ -67596,12 +67883,6 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
67596
67883
|
]
|
|
67597
67884
|
}
|
|
67598
67885
|
|
|
67599
|
-
${COMPLEXITY_GUIDE}
|
|
67600
|
-
|
|
67601
|
-
${TEST_STRATEGY_GUIDE}
|
|
67602
|
-
|
|
67603
|
-
${GROUPING_RULES}
|
|
67604
|
-
|
|
67605
67886
|
${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
|
|
67606
67887
|
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."}`;
|
|
67607
67888
|
}
|
|
@@ -68243,6 +68524,45 @@ class PipelineEventEmitter {
|
|
|
68243
68524
|
init_stages();
|
|
68244
68525
|
init_prd();
|
|
68245
68526
|
|
|
68527
|
+
// src/cli/prompts-shared.ts
|
|
68528
|
+
function buildFrontmatter(story, ctx, role) {
|
|
68529
|
+
const lines = [];
|
|
68530
|
+
lines.push(`storyId: ${story.id}`);
|
|
68531
|
+
lines.push(`title: "${story.title}"`);
|
|
68532
|
+
lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
|
|
68533
|
+
lines.push(`modelTier: ${ctx.routing.modelTier}`);
|
|
68534
|
+
if (role) {
|
|
68535
|
+
lines.push(`role: ${role}`);
|
|
68536
|
+
}
|
|
68537
|
+
const builtContext = ctx.builtContext;
|
|
68538
|
+
const contextTokens = builtContext?.totalTokens ?? 0;
|
|
68539
|
+
const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
|
|
68540
|
+
lines.push(`contextTokens: ${contextTokens}`);
|
|
68541
|
+
lines.push(`promptTokens: ${promptTokens}`);
|
|
68542
|
+
if (story.dependencies && story.dependencies.length > 0) {
|
|
68543
|
+
lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
|
|
68544
|
+
}
|
|
68545
|
+
lines.push("contextElements:");
|
|
68546
|
+
if (builtContext) {
|
|
68547
|
+
for (const element of builtContext.elements) {
|
|
68548
|
+
lines.push(` - type: ${element.type}`);
|
|
68549
|
+
if (element.storyId) {
|
|
68550
|
+
lines.push(` storyId: ${element.storyId}`);
|
|
68551
|
+
}
|
|
68552
|
+
if (element.filePath) {
|
|
68553
|
+
lines.push(` filePath: ${element.filePath}`);
|
|
68554
|
+
}
|
|
68555
|
+
lines.push(` tokens: ${element.tokens}`);
|
|
68556
|
+
}
|
|
68557
|
+
}
|
|
68558
|
+
if (builtContext?.truncated) {
|
|
68559
|
+
lines.push("truncated: true");
|
|
68560
|
+
}
|
|
68561
|
+
return `${lines.join(`
|
|
68562
|
+
`)}
|
|
68563
|
+
`;
|
|
68564
|
+
}
|
|
68565
|
+
|
|
68246
68566
|
// src/cli/prompts-tdd.ts
|
|
68247
68567
|
init_prompts2();
|
|
68248
68568
|
import { join as join28 } from "path";
|
|
@@ -68388,43 +68708,6 @@ ${"=".repeat(80)}`);
|
|
|
68388
68708
|
});
|
|
68389
68709
|
return processedStories;
|
|
68390
68710
|
}
|
|
68391
|
-
function buildFrontmatter(story, ctx, role) {
|
|
68392
|
-
const lines = [];
|
|
68393
|
-
lines.push(`storyId: ${story.id}`);
|
|
68394
|
-
lines.push(`title: "${story.title}"`);
|
|
68395
|
-
lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
|
|
68396
|
-
lines.push(`modelTier: ${ctx.routing.modelTier}`);
|
|
68397
|
-
if (role) {
|
|
68398
|
-
lines.push(`role: ${role}`);
|
|
68399
|
-
}
|
|
68400
|
-
const builtContext = ctx.builtContext;
|
|
68401
|
-
const contextTokens = builtContext?.totalTokens ?? 0;
|
|
68402
|
-
const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
|
|
68403
|
-
lines.push(`contextTokens: ${contextTokens}`);
|
|
68404
|
-
lines.push(`promptTokens: ${promptTokens}`);
|
|
68405
|
-
if (story.dependencies && story.dependencies.length > 0) {
|
|
68406
|
-
lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
|
|
68407
|
-
}
|
|
68408
|
-
lines.push("contextElements:");
|
|
68409
|
-
if (builtContext) {
|
|
68410
|
-
for (const element of builtContext.elements) {
|
|
68411
|
-
lines.push(` - type: ${element.type}`);
|
|
68412
|
-
if (element.storyId) {
|
|
68413
|
-
lines.push(` storyId: ${element.storyId}`);
|
|
68414
|
-
}
|
|
68415
|
-
if (element.filePath) {
|
|
68416
|
-
lines.push(` filePath: ${element.filePath}`);
|
|
68417
|
-
}
|
|
68418
|
-
lines.push(` tokens: ${element.tokens}`);
|
|
68419
|
-
}
|
|
68420
|
-
}
|
|
68421
|
-
if (builtContext?.truncated) {
|
|
68422
|
-
lines.push("truncated: true");
|
|
68423
|
-
}
|
|
68424
|
-
return `${lines.join(`
|
|
68425
|
-
`)}
|
|
68426
|
-
`;
|
|
68427
|
-
}
|
|
68428
68711
|
// src/cli/prompts-init.ts
|
|
68429
68712
|
import { existsSync as existsSync19, mkdirSync as mkdirSync4 } from "fs";
|
|
68430
68713
|
import { join as join30 } from "path";
|
|
@@ -77916,15 +78199,15 @@ Next: nax generate --package ${options.package}`));
|
|
|
77916
78199
|
}
|
|
77917
78200
|
return;
|
|
77918
78201
|
}
|
|
77919
|
-
const naxDir =
|
|
78202
|
+
const naxDir = join52(workdir, "nax");
|
|
77920
78203
|
if (existsSync34(naxDir) && !options.force) {
|
|
77921
78204
|
console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
|
|
77922
78205
|
return;
|
|
77923
78206
|
}
|
|
77924
|
-
mkdirSync6(
|
|
77925
|
-
mkdirSync6(
|
|
77926
|
-
await Bun.write(
|
|
77927
|
-
await Bun.write(
|
|
78207
|
+
mkdirSync6(join52(naxDir, "features"), { recursive: true });
|
|
78208
|
+
mkdirSync6(join52(naxDir, "hooks"), { recursive: true });
|
|
78209
|
+
await Bun.write(join52(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
78210
|
+
await Bun.write(join52(naxDir, "hooks.json"), JSON.stringify({
|
|
77928
78211
|
hooks: {
|
|
77929
78212
|
"on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
|
|
77930
78213
|
"on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
|
|
@@ -77932,12 +78215,12 @@ Next: nax generate --package ${options.package}`));
|
|
|
77932
78215
|
"on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
|
|
77933
78216
|
}
|
|
77934
78217
|
}, null, 2));
|
|
77935
|
-
await Bun.write(
|
|
78218
|
+
await Bun.write(join52(naxDir, ".gitignore"), `# nax temp files
|
|
77936
78219
|
*.tmp
|
|
77937
78220
|
.paused.json
|
|
77938
78221
|
.nax-verifier-verdict.json
|
|
77939
78222
|
`);
|
|
77940
|
-
await Bun.write(
|
|
78223
|
+
await Bun.write(join52(naxDir, "context.md"), `# Project Context
|
|
77941
78224
|
|
|
77942
78225
|
This document defines coding standards, architectural decisions, and forbidden patterns for this project.
|
|
77943
78226
|
Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
|
|
@@ -78063,8 +78346,8 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
78063
78346
|
console.error(source_default.red("nax not initialized. Run: nax init"));
|
|
78064
78347
|
process.exit(1);
|
|
78065
78348
|
}
|
|
78066
|
-
const featureDir =
|
|
78067
|
-
const prdPath =
|
|
78349
|
+
const featureDir = join52(naxDir, "features", options.feature);
|
|
78350
|
+
const prdPath = join52(featureDir, "prd.json");
|
|
78068
78351
|
if (options.plan && options.from) {
|
|
78069
78352
|
if (existsSync34(prdPath) && !options.force) {
|
|
78070
78353
|
console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
|
|
@@ -78086,10 +78369,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
78086
78369
|
}
|
|
78087
78370
|
}
|
|
78088
78371
|
try {
|
|
78089
|
-
const planLogDir =
|
|
78372
|
+
const planLogDir = join52(featureDir, "plan");
|
|
78090
78373
|
mkdirSync6(planLogDir, { recursive: true });
|
|
78091
78374
|
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
78092
|
-
const planLogPath =
|
|
78375
|
+
const planLogPath = join52(planLogDir, `${planLogId}.jsonl`);
|
|
78093
78376
|
initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
|
|
78094
78377
|
console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
|
|
78095
78378
|
console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
|
|
@@ -78127,10 +78410,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
78127
78410
|
process.exit(1);
|
|
78128
78411
|
}
|
|
78129
78412
|
resetLogger();
|
|
78130
|
-
const runsDir =
|
|
78413
|
+
const runsDir = join52(featureDir, "runs");
|
|
78131
78414
|
mkdirSync6(runsDir, { recursive: true });
|
|
78132
78415
|
const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
78133
|
-
const logFilePath =
|
|
78416
|
+
const logFilePath = join52(runsDir, `${runId}.jsonl`);
|
|
78134
78417
|
const isTTY = process.stdout.isTTY ?? false;
|
|
78135
78418
|
const headlessFlag = options.headless ?? false;
|
|
78136
78419
|
const headlessEnv = process.env.NAX_HEADLESS === "1";
|
|
@@ -78146,7 +78429,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
78146
78429
|
config2.autoMode.defaultAgent = options.agent;
|
|
78147
78430
|
}
|
|
78148
78431
|
config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
|
|
78149
|
-
const globalNaxDir =
|
|
78432
|
+
const globalNaxDir = join52(homedir10(), ".nax");
|
|
78150
78433
|
const hooks = await loadHooksConfig(naxDir, globalNaxDir);
|
|
78151
78434
|
const eventEmitter = new PipelineEventEmitter;
|
|
78152
78435
|
let tuiInstance;
|
|
@@ -78169,7 +78452,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
78169
78452
|
} else {
|
|
78170
78453
|
console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
|
|
78171
78454
|
}
|
|
78172
|
-
const statusFilePath =
|
|
78455
|
+
const statusFilePath = join52(workdir, "nax", "status.json");
|
|
78173
78456
|
let parallel;
|
|
78174
78457
|
if (options.parallel !== undefined) {
|
|
78175
78458
|
parallel = Number.parseInt(options.parallel, 10);
|
|
@@ -78195,7 +78478,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
78195
78478
|
headless: useHeadless,
|
|
78196
78479
|
skipPrecheck: options.skipPrecheck ?? false
|
|
78197
78480
|
});
|
|
78198
|
-
const latestSymlink =
|
|
78481
|
+
const latestSymlink = join52(runsDir, "latest.jsonl");
|
|
78199
78482
|
try {
|
|
78200
78483
|
if (existsSync34(latestSymlink)) {
|
|
78201
78484
|
Bun.spawnSync(["rm", latestSymlink]);
|
|
@@ -78233,9 +78516,9 @@ features.command("create <name>").description("Create a new feature").option("-d
|
|
|
78233
78516
|
console.error(source_default.red("nax not initialized. Run: nax init"));
|
|
78234
78517
|
process.exit(1);
|
|
78235
78518
|
}
|
|
78236
|
-
const featureDir =
|
|
78519
|
+
const featureDir = join52(naxDir, "features", name);
|
|
78237
78520
|
mkdirSync6(featureDir, { recursive: true });
|
|
78238
|
-
await Bun.write(
|
|
78521
|
+
await Bun.write(join52(featureDir, "spec.md"), `# Feature: ${name}
|
|
78239
78522
|
|
|
78240
78523
|
## Overview
|
|
78241
78524
|
|
|
@@ -78243,7 +78526,7 @@ features.command("create <name>").description("Create a new feature").option("-d
|
|
|
78243
78526
|
|
|
78244
78527
|
## Acceptance Criteria
|
|
78245
78528
|
`);
|
|
78246
|
-
await Bun.write(
|
|
78529
|
+
await Bun.write(join52(featureDir, "plan.md"), `# Plan: ${name}
|
|
78247
78530
|
|
|
78248
78531
|
## Architecture
|
|
78249
78532
|
|
|
@@ -78251,7 +78534,7 @@ features.command("create <name>").description("Create a new feature").option("-d
|
|
|
78251
78534
|
|
|
78252
78535
|
## Dependencies
|
|
78253
78536
|
`);
|
|
78254
|
-
await Bun.write(
|
|
78537
|
+
await Bun.write(join52(featureDir, "tasks.md"), `# Tasks: ${name}
|
|
78255
78538
|
|
|
78256
78539
|
## US-001: [Title]
|
|
78257
78540
|
|
|
@@ -78260,7 +78543,7 @@ features.command("create <name>").description("Create a new feature").option("-d
|
|
|
78260
78543
|
### Acceptance Criteria
|
|
78261
78544
|
- [ ] Criterion 1
|
|
78262
78545
|
`);
|
|
78263
|
-
await Bun.write(
|
|
78546
|
+
await Bun.write(join52(featureDir, "progress.txt"), `# Progress: ${name}
|
|
78264
78547
|
|
|
78265
78548
|
Created: ${new Date().toISOString()}
|
|
78266
78549
|
|
|
@@ -78288,7 +78571,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
|
|
|
78288
78571
|
console.error(source_default.red("nax not initialized."));
|
|
78289
78572
|
process.exit(1);
|
|
78290
78573
|
}
|
|
78291
|
-
const featuresDir =
|
|
78574
|
+
const featuresDir = join52(naxDir, "features");
|
|
78292
78575
|
if (!existsSync34(featuresDir)) {
|
|
78293
78576
|
console.log(source_default.dim("No features yet."));
|
|
78294
78577
|
return;
|
|
@@ -78303,7 +78586,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
|
|
|
78303
78586
|
Features:
|
|
78304
78587
|
`));
|
|
78305
78588
|
for (const name of entries) {
|
|
78306
|
-
const prdPath =
|
|
78589
|
+
const prdPath = join52(featuresDir, name, "prd.json");
|
|
78307
78590
|
if (existsSync34(prdPath)) {
|
|
78308
78591
|
const prd = await loadPRD(prdPath);
|
|
78309
78592
|
const c = countStories(prd);
|
|
@@ -78334,10 +78617,10 @@ Use: nax plan -f <feature> --from <spec>`));
|
|
|
78334
78617
|
process.exit(1);
|
|
78335
78618
|
}
|
|
78336
78619
|
const config2 = await loadConfig(workdir);
|
|
78337
|
-
const featureLogDir =
|
|
78620
|
+
const featureLogDir = join52(naxDir, "features", options.feature, "plan");
|
|
78338
78621
|
mkdirSync6(featureLogDir, { recursive: true });
|
|
78339
78622
|
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
78340
|
-
const planLogPath =
|
|
78623
|
+
const planLogPath = join52(featureLogDir, `${planLogId}.jsonl`);
|
|
78341
78624
|
initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
|
|
78342
78625
|
console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
|
|
78343
78626
|
try {
|
|
@@ -78374,7 +78657,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
|
|
|
78374
78657
|
console.error(source_default.red("nax not initialized. Run: nax init"));
|
|
78375
78658
|
process.exit(1);
|
|
78376
78659
|
}
|
|
78377
|
-
const featureDir =
|
|
78660
|
+
const featureDir = join52(naxDir, "features", options.feature);
|
|
78378
78661
|
if (!existsSync34(featureDir)) {
|
|
78379
78662
|
console.error(source_default.red(`Feature "${options.feature}" not found.`));
|
|
78380
78663
|
process.exit(1);
|
|
@@ -78390,7 +78673,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
|
|
|
78390
78673
|
specPath: options.from,
|
|
78391
78674
|
reclassify: options.reclassify
|
|
78392
78675
|
});
|
|
78393
|
-
const prdPath =
|
|
78676
|
+
const prdPath = join52(featureDir, "prd.json");
|
|
78394
78677
|
await Bun.write(prdPath, JSON.stringify(prd, null, 2));
|
|
78395
78678
|
const c = countStories(prd);
|
|
78396
78679
|
console.log(source_default.green(`
|