@nathapp/nax 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nax.js +94 -46
- package/package.json +1 -1
- package/src/agents/claude-decompose.ts +6 -21
- package/src/agents/types-extended.ts +1 -1
- package/src/cli/plan.ts +4 -11
- package/src/config/test-strategy.ts +70 -0
- package/src/execution/lifecycle/acceptance-loop.ts +2 -0
- package/src/execution/parallel-coordinator.ts +3 -1
- package/src/execution/parallel-executor.ts +3 -0
- package/src/execution/runner-execution.ts +16 -2
- package/src/execution/story-context.ts +6 -0
- package/src/prd/schema.ts +4 -14
package/dist/nax.js
CHANGED
|
@@ -3294,6 +3294,55 @@ var init_claude_complete = __esm(() => {
|
|
|
3294
3294
|
};
|
|
3295
3295
|
});
|
|
3296
3296
|
|
|
3297
|
+
// src/config/test-strategy.ts
|
|
3298
|
+
function resolveTestStrategy(raw) {
|
|
3299
|
+
if (!raw)
|
|
3300
|
+
return "test-after";
|
|
3301
|
+
if (VALID_TEST_STRATEGIES.includes(raw))
|
|
3302
|
+
return raw;
|
|
3303
|
+
if (raw === "tdd")
|
|
3304
|
+
return "tdd-simple";
|
|
3305
|
+
if (raw === "three-session")
|
|
3306
|
+
return "three-session-tdd";
|
|
3307
|
+
if (raw === "tdd-lite")
|
|
3308
|
+
return "three-session-tdd-lite";
|
|
3309
|
+
return "test-after";
|
|
3310
|
+
}
|
|
3311
|
+
var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guide
|
|
3312
|
+
|
|
3313
|
+
- simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
|
|
3314
|
+
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
|
|
3315
|
+
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
3316
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
|
|
3317
|
+
|
|
3318
|
+
### Security Override
|
|
3319
|
+
|
|
3320
|
+
Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
|
|
3321
|
+
password hashing, access control) must be classified at MINIMUM "medium" complexity
|
|
3322
|
+
regardless of LOC count. These require at minimum "tdd-simple" test strategy.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
|
|
3323
|
+
|
|
3324
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
3325
|
+
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
3326
|
+
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
3327
|
+
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.`, GROUPING_RULES = `## Grouping Rules
|
|
3328
|
+
|
|
3329
|
+
- Combine small, related tasks into a single "simple" or "medium" story.
|
|
3330
|
+
- Do NOT create separate stories for every single file or function unless complex.
|
|
3331
|
+
- Do NOT create standalone stories purely for test coverage or testing.
|
|
3332
|
+
Each story's testStrategy already handles testing (tdd-simple writes tests first,
|
|
3333
|
+
three-session-tdd uses separate test-writer session, test-after writes tests after).
|
|
3334
|
+
Only create a dedicated test story for unique integration/E2E test logic that spans
|
|
3335
|
+
multiple stories and cannot be covered by individual story test strategies.
|
|
3336
|
+
- Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
|
|
3337
|
+
var init_test_strategy = __esm(() => {
|
|
3338
|
+
VALID_TEST_STRATEGIES = [
|
|
3339
|
+
"test-after",
|
|
3340
|
+
"tdd-simple",
|
|
3341
|
+
"three-session-tdd",
|
|
3342
|
+
"three-session-tdd-lite"
|
|
3343
|
+
];
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3297
3346
|
// src/agents/claude-decompose.ts
|
|
3298
3347
|
function buildDecomposePrompt(options) {
|
|
3299
3348
|
return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
|
|
@@ -3316,24 +3365,13 @@ Decompose this spec into user stories. For each story, provide:
|
|
|
3316
3365
|
9. reasoning: Why this complexity level
|
|
3317
3366
|
10. estimatedLOC: Estimated lines of code to change
|
|
3318
3367
|
11. risks: Array of implementation risks
|
|
3319
|
-
12. testStrategy: "three-session-tdd" | "
|
|
3368
|
+
12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
|
|
3320
3369
|
|
|
3321
|
-
|
|
3322
|
-
- "three-session-tdd": ONLY for complex/expert tasks that are security-critical (auth, encryption, tokens, credentials) or define public API contracts consumers depend on
|
|
3323
|
-
- "test-after": for all other tasks including simple/medium complexity
|
|
3324
|
-
- A "simple" complexity task should almost never be "three-session-tdd"
|
|
3370
|
+
${COMPLEXITY_GUIDE}
|
|
3325
3371
|
|
|
3326
|
-
|
|
3327
|
-
- simple: 1-3 files, <100 LOC, straightforward implementation, existing patterns
|
|
3328
|
-
- medium: 3-6 files, 100-300 LOC, moderate logic, some new patterns
|
|
3329
|
-
- complex: 6+ files, 300-800 LOC, architectural changes, cross-cutting concerns
|
|
3330
|
-
- expert: Security/crypto/real-time/distributed systems, >800 LOC, new infrastructure
|
|
3372
|
+
${TEST_STRATEGY_GUIDE}
|
|
3331
3373
|
|
|
3332
|
-
|
|
3333
|
-
- Combine small, related tasks (e.g., multiple utility functions, interfaces) into a single "simple" or "medium" story.
|
|
3334
|
-
- Do NOT create separate stories for every single file or function unless complex.
|
|
3335
|
-
- Aim for coherent units of value (e.g., "Implement User Authentication" vs "Create User Interface", "Create Login Service").
|
|
3336
|
-
- Maximum recommended stories: 10-15 per feature. Group aggressively if list grows too long.
|
|
3374
|
+
${GROUPING_RULES}
|
|
3337
3375
|
|
|
3338
3376
|
Consider:
|
|
3339
3377
|
1. Does infrastructure exist? (e.g., "add caching" when no cache layer exists = complex)
|
|
@@ -3402,7 +3440,7 @@ ${output.slice(0, 500)}`);
|
|
|
3402
3440
|
reasoning: String(record.reasoning || "No reasoning provided"),
|
|
3403
3441
|
estimatedLOC: Number(record.estimatedLOC) || 0,
|
|
3404
3442
|
risks: Array.isArray(record.risks) ? record.risks : [],
|
|
3405
|
-
testStrategy: record.testStrategy === "
|
|
3443
|
+
testStrategy: resolveTestStrategy(typeof record.testStrategy === "string" ? record.testStrategy : undefined)
|
|
3406
3444
|
};
|
|
3407
3445
|
});
|
|
3408
3446
|
if (stories.length === 0) {
|
|
@@ -3416,6 +3454,9 @@ function coerceComplexity(value) {
|
|
|
3416
3454
|
}
|
|
3417
3455
|
return "medium";
|
|
3418
3456
|
}
|
|
3457
|
+
var init_claude_decompose = __esm(() => {
|
|
3458
|
+
init_test_strategy();
|
|
3459
|
+
});
|
|
3419
3460
|
|
|
3420
3461
|
// src/agents/cost.ts
|
|
3421
3462
|
function parseTokenUsage(output) {
|
|
@@ -18398,6 +18439,7 @@ var init_claude = __esm(() => {
|
|
|
18398
18439
|
init_pid_registry();
|
|
18399
18440
|
init_logger2();
|
|
18400
18441
|
init_claude_complete();
|
|
18442
|
+
init_claude_decompose();
|
|
18401
18443
|
init_claude_execution();
|
|
18402
18444
|
init_claude_interactive();
|
|
18403
18445
|
init_claude_plan();
|
|
@@ -19730,6 +19772,7 @@ class AcpAgentAdapter {
|
|
|
19730
19772
|
var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps, MAX_SESSION_AGE_MS;
|
|
19731
19773
|
var init_adapter = __esm(() => {
|
|
19732
19774
|
init_logger2();
|
|
19775
|
+
init_claude_decompose();
|
|
19733
19776
|
init_spawn_client();
|
|
19734
19777
|
init_types2();
|
|
19735
19778
|
init_cost2();
|
|
@@ -22042,7 +22085,7 @@ var package_default;
|
|
|
22042
22085
|
var init_package = __esm(() => {
|
|
22043
22086
|
package_default = {
|
|
22044
22087
|
name: "@nathapp/nax",
|
|
22045
|
-
version: "0.
|
|
22088
|
+
version: "0.45.0",
|
|
22046
22089
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22047
22090
|
type: "module",
|
|
22048
22091
|
bin: {
|
|
@@ -22115,8 +22158,8 @@ var init_version = __esm(() => {
|
|
|
22115
22158
|
NAX_VERSION = package_default.version;
|
|
22116
22159
|
NAX_COMMIT = (() => {
|
|
22117
22160
|
try {
|
|
22118
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22119
|
-
return "
|
|
22161
|
+
if (/^[0-9a-f]{6,10}$/.test("d6bdccb"))
|
|
22162
|
+
return "d6bdccb";
|
|
22120
22163
|
} catch {}
|
|
22121
22164
|
try {
|
|
22122
22165
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -25394,6 +25437,11 @@ async function buildStoryContextFull(prd, story, config2) {
|
|
|
25394
25437
|
}
|
|
25395
25438
|
function getAllReadyStories(prd) {
|
|
25396
25439
|
const completedIds = new Set(prd.userStories.filter((s) => s.passes || s.status === "skipped").map((s) => s.id));
|
|
25440
|
+
const logger = getSafeLogger2();
|
|
25441
|
+
logger?.debug("routing", "getAllReadyStories: completed set", {
|
|
25442
|
+
completedIds: [...completedIds],
|
|
25443
|
+
totalStories: prd.userStories.length
|
|
25444
|
+
});
|
|
25397
25445
|
return prd.userStories.filter((s) => !s.passes && s.status !== "skipped" && s.status !== "failed" && s.status !== "paused" && s.status !== "blocked" && s.dependencies.every((dep) => completedIds.has(dep)));
|
|
25398
25446
|
}
|
|
25399
25447
|
var CONTEXT_MAX_TOKENS = 1e5, CONTEXT_RESERVED_TOKENS = 1e4;
|
|
@@ -31237,7 +31285,8 @@ async function executeFixStory(ctx, story, prd, iterations) {
|
|
|
31237
31285
|
featureDir: ctx.featureDir,
|
|
31238
31286
|
hooks: ctx.hooks,
|
|
31239
31287
|
plugins: ctx.pluginRegistry,
|
|
31240
|
-
storyStartTime: new Date().toISOString()
|
|
31288
|
+
storyStartTime: new Date().toISOString(),
|
|
31289
|
+
agentGetFn: ctx.agentGetFn
|
|
31241
31290
|
};
|
|
31242
31291
|
const result = await runPipeline(defaultPipeline, fixContext, ctx.eventEmitter);
|
|
31243
31292
|
logger?.info("acceptance", `Fix story ${story.id} ${result.success ? "passed" : "failed"}`);
|
|
@@ -31273,7 +31322,8 @@ async function runAcceptanceLoop(ctx) {
|
|
|
31273
31322
|
workdir: ctx.workdir,
|
|
31274
31323
|
featureDir: ctx.featureDir,
|
|
31275
31324
|
hooks: ctx.hooks,
|
|
31276
|
-
plugins: ctx.pluginRegistry
|
|
31325
|
+
plugins: ctx.pluginRegistry,
|
|
31326
|
+
agentGetFn: ctx.agentGetFn
|
|
31277
31327
|
};
|
|
31278
31328
|
const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
|
|
31279
31329
|
const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
|
|
@@ -32254,7 +32304,7 @@ function resolveMaxConcurrency(parallel) {
|
|
|
32254
32304
|
}
|
|
32255
32305
|
return Math.max(1, parallel);
|
|
32256
32306
|
}
|
|
32257
|
-
async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter) {
|
|
32307
|
+
async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn) {
|
|
32258
32308
|
const logger = getSafeLogger();
|
|
32259
32309
|
const maxConcurrency = resolveMaxConcurrency(parallel);
|
|
32260
32310
|
const worktreeManager = new WorktreeManager;
|
|
@@ -32284,7 +32334,8 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
|
|
|
32284
32334
|
featureDir,
|
|
32285
32335
|
hooks,
|
|
32286
32336
|
plugins,
|
|
32287
|
-
storyStartTime: new Date().toISOString()
|
|
32337
|
+
storyStartTime: new Date().toISOString(),
|
|
32338
|
+
agentGetFn
|
|
32288
32339
|
};
|
|
32289
32340
|
const worktreePaths = new Map;
|
|
32290
32341
|
for (const story of batch) {
|
|
@@ -32658,7 +32709,7 @@ async function runParallelExecution(options, initialPrd) {
|
|
|
32658
32709
|
const batchStoryMetrics = [];
|
|
32659
32710
|
let conflictedStories = [];
|
|
32660
32711
|
try {
|
|
32661
|
-
const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter);
|
|
32712
|
+
const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn);
|
|
32662
32713
|
const batchDurationMs = Date.now() - batchStartMs;
|
|
32663
32714
|
const batchCompletedAt = new Date().toISOString();
|
|
32664
32715
|
prd = parallelResult.updatedPrd;
|
|
@@ -65854,17 +65905,13 @@ init_registry();
|
|
|
65854
65905
|
import { existsSync as existsSync9 } from "fs";
|
|
65855
65906
|
import { join as join10 } from "path";
|
|
65856
65907
|
import { createInterface } from "readline";
|
|
65908
|
+
init_test_strategy();
|
|
65857
65909
|
init_pid_registry();
|
|
65858
65910
|
init_logger2();
|
|
65859
65911
|
|
|
65860
65912
|
// src/prd/schema.ts
|
|
65913
|
+
init_test_strategy();
|
|
65861
65914
|
var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
|
|
65862
|
-
var VALID_TEST_STRATEGIES = [
|
|
65863
|
-
"test-after",
|
|
65864
|
-
"tdd-simple",
|
|
65865
|
-
"three-session-tdd",
|
|
65866
|
-
"three-session-tdd-lite"
|
|
65867
|
-
];
|
|
65868
65915
|
var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
|
|
65869
65916
|
function extractJsonFromMarkdown(text) {
|
|
65870
65917
|
const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
|
|
@@ -65934,9 +65981,7 @@ function validateStory(raw, index, allIds) {
|
|
|
65934
65981
|
throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
|
|
65935
65982
|
}
|
|
65936
65983
|
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
65937
|
-
const
|
|
65938
|
-
const normalizedStrategy = typeof rawTestStrategy === "string" ? STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy : rawTestStrategy;
|
|
65939
|
-
const testStrategy = normalizedStrategy !== undefined && VALID_TEST_STRATEGIES.includes(normalizedStrategy) ? normalizedStrategy : "tdd-simple";
|
|
65984
|
+
const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
|
|
65940
65985
|
const rawDeps = s.dependencies;
|
|
65941
65986
|
const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
|
|
65942
65987
|
for (const dep of dependencies) {
|
|
@@ -66203,19 +66248,11 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
|
|
|
66203
66248
|
]
|
|
66204
66249
|
}
|
|
66205
66250
|
|
|
66206
|
-
|
|
66251
|
+
${COMPLEXITY_GUIDE}
|
|
66207
66252
|
|
|
66208
|
-
|
|
66209
|
-
- medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
|
|
66210
|
-
- complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
|
|
66211
|
-
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
|
|
66253
|
+
${TEST_STRATEGY_GUIDE}
|
|
66212
66254
|
|
|
66213
|
-
|
|
66214
|
-
|
|
66215
|
-
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
66216
|
-
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
66217
|
-
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
66218
|
-
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
|
|
66255
|
+
${GROUPING_RULES}
|
|
66219
66256
|
|
|
66220
66257
|
${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
|
|
66221
66258
|
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."}`;
|
|
@@ -69227,9 +69264,20 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
|
|
|
69227
69264
|
batchingEnabled: options.useBatch
|
|
69228
69265
|
});
|
|
69229
69266
|
clearCache();
|
|
69230
|
-
const
|
|
69267
|
+
const readyStories = getAllReadyStories(prd);
|
|
69268
|
+
logger?.debug("routing", "Ready stories for batch routing", {
|
|
69269
|
+
readyCount: readyStories.length,
|
|
69270
|
+
readyIds: readyStories.map((s) => s.id),
|
|
69271
|
+
allStories: prd.userStories.map((s) => ({
|
|
69272
|
+
id: s.id,
|
|
69273
|
+
status: s.status,
|
|
69274
|
+
passes: s.passes,
|
|
69275
|
+
deps: s.dependencies
|
|
69276
|
+
}))
|
|
69277
|
+
});
|
|
69278
|
+
const batchPlan = options.useBatch ? precomputeBatchPlan(readyStories, 4) : [];
|
|
69231
69279
|
if (options.useBatch) {
|
|
69232
|
-
await tryLlmBatchRoute(options.config,
|
|
69280
|
+
await tryLlmBatchRoute(options.config, readyStories, "routing");
|
|
69233
69281
|
}
|
|
69234
69282
|
if (options.parallel !== undefined) {
|
|
69235
69283
|
const runParallelExecution2 = options.runParallelExecution ?? (await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor))).runParallelExecution;
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* parseDecomposeOutput(), validateComplexity()
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { COMPLEXITY_GUIDE, GROUPING_RULES, TEST_STRATEGY_GUIDE, resolveTestStrategy } from "../config/test-strategy";
|
|
8
9
|
import type { DecomposeOptions, DecomposeResult, DecomposedStory } from "./types";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -31,24 +32,13 @@ Decompose this spec into user stories. For each story, provide:
|
|
|
31
32
|
9. reasoning: Why this complexity level
|
|
32
33
|
10. estimatedLOC: Estimated lines of code to change
|
|
33
34
|
11. risks: Array of implementation risks
|
|
34
|
-
12. testStrategy: "three-session-tdd" | "
|
|
35
|
+
12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
- "three-session-tdd": ONLY for complex/expert tasks that are security-critical (auth, encryption, tokens, credentials) or define public API contracts consumers depend on
|
|
38
|
-
- "test-after": for all other tasks including simple/medium complexity
|
|
39
|
-
- A "simple" complexity task should almost never be "three-session-tdd"
|
|
37
|
+
${COMPLEXITY_GUIDE}
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
- simple: 1-3 files, <100 LOC, straightforward implementation, existing patterns
|
|
43
|
-
- medium: 3-6 files, 100-300 LOC, moderate logic, some new patterns
|
|
44
|
-
- complex: 6+ files, 300-800 LOC, architectural changes, cross-cutting concerns
|
|
45
|
-
- expert: Security/crypto/real-time/distributed systems, >800 LOC, new infrastructure
|
|
39
|
+
${TEST_STRATEGY_GUIDE}
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
- Combine small, related tasks (e.g., multiple utility functions, interfaces) into a single "simple" or "medium" story.
|
|
49
|
-
- Do NOT create separate stories for every single file or function unless complex.
|
|
50
|
-
- Aim for coherent units of value (e.g., "Implement User Authentication" vs "Create User Interface", "Create Login Service").
|
|
51
|
-
- Maximum recommended stories: 10-15 per feature. Group aggressively if list grows too long.
|
|
41
|
+
${GROUPING_RULES}
|
|
52
42
|
|
|
53
43
|
Consider:
|
|
54
44
|
1. Does infrastructure exist? (e.g., "add caching" when no cache layer exists = complex)
|
|
@@ -141,12 +131,7 @@ export function parseDecomposeOutput(output: string): DecomposedStory[] {
|
|
|
141
131
|
reasoning: String(record.reasoning || "No reasoning provided"),
|
|
142
132
|
estimatedLOC: Number(record.estimatedLOC) || 0,
|
|
143
133
|
risks: Array.isArray(record.risks) ? record.risks : [],
|
|
144
|
-
testStrategy:
|
|
145
|
-
record.testStrategy === "three-session-tdd"
|
|
146
|
-
? "three-session-tdd"
|
|
147
|
-
: record.testStrategy === "test-after"
|
|
148
|
-
? "test-after"
|
|
149
|
-
: undefined,
|
|
134
|
+
testStrategy: resolveTestStrategy(typeof record.testStrategy === "string" ? record.testStrategy : undefined),
|
|
150
135
|
};
|
|
151
136
|
});
|
|
152
137
|
|
|
@@ -117,7 +117,7 @@ export interface DecomposedStory {
|
|
|
117
117
|
/** Implementation risks */
|
|
118
118
|
risks: string[];
|
|
119
119
|
/** Test strategy recommendation from LLM */
|
|
120
|
-
testStrategy?: "
|
|
120
|
+
testStrategy?: import("../config/test-strategy").TestStrategy;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
package/src/cli/plan.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { scanCodebase } from "../analyze/scanner";
|
|
|
16
16
|
import type { CodebaseScan } from "../analyze/types";
|
|
17
17
|
import type { NaxConfig } from "../config";
|
|
18
18
|
import { resolvePermissions } from "../config/permissions";
|
|
19
|
+
import { COMPLEXITY_GUIDE, GROUPING_RULES, TEST_STRATEGY_GUIDE } from "../config/test-strategy";
|
|
19
20
|
import { PidRegistry } from "../execution/pid-registry";
|
|
20
21
|
import { getLogger } from "../logger";
|
|
21
22
|
import { validatePlanOutput } from "../prd/schema";
|
|
@@ -320,19 +321,11 @@ Generate a JSON object with this exact structure (no markdown, no explanation
|
|
|
320
321
|
]
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
|
|
324
|
+
${COMPLEXITY_GUIDE}
|
|
324
325
|
|
|
325
|
-
|
|
326
|
-
- medium: 50–200 LOC, 2–5 files, standard patterns, clear requirements → tdd-simple
|
|
327
|
-
- complex: 200–500 LOC, multiple modules, new abstractions or integrations → three-session-tdd
|
|
328
|
-
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk → three-session-tdd-lite
|
|
326
|
+
${TEST_STRATEGY_GUIDE}
|
|
329
327
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
333
|
-
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
334
|
-
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
335
|
-
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
|
|
328
|
+
${GROUPING_RULES}
|
|
336
329
|
|
|
337
330
|
${
|
|
338
331
|
outputFilePath
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Strategy — Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* Defines all valid test strategies, the normalizer, and shared prompt
|
|
5
|
+
* fragments used by plan.ts and claude-decompose.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TestStrategy } from "./schema-types";
|
|
9
|
+
|
|
10
|
+
// ─── Re-export type ───────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type { TestStrategy };
|
|
13
|
+
|
|
14
|
+
// ─── Valid values ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const VALID_TEST_STRATEGIES: readonly TestStrategy[] = [
|
|
17
|
+
"test-after",
|
|
18
|
+
"tdd-simple",
|
|
19
|
+
"three-session-tdd",
|
|
20
|
+
"three-session-tdd-lite",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// ─── Resolver ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate and normalize a test strategy string.
|
|
27
|
+
* Returns a valid TestStrategy or falls back to "test-after".
|
|
28
|
+
*/
|
|
29
|
+
export function resolveTestStrategy(raw: string | undefined): TestStrategy {
|
|
30
|
+
if (!raw) return "test-after";
|
|
31
|
+
if (VALID_TEST_STRATEGIES.includes(raw as TestStrategy)) return raw as TestStrategy;
|
|
32
|
+
// Map legacy/typo values
|
|
33
|
+
if (raw === "tdd") return "tdd-simple";
|
|
34
|
+
if (raw === "three-session") return "three-session-tdd";
|
|
35
|
+
if (raw === "tdd-lite") return "three-session-tdd-lite";
|
|
36
|
+
return "test-after"; // safe fallback
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Prompt fragments (shared by plan.ts and claude-decompose.ts) ────────────
|
|
40
|
+
|
|
41
|
+
export const COMPLEXITY_GUIDE = `## Complexity Classification Guide
|
|
42
|
+
|
|
43
|
+
- simple: ≤50 LOC, single-file change, purely additive, no new dependencies → test-after
|
|
44
|
+
- medium: 50–200 LOC, 2–5 files, standard patterns, clear requirements → tdd-simple
|
|
45
|
+
- complex: 200–500 LOC, multiple modules, new abstractions or integrations → three-session-tdd
|
|
46
|
+
- expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk → three-session-tdd-lite
|
|
47
|
+
|
|
48
|
+
### Security Override
|
|
49
|
+
|
|
50
|
+
Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
|
|
51
|
+
password hashing, access control) must be classified at MINIMUM "medium" complexity
|
|
52
|
+
regardless of LOC count. These require at minimum "tdd-simple" test strategy.`;
|
|
53
|
+
|
|
54
|
+
export const TEST_STRATEGY_GUIDE = `## Test Strategy Guide
|
|
55
|
+
|
|
56
|
+
- test-after: Simple changes with well-understood behavior. Write tests after implementation.
|
|
57
|
+
- tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
|
|
58
|
+
- three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
|
|
59
|
+
- three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.`;
|
|
60
|
+
|
|
61
|
+
export const GROUPING_RULES = `## Grouping Rules
|
|
62
|
+
|
|
63
|
+
- Combine small, related tasks into a single "simple" or "medium" story.
|
|
64
|
+
- Do NOT create separate stories for every single file or function unless complex.
|
|
65
|
+
- Do NOT create standalone stories purely for test coverage or testing.
|
|
66
|
+
Each story's testStrategy already handles testing (tdd-simple writes tests first,
|
|
67
|
+
three-session-tdd uses separate test-writer session, test-after writes tests after).
|
|
68
|
+
Only create a dedicated test story for unique integration/E2E test logic that spans
|
|
69
|
+
multiple stories and cannot be covered by individual story test strategies.
|
|
70
|
+
- Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
|
|
@@ -143,6 +143,7 @@ async function executeFixStory(
|
|
|
143
143
|
hooks: ctx.hooks,
|
|
144
144
|
plugins: ctx.pluginRegistry,
|
|
145
145
|
storyStartTime: new Date().toISOString(),
|
|
146
|
+
agentGetFn: ctx.agentGetFn,
|
|
146
147
|
};
|
|
147
148
|
const result = await runPipeline(defaultPipeline, fixContext, ctx.eventEmitter);
|
|
148
149
|
logger?.info("acceptance", `Fix story ${story.id} ${result.success ? "passed" : "failed"}`);
|
|
@@ -189,6 +190,7 @@ export async function runAcceptanceLoop(ctx: AcceptanceLoopContext): Promise<Acc
|
|
|
189
190
|
featureDir: ctx.featureDir,
|
|
190
191
|
hooks: ctx.hooks,
|
|
191
192
|
plugins: ctx.pluginRegistry,
|
|
193
|
+
agentGetFn: ctx.agentGetFn,
|
|
192
194
|
};
|
|
193
195
|
|
|
194
196
|
const { acceptanceStage } = await import("../../pipeline/stages/acceptance");
|
|
@@ -8,7 +8,7 @@ import type { NaxConfig } from "../config";
|
|
|
8
8
|
import type { LoadedHooksConfig } from "../hooks";
|
|
9
9
|
import { getSafeLogger } from "../logger";
|
|
10
10
|
import type { PipelineEventEmitter } from "../pipeline/events";
|
|
11
|
-
import type {
|
|
11
|
+
import type { AgentGetFn } from "../pipeline/types";
|
|
12
12
|
import type { PluginRegistry } from "../plugins/registry";
|
|
13
13
|
import type { PRD, UserStory } from "../prd";
|
|
14
14
|
import { markStoryFailed, markStoryPassed, savePRD } from "../prd";
|
|
@@ -108,6 +108,7 @@ export async function executeParallel(
|
|
|
108
108
|
featureDir: string | undefined,
|
|
109
109
|
parallel: number,
|
|
110
110
|
eventEmitter?: PipelineEventEmitter,
|
|
111
|
+
agentGetFn?: AgentGetFn,
|
|
111
112
|
): Promise<{
|
|
112
113
|
storiesCompleted: number;
|
|
113
114
|
totalCost: number;
|
|
@@ -152,6 +153,7 @@ export async function executeParallel(
|
|
|
152
153
|
hooks,
|
|
153
154
|
plugins,
|
|
154
155
|
storyStartTime: new Date().toISOString(),
|
|
156
|
+
agentGetFn,
|
|
155
157
|
};
|
|
156
158
|
|
|
157
159
|
// Create worktrees for all stories in batch
|
|
@@ -17,6 +17,7 @@ import { fireHook } from "../hooks";
|
|
|
17
17
|
import { getSafeLogger } from "../logger";
|
|
18
18
|
import type { StoryMetrics } from "../metrics";
|
|
19
19
|
import type { PipelineEventEmitter } from "../pipeline/events";
|
|
20
|
+
import type { AgentGetFn } from "../pipeline/types";
|
|
20
21
|
import type { PluginRegistry } from "../plugins/registry";
|
|
21
22
|
import type { PRD } from "../prd";
|
|
22
23
|
import { countStories, isComplete } from "../prd";
|
|
@@ -57,6 +58,7 @@ export interface ParallelExecutorOptions {
|
|
|
57
58
|
pluginRegistry: PluginRegistry;
|
|
58
59
|
formatterMode: "quiet" | "normal" | "verbose" | "json";
|
|
59
60
|
headless: boolean;
|
|
61
|
+
agentGetFn?: AgentGetFn;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export interface RectificationStats {
|
|
@@ -158,6 +160,7 @@ export async function runParallelExecution(
|
|
|
158
160
|
featureDir,
|
|
159
161
|
parallelCount,
|
|
160
162
|
eventEmitter,
|
|
163
|
+
options.agentGetFn,
|
|
161
164
|
);
|
|
162
165
|
|
|
163
166
|
const batchDurationMs = Date.now() - batchStartMs;
|
|
@@ -129,10 +129,24 @@ export async function runExecutionPhase(
|
|
|
129
129
|
clearLlmCache();
|
|
130
130
|
|
|
131
131
|
// PERF-1: Precompute batch plan once from ready stories
|
|
132
|
-
const
|
|
132
|
+
const readyStories = getAllReadyStories(prd);
|
|
133
|
+
|
|
134
|
+
// BUG-068: debug log to diagnose unexpected storyCount in batch routing
|
|
135
|
+
logger?.debug("routing", "Ready stories for batch routing", {
|
|
136
|
+
readyCount: readyStories.length,
|
|
137
|
+
readyIds: readyStories.map((s) => s.id),
|
|
138
|
+
allStories: prd.userStories.map((s) => ({
|
|
139
|
+
id: s.id,
|
|
140
|
+
status: s.status,
|
|
141
|
+
passes: s.passes,
|
|
142
|
+
deps: s.dependencies,
|
|
143
|
+
})),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const batchPlan = options.useBatch ? precomputeBatchPlan(readyStories, 4) : [];
|
|
133
147
|
|
|
134
148
|
if (options.useBatch) {
|
|
135
|
-
await tryLlmBatchRoute(options.config,
|
|
149
|
+
await tryLlmBatchRoute(options.config, readyStories, "routing");
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
// Parallel Execution Path (when --parallel is set)
|
|
@@ -175,6 +175,12 @@ export async function buildStoryContextFull(
|
|
|
175
175
|
export function getAllReadyStories(prd: PRD): UserStory[] {
|
|
176
176
|
const completedIds = new Set(prd.userStories.filter((s) => s.passes || s.status === "skipped").map((s) => s.id));
|
|
177
177
|
|
|
178
|
+
const logger = getSafeLogger();
|
|
179
|
+
logger?.debug("routing", "getAllReadyStories: completed set", {
|
|
180
|
+
completedIds: [...completedIds],
|
|
181
|
+
totalStories: prd.userStories.length,
|
|
182
|
+
});
|
|
183
|
+
|
|
178
184
|
return prd.userStories.filter(
|
|
179
185
|
(s) =>
|
|
180
186
|
!s.passes &&
|
package/src/prd/schema.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Complexity, TestStrategy } from "../config";
|
|
8
|
+
import { resolveTestStrategy } from "../config/test-strategy";
|
|
8
9
|
import type { PRD, UserStory } from "./types";
|
|
9
10
|
import { validateStoryId } from "./validate";
|
|
10
11
|
|
|
@@ -13,12 +14,6 @@ import { validateStoryId } from "./validate";
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
|
|
15
16
|
const VALID_COMPLEXITY: Complexity[] = ["simple", "medium", "complex", "expert"];
|
|
16
|
-
const VALID_TEST_STRATEGIES: TestStrategy[] = [
|
|
17
|
-
"test-after",
|
|
18
|
-
"tdd-simple",
|
|
19
|
-
"three-session-tdd",
|
|
20
|
-
"three-session-tdd-lite",
|
|
21
|
-
];
|
|
22
17
|
|
|
23
18
|
/** Pattern matching ST001 → ST-001 style IDs (prefix letters + digits, no separator) */
|
|
24
19
|
const STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
|
|
@@ -140,15 +135,10 @@ function validateStory(raw: unknown, index: number, allIds: Set<string>): UserSt
|
|
|
140
135
|
}
|
|
141
136
|
|
|
142
137
|
// testStrategy — accept from routing.testStrategy or top-level testStrategy
|
|
143
|
-
// Also map legacy/LLM-hallucinated aliases: tdd-lite → tdd-simple
|
|
144
138
|
const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const testStrategy: TestStrategy =
|
|
149
|
-
normalizedStrategy !== undefined && (VALID_TEST_STRATEGIES as unknown[]).includes(normalizedStrategy)
|
|
150
|
-
? (normalizedStrategy as TestStrategy)
|
|
151
|
-
: "tdd-simple";
|
|
139
|
+
const testStrategy: TestStrategy = resolveTestStrategy(
|
|
140
|
+
typeof rawTestStrategy === "string" ? rawTestStrategy : undefined,
|
|
141
|
+
);
|
|
152
142
|
|
|
153
143
|
// dependencies
|
|
154
144
|
const rawDeps = s.dependencies;
|