@nathapp/nax 0.43.1 → 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/bin/nax.ts +22 -0
- package/dist/nax.js +320 -88
- package/package.json +1 -1
- package/src/agents/acp/adapter.ts +98 -5
- 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/cli/status-features.ts +19 -0
- package/src/config/test-strategy.ts +70 -0
- package/src/execution/lifecycle/acceptance-loop.ts +2 -0
- package/src/execution/lifecycle/run-setup.ts +4 -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/runner.ts +4 -0
- package/src/execution/story-context.ts +6 -0
- package/src/prd/schema.ts +4 -14
- package/src/precheck/index.ts +155 -44
- package/src/verification/rectification-loop.ts +18 -5
package/package.json
CHANGED
|
@@ -307,6 +307,88 @@ export async function readAcpSession(workdir: string, featureName: string, story
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
311
|
+
// Session sweep — close open sessions at run boundaries
|
|
312
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
const MAX_SESSION_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Close all open sessions tracked in the sidecar file for a feature.
|
|
318
|
+
* Called at run-end to ensure no sessions leak past the run boundary.
|
|
319
|
+
*/
|
|
320
|
+
export async function sweepFeatureSessions(workdir: string, featureName: string): Promise<void> {
|
|
321
|
+
const path = acpSessionsPath(workdir, featureName);
|
|
322
|
+
let sessions: Record<string, string>;
|
|
323
|
+
try {
|
|
324
|
+
const text = await Bun.file(path).text();
|
|
325
|
+
sessions = JSON.parse(text) as Record<string, string>;
|
|
326
|
+
} catch {
|
|
327
|
+
return; // No sidecar — nothing to sweep
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const entries = Object.entries(sessions);
|
|
331
|
+
if (entries.length === 0) return;
|
|
332
|
+
|
|
333
|
+
const logger = getSafeLogger();
|
|
334
|
+
logger?.info("acp-adapter", `[sweep] Closing ${entries.length} open sessions for feature: ${featureName}`);
|
|
335
|
+
|
|
336
|
+
const cmdStr = "acpx claude";
|
|
337
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
338
|
+
try {
|
|
339
|
+
await client.start();
|
|
340
|
+
for (const [, sessionName] of entries) {
|
|
341
|
+
try {
|
|
342
|
+
if (client.loadSession) {
|
|
343
|
+
const session = await client.loadSession(sessionName, "claude", "approve-reads");
|
|
344
|
+
if (session) {
|
|
345
|
+
await session.close().catch(() => {});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
await client.close().catch(() => {});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Clear sidecar after sweep
|
|
357
|
+
try {
|
|
358
|
+
await Bun.write(path, JSON.stringify({}, null, 2));
|
|
359
|
+
} catch (err) {
|
|
360
|
+
logger?.warn("acp-adapter", "[sweep] Failed to clear sidecar after sweep", { error: String(err) });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Sweep stale sessions if the sidecar file is older than maxAgeMs.
|
|
366
|
+
* Called at startup as a safety net for sessions orphaned by crashes.
|
|
367
|
+
*/
|
|
368
|
+
export async function sweepStaleFeatureSessions(
|
|
369
|
+
workdir: string,
|
|
370
|
+
featureName: string,
|
|
371
|
+
maxAgeMs = MAX_SESSION_AGE_MS,
|
|
372
|
+
): Promise<void> {
|
|
373
|
+
const path = acpSessionsPath(workdir, featureName);
|
|
374
|
+
const file = Bun.file(path);
|
|
375
|
+
if (!(await file.exists())) return;
|
|
376
|
+
|
|
377
|
+
const ageMs = Date.now() - file.lastModified;
|
|
378
|
+
if (ageMs < maxAgeMs) return; // Recent sidecar — skip
|
|
379
|
+
|
|
380
|
+
getSafeLogger()?.info(
|
|
381
|
+
"acp-adapter",
|
|
382
|
+
`[sweep] Sidecar is ${Math.round(ageMs / 60000)}m old — sweeping stale sessions`,
|
|
383
|
+
{
|
|
384
|
+
featureName,
|
|
385
|
+
ageMs,
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
await sweepFeatureSessions(workdir, featureName);
|
|
390
|
+
}
|
|
391
|
+
|
|
310
392
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
311
393
|
// Output helpers
|
|
312
394
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -470,6 +552,9 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
470
552
|
|
|
471
553
|
let lastResponse: AcpSessionResponse | null = null;
|
|
472
554
|
let timedOut = false;
|
|
555
|
+
// Tracks whether the run completed successfully — used by finally to decide
|
|
556
|
+
// whether to close the session (success) or keep it open for retry (failure).
|
|
557
|
+
const runState = { succeeded: false };
|
|
473
558
|
const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
|
|
474
559
|
|
|
475
560
|
try {
|
|
@@ -525,13 +610,21 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
525
610
|
if (turnCount >= MAX_TURNS && options.interactionBridge) {
|
|
526
611
|
getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
|
|
527
612
|
}
|
|
613
|
+
|
|
614
|
+
// Compute success here so finally can use it for conditional close.
|
|
615
|
+
runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
|
|
528
616
|
} finally {
|
|
529
|
-
// 6. Cleanup —
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
617
|
+
// 6. Cleanup — close session and clear sidecar only on success.
|
|
618
|
+
// On failure, keep session open so retry can resume with full context.
|
|
619
|
+
if (runState.succeeded) {
|
|
620
|
+
await closeAcpSession(session);
|
|
621
|
+
if (options.featureName && options.storyId) {
|
|
622
|
+
await clearAcpSession(options.workdir, options.featureName, options.storyId);
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
|
|
534
626
|
}
|
|
627
|
+
await client.close().catch(() => {});
|
|
535
628
|
}
|
|
536
629
|
|
|
537
630
|
const durationMs = Date.now() - startTime;
|
|
@@ -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
|
|
@@ -85,6 +85,17 @@ async function loadProjectStatusFile(projectDir: string): Promise<NaxStatusFile
|
|
|
85
85
|
async function getFeatureSummary(featureName: string, featureDir: string): Promise<FeatureSummary> {
|
|
86
86
|
const prdPath = join(featureDir, "prd.json");
|
|
87
87
|
|
|
88
|
+
// Guard: prd.json may not exist (e.g. plan failed before writing it)
|
|
89
|
+
if (!existsSync(prdPath)) {
|
|
90
|
+
return {
|
|
91
|
+
name: featureName,
|
|
92
|
+
done: 0,
|
|
93
|
+
failed: 0,
|
|
94
|
+
pending: 0,
|
|
95
|
+
total: 0,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
// Load PRD for story counts
|
|
89
100
|
const prd = await loadPRD(prdPath);
|
|
90
101
|
const counts = countStories(prd);
|
|
@@ -240,6 +251,14 @@ async function displayAllFeatures(projectDir: string): Promise<void> {
|
|
|
240
251
|
/** Display single feature details */
|
|
241
252
|
async function displayFeatureDetails(featureName: string, featureDir: string): Promise<void> {
|
|
242
253
|
const prdPath = join(featureDir, "prd.json");
|
|
254
|
+
|
|
255
|
+
// Guard: prd.json may not exist (e.g. plan failed or feature just created)
|
|
256
|
+
if (!existsSync(prdPath)) {
|
|
257
|
+
console.log(chalk.bold(`\n📊 ${featureName}\n`));
|
|
258
|
+
console.log(chalk.dim(`No prd.json found. Run: nax plan -f ${featureName} --from <spec>`));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
243
262
|
const prd = await loadPRD(prdPath);
|
|
244
263
|
const counts = countStories(prd);
|
|
245
264
|
|
|
@@ -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");
|
|
@@ -159,6 +159,10 @@ export async function setupRun(options: RunSetupOptions): Promise<RunSetupResult
|
|
|
159
159
|
logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Sweep stale ACP sessions from previous crashed runs (safety net)
|
|
163
|
+
const { sweepStaleFeatureSessions } = await import("../../agents/acp/adapter");
|
|
164
|
+
await sweepStaleFeatureSessions(workdir, feature).catch(() => {});
|
|
165
|
+
|
|
162
166
|
// Acquire lock to prevent concurrent execution
|
|
163
167
|
const lockAcquired = await acquireLock(workdir);
|
|
164
168
|
if (!lockAcquired) {
|
|
@@ -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)
|
package/src/execution/runner.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - runner-completion.ts: Acceptance loop, hooks, metrics
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { sweepFeatureSessions } from "../agents/acp/adapter";
|
|
16
17
|
import { createAgentRegistry } from "../agents/registry";
|
|
17
18
|
import type { NaxConfig } from "../config";
|
|
18
19
|
import type { LoadedHooksConfig } from "../hooks";
|
|
@@ -241,6 +242,9 @@ export async function run(options: RunOptions): Promise<RunResult> {
|
|
|
241
242
|
// Cleanup crash handlers (MEM-1 fix)
|
|
242
243
|
cleanupCrashHandlers();
|
|
243
244
|
|
|
245
|
+
// Sweep any remaining open ACP sessions for this feature
|
|
246
|
+
await sweepFeatureSessions(workdir, feature).catch(() => {});
|
|
247
|
+
|
|
244
248
|
// Execute cleanup operations
|
|
245
249
|
const { cleanupRun } = await import("./lifecycle/run-cleanup");
|
|
246
250
|
await cleanupRun({
|
|
@@ -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;
|