@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 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" | "test-after"
3368
+ 12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
3320
3369
 
3321
- testStrategy rules:
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
- Complexity classification rules:
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
- Grouping Guidelines:
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 === "three-session-tdd" ? "three-session-tdd" : record.testStrategy === "test-after" ? "test-after" : undefined
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.44.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("05b2442"))
22119
- return "05b2442";
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 STRATEGY_ALIASES = { "tdd-lite": "three-session-tdd-lite" };
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
- ## Complexity Classification Guide
66251
+ ${COMPLEXITY_GUIDE}
66207
66252
 
66208
- - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
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
- ## Test Strategy Guide
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 batchPlan = options.useBatch ? precomputeBatchPlan(getAllReadyStories(prd), 4) : [];
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, getAllReadyStories(prd), "routing");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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" | "test-after"
35
+ 12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
35
36
 
36
- testStrategy rules:
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
- Complexity classification rules:
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
- Grouping Guidelines:
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?: "three-session-tdd" | "test-after";
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
- ## Complexity Classification Guide
324
+ ${COMPLEXITY_GUIDE}
324
325
 
325
- - simple: ≤50 LOC, single-file change, purely additive, no new dependencies → test-after
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
- ## Test Strategy Guide
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 { PipelineContext } from "../pipeline/types";
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 batchPlan = options.useBatch ? precomputeBatchPlan(getAllReadyStories(prd), 4) : [];
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, getAllReadyStories(prd), "routing");
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 STRATEGY_ALIASES: Record<string, TestStrategy> = { "tdd-lite": "three-session-tdd-lite" };
146
- const normalizedStrategy =
147
- typeof rawTestStrategy === "string" ? (STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy) : rawTestStrategy;
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;