@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/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();
@@ -19233,6 +19275,20 @@ var init_cost2 = __esm(() => {
19233
19275
  });
19234
19276
 
19235
19277
  // src/agents/acp/adapter.ts
19278
+ var exports_adapter = {};
19279
+ __export(exports_adapter, {
19280
+ sweepStaleFeatureSessions: () => sweepStaleFeatureSessions,
19281
+ sweepFeatureSessions: () => sweepFeatureSessions,
19282
+ saveAcpSession: () => saveAcpSession,
19283
+ runSessionPrompt: () => runSessionPrompt,
19284
+ readAcpSession: () => readAcpSession,
19285
+ ensureAcpSession: () => ensureAcpSession,
19286
+ closeAcpSession: () => closeAcpSession,
19287
+ clearAcpSession: () => clearAcpSession,
19288
+ buildSessionName: () => buildSessionName,
19289
+ _acpAdapterDeps: () => _acpAdapterDeps,
19290
+ AcpAgentAdapter: () => AcpAgentAdapter
19291
+ });
19236
19292
  import { createHash } from "crypto";
19237
19293
  import { join as join3 } from "path";
19238
19294
  function resolveRegistryEntry(agentName) {
@@ -19336,6 +19392,59 @@ async function readAcpSession(workdir, featureName, storyId) {
19336
19392
  return null;
19337
19393
  }
19338
19394
  }
19395
+ async function sweepFeatureSessions(workdir, featureName) {
19396
+ const path = acpSessionsPath(workdir, featureName);
19397
+ let sessions;
19398
+ try {
19399
+ const text = await Bun.file(path).text();
19400
+ sessions = JSON.parse(text);
19401
+ } catch {
19402
+ return;
19403
+ }
19404
+ const entries = Object.entries(sessions);
19405
+ if (entries.length === 0)
19406
+ return;
19407
+ const logger = getSafeLogger();
19408
+ logger?.info("acp-adapter", `[sweep] Closing ${entries.length} open sessions for feature: ${featureName}`);
19409
+ const cmdStr = "acpx claude";
19410
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19411
+ try {
19412
+ await client.start();
19413
+ for (const [, sessionName] of entries) {
19414
+ try {
19415
+ if (client.loadSession) {
19416
+ const session = await client.loadSession(sessionName, "claude", "approve-reads");
19417
+ if (session) {
19418
+ await session.close().catch(() => {});
19419
+ }
19420
+ }
19421
+ } catch (err) {
19422
+ logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
19423
+ }
19424
+ }
19425
+ } finally {
19426
+ await client.close().catch(() => {});
19427
+ }
19428
+ try {
19429
+ await Bun.write(path, JSON.stringify({}, null, 2));
19430
+ } catch (err) {
19431
+ logger?.warn("acp-adapter", "[sweep] Failed to clear sidecar after sweep", { error: String(err) });
19432
+ }
19433
+ }
19434
+ async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS) {
19435
+ const path = acpSessionsPath(workdir, featureName);
19436
+ const file2 = Bun.file(path);
19437
+ if (!await file2.exists())
19438
+ return;
19439
+ const ageMs = Date.now() - file2.lastModified;
19440
+ if (ageMs < maxAgeMs)
19441
+ return;
19442
+ getSafeLogger()?.info("acp-adapter", `[sweep] Sidecar is ${Math.round(ageMs / 60000)}m old \u2014 sweeping stale sessions`, {
19443
+ featureName,
19444
+ ageMs
19445
+ });
19446
+ await sweepFeatureSessions(workdir, featureName);
19447
+ }
19339
19448
  function extractOutput(response) {
19340
19449
  if (!response)
19341
19450
  return "";
@@ -19459,6 +19568,7 @@ class AcpAgentAdapter {
19459
19568
  }
19460
19569
  let lastResponse = null;
19461
19570
  let timedOut = false;
19571
+ const runState = { succeeded: false };
19462
19572
  const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
19463
19573
  try {
19464
19574
  let currentPrompt = options.prompt;
@@ -19499,12 +19609,17 @@ class AcpAgentAdapter {
19499
19609
  if (turnCount >= MAX_TURNS && options.interactionBridge) {
19500
19610
  getSafeLogger()?.warn("acp-adapter", "Reached max turns limit", { sessionName, maxTurns: MAX_TURNS });
19501
19611
  }
19612
+ runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
19502
19613
  } finally {
19503
- await closeAcpSession(session);
19504
- await client.close().catch(() => {});
19505
- if (options.featureName && options.storyId) {
19506
- await clearAcpSession(options.workdir, options.featureName, options.storyId);
19614
+ if (runState.succeeded) {
19615
+ await closeAcpSession(session);
19616
+ if (options.featureName && options.storyId) {
19617
+ await clearAcpSession(options.workdir, options.featureName, options.storyId);
19618
+ }
19619
+ } else {
19620
+ getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
19507
19621
  }
19622
+ await client.close().catch(() => {});
19508
19623
  }
19509
19624
  const durationMs = Date.now() - startTime;
19510
19625
  if (timedOut) {
@@ -19654,9 +19769,10 @@ class AcpAgentAdapter {
19654
19769
  return { stories };
19655
19770
  }
19656
19771
  }
19657
- var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps;
19772
+ var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps, MAX_SESSION_AGE_MS;
19658
19773
  var init_adapter = __esm(() => {
19659
19774
  init_logger2();
19775
+ init_claude_decompose();
19660
19776
  init_spawn_client();
19661
19777
  init_types2();
19662
19778
  init_cost2();
@@ -19698,6 +19814,7 @@ var init_adapter = __esm(() => {
19698
19814
  return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
19699
19815
  }
19700
19816
  };
19817
+ MAX_SESSION_AGE_MS = 2 * 60 * 60 * 1000;
19701
19818
  });
19702
19819
 
19703
19820
  // src/agents/adapters/aider.ts
@@ -21968,7 +22085,7 @@ var package_default;
21968
22085
  var init_package = __esm(() => {
21969
22086
  package_default = {
21970
22087
  name: "@nathapp/nax",
21971
- version: "0.43.1",
22088
+ version: "0.45.0",
21972
22089
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21973
22090
  type: "module",
21974
22091
  bin: {
@@ -22041,8 +22158,8 @@ var init_version = __esm(() => {
22041
22158
  NAX_VERSION = package_default.version;
22042
22159
  NAX_COMMIT = (() => {
22043
22160
  try {
22044
- if (/^[0-9a-f]{6,10}$/.test("82a45aa"))
22045
- return "82a45aa";
22161
+ if (/^[0-9a-f]{6,10}$/.test("d6bdccb"))
22162
+ return "d6bdccb";
22046
22163
  } catch {}
22047
22164
  try {
22048
22165
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -25320,6 +25437,11 @@ async function buildStoryContextFull(prd, story, config2) {
25320
25437
  }
25321
25438
  function getAllReadyStories(prd) {
25322
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
+ });
25323
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)));
25324
25446
  }
25325
25447
  var CONTEXT_MAX_TOKENS = 1e5, CONTEXT_RESERVED_TOKENS = 1e4;
@@ -28235,7 +28357,7 @@ var init_test_output_parser = () => {};
28235
28357
 
28236
28358
  // src/verification/rectification-loop.ts
28237
28359
  async function runRectificationLoop2(opts) {
28238
- const { config: config2, workdir, story, testCommand, timeoutSeconds, testOutput, promptPrefix } = opts;
28360
+ const { config: config2, workdir, story, testCommand, timeoutSeconds, testOutput, promptPrefix, featureName } = opts;
28239
28361
  const logger = getSafeLogger();
28240
28362
  const rectificationConfig = config2.execution.rectification;
28241
28363
  const testSummary = parseBunTestOutput(testOutput);
@@ -28261,7 +28383,7 @@ async function runRectificationLoop2(opts) {
28261
28383
  rectificationPrompt = `${promptPrefix}
28262
28384
 
28263
28385
  ${rectificationPrompt}`;
28264
- const agent = getAgent(config2.autoMode.defaultAgent);
28386
+ const agent = _rectificationDeps.getAgent(config2.autoMode.defaultAgent);
28265
28387
  if (!agent) {
28266
28388
  logger?.error("rectification", "Agent not found, cannot retry");
28267
28389
  break;
@@ -28277,7 +28399,10 @@ ${rectificationPrompt}`;
28277
28399
  dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
28278
28400
  pipelineStage: "rectification",
28279
28401
  config: config2,
28280
- maxInteractionTurns: config2.agent?.maxInteractionTurns
28402
+ maxInteractionTurns: config2.agent?.maxInteractionTurns,
28403
+ featureName,
28404
+ storyId: story.id,
28405
+ sessionRole: "implementer"
28281
28406
  });
28282
28407
  if (agentResult.success) {
28283
28408
  logger?.info("rectification", `Agent ${label} session complete`, {
@@ -28292,7 +28417,7 @@ ${rectificationPrompt}`;
28292
28417
  exitCode: agentResult.exitCode
28293
28418
  });
28294
28419
  }
28295
- const retryVerification = await fullSuite({
28420
+ const retryVerification = await _rectificationDeps.runVerification({
28296
28421
  workdir,
28297
28422
  expectedFiles: getExpectedFiles(story),
28298
28423
  command: testCommand,
@@ -28342,6 +28467,7 @@ ${rectificationPrompt}`;
28342
28467
  }
28343
28468
  return false;
28344
28469
  }
28470
+ var _rectificationDeps;
28345
28471
  var init_rectification_loop = __esm(() => {
28346
28472
  init_agents();
28347
28473
  init_config();
@@ -28350,6 +28476,10 @@ var init_rectification_loop = __esm(() => {
28350
28476
  init_prd();
28351
28477
  init_rectification();
28352
28478
  init_runners();
28479
+ _rectificationDeps = {
28480
+ getAgent,
28481
+ runVerification: fullSuite
28482
+ };
28353
28483
  });
28354
28484
 
28355
28485
  // src/pipeline/stages/rectify.ts
@@ -30451,8 +30581,85 @@ async function checkStorySizeGate(config2, prd) {
30451
30581
  var exports_precheck = {};
30452
30582
  __export(exports_precheck, {
30453
30583
  runPrecheck: () => runPrecheck,
30584
+ runEnvironmentPrecheck: () => runEnvironmentPrecheck,
30454
30585
  EXIT_CODES: () => EXIT_CODES
30455
30586
  });
30587
+ function getEarlyEnvironmentBlockers(workdir) {
30588
+ return [() => checkGitRepoExists(workdir), () => checkWorkingTreeClean(workdir), () => checkStaleLock(workdir)];
30589
+ }
30590
+ function getLateEnvironmentBlockers(config2, workdir) {
30591
+ return [
30592
+ () => checkAgentCLI(config2),
30593
+ () => checkDependenciesInstalled(workdir),
30594
+ () => checkTestCommand(config2),
30595
+ () => checkLintCommand(config2),
30596
+ () => checkTypecheckCommand(config2),
30597
+ () => checkGitUserConfigured(workdir)
30598
+ ];
30599
+ }
30600
+ function getEnvironmentBlockers(config2, workdir) {
30601
+ return [...getEarlyEnvironmentBlockers(workdir), ...getLateEnvironmentBlockers(config2, workdir)];
30602
+ }
30603
+ function getEnvironmentWarnings(config2, workdir) {
30604
+ return [
30605
+ () => checkClaudeMdExists(workdir),
30606
+ () => checkDiskSpace(),
30607
+ () => checkOptionalCommands(config2, workdir),
30608
+ () => checkGitignoreCoversNax(workdir),
30609
+ () => checkPromptOverrideFiles(config2, workdir),
30610
+ () => checkMultiAgentHealth()
30611
+ ];
30612
+ }
30613
+ function getProjectBlockers(prd) {
30614
+ return [() => checkPRDValid(prd)];
30615
+ }
30616
+ function getProjectWarnings(prd) {
30617
+ return [() => checkPendingStories(prd)];
30618
+ }
30619
+ function normalizeChecks(result) {
30620
+ return Array.isArray(result) ? result : [result];
30621
+ }
30622
+ async function runEnvironmentPrecheck(config2, workdir, options) {
30623
+ const format = options?.format ?? "human";
30624
+ const silent = options?.silent ?? false;
30625
+ const passed = [];
30626
+ const blockers = [];
30627
+ const warnings = [];
30628
+ for (const checkFn of getEnvironmentBlockers(config2, workdir)) {
30629
+ const checks3 = normalizeChecks(await checkFn());
30630
+ let blocked = false;
30631
+ for (const check2 of checks3) {
30632
+ if (!silent && format === "human")
30633
+ printCheckResult(check2);
30634
+ if (check2.passed) {
30635
+ passed.push(check2);
30636
+ } else {
30637
+ blockers.push(check2);
30638
+ blocked = true;
30639
+ break;
30640
+ }
30641
+ }
30642
+ if (blocked)
30643
+ break;
30644
+ }
30645
+ if (blockers.length === 0) {
30646
+ for (const checkFn of getEnvironmentWarnings(config2, workdir)) {
30647
+ for (const check2 of normalizeChecks(await checkFn())) {
30648
+ if (!silent && format === "human")
30649
+ printCheckResult(check2);
30650
+ if (check2.passed) {
30651
+ passed.push(check2);
30652
+ } else {
30653
+ warnings.push(check2);
30654
+ }
30655
+ }
30656
+ }
30657
+ }
30658
+ if (!silent && format === "json") {
30659
+ console.log(JSON.stringify({ passed: blockers.length === 0, blockers, warnings }, null, 2));
30660
+ }
30661
+ return { passed: blockers.length === 0, blockers, warnings };
30662
+ }
30456
30663
  async function runPrecheck(config2, prd, options) {
30457
30664
  const workdir = options?.workdir || process.cwd();
30458
30665
  const format = options?.format || "human";
@@ -30461,47 +30668,33 @@ async function runPrecheck(config2, prd, options) {
30461
30668
  const blockers = [];
30462
30669
  const warnings = [];
30463
30670
  const tier1Checks = [
30464
- () => checkGitRepoExists(workdir),
30465
- () => checkWorkingTreeClean(workdir),
30466
- () => checkStaleLock(workdir),
30467
- () => checkPRDValid(prd),
30468
- () => checkAgentCLI(config2),
30469
- () => checkDependenciesInstalled(workdir),
30470
- () => checkTestCommand(config2),
30471
- () => checkLintCommand(config2),
30472
- () => checkTypecheckCommand(config2),
30473
- () => checkGitUserConfigured(workdir)
30671
+ ...getEarlyEnvironmentBlockers(workdir),
30672
+ ...getProjectBlockers(prd),
30673
+ ...getLateEnvironmentBlockers(config2, workdir)
30474
30674
  ];
30675
+ let tier1Blocked = false;
30475
30676
  for (const checkFn of tier1Checks) {
30476
- const result = await checkFn();
30477
- if (format === "human") {
30478
- printCheckResult(result);
30677
+ for (const check2 of normalizeChecks(await checkFn())) {
30678
+ if (format === "human")
30679
+ printCheckResult(check2);
30680
+ if (check2.passed) {
30681
+ passed.push(check2);
30682
+ } else {
30683
+ blockers.push(check2);
30684
+ tier1Blocked = true;
30685
+ break;
30686
+ }
30479
30687
  }
30480
- if (result.passed) {
30481
- passed.push(result);
30482
- } else {
30483
- blockers.push(result);
30688
+ if (tier1Blocked)
30484
30689
  break;
30485
- }
30486
30690
  }
30487
30691
  let flaggedStories = [];
30488
30692
  if (blockers.length === 0) {
30489
- const tier2Checks = [
30490
- () => checkClaudeMdExists(workdir),
30491
- () => checkDiskSpace(),
30492
- () => checkPendingStories(prd),
30493
- () => checkOptionalCommands(config2, workdir),
30494
- () => checkGitignoreCoversNax(workdir),
30495
- () => checkPromptOverrideFiles(config2, workdir),
30496
- () => checkMultiAgentHealth()
30497
- ];
30693
+ const tier2Checks = [...getEnvironmentWarnings(config2, workdir), ...getProjectWarnings(prd)];
30498
30694
  for (const checkFn of tier2Checks) {
30499
- const result = await checkFn();
30500
- const checksToProcess = Array.isArray(result) ? result : [result];
30501
- for (const check2 of checksToProcess) {
30502
- if (format === "human") {
30695
+ for (const check2 of normalizeChecks(await checkFn())) {
30696
+ if (format === "human")
30503
30697
  printCheckResult(check2);
30504
- }
30505
30698
  if (check2.passed) {
30506
30699
  passed.push(check2);
30507
30700
  } else {
@@ -31092,7 +31285,8 @@ async function executeFixStory(ctx, story, prd, iterations) {
31092
31285
  featureDir: ctx.featureDir,
31093
31286
  hooks: ctx.hooks,
31094
31287
  plugins: ctx.pluginRegistry,
31095
- storyStartTime: new Date().toISOString()
31288
+ storyStartTime: new Date().toISOString(),
31289
+ agentGetFn: ctx.agentGetFn
31096
31290
  };
31097
31291
  const result = await runPipeline(defaultPipeline, fixContext, ctx.eventEmitter);
31098
31292
  logger?.info("acceptance", `Fix story ${story.id} ${result.success ? "passed" : "failed"}`);
@@ -31128,7 +31322,8 @@ async function runAcceptanceLoop(ctx) {
31128
31322
  workdir: ctx.workdir,
31129
31323
  featureDir: ctx.featureDir,
31130
31324
  hooks: ctx.hooks,
31131
- plugins: ctx.pluginRegistry
31325
+ plugins: ctx.pluginRegistry,
31326
+ agentGetFn: ctx.agentGetFn
31132
31327
  };
31133
31328
  const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
31134
31329
  const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
@@ -32109,7 +32304,7 @@ function resolveMaxConcurrency(parallel) {
32109
32304
  }
32110
32305
  return Math.max(1, parallel);
32111
32306
  }
32112
- 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) {
32113
32308
  const logger = getSafeLogger();
32114
32309
  const maxConcurrency = resolveMaxConcurrency(parallel);
32115
32310
  const worktreeManager = new WorktreeManager;
@@ -32139,7 +32334,8 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
32139
32334
  featureDir,
32140
32335
  hooks,
32141
32336
  plugins,
32142
- storyStartTime: new Date().toISOString()
32337
+ storyStartTime: new Date().toISOString(),
32338
+ agentGetFn
32143
32339
  };
32144
32340
  const worktreePaths = new Map;
32145
32341
  for (const story of batch) {
@@ -32513,7 +32709,7 @@ async function runParallelExecution(options, initialPrd) {
32513
32709
  const batchStoryMetrics = [];
32514
32710
  let conflictedStories = [];
32515
32711
  try {
32516
- 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);
32517
32713
  const batchDurationMs = Date.now() - batchStartMs;
32518
32714
  const batchCompletedAt = new Date().toISOString();
32519
32715
  prd = parallelResult.updatedPrd;
@@ -34258,6 +34454,8 @@ async function setupRun(options) {
34258
34454
  } else {
34259
34455
  logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
34260
34456
  }
34457
+ const { sweepStaleFeatureSessions: sweepStaleFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
34458
+ await sweepStaleFeatureSessions2(workdir, feature).catch(() => {});
34261
34459
  const lockAcquired = await acquireLock(workdir);
34262
34460
  if (!lockAcquired) {
34263
34461
  logger?.error("execution", "Another nax process is already running in this directory");
@@ -65707,17 +65905,13 @@ init_registry();
65707
65905
  import { existsSync as existsSync9 } from "fs";
65708
65906
  import { join as join10 } from "path";
65709
65907
  import { createInterface } from "readline";
65908
+ init_test_strategy();
65710
65909
  init_pid_registry();
65711
65910
  init_logger2();
65712
65911
 
65713
65912
  // src/prd/schema.ts
65913
+ init_test_strategy();
65714
65914
  var VALID_COMPLEXITY = ["simple", "medium", "complex", "expert"];
65715
- var VALID_TEST_STRATEGIES = [
65716
- "test-after",
65717
- "tdd-simple",
65718
- "three-session-tdd",
65719
- "three-session-tdd-lite"
65720
- ];
65721
65915
  var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
65722
65916
  function extractJsonFromMarkdown(text) {
65723
65917
  const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
@@ -65787,9 +65981,7 @@ function validateStory(raw, index, allIds) {
65787
65981
  throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
65788
65982
  }
65789
65983
  const rawTestStrategy = routing.testStrategy ?? s.testStrategy;
65790
- const STRATEGY_ALIASES = { "tdd-lite": "three-session-tdd-lite" };
65791
- const normalizedStrategy = typeof rawTestStrategy === "string" ? STRATEGY_ALIASES[rawTestStrategy] ?? rawTestStrategy : rawTestStrategy;
65792
- const testStrategy = normalizedStrategy !== undefined && VALID_TEST_STRATEGIES.includes(normalizedStrategy) ? normalizedStrategy : "tdd-simple";
65984
+ const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
65793
65985
  const rawDeps = s.dependencies;
65794
65986
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
65795
65987
  for (const dep of dependencies) {
@@ -66056,19 +66248,11 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
66056
66248
  ]
66057
66249
  }
66058
66250
 
66059
- ## Complexity Classification Guide
66251
+ ${COMPLEXITY_GUIDE}
66060
66252
 
66061
- - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
66062
- - medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
66063
- - complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
66064
- - expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
66253
+ ${TEST_STRATEGY_GUIDE}
66065
66254
 
66066
- ## Test Strategy Guide
66067
-
66068
- - test-after: Simple changes with well-understood behavior. Write tests after implementation.
66069
- - tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
66070
- - three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
66071
- - three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.
66255
+ ${GROUPING_RULES}
66072
66256
 
66073
66257
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
66074
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."}`;
@@ -66342,6 +66526,15 @@ async function loadProjectStatusFile(projectDir) {
66342
66526
  }
66343
66527
  async function getFeatureSummary(featureName, featureDir) {
66344
66528
  const prdPath = join13(featureDir, "prd.json");
66529
+ if (!existsSync11(prdPath)) {
66530
+ return {
66531
+ name: featureName,
66532
+ done: 0,
66533
+ failed: 0,
66534
+ pending: 0,
66535
+ total: 0
66536
+ };
66537
+ }
66345
66538
  const prd = await loadPRD(prdPath);
66346
66539
  const counts = countStories(prd);
66347
66540
  const summary = {
@@ -66454,6 +66647,13 @@ async function displayAllFeatures(projectDir) {
66454
66647
  }
66455
66648
  async function displayFeatureDetails(featureName, featureDir) {
66456
66649
  const prdPath = join13(featureDir, "prd.json");
66650
+ if (!existsSync11(prdPath)) {
66651
+ console.log(source_default.bold(`
66652
+ \uD83D\uDCCA ${featureName}
66653
+ `));
66654
+ console.log(source_default.dim(`No prd.json found. Run: nax plan -f ${featureName} --from <spec>`));
66655
+ return;
66656
+ }
66457
66657
  const prd = await loadPRD(prdPath);
66458
66658
  const counts = countStories(prd);
66459
66659
  const status = await loadStatusFile(featureDir);
@@ -68888,6 +69088,7 @@ async function unlockCommand(options) {
68888
69088
  init_config();
68889
69089
 
68890
69090
  // src/execution/runner.ts
69091
+ init_adapter();
68891
69092
  init_registry();
68892
69093
  init_hooks();
68893
69094
  init_logger2();
@@ -69063,9 +69264,20 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
69063
69264
  batchingEnabled: options.useBatch
69064
69265
  });
69065
69266
  clearCache();
69066
- 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) : [];
69067
69279
  if (options.useBatch) {
69068
- await tryLlmBatchRoute(options.config, getAllReadyStories(prd), "routing");
69280
+ await tryLlmBatchRoute(options.config, readyStories, "routing");
69069
69281
  }
69070
69282
  if (options.parallel !== undefined) {
69071
69283
  const runParallelExecution2 = options.runParallelExecution ?? (await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor))).runParallelExecution;
@@ -69301,6 +69513,7 @@ async function run(options) {
69301
69513
  } finally {
69302
69514
  stopHeartbeat();
69303
69515
  cleanupCrashHandlers();
69516
+ await sweepFeatureSessions(workdir, feature).catch(() => {});
69304
69517
  const { cleanupRun: cleanupRun2 } = await Promise.resolve().then(() => (init_run_cleanup(), exports_run_cleanup));
69305
69518
  await cleanupRun2({
69306
69519
  runId,
@@ -76762,7 +76975,7 @@ Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cu
76762
76975
  console.log(source_default.dim(`
76763
76976
  Next: nax features create <name>`));
76764
76977
  });
76765
- program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
76978
+ program2.command("run").description("Run the orchestration loop for a feature").requiredOption("-f, --feature <name>", "Feature name").option("-a, --agent <name>", "Force a specific agent").option("-m, --max-iterations <n>", "Max iterations", "20").option("--dry-run", "Show plan without executing", false).option("--no-context", "Disable context builder (skip file context in prompts)").option("--no-batch", "Disable story batching (execute all stories individually)").option("--parallel <n>", "Max parallel sessions (0=auto, omit=sequential)").option("--plan", "Run plan phase first before execution", false).option("--from <spec-path>", "Path to spec file (required when --plan is used)").option("--one-shot", "Skip interactive planning Q&A, use single LLM call (ACP only)", false).option("--force", "Force overwrite existing prd.json when using --plan", false).option("--headless", "Force headless mode (disable TUI, use pipe mode)", false).option("--verbose", "Enable verbose logging (debug level)", false).option("--quiet", "Quiet mode (warnings and errors only)", false).option("--silent", "Silent mode (errors only)", false).option("--json", "JSON mode (raw JSONL output to stdout)", false).option("-d, --dir <path>", "Working directory", process.cwd()).option("--skip-precheck", "Skip precheck validations (advanced users only)", false).action(async (options) => {
76766
76979
  let workdir;
76767
76980
  try {
76768
76981
  workdir = validateDirectory(options.dir);
@@ -76806,6 +77019,25 @@ program2.command("run").description("Run the orchestration loop for a feature").
76806
77019
  const featureDir = join43(naxDir, "features", options.feature);
76807
77020
  const prdPath = join43(featureDir, "prd.json");
76808
77021
  if (options.plan && options.from) {
77022
+ if (existsSync32(prdPath) && !options.force) {
77023
+ console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
77024
+ console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
77025
+ process.exit(1);
77026
+ }
77027
+ if (!options.skipPrecheck) {
77028
+ const { runEnvironmentPrecheck: runEnvironmentPrecheck2 } = await Promise.resolve().then(() => (init_precheck(), exports_precheck));
77029
+ console.log(source_default.dim(`
77030
+ [Pre-plan environment check]`));
77031
+ const envResult = await runEnvironmentPrecheck2(config2, workdir);
77032
+ if (!envResult.passed) {
77033
+ console.error(source_default.red(`
77034
+ \u274C Environment precheck failed \u2014 cannot proceed with planning.`));
77035
+ for (const b of envResult.blockers) {
77036
+ console.error(source_default.red(` ${b.name}: ${b.message}`));
77037
+ }
77038
+ process.exit(1);
77039
+ }
77040
+ }
76809
77041
  try {
76810
77042
  mkdirSync6(featureDir, { recursive: true });
76811
77043
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");