@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@ import { createHash } from "node:crypto";
15
15
  import { join } from "node:path";
16
16
  import { resolvePermissions } from "../../config/permissions";
17
17
  import { getSafeLogger } from "../../logger";
18
- import { buildDecomposePrompt, parseDecomposeOutput } from "../claude-decompose";
18
+ import { buildDecomposePrompt, parseDecomposeOutput } from "../shared/decompose";
19
19
  import { createSpawnAcpClient } from "./spawn-client";
20
20
 
21
21
  import type {
@@ -80,7 +80,14 @@ const DEFAULT_ENTRY: AgentRegistryEntry = {
80
80
  export interface AcpSessionResponse {
81
81
  messages: Array<{ role: string; content: string }>;
82
82
  stopReason: string;
83
- cumulative_token_usage?: { input_tokens: number; output_tokens: number };
83
+ cumulative_token_usage?: {
84
+ input_tokens: number;
85
+ output_tokens: number;
86
+ cache_read_input_tokens?: number;
87
+ cache_creation_input_tokens?: number;
88
+ };
89
+ /** Exact cost in USD from acpx usage_update event. Preferred over token-based estimation. */
90
+ exactCostUsd?: number;
84
91
  }
85
92
 
86
93
  export interface AcpSession {
@@ -555,7 +562,13 @@ export class AcpAgentAdapter implements AgentAdapter {
555
562
  // Tracks whether the run completed successfully — used by finally to decide
556
563
  // whether to close the session (success) or keep it open for retry (failure).
557
564
  const runState = { succeeded: false };
558
- const totalTokenUsage = { input_tokens: 0, output_tokens: 0 };
565
+ const totalTokenUsage = {
566
+ input_tokens: 0,
567
+ output_tokens: 0,
568
+ cache_read_input_tokens: 0,
569
+ cache_creation_input_tokens: 0,
570
+ };
571
+ let totalExactCostUsd: number | undefined;
559
572
 
560
573
  try {
561
574
  // 5. Multi-turn loop
@@ -577,10 +590,16 @@ export class AcpAgentAdapter implements AgentAdapter {
577
590
  lastResponse = turnResult.response;
578
591
  if (!lastResponse) break;
579
592
 
580
- // Accumulate token usage
593
+ // Accumulate token usage and exact cost
581
594
  if (lastResponse.cumulative_token_usage) {
582
595
  totalTokenUsage.input_tokens += lastResponse.cumulative_token_usage.input_tokens ?? 0;
583
596
  totalTokenUsage.output_tokens += lastResponse.cumulative_token_usage.output_tokens ?? 0;
597
+ totalTokenUsage.cache_read_input_tokens += lastResponse.cumulative_token_usage.cache_read_input_tokens ?? 0;
598
+ totalTokenUsage.cache_creation_input_tokens +=
599
+ lastResponse.cumulative_token_usage.cache_creation_input_tokens ?? 0;
600
+ }
601
+ if (lastResponse.exactCostUsd !== undefined) {
602
+ totalExactCostUsd = (totalExactCostUsd ?? 0) + lastResponse.exactCostUsd;
584
603
  }
585
604
 
586
605
  // Check for agent question → route to interaction bridge
@@ -643,10 +662,12 @@ export class AcpAgentAdapter implements AgentAdapter {
643
662
  const success = lastResponse?.stopReason === "end_turn";
644
663
  const output = extractOutput(lastResponse);
645
664
 
665
+ // Prefer exact cost from acpx usage_update; fall back to token-based estimation
646
666
  const estimatedCost =
647
- totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0
667
+ totalExactCostUsd ??
668
+ (totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0
648
669
  ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model)
649
- : 0;
670
+ : 0);
650
671
 
651
672
  return {
652
673
  success,
@@ -719,6 +740,13 @@ export class AcpAgentAdapter implements AgentAdapter {
719
740
  throw new CompleteError("complete() returned empty output");
720
741
  }
721
742
 
743
+ if (response.exactCostUsd !== undefined) {
744
+ getSafeLogger()?.info("acp-adapter", "complete() cost", {
745
+ costUsd: response.exactCostUsd,
746
+ model,
747
+ });
748
+ }
749
+
722
750
  return unwrapped;
723
751
  } catch (err) {
724
752
  const error = err instanceof Error ? err : new Error(String(err));
@@ -4,6 +4,4 @@
4
4
 
5
5
  export { AcpAgentAdapter, _acpAdapterDeps } from "./adapter";
6
6
  export { createSpawnAcpClient } from "./spawn-client";
7
- export { estimateCostFromTokenUsage } from "./cost";
8
- export type { SessionTokenUsage } from "./cost";
9
7
  export type { AgentRegistryEntry } from "./types";
@@ -2,11 +2,9 @@
2
2
  * ACP adapter — NDJSON and JSON-RPC output parsing helpers.
3
3
  *
4
4
  * Extracted from adapter.ts to keep that file within the 800-line limit.
5
- * Used only by _runOnce() (the spawn-based legacy path).
5
+ * Used by SpawnAcpSession.prompt() to parse acpx stdout.
6
6
  */
7
7
 
8
- import type { AgentRunOptions } from "../types";
9
-
10
8
  // ─────────────────────────────────────────────────────────────────────────────
11
9
  // Types
12
10
  // ─────────────────────────────────────────────────────────────────────────────
@@ -15,131 +13,86 @@ import type { AgentRunOptions } from "../types";
15
13
  export interface AcpxTokenUsage {
16
14
  input_tokens: number;
17
15
  output_tokens: number;
18
- }
19
-
20
- /** JSON-RPC message from acpx --format json --json-strict */
21
- interface JsonRpcMessage {
22
- jsonrpc: "2.0";
23
- method?: string;
24
- params?: {
25
- sessionId: string;
26
- update?: {
27
- sessionUpdate: string;
28
- content?: { type: string; text?: string };
29
- used?: number;
30
- size?: number;
31
- cost?: { amount: number; currency: string };
32
- };
33
- };
34
- id?: number | string;
35
- result?: unknown;
36
- error?: { code: number; message: string };
16
+ cache_read_input_tokens?: number;
17
+ cache_creation_input_tokens?: number;
37
18
  }
38
19
 
39
20
  // ─────────────────────────────────────────────────────────────────────────────
40
- // streamJsonRpcEvents
21
+ // parseAcpxJsonOutput
41
22
  // ─────────────────────────────────────────────────────────────────────────────
42
23
 
43
24
  /**
44
- * Stream stdout line-by-line, parse JSON-RPC, detect questions, call bridge.
25
+ * Parse acpx NDJSON output for assistant text, token usage, and exact cost.
26
+ *
27
+ * Handles the JSON-RPC envelope format emitted by acpx:
28
+ * - session/update agent_message_chunk → text accumulation
29
+ * - session/update usage_update → exact cost (cost.amount) + context size
30
+ * - id/result → token breakdown (inputTokens, outputTokens, cachedWriteTokens, cachedReadTokens)
31
+ *
32
+ * Also handles legacy flat NDJSON format for backward compatibility.
45
33
  */
46
- export async function streamJsonRpcEvents(
47
- stdout: ReadableStream<Uint8Array>,
48
- bridge: AgentRunOptions["interactionBridge"],
49
- _sessionId: string,
50
- ): Promise<{ text: string; tokenUsage?: AcpxTokenUsage }> {
51
- let accumulatedText = "";
34
+ export function parseAcpxJsonOutput(rawOutput: string): {
35
+ text: string;
36
+ tokenUsage?: AcpxTokenUsage;
37
+ exactCostUsd?: number;
38
+ stopReason?: string;
39
+ error?: string;
40
+ } {
41
+ const lines = rawOutput.split("\n").filter((l) => l.trim());
42
+ let text = "";
52
43
  let tokenUsage: AcpxTokenUsage | undefined;
53
- const decoder = new TextDecoder();
54
- let buffer = "";
55
-
56
- const reader = stdout.getReader();
57
-
58
- try {
59
- while (true) {
60
- const { done, value } = await reader.read();
61
- if (done) break;
62
-
63
- buffer += decoder.decode(value, { stream: true });
64
- const lines = buffer.split("\n");
65
- buffer = lines.pop() ?? "";
66
-
67
- for (const line of lines) {
68
- if (!line.trim()) continue;
44
+ let exactCostUsd: number | undefined;
45
+ let stopReason: string | undefined;
46
+ let error: string | undefined;
69
47
 
70
- let msg: JsonRpcMessage;
71
- try {
72
- msg = JSON.parse(line);
73
- } catch {
74
- continue;
75
- }
48
+ for (const line of lines) {
49
+ try {
50
+ const event = JSON.parse(line);
76
51
 
77
- if (msg.method === "session/update" && msg.params?.update) {
78
- const update = msg.params.update;
52
+ // ── JSON-RPC envelope format (acpx v0.3+) ──────────────────────────────
53
+ if (event.jsonrpc === "2.0") {
54
+ // session/update events
55
+ if (event.method === "session/update" && event.params?.update) {
56
+ const update = event.params.update;
79
57
 
58
+ // Text chunks
80
59
  if (
81
60
  update.sessionUpdate === "agent_message_chunk" &&
82
61
  update.content?.type === "text" &&
83
62
  update.content.text
84
63
  ) {
85
- accumulatedText += update.content.text;
86
-
87
- if (bridge?.detectQuestion && bridge.onQuestionDetected) {
88
- const isQuestion = await bridge.detectQuestion(accumulatedText);
89
- if (isQuestion) {
90
- const response = await bridge.onQuestionDetected(accumulatedText);
91
- accumulatedText += `\n\n[Human response: ${response}]`;
92
- }
93
- }
64
+ text += update.content.text;
94
65
  }
95
66
 
96
- if (update.sessionUpdate === "usage_update" && update.used !== undefined) {
97
- const total = update.used;
98
- tokenUsage = {
99
- input_tokens: Math.floor(total * 0.3),
100
- output_tokens: Math.floor(total * 0.7),
101
- };
67
+ // Exact cost from usage_update
68
+ if (update.sessionUpdate === "usage_update" && typeof update.cost?.amount === "number") {
69
+ exactCostUsd = update.cost.amount;
102
70
  }
103
71
  }
104
72
 
105
- if (msg.result) {
106
- const result = msg.result as Record<string, unknown>;
107
- if (typeof result === "string") {
108
- accumulatedText += result;
109
- }
110
- }
111
- }
112
- }
113
- } finally {
114
- reader.releaseLock();
115
- }
116
-
117
- return { text: accumulatedText.trim(), tokenUsage };
118
- }
73
+ // Final result with token breakdown (camelCase from acpx)
74
+ if (event.id !== undefined && event.result && typeof event.result === "object") {
75
+ const result = event.result as Record<string, unknown>;
119
76
 
120
- // ─────────────────────────────────────────────────────────────────────────────
121
- // parseAcpxJsonOutput
122
- // ─────────────────────────────────────────────────────────────────────────────
77
+ if (result.stopReason) stopReason = result.stopReason as string;
78
+ if (result.stop_reason) stopReason = result.stop_reason as string;
123
79
 
124
- /**
125
- * Parse acpx NDJSON output for assistant text and token usage.
126
- */
127
- export function parseAcpxJsonOutput(rawOutput: string): {
128
- text: string;
129
- tokenUsage?: AcpxTokenUsage;
130
- stopReason?: string;
131
- error?: string;
132
- } {
133
- const lines = rawOutput.split("\n").filter((l) => l.trim());
134
- let text = "";
135
- let tokenUsage: AcpxTokenUsage | undefined;
136
- let stopReason: string | undefined;
137
- let error: string | undefined;
80
+ if (result.usage && typeof result.usage === "object") {
81
+ const u = result.usage as Record<string, unknown>;
82
+ tokenUsage = {
83
+ input_tokens: (u.inputTokens as number) ?? (u.input_tokens as number) ?? 0,
84
+ output_tokens: (u.outputTokens as number) ?? (u.output_tokens as number) ?? 0,
85
+ cache_read_input_tokens: (u.cachedReadTokens as number) ?? (u.cache_read_input_tokens as number) ?? 0,
86
+ cache_creation_input_tokens:
87
+ (u.cachedWriteTokens as number) ?? (u.cache_creation_input_tokens as number) ?? 0,
88
+ };
89
+ }
90
+ }
138
91
 
139
- for (const line of lines) {
140
- try {
141
- const event = JSON.parse(line);
92
+ continue;
93
+ }
142
94
 
95
+ // ── Legacy flat NDJSON format ───────────────────────────────────────────
143
96
  if (event.content && typeof event.content === "string") text += event.content;
144
97
  if (event.text && typeof event.text === "string") text += event.text;
145
98
  if (event.result && typeof event.result === "string") text = event.result;
@@ -162,5 +115,5 @@ export function parseAcpxJsonOutput(rawOutput: string): {
162
115
  }
163
116
  }
164
117
 
165
- return { text: text.trim(), tokenUsage, stopReason, error };
118
+ return { text: text.trim(), tokenUsage, exactCostUsd, stopReason, error };
166
119
  }
@@ -180,8 +180,9 @@ class SpawnAcpSession implements AcpSession {
180
180
  const parsed = parseAcpxJsonOutput(stdout);
181
181
  return {
182
182
  messages: [{ role: "assistant", content: parsed.text || "" }],
183
- stopReason: "end_turn",
183
+ stopReason: parsed.stopReason ?? "end_turn",
184
184
  cumulative_token_usage: parsed.tokenUsage,
185
+ exactCostUsd: parsed.exactCostUsd,
185
186
  };
186
187
  } catch (err) {
187
188
  getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
@@ -4,15 +4,11 @@
4
4
  * Main adapter class coordinating execution, completion, decomposition, and interactive modes.
5
5
  */
6
6
 
7
- import { resolvePermissions } from "../config/permissions";
8
- import { PidRegistry } from "../execution/pid-registry";
9
- import { withProcessTimeout } from "../execution/timeout-handler";
10
- import { getLogger } from "../logger";
11
- import { _completeDeps, executeComplete } from "./claude-complete";
12
- import { buildDecomposePrompt, parseDecomposeOutput } from "./claude-decompose";
13
- import { _runOnceDeps, buildAllowedEnv, buildCommand, executeOnce } from "./claude-execution";
14
- import { runInteractiveMode } from "./claude-interactive";
15
- import { runPlan } from "./claude-plan";
7
+ import { resolvePermissions } from "../../config/permissions";
8
+ import { PidRegistry } from "../../execution/pid-registry";
9
+ import { withProcessTimeout } from "../../execution/timeout-handler";
10
+ import { getLogger } from "../../logger";
11
+ import { buildDecomposePrompt, parseDecomposeOutput } from "../shared/decompose";
16
12
  import type {
17
13
  AgentAdapter,
18
14
  AgentCapabilities,
@@ -25,7 +21,11 @@ import type {
25
21
  PlanOptions,
26
22
  PlanResult,
27
23
  PtyHandle,
28
- } from "./types";
24
+ } from "../types";
25
+ import { _completeDeps, executeComplete } from "./complete";
26
+ import { _runOnceDeps, buildAllowedEnv, buildCommand, executeOnce } from "./execution";
27
+ import { runInteractiveMode } from "./interactive";
28
+ import { runPlan } from "./plan";
29
29
 
30
30
  /**
31
31
  * Injectable dependencies for decompose() — allows tests to intercept
@@ -174,7 +174,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
174
174
  }
175
175
 
176
176
  async decompose(options: DecomposeOptions): Promise<DecomposeResult> {
177
- const { resolveBalancedModelDef } = await import("./model-resolution");
177
+ const { resolveBalancedModelDef } = await import("../shared/model-resolution");
178
178
 
179
179
  const prompt = buildDecomposePrompt(options);
180
180
 
@@ -186,7 +186,10 @@ export class ClaudeCodeAdapter implements AgentAdapter {
186
186
  modelDef = resolveBalancedModelDef(options.config);
187
187
  }
188
188
 
189
- const { skipPermissions } = resolvePermissions(options.config as import("../config").NaxConfig | undefined, "run");
189
+ const { skipPermissions } = resolvePermissions(
190
+ options.config as import("../../config").NaxConfig | undefined,
191
+ "run",
192
+ );
190
193
  const cmd = [this.binary, "--model", modelDef.model, "-p", prompt];
191
194
  if (skipPermissions) {
192
195
  cmd.splice(cmd.length - 2, 0, "--dangerously-skip-permissions");
@@ -4,9 +4,9 @@
4
4
  * Standalone completion endpoint for simple prompts.
5
5
  */
6
6
 
7
- import { resolvePermissions } from "../config/permissions";
8
- import type { CompleteOptions } from "./types";
9
- import { CompleteError } from "./types";
7
+ import { resolvePermissions } from "../../config/permissions";
8
+ import type { CompleteOptions } from "../types";
9
+ import { CompleteError } from "../types";
10
10
 
11
11
  /**
12
12
  * Injectable dependencies for complete() — allows tests to intercept
@@ -5,7 +5,7 @@
5
5
  * Parses agent output for token usage and calculates costs.
6
6
  */
7
7
 
8
- import type { ModelTier } from "../config/schema";
8
+ import type { ModelTier } from "../../config/schema";
9
9
 
10
10
  /** Cost rates per 1M tokens (USD) */
11
11
  export interface ModelCostRates {
@@ -4,12 +4,12 @@
4
4
  * Handles building commands, preparing environment, and process execution.
5
5
  */
6
6
 
7
- import { resolvePermissions } from "../config/permissions";
8
- import type { PidRegistry } from "../execution/pid-registry";
9
- import { withProcessTimeout } from "../execution/timeout-handler";
10
- import { getLogger } from "../logger";
7
+ import { resolvePermissions } from "../../config/permissions";
8
+ import type { PidRegistry } from "../../execution/pid-registry";
9
+ import { withProcessTimeout } from "../../execution/timeout-handler";
10
+ import { getLogger } from "../../logger";
11
+ import type { AgentResult, AgentRunOptions } from "../types";
11
12
  import { estimateCostByDuration, estimateCostFromOutput } from "./cost";
12
- import type { AgentResult, AgentRunOptions } from "./types";
13
13
 
14
14
  /**
15
15
  * Maximum characters to capture from agent stdout.
@@ -0,0 +1,3 @@
1
+ // Re-export everything external callers need from claude/
2
+ export { ClaudeCodeAdapter, _completeDeps, _claudeAdapterDeps } from "./adapter";
3
+ export { _runOnceDeps } from "./execution";
@@ -4,10 +4,10 @@
4
4
  * Handles terminal UI interactions with the Claude agent.
5
5
  */
6
6
 
7
- import type { PidRegistry } from "../execution/pid-registry";
8
- import { getLogger } from "../logger";
9
- import { buildAllowedEnv } from "./claude-execution";
10
- import type { AgentRunOptions, InteractiveRunOptions, PtyHandle } from "./types";
7
+ import type { PidRegistry } from "../../execution/pid-registry";
8
+ import { getLogger } from "../../logger";
9
+ import type { AgentRunOptions, InteractiveRunOptions, PtyHandle } from "../types";
10
+ import { buildAllowedEnv } from "./execution";
11
11
 
12
12
  /**
13
13
  * Run Claude agent in interactive (TTY) mode for TUI output.
@@ -7,13 +7,13 @@ import { join } from "node:path";
7
7
  * Extracted from claude.ts: plan(), buildPlanCommand()
8
8
  */
9
9
 
10
- import { resolvePermissions } from "../config/permissions";
11
- import type { PidRegistry } from "../execution/pid-registry";
12
- import { withProcessTimeout } from "../execution/timeout-handler";
13
- import { getLogger } from "../logger";
14
- import { resolveBalancedModelDef } from "./model-resolution";
15
- import type { AgentRunOptions } from "./types";
16
- import type { PlanOptions, PlanResult } from "./types-extended";
10
+ import { resolvePermissions } from "../../config/permissions";
11
+ import type { PidRegistry } from "../../execution/pid-registry";
12
+ import { withProcessTimeout } from "../../execution/timeout-handler";
13
+ import { getLogger } from "../../logger";
14
+ import { resolveBalancedModelDef } from "../shared/model-resolution";
15
+ import type { PlanOptions, PlanResult } from "../shared/types-extended";
16
+ import type { AgentRunOptions } from "../types";
17
17
 
18
18
  /**
19
19
  * Build the CLI command for plan mode.
@@ -32,7 +32,10 @@ export function buildPlanCommand(binary: string, options: PlanOptions): string[]
32
32
  }
33
33
 
34
34
  // Resolve permission mode from config
35
- const { skipPermissions } = resolvePermissions(options.config as import("../config").NaxConfig | undefined, "plan");
35
+ const { skipPermissions } = resolvePermissions(
36
+ options.config as import("../../config").NaxConfig | undefined,
37
+ "plan",
38
+ );
36
39
  if (skipPermissions) {
37
40
  cmd.push("--dangerously-skip-permissions");
38
41
  }
@@ -75,7 +78,7 @@ export async function runPlan(
75
78
  pidRegistry: PidRegistry,
76
79
  buildAllowedEnv: (options: AgentRunOptions) => Record<string, string | undefined>,
77
80
  ): Promise<PlanResult> {
78
- const { resolveBalancedModelDef } = await import("./model-resolution");
81
+ const { resolveBalancedModelDef } = await import("../shared/model-resolution");
79
82
 
80
83
  const cmd = buildPlanCommand(binary, options);
81
84
 
@@ -2,7 +2,7 @@ export type { AgentAdapter, AgentCapabilities, AgentResult, AgentRunOptions, Com
2
2
  export { CompleteError } from "./types";
3
3
  export { ClaudeCodeAdapter } from "./claude";
4
4
  export { getAllAgentNames, getAgent, getInstalledAgents, checkAgentHealth } from "./registry";
5
- export type { ModelCostRates, TokenUsage, CostEstimate, TokenUsageWithConfidence } from "./cost";
5
+ export type { ModelCostRates, TokenUsage, CostEstimate, TokenUsageWithConfidence } from "./claude/cost";
6
6
  export {
7
7
  COST_RATES,
8
8
  parseTokenUsage,
@@ -10,7 +10,7 @@ export {
10
10
  estimateCostFromOutput,
11
11
  estimateCostByDuration,
12
12
  formatCostWithConfidence,
13
- } from "./cost";
14
- export { validateAgentForTier, validateAgentFeature, describeAgentCapabilities } from "./validation";
15
- export type { AgentVersionInfo } from "./version-detection";
16
- export { getAgentVersion, getAgentVersions } from "./version-detection";
13
+ } from "./claude/cost";
14
+ export { validateAgentForTier, validateAgentFeature, describeAgentCapabilities } from "./shared/validation";
15
+ export type { AgentVersionInfo } from "./shared/version-detection";
16
+ export { getAgentVersion, getAgentVersions } from "./shared/version-detection";
@@ -7,11 +7,11 @@
7
7
  import type { NaxConfig } from "../config/schema";
8
8
  import { getLogger } from "../logger";
9
9
  import { AcpAgentAdapter } from "./acp/adapter";
10
- import { AiderAdapter } from "./adapters/aider";
11
- import { CodexAdapter } from "./adapters/codex";
12
- import { GeminiAdapter } from "./adapters/gemini";
13
- import { OpenCodeAdapter } from "./adapters/opencode";
14
- import { ClaudeCodeAdapter } from "./claude";
10
+ import { AiderAdapter } from "./aider/adapter";
11
+ import { ClaudeCodeAdapter } from "./claude/adapter";
12
+ import { CodexAdapter } from "./codex/adapter";
13
+ import { GeminiAdapter } from "./gemini/adapter";
14
+ import { OpenCodeAdapter } from "./opencode/adapter";
15
15
  import type { AgentAdapter } from "./types";
16
16
 
17
17
  /** All known agent adapters */
@@ -5,7 +5,8 @@
5
5
  * parseDecomposeOutput(), validateComplexity()
6
6
  */
7
7
 
8
- import type { DecomposeOptions, DecomposeResult, DecomposedStory } from "./types";
8
+ import { COMPLEXITY_GUIDE, GROUPING_RULES, TEST_STRATEGY_GUIDE, resolveTestStrategy } from "../../config/test-strategy";
9
+ import type { DecomposeOptions, DecomposeResult, DecomposedStory } from "../types";
9
10
 
10
11
  /**
11
12
  * Build the decompose prompt combining spec content and codebase context.
@@ -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
 
@@ -7,8 +7,8 @@
7
7
  * Implementation placeholder — logic to be filled in by the implementer.
8
8
  */
9
9
 
10
- import { resolveModel } from "../config/schema";
11
- import type { ModelDef, NaxConfig } from "../config/schema";
10
+ import { resolveModel } from "../../config/schema";
11
+ import type { ModelDef, NaxConfig } from "../../config/schema";
12
12
 
13
13
  /**
14
14
  * Resolve the balanced model definition from config, with optional adapter default fallback.
@@ -5,7 +5,7 @@
5
5
  * Separated from core types to keep each file under 400 lines.
6
6
  */
7
7
 
8
- import type { ModelDef, ModelTier, NaxConfig } from "../config/schema";
8
+ import type { ModelDef, ModelTier, NaxConfig } from "../../config/schema";
9
9
 
10
10
  /**
11
11
  * Configuration options for running an agent in plan mode.
@@ -56,7 +56,7 @@ export interface PlanOptions {
56
56
  */
57
57
  onAcpSessionCreated?: (sessionName: string) => Promise<void> | void;
58
58
  /** PID registry for tracking spawned agent processes — cleanup on crash/SIGTERM */
59
- pidRegistry?: import("../execution/pid-registry").PidRegistry;
59
+ pidRegistry?: import("../../execution/pid-registry").PidRegistry;
60
60
  }
61
61
 
62
62
  /**
@@ -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
  /**
@@ -161,4 +161,4 @@ export interface InteractiveRunOptions extends AgentRunOptions {
161
161
  }
162
162
 
163
163
  // Re-import for the extends clause
164
- import type { AgentRunOptions } from "./types";
164
+ import type { AgentRunOptions } from "../types";
@@ -4,8 +4,8 @@
4
4
  * Runtime validation for agent capabilities and tier compatibility.
5
5
  */
6
6
 
7
- import type { ModelTier } from "../config/schema";
8
- import type { AgentAdapter } from "./types";
7
+ import type { ModelTier } from "../../config/schema";
8
+ import type { AgentAdapter } from "../types";
9
9
 
10
10
  /**
11
11
  * Check if an agent supports a given model tier.