@nathapp/nax 0.44.0 → 0.46.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/nax.ts +7 -6
  3. package/dist/nax.js +266 -161
  4. package/package.json +1 -1
  5. package/src/agents/acp/adapter.ts +34 -6
  6. package/src/agents/acp/index.ts +0 -2
  7. package/src/agents/acp/parser.ts +57 -104
  8. package/src/agents/acp/spawn-client.ts +2 -1
  9. package/src/agents/{claude.ts → claude/adapter.ts} +15 -12
  10. package/src/agents/{claude-complete.ts → claude/complete.ts} +3 -3
  11. package/src/agents/{cost.ts → claude/cost.ts} +1 -1
  12. package/src/agents/{claude-execution.ts → claude/execution.ts} +5 -5
  13. package/src/agents/claude/index.ts +3 -0
  14. package/src/agents/{claude-interactive.ts → claude/interactive.ts} +4 -4
  15. package/src/agents/{claude-plan.ts → claude/plan.ts} +12 -9
  16. package/src/agents/index.ts +5 -5
  17. package/src/agents/registry.ts +5 -5
  18. package/src/agents/{claude-decompose.ts → shared/decompose.ts} +7 -22
  19. package/src/agents/{model-resolution.ts → shared/model-resolution.ts} +2 -2
  20. package/src/agents/{types-extended.ts → shared/types-extended.ts} +4 -4
  21. package/src/agents/{validation.ts → shared/validation.ts} +2 -2
  22. package/src/agents/{version-detection.ts → shared/version-detection.ts} +3 -3
  23. package/src/agents/types.ts +8 -4
  24. package/src/cli/agents.ts +1 -1
  25. package/src/cli/plan.ts +4 -11
  26. package/src/config/test-strategy.ts +70 -0
  27. package/src/execution/lifecycle/acceptance-loop.ts +2 -0
  28. package/src/execution/parallel-coordinator.ts +3 -1
  29. package/src/execution/parallel-executor.ts +3 -0
  30. package/src/execution/runner-execution.ts +16 -2
  31. package/src/execution/story-context.ts +6 -0
  32. package/src/pipeline/stages/acceptance.ts +5 -8
  33. package/src/pipeline/stages/regression.ts +2 -0
  34. package/src/pipeline/stages/verify.ts +5 -10
  35. package/src/prd/schema.ts +4 -14
  36. package/src/precheck/checks-agents.ts +1 -1
  37. package/src/utils/log-test-output.ts +25 -0
  38. /package/src/agents/{adapters/aider.ts → aider/adapter.ts} +0 -0
  39. /package/src/agents/{adapters/codex.ts → codex/adapter.ts} +0 -0
  40. /package/src/agents/{adapters/gemini.ts → gemini/adapter.ts} +0 -0
  41. /package/src/agents/{adapters/opencode.ts → opencode/adapter.ts} +0 -0
package/dist/nax.js CHANGED
@@ -3240,61 +3240,56 @@ async function withProcessTimeout(proc, timeoutMs, opts) {
3240
3240
  return { exitCode, timedOut };
3241
3241
  }
3242
3242
 
3243
- // src/agents/types.ts
3244
- var CompleteError;
3245
- var init_types2 = __esm(() => {
3246
- CompleteError = class CompleteError extends Error {
3247
- exitCode;
3248
- constructor(message, exitCode) {
3249
- super(message);
3250
- this.exitCode = exitCode;
3251
- this.name = "CompleteError";
3252
- }
3253
- };
3254
- });
3255
-
3256
- // src/agents/claude-complete.ts
3257
- async function executeComplete(binary, prompt, options) {
3258
- const cmd = [binary, "-p", prompt];
3259
- if (options?.model) {
3260
- cmd.push("--model", options.model);
3261
- }
3262
- if (options?.jsonMode) {
3263
- cmd.push("--output-format", "json");
3264
- }
3265
- const { skipPermissions } = resolvePermissions(options?.config, "complete");
3266
- if (skipPermissions) {
3267
- cmd.push("--dangerously-skip-permissions");
3268
- }
3269
- const spawnOpts = { stdout: "pipe", stderr: "pipe" };
3270
- if (options?.workdir)
3271
- spawnOpts.cwd = options.workdir;
3272
- const proc = _completeDeps.spawn(cmd, spawnOpts);
3273
- const exitCode = await proc.exited;
3274
- const stdout = await new Response(proc.stdout).text();
3275
- const stderr = await new Response(proc.stderr).text();
3276
- const trimmed = stdout.trim();
3277
- if (exitCode !== 0) {
3278
- const errorDetails = stderr.trim() || trimmed;
3279
- const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
3280
- throw new CompleteError(errorMessage, exitCode);
3281
- }
3282
- if (!trimmed) {
3283
- throw new CompleteError("complete() returned empty output");
3284
- }
3285
- return trimmed;
3243
+ // src/config/test-strategy.ts
3244
+ function resolveTestStrategy(raw) {
3245
+ if (!raw)
3246
+ return "test-after";
3247
+ if (VALID_TEST_STRATEGIES.includes(raw))
3248
+ return raw;
3249
+ if (raw === "tdd")
3250
+ return "tdd-simple";
3251
+ if (raw === "three-session")
3252
+ return "three-session-tdd";
3253
+ if (raw === "tdd-lite")
3254
+ return "three-session-tdd-lite";
3255
+ return "test-after";
3286
3256
  }
3287
- var _completeDeps;
3288
- var init_claude_complete = __esm(() => {
3289
- init_types2();
3290
- _completeDeps = {
3291
- spawn(cmd, opts) {
3292
- return Bun.spawn(cmd, opts);
3293
- }
3294
- };
3257
+ var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guide
3258
+
3259
+ - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
3260
+ - medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
3261
+ - complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
3262
+ - expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
3263
+
3264
+ ### Security Override
3265
+
3266
+ Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
3267
+ password hashing, access control) must be classified at MINIMUM "medium" complexity
3268
+ regardless of LOC count. These require at minimum "tdd-simple" test strategy.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
3269
+
3270
+ - test-after: Simple changes with well-understood behavior. Write tests after implementation.
3271
+ - tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
3272
+ - three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
3273
+ - three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.`, GROUPING_RULES = `## Grouping Rules
3274
+
3275
+ - Combine small, related tasks into a single "simple" or "medium" story.
3276
+ - Do NOT create separate stories for every single file or function unless complex.
3277
+ - Do NOT create standalone stories purely for test coverage or testing.
3278
+ Each story's testStrategy already handles testing (tdd-simple writes tests first,
3279
+ three-session-tdd uses separate test-writer session, test-after writes tests after).
3280
+ Only create a dedicated test story for unique integration/E2E test logic that spans
3281
+ multiple stories and cannot be covered by individual story test strategies.
3282
+ - Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
3283
+ var init_test_strategy = __esm(() => {
3284
+ VALID_TEST_STRATEGIES = [
3285
+ "test-after",
3286
+ "tdd-simple",
3287
+ "three-session-tdd",
3288
+ "three-session-tdd-lite"
3289
+ ];
3295
3290
  });
3296
3291
 
3297
- // src/agents/claude-decompose.ts
3292
+ // src/agents/shared/decompose.ts
3298
3293
  function buildDecomposePrompt(options) {
3299
3294
  return `You are a requirements analyst. Break down the following feature specification into user stories and classify each story's complexity.
3300
3295
 
@@ -3316,24 +3311,13 @@ Decompose this spec into user stories. For each story, provide:
3316
3311
  9. reasoning: Why this complexity level
3317
3312
  10. estimatedLOC: Estimated lines of code to change
3318
3313
  11. risks: Array of implementation risks
3319
- 12. testStrategy: "three-session-tdd" | "test-after"
3314
+ 12. testStrategy: "test-after" | "tdd-simple" | "three-session-tdd" | "three-session-tdd-lite"
3320
3315
 
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"
3316
+ ${COMPLEXITY_GUIDE}
3325
3317
 
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
3318
+ ${TEST_STRATEGY_GUIDE}
3331
3319
 
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.
3320
+ ${GROUPING_RULES}
3337
3321
 
3338
3322
  Consider:
3339
3323
  1. Does infrastructure exist? (e.g., "add caching" when no cache layer exists = complex)
@@ -3402,7 +3386,7 @@ ${output.slice(0, 500)}`);
3402
3386
  reasoning: String(record.reasoning || "No reasoning provided"),
3403
3387
  estimatedLOC: Number(record.estimatedLOC) || 0,
3404
3388
  risks: Array.isArray(record.risks) ? record.risks : [],
3405
- testStrategy: record.testStrategy === "three-session-tdd" ? "three-session-tdd" : record.testStrategy === "test-after" ? "test-after" : undefined
3389
+ testStrategy: resolveTestStrategy(typeof record.testStrategy === "string" ? record.testStrategy : undefined)
3406
3390
  };
3407
3391
  });
3408
3392
  if (stories.length === 0) {
@@ -3416,8 +3400,65 @@ function coerceComplexity(value) {
3416
3400
  }
3417
3401
  return "medium";
3418
3402
  }
3403
+ var init_decompose = __esm(() => {
3404
+ init_test_strategy();
3405
+ });
3419
3406
 
3420
- // src/agents/cost.ts
3407
+ // src/agents/types.ts
3408
+ var CompleteError;
3409
+ var init_types2 = __esm(() => {
3410
+ CompleteError = class CompleteError extends Error {
3411
+ exitCode;
3412
+ constructor(message, exitCode) {
3413
+ super(message);
3414
+ this.exitCode = exitCode;
3415
+ this.name = "CompleteError";
3416
+ }
3417
+ };
3418
+ });
3419
+
3420
+ // src/agents/claude/complete.ts
3421
+ async function executeComplete(binary, prompt, options) {
3422
+ const cmd = [binary, "-p", prompt];
3423
+ if (options?.model) {
3424
+ cmd.push("--model", options.model);
3425
+ }
3426
+ if (options?.jsonMode) {
3427
+ cmd.push("--output-format", "json");
3428
+ }
3429
+ const { skipPermissions } = resolvePermissions(options?.config, "complete");
3430
+ if (skipPermissions) {
3431
+ cmd.push("--dangerously-skip-permissions");
3432
+ }
3433
+ const spawnOpts = { stdout: "pipe", stderr: "pipe" };
3434
+ if (options?.workdir)
3435
+ spawnOpts.cwd = options.workdir;
3436
+ const proc = _completeDeps.spawn(cmd, spawnOpts);
3437
+ const exitCode = await proc.exited;
3438
+ const stdout = await new Response(proc.stdout).text();
3439
+ const stderr = await new Response(proc.stderr).text();
3440
+ const trimmed = stdout.trim();
3441
+ if (exitCode !== 0) {
3442
+ const errorDetails = stderr.trim() || trimmed;
3443
+ const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
3444
+ throw new CompleteError(errorMessage, exitCode);
3445
+ }
3446
+ if (!trimmed) {
3447
+ throw new CompleteError("complete() returned empty output");
3448
+ }
3449
+ return trimmed;
3450
+ }
3451
+ var _completeDeps;
3452
+ var init_complete = __esm(() => {
3453
+ init_types2();
3454
+ _completeDeps = {
3455
+ spawn(cmd, opts) {
3456
+ return Bun.spawn(cmd, opts);
3457
+ }
3458
+ };
3459
+ });
3460
+
3461
+ // src/agents/claude/cost.ts
3421
3462
  function parseTokenUsage(output) {
3422
3463
  try {
3423
3464
  const jsonMatch = output.match(/\{[^}]*"usage"\s*:\s*\{[^}]*"input_tokens"\s*:\s*(\d+)[^}]*"output_tokens"\s*:\s*(\d+)[^}]*\}[^}]*\}/);
@@ -3520,7 +3561,7 @@ var init_cost = __esm(() => {
3520
3561
  };
3521
3562
  });
3522
3563
 
3523
- // src/agents/claude-execution.ts
3564
+ // src/agents/claude/execution.ts
3524
3565
  function buildCommand(binary, options) {
3525
3566
  const model = options.modelDef.model;
3526
3567
  const { skipPermissions } = resolvePermissions(options.config, options.pipelineStage ?? "run");
@@ -3622,7 +3663,7 @@ async function executeOnce(binary, options, pidRegistry) {
3622
3663
  };
3623
3664
  }
3624
3665
  var MAX_AGENT_OUTPUT_CHARS = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_PERIOD_MS = 5000, _runOnceDeps;
3625
- var init_claude_execution = __esm(() => {
3666
+ var init_execution = __esm(() => {
3626
3667
  init_logger2();
3627
3668
  init_cost();
3628
3669
  _runOnceDeps = {
@@ -3635,7 +3676,7 @@ var init_claude_execution = __esm(() => {
3635
3676
  };
3636
3677
  });
3637
3678
 
3638
- // src/agents/claude-interactive.ts
3679
+ // src/agents/claude/interactive.ts
3639
3680
  function runInteractiveMode(binary, options, pidRegistry) {
3640
3681
  const model = options.modelDef.model;
3641
3682
  const cmd = [binary, "--model", model, options.prompt];
@@ -3674,9 +3715,9 @@ function runInteractiveMode(binary, options, pidRegistry) {
3674
3715
  pid: proc.pid
3675
3716
  };
3676
3717
  }
3677
- var init_claude_interactive = __esm(() => {
3718
+ var init_interactive = __esm(() => {
3678
3719
  init_logger2();
3679
- init_claude_execution();
3720
+ init_execution();
3680
3721
  });
3681
3722
 
3682
3723
  // src/config/schema-types.ts
@@ -18098,7 +18139,7 @@ var init_schema = __esm(() => {
18098
18139
  init_defaults();
18099
18140
  });
18100
18141
 
18101
- // src/agents/model-resolution.ts
18142
+ // src/agents/shared/model-resolution.ts
18102
18143
  var exports_model_resolution = {};
18103
18144
  __export(exports_model_resolution, {
18104
18145
  resolveBalancedModelDef: () => resolveBalancedModelDef
@@ -18119,7 +18160,7 @@ var init_model_resolution = __esm(() => {
18119
18160
  init_schema();
18120
18161
  });
18121
18162
 
18122
- // src/agents/claude-plan.ts
18163
+ // src/agents/claude/plan.ts
18123
18164
  import { mkdtempSync, rmSync } from "fs";
18124
18165
  import { tmpdir } from "os";
18125
18166
  import { join } from "path";
@@ -18238,12 +18279,12 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
18238
18279
  }
18239
18280
  }
18240
18281
  }
18241
- var init_claude_plan = __esm(() => {
18282
+ var init_plan = __esm(() => {
18242
18283
  init_logger2();
18243
18284
  init_model_resolution();
18244
18285
  });
18245
18286
 
18246
- // src/agents/claude.ts
18287
+ // src/agents/claude/adapter.ts
18247
18288
  class ClaudeCodeAdapter {
18248
18289
  name = "claude";
18249
18290
  displayName = "Claude Code";
@@ -18394,13 +18435,14 @@ class ClaudeCodeAdapter {
18394
18435
  }
18395
18436
  }
18396
18437
  var _decomposeDeps, _claudeAdapterDeps;
18397
- var init_claude = __esm(() => {
18438
+ var init_adapter = __esm(() => {
18398
18439
  init_pid_registry();
18399
18440
  init_logger2();
18400
- init_claude_complete();
18401
- init_claude_execution();
18402
- init_claude_interactive();
18403
- init_claude_plan();
18441
+ init_decompose();
18442
+ init_complete();
18443
+ init_execution();
18444
+ init_interactive();
18445
+ init_plan();
18404
18446
  _decomposeDeps = {
18405
18447
  spawn(cmd, opts) {
18406
18448
  return Bun.spawn(cmd, opts);
@@ -18411,6 +18453,12 @@ var init_claude = __esm(() => {
18411
18453
  };
18412
18454
  });
18413
18455
 
18456
+ // src/agents/claude/index.ts
18457
+ var init_claude = __esm(() => {
18458
+ init_adapter();
18459
+ init_execution();
18460
+ });
18461
+
18414
18462
  // src/utils/errors.ts
18415
18463
  function errorMessage(err) {
18416
18464
  return err instanceof Error ? err.message : String(err);
@@ -18935,11 +18983,40 @@ function parseAcpxJsonOutput(rawOutput) {
18935
18983
  `).filter((l) => l.trim());
18936
18984
  let text = "";
18937
18985
  let tokenUsage;
18986
+ let exactCostUsd;
18938
18987
  let stopReason;
18939
18988
  let error48;
18940
18989
  for (const line of lines) {
18941
18990
  try {
18942
18991
  const event = JSON.parse(line);
18992
+ if (event.jsonrpc === "2.0") {
18993
+ if (event.method === "session/update" && event.params?.update) {
18994
+ const update = event.params.update;
18995
+ if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
18996
+ text += update.content.text;
18997
+ }
18998
+ if (update.sessionUpdate === "usage_update" && typeof update.cost?.amount === "number") {
18999
+ exactCostUsd = update.cost.amount;
19000
+ }
19001
+ }
19002
+ if (event.id !== undefined && event.result && typeof event.result === "object") {
19003
+ const result = event.result;
19004
+ if (result.stopReason)
19005
+ stopReason = result.stopReason;
19006
+ if (result.stop_reason)
19007
+ stopReason = result.stop_reason;
19008
+ if (result.usage && typeof result.usage === "object") {
19009
+ const u = result.usage;
19010
+ tokenUsage = {
19011
+ input_tokens: u.inputTokens ?? u.input_tokens ?? 0,
19012
+ output_tokens: u.outputTokens ?? u.output_tokens ?? 0,
19013
+ cache_read_input_tokens: u.cachedReadTokens ?? u.cache_read_input_tokens ?? 0,
19014
+ cache_creation_input_tokens: u.cachedWriteTokens ?? u.cache_creation_input_tokens ?? 0
19015
+ };
19016
+ }
19017
+ }
19018
+ continue;
19019
+ }
18943
19020
  if (event.content && typeof event.content === "string")
18944
19021
  text += event.content;
18945
19022
  if (event.text && typeof event.text === "string")
@@ -18966,7 +19043,7 @@ function parseAcpxJsonOutput(rawOutput) {
18966
19043
  text = line;
18967
19044
  }
18968
19045
  }
18969
- return { text: text.trim(), tokenUsage, stopReason, error: error48 };
19046
+ return { text: text.trim(), tokenUsage, exactCostUsd, stopReason, error: error48 };
18970
19047
  }
18971
19048
 
18972
19049
  // src/agents/acp/spawn-client.ts
@@ -19065,8 +19142,9 @@ class SpawnAcpSession {
19065
19142
  const parsed = parseAcpxJsonOutput(stdout);
19066
19143
  return {
19067
19144
  messages: [{ role: "assistant", content: parsed.text || "" }],
19068
- stopReason: "end_turn",
19069
- cumulative_token_usage: parsed.tokenUsage
19145
+ stopReason: parsed.stopReason ?? "end_turn",
19146
+ cumulative_token_usage: parsed.tokenUsage,
19147
+ exactCostUsd: parsed.exactCostUsd
19070
19148
  };
19071
19149
  } catch (err) {
19072
19150
  getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
@@ -19527,7 +19605,13 @@ class AcpAgentAdapter {
19527
19605
  let lastResponse = null;
19528
19606
  let timedOut = false;
19529
19607
  const runState = { succeeded: false };
19530
- const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
19608
+ const totalTokenUsage = {
19609
+ input_tokens: 0,
19610
+ output_tokens: 0,
19611
+ cache_read_input_tokens: 0,
19612
+ cache_creation_input_tokens: 0
19613
+ };
19614
+ let totalExactCostUsd;
19531
19615
  try {
19532
19616
  let currentPrompt = options.prompt;
19533
19617
  let turnCount = 0;
@@ -19546,6 +19630,11 @@ class AcpAgentAdapter {
19546
19630
  if (lastResponse.cumulative_token_usage) {
19547
19631
  totalTokenUsage.input_tokens += lastResponse.cumulative_token_usage.input_tokens ?? 0;
19548
19632
  totalTokenUsage.output_tokens += lastResponse.cumulative_token_usage.output_tokens ?? 0;
19633
+ totalTokenUsage.cache_read_input_tokens += lastResponse.cumulative_token_usage.cache_read_input_tokens ?? 0;
19634
+ totalTokenUsage.cache_creation_input_tokens += lastResponse.cumulative_token_usage.cache_creation_input_tokens ?? 0;
19635
+ }
19636
+ if (lastResponse.exactCostUsd !== undefined) {
19637
+ totalExactCostUsd = (totalExactCostUsd ?? 0) + lastResponse.exactCostUsd;
19549
19638
  }
19550
19639
  const outputText = extractOutput(lastResponse);
19551
19640
  const question = extractQuestion(outputText);
@@ -19592,7 +19681,7 @@ class AcpAgentAdapter {
19592
19681
  }
19593
19682
  const success2 = lastResponse?.stopReason === "end_turn";
19594
19683
  const output = extractOutput(lastResponse);
19595
- const estimatedCost = totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model) : 0;
19684
+ const estimatedCost = totalExactCostUsd ?? (totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model) : 0);
19596
19685
  return {
19597
19686
  success: success2,
19598
19687
  exitCode: success2 ? 0 : 1,
@@ -19642,6 +19731,12 @@ class AcpAgentAdapter {
19642
19731
  if (!unwrapped) {
19643
19732
  throw new CompleteError("complete() returned empty output");
19644
19733
  }
19734
+ if (response.exactCostUsd !== undefined) {
19735
+ getSafeLogger()?.info("acp-adapter", "complete() cost", {
19736
+ costUsd: response.exactCostUsd,
19737
+ model
19738
+ });
19739
+ }
19645
19740
  return unwrapped;
19646
19741
  } catch (err) {
19647
19742
  const error48 = err instanceof Error ? err : new Error(String(err));
@@ -19728,8 +19823,9 @@ class AcpAgentAdapter {
19728
19823
  }
19729
19824
  }
19730
19825
  var MAX_AGENT_OUTPUT_CHARS2 = 5000, MAX_RATE_LIMIT_RETRIES = 3, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, _acpAdapterDeps, MAX_SESSION_AGE_MS;
19731
- var init_adapter = __esm(() => {
19826
+ var init_adapter2 = __esm(() => {
19732
19827
  init_logger2();
19828
+ init_decompose();
19733
19829
  init_spawn_client();
19734
19830
  init_types2();
19735
19831
  init_cost2();
@@ -19774,7 +19870,7 @@ var init_adapter = __esm(() => {
19774
19870
  MAX_SESSION_AGE_MS = 2 * 60 * 60 * 1000;
19775
19871
  });
19776
19872
 
19777
- // src/agents/adapters/aider.ts
19873
+ // src/agents/aider/adapter.ts
19778
19874
  class AiderAdapter {
19779
19875
  name = "aider";
19780
19876
  displayName = "Aider";
@@ -19839,7 +19935,7 @@ class AiderAdapter {
19839
19935
  }
19840
19936
  }
19841
19937
  var _aiderCompleteDeps, MAX_AGENT_OUTPUT_CHARS3 = 5000;
19842
- var init_aider = __esm(() => {
19938
+ var init_adapter3 = __esm(() => {
19843
19939
  init_types2();
19844
19940
  _aiderCompleteDeps = {
19845
19941
  which(name) {
@@ -19851,7 +19947,7 @@ var init_aider = __esm(() => {
19851
19947
  };
19852
19948
  });
19853
19949
 
19854
- // src/agents/adapters/codex.ts
19950
+ // src/agents/codex/adapter.ts
19855
19951
  class CodexAdapter {
19856
19952
  name = "codex";
19857
19953
  displayName = "Codex";
@@ -19914,7 +20010,7 @@ class CodexAdapter {
19914
20010
  }
19915
20011
  }
19916
20012
  var _codexRunDeps, _codexCompleteDeps, MAX_AGENT_OUTPUT_CHARS4 = 5000;
19917
- var init_codex = __esm(() => {
20013
+ var init_adapter4 = __esm(() => {
19918
20014
  init_types2();
19919
20015
  _codexRunDeps = {
19920
20016
  which(name) {
@@ -19931,7 +20027,7 @@ var init_codex = __esm(() => {
19931
20027
  };
19932
20028
  });
19933
20029
 
19934
- // src/agents/adapters/gemini.ts
20030
+ // src/agents/gemini/adapter.ts
19935
20031
  class GeminiAdapter {
19936
20032
  name = "gemini";
19937
20033
  displayName = "Gemini CLI";
@@ -20014,7 +20110,7 @@ class GeminiAdapter {
20014
20110
  }
20015
20111
  }
20016
20112
  var _geminiRunDeps, _geminiCompleteDeps, MAX_AGENT_OUTPUT_CHARS5 = 5000;
20017
- var init_gemini = __esm(() => {
20113
+ var init_adapter5 = __esm(() => {
20018
20114
  init_types2();
20019
20115
  _geminiRunDeps = {
20020
20116
  which(name) {
@@ -20031,7 +20127,7 @@ var init_gemini = __esm(() => {
20031
20127
  };
20032
20128
  });
20033
20129
 
20034
- // src/agents/adapters/opencode.ts
20130
+ // src/agents/opencode/adapter.ts
20035
20131
  class OpenCodeAdapter {
20036
20132
  name = "opencode";
20037
20133
  displayName = "OpenCode";
@@ -20076,7 +20172,7 @@ class OpenCodeAdapter {
20076
20172
  }
20077
20173
  }
20078
20174
  var _opencodeCompleteDeps;
20079
- var init_opencode = __esm(() => {
20175
+ var init_adapter6 = __esm(() => {
20080
20176
  init_types2();
20081
20177
  _opencodeCompleteDeps = {
20082
20178
  which(name) {
@@ -20168,12 +20264,12 @@ function createAgentRegistry(config2) {
20168
20264
  var ALL_AGENTS;
20169
20265
  var init_registry = __esm(() => {
20170
20266
  init_logger2();
20267
+ init_adapter2();
20268
+ init_adapter3();
20171
20269
  init_adapter();
20172
- init_aider();
20173
- init_codex();
20174
- init_gemini();
20175
- init_opencode();
20176
- init_claude();
20270
+ init_adapter4();
20271
+ init_adapter5();
20272
+ init_adapter6();
20177
20273
  ALL_AGENTS = [
20178
20274
  new ClaudeCodeAdapter,
20179
20275
  new CodexAdapter,
@@ -22042,7 +22138,7 @@ var package_default;
22042
22138
  var init_package = __esm(() => {
22043
22139
  package_default = {
22044
22140
  name: "@nathapp/nax",
22045
- version: "0.44.0",
22141
+ version: "0.46.0",
22046
22142
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22047
22143
  type: "module",
22048
22144
  bin: {
@@ -22115,8 +22211,8 @@ var init_version = __esm(() => {
22115
22211
  NAX_VERSION = package_default.version;
22116
22212
  NAX_COMMIT = (() => {
22117
22213
  try {
22118
- if (/^[0-9a-f]{6,10}$/.test("05b2442"))
22119
- return "05b2442";
22214
+ if (/^[0-9a-f]{6,10}$/.test("6a485b9"))
22215
+ return "6a485b9";
22120
22216
  } catch {}
22121
22217
  try {
22122
22218
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -23716,6 +23812,20 @@ var init_runner = __esm(() => {
23716
23812
  init_logger2();
23717
23813
  });
23718
23814
 
23815
+ // src/utils/log-test-output.ts
23816
+ function logTestOutput(logger, stage, output, opts = {}) {
23817
+ if (!logger || !output)
23818
+ return;
23819
+ const tailLines = opts.tailLines ?? 20;
23820
+ const lines = output.split(`
23821
+ `).slice(-tailLines).join(`
23822
+ `);
23823
+ logger.debug(stage, "Test output (tail)", {
23824
+ ...opts.storyId !== undefined && { storyId: opts.storyId },
23825
+ output: lines
23826
+ });
23827
+ }
23828
+
23719
23829
  // src/pipeline/stages/acceptance.ts
23720
23830
  var exports_acceptance = {};
23721
23831
  __export(exports_acceptance, {
@@ -23799,10 +23909,8 @@ ${stderr}`;
23799
23909
  return { action: "continue" };
23800
23910
  }
23801
23911
  if (failedACs.length === 0 && exitCode !== 0) {
23802
- logger.error("acceptance", "Tests errored with no AC failures parsed", {
23803
- exitCode,
23804
- output
23805
- });
23912
+ logger.error("acceptance", "Tests errored with no AC failures parsed", { exitCode });
23913
+ logTestOutput(logger, "acceptance", output);
23806
23914
  ctx.acceptanceFailures = {
23807
23915
  failedACs: ["AC-ERROR"],
23808
23916
  testOutput: output
@@ -23820,10 +23928,8 @@ ${stderr}`;
23820
23928
  overrides: overriddenFailures.map((acId) => ({ acId, reason: overrides[acId] }))
23821
23929
  });
23822
23930
  }
23823
- logger.error("acceptance", "Acceptance tests failed", {
23824
- failedACs: actualFailures,
23825
- output
23826
- });
23931
+ logger.error("acceptance", "Acceptance tests failed", { failedACs: actualFailures });
23932
+ logTestOutput(logger, "acceptance", output);
23827
23933
  ctx.acceptanceFailures = {
23828
23934
  failedACs: actualFailures,
23829
23935
  testOutput: output
@@ -25394,6 +25500,11 @@ async function buildStoryContextFull(prd, story, config2) {
25394
25500
  }
25395
25501
  function getAllReadyStories(prd) {
25396
25502
  const completedIds = new Set(prd.userStories.filter((s) => s.passes || s.status === "skipped").map((s) => s.id));
25503
+ const logger = getSafeLogger2();
25504
+ logger?.debug("routing", "getAllReadyStories: completed set", {
25505
+ completedIds: [...completedIds],
25506
+ totalStories: prd.userStories.length
25507
+ });
25397
25508
  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
25509
  }
25399
25510
  var CONTEXT_MAX_TOKENS = 1e5, CONTEXT_RESERVED_TOKENS = 1e4;
@@ -25567,7 +25678,7 @@ ${pluginMarkdown}` : pluginMarkdown;
25567
25678
  };
25568
25679
  });
25569
25680
 
25570
- // src/agents/validation.ts
25681
+ // src/agents/shared/validation.ts
25571
25682
  function validateAgentForTier(agent, tier) {
25572
25683
  return agent.capabilities.supportedTiers.includes(tier);
25573
25684
  }
@@ -25581,7 +25692,7 @@ function describeAgentCapabilities(agent) {
25581
25692
  return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
25582
25693
  }
25583
25694
 
25584
- // src/agents/version-detection.ts
25695
+ // src/agents/shared/version-detection.ts
25585
25696
  async function getAgentVersion(binaryName) {
25586
25697
  try {
25587
25698
  const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
@@ -27694,7 +27805,7 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason) {
27694
27805
  };
27695
27806
  }
27696
27807
  var executionStage, _executionDeps;
27697
- var init_execution = __esm(() => {
27808
+ var init_execution2 = __esm(() => {
27698
27809
  init_agents();
27699
27810
  init_config();
27700
27811
  init_triggers();
@@ -29049,6 +29160,7 @@ var init_regression2 = __esm(() => {
29049
29160
  storyId: ctx.story.id,
29050
29161
  failCount: result.failCount
29051
29162
  });
29163
+ logTestOutput(logger, "regression", result.rawOutput, { storyId: ctx.story.id });
29052
29164
  pipelineEventBus.emit({
29053
29165
  type: "regression:detected",
29054
29166
  storyId: ctx.story.id,
@@ -29347,16 +29459,8 @@ var init_verify = __esm(() => {
29347
29459
  storyId: ctx.story.id
29348
29460
  });
29349
29461
  }
29350
- if (result.output && result.status !== "TIMEOUT") {
29351
- const outputLines = result.output.split(`
29352
- `).slice(-20);
29353
- if (outputLines.length > 0) {
29354
- logger.debug("verify", "Test output preview", {
29355
- storyId: ctx.story.id,
29356
- output: outputLines.join(`
29357
- `)
29358
- });
29359
- }
29462
+ if (result.status !== "TIMEOUT") {
29463
+ logTestOutput(logger, "verify", result.output, { storyId: ctx.story.id });
29360
29464
  }
29361
29465
  return {
29362
29466
  action: "escalate",
@@ -29401,7 +29505,7 @@ var init_stages = __esm(() => {
29401
29505
  init_completion();
29402
29506
  init_constitution2();
29403
29507
  init_context2();
29404
- init_execution();
29508
+ init_execution2();
29405
29509
  init_optimizer2();
29406
29510
  init_prompt();
29407
29511
  init_queue_check();
@@ -29416,7 +29520,7 @@ var init_stages = __esm(() => {
29416
29520
  init_context2();
29417
29521
  init_prompt();
29418
29522
  init_optimizer2();
29419
- init_execution();
29523
+ init_execution2();
29420
29524
  init_verify();
29421
29525
  init_rectify();
29422
29526
  init_review();
@@ -31237,7 +31341,8 @@ async function executeFixStory(ctx, story, prd, iterations) {
31237
31341
  featureDir: ctx.featureDir,
31238
31342
  hooks: ctx.hooks,
31239
31343
  plugins: ctx.pluginRegistry,
31240
- storyStartTime: new Date().toISOString()
31344
+ storyStartTime: new Date().toISOString(),
31345
+ agentGetFn: ctx.agentGetFn
31241
31346
  };
31242
31347
  const result = await runPipeline(defaultPipeline, fixContext, ctx.eventEmitter);
31243
31348
  logger?.info("acceptance", `Fix story ${story.id} ${result.success ? "passed" : "failed"}`);
@@ -31273,7 +31378,8 @@ async function runAcceptanceLoop(ctx) {
31273
31378
  workdir: ctx.workdir,
31274
31379
  featureDir: ctx.featureDir,
31275
31380
  hooks: ctx.hooks,
31276
- plugins: ctx.pluginRegistry
31381
+ plugins: ctx.pluginRegistry,
31382
+ agentGetFn: ctx.agentGetFn
31277
31383
  };
31278
31384
  const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
31279
31385
  const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
@@ -32254,7 +32360,7 @@ function resolveMaxConcurrency(parallel) {
32254
32360
  }
32255
32361
  return Math.max(1, parallel);
32256
32362
  }
32257
- async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter) {
32363
+ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn) {
32258
32364
  const logger = getSafeLogger();
32259
32365
  const maxConcurrency = resolveMaxConcurrency(parallel);
32260
32366
  const worktreeManager = new WorktreeManager;
@@ -32284,7 +32390,8 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
32284
32390
  featureDir,
32285
32391
  hooks,
32286
32392
  plugins,
32287
- storyStartTime: new Date().toISOString()
32393
+ storyStartTime: new Date().toISOString(),
32394
+ agentGetFn
32288
32395
  };
32289
32396
  const worktreePaths = new Map;
32290
32397
  for (const story of batch) {
@@ -32658,7 +32765,7 @@ async function runParallelExecution(options, initialPrd) {
32658
32765
  const batchStoryMetrics = [];
32659
32766
  let conflictedStories = [];
32660
32767
  try {
32661
- const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter);
32768
+ const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn);
32662
32769
  const batchDurationMs = Date.now() - batchStartMs;
32663
32770
  const batchCompletedAt = new Date().toISOString();
32664
32771
  prd = parallelResult.updatedPrd;
@@ -34403,7 +34510,7 @@ async function setupRun(options) {
34403
34510
  } else {
34404
34511
  logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
34405
34512
  }
34406
- const { sweepStaleFeatureSessions: sweepStaleFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter(), exports_adapter));
34513
+ const { sweepStaleFeatureSessions: sweepStaleFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter2(), exports_adapter));
34407
34514
  await sweepStaleFeatureSessions2(workdir, feature).catch(() => {});
34408
34515
  const lockAcquired = await acquireLock(workdir);
34409
34516
  if (!lockAcquired) {
@@ -65854,17 +65961,13 @@ init_registry();
65854
65961
  import { existsSync as existsSync9 } from "fs";
65855
65962
  import { join as join10 } from "path";
65856
65963
  import { createInterface } from "readline";
65964
+ init_test_strategy();
65857
65965
  init_pid_registry();
65858
65966
  init_logger2();
65859
65967
 
65860
65968
  // src/prd/schema.ts
65969
+ init_test_strategy();
65861
65970
  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
65971
  var STORY_ID_NO_SEPARATOR = /^([A-Za-z]+)(\d+)$/;
65869
65972
  function extractJsonFromMarkdown(text) {
65870
65973
  const match = text.match(/```(?:json)?\s*\n([\s\S]*?)\n?\s*```/);
@@ -65934,9 +66037,7 @@ function validateStory(raw, index, allIds) {
65934
66037
  throw new Error(`[schema] story[${index}].routing.complexity "${rawComplexity}" is invalid. Valid values: ${VALID_COMPLEXITY.join(", ")}`);
65935
66038
  }
65936
66039
  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";
66040
+ const testStrategy = resolveTestStrategy(typeof rawTestStrategy === "string" ? rawTestStrategy : undefined);
65940
66041
  const rawDeps = s.dependencies;
65941
66042
  const dependencies = Array.isArray(rawDeps) ? rawDeps : [];
65942
66043
  for (const dep of dependencies) {
@@ -66203,19 +66304,11 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
66203
66304
  ]
66204
66305
  }
66205
66306
 
66206
- ## Complexity Classification Guide
66307
+ ${COMPLEXITY_GUIDE}
66207
66308
 
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
66309
+ ${TEST_STRATEGY_GUIDE}
66212
66310
 
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.
66311
+ ${GROUPING_RULES}
66219
66312
 
66220
66313
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
66221
66314
  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."}`;
@@ -69051,7 +69144,7 @@ async function unlockCommand(options) {
69051
69144
  init_config();
69052
69145
 
69053
69146
  // src/execution/runner.ts
69054
- init_adapter();
69147
+ init_adapter2();
69055
69148
  init_registry();
69056
69149
  init_hooks();
69057
69150
  init_logger2();
@@ -69227,9 +69320,20 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
69227
69320
  batchingEnabled: options.useBatch
69228
69321
  });
69229
69322
  clearCache();
69230
- const batchPlan = options.useBatch ? precomputeBatchPlan(getAllReadyStories(prd), 4) : [];
69323
+ const readyStories = getAllReadyStories(prd);
69324
+ logger?.debug("routing", "Ready stories for batch routing", {
69325
+ readyCount: readyStories.length,
69326
+ readyIds: readyStories.map((s) => s.id),
69327
+ allStories: prd.userStories.map((s) => ({
69328
+ id: s.id,
69329
+ status: s.status,
69330
+ passes: s.passes,
69331
+ deps: s.dependencies
69332
+ }))
69333
+ });
69334
+ const batchPlan = options.useBatch ? precomputeBatchPlan(readyStories, 4) : [];
69231
69335
  if (options.useBatch) {
69232
- await tryLlmBatchRoute(options.config, getAllReadyStories(prd), "routing");
69336
+ await tryLlmBatchRoute(options.config, readyStories, "routing");
69233
69337
  }
69234
69338
  if (options.parallel !== undefined) {
69235
69339
  const runParallelExecution2 = options.runParallelExecution ?? (await Promise.resolve().then(() => (init_parallel_executor(), exports_parallel_executor))).runParallelExecution;
@@ -76991,9 +77095,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
76991
77095
  }
76992
77096
  }
76993
77097
  try {
76994
- mkdirSync6(featureDir, { recursive: true });
77098
+ const planLogDir = join43(featureDir, "plan");
77099
+ mkdirSync6(planLogDir, { recursive: true });
76995
77100
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
76996
- const planLogPath = join43(featureDir, `plan-${planLogId}.jsonl`);
77101
+ const planLogPath = join43(planLogDir, `${planLogId}.jsonl`);
76997
77102
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
76998
77103
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
76999
77104
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -77238,10 +77343,10 @@ Use: nax plan -f <feature> --from <spec>`));
77238
77343
  process.exit(1);
77239
77344
  }
77240
77345
  const config2 = await loadConfig(workdir);
77241
- const featureLogDir = join43(naxDir, "features", options.feature);
77346
+ const featureLogDir = join43(naxDir, "features", options.feature, "plan");
77242
77347
  mkdirSync6(featureLogDir, { recursive: true });
77243
77348
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
77244
- const planLogPath = join43(featureLogDir, `plan-${planLogId}.jsonl`);
77349
+ const planLogPath = join43(featureLogDir, `${planLogId}.jsonl`);
77245
77350
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
77246
77351
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
77247
77352
  try {