@nathapp/nax 0.34.0 → 0.35.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,6 @@
20
20
  "prepublishOnly": "bun run build"
21
21
  },
22
22
  "dependencies": {
23
- "@anthropic-ai/sdk": "^0.74.0",
24
23
  "@types/react": "^19.2.14",
25
24
  "chalk": "^5.6.2",
26
25
  "commander": "^13.1.0",
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Codex Agent Adapter — implements AgentAdapter interface
3
+ *
4
+ * Provides uniform interface for spawning Codex agent processes,
5
+ * supporting one-shot completions and headless execution.
6
+ */
7
+
8
+ import type {
9
+ AgentAdapter,
10
+ AgentCapabilities,
11
+ AgentResult,
12
+ AgentRunOptions,
13
+ CompleteOptions,
14
+ DecomposeOptions,
15
+ DecomposeResult,
16
+ PlanOptions,
17
+ PlanResult,
18
+ } from "../types";
19
+ import { CompleteError } from "../types";
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Injectable dependencies — matches the _deps pattern used in claude.ts
23
+ // These are replaced in unit tests to intercept Bun.spawn calls.
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ export const _codexRunDeps = {
27
+ which(name: string): string | null {
28
+ return Bun.which(name);
29
+ },
30
+ spawn(
31
+ cmd: string[],
32
+ opts: { cwd?: string; stdout: "pipe"; stderr: "pipe" | "inherit"; env?: Record<string, string | undefined> },
33
+ ): {
34
+ stdout: ReadableStream<Uint8Array>;
35
+ stderr: ReadableStream<Uint8Array>;
36
+ exited: Promise<number>;
37
+ pid: number;
38
+ kill(signal?: number | NodeJS.Signals): void;
39
+ } {
40
+ return Bun.spawn(cmd, opts) as unknown as {
41
+ stdout: ReadableStream<Uint8Array>;
42
+ stderr: ReadableStream<Uint8Array>;
43
+ exited: Promise<number>;
44
+ pid: number;
45
+ kill(signal?: number | NodeJS.Signals): void;
46
+ };
47
+ },
48
+ };
49
+
50
+ export const _codexCompleteDeps = {
51
+ spawn(
52
+ cmd: string[],
53
+ opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
54
+ ): {
55
+ stdout: ReadableStream<Uint8Array>;
56
+ stderr: ReadableStream<Uint8Array>;
57
+ exited: Promise<number>;
58
+ pid: number;
59
+ } {
60
+ return Bun.spawn(cmd, opts) as unknown as {
61
+ stdout: ReadableStream<Uint8Array>;
62
+ stderr: ReadableStream<Uint8Array>;
63
+ exited: Promise<number>;
64
+ pid: number;
65
+ };
66
+ },
67
+ };
68
+
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+ // CodexAdapter implementation
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Maximum characters to capture from agent stdout.
75
+ */
76
+ const MAX_AGENT_OUTPUT_CHARS = 5000;
77
+
78
+ export class CodexAdapter implements AgentAdapter {
79
+ readonly name = "codex";
80
+ readonly displayName = "Codex";
81
+ readonly binary = "codex";
82
+
83
+ readonly capabilities: AgentCapabilities = {
84
+ supportedTiers: ["fast", "balanced"],
85
+ maxContextTokens: 8_000,
86
+ features: new Set<"tdd" | "review" | "refactor" | "batch">(["tdd", "refactor"]),
87
+ };
88
+
89
+ async isInstalled(): Promise<boolean> {
90
+ const path = _codexRunDeps.which("codex");
91
+ return path !== null;
92
+ }
93
+
94
+ buildCommand(options: AgentRunOptions): string[] {
95
+ return ["codex", "-q", "--prompt", options.prompt];
96
+ }
97
+
98
+ async run(options: AgentRunOptions): Promise<AgentResult> {
99
+ const cmd = this.buildCommand(options);
100
+ const startTime = Date.now();
101
+
102
+ const proc = _codexRunDeps.spawn(cmd, {
103
+ cwd: options.workdir,
104
+ stdout: "pipe",
105
+ stderr: "inherit",
106
+ });
107
+
108
+ const exitCode = await proc.exited;
109
+ const stdout = await new Response(proc.stdout).text();
110
+ const durationMs = Date.now() - startTime;
111
+
112
+ return {
113
+ success: exitCode === 0,
114
+ exitCode,
115
+ output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
116
+ rateLimited: false,
117
+ durationMs,
118
+ estimatedCost: 0,
119
+ pid: proc.pid,
120
+ };
121
+ }
122
+
123
+ async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
124
+ const cmd = ["codex", "-q", "--prompt", prompt];
125
+
126
+ const proc = _codexCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
127
+ const exitCode = await proc.exited;
128
+
129
+ const stdout = await new Response(proc.stdout).text();
130
+ const stderr = await new Response(proc.stderr).text();
131
+ const trimmed = stdout.trim();
132
+
133
+ if (exitCode !== 0) {
134
+ const errorDetails = stderr.trim() || trimmed;
135
+ const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
136
+ throw new CompleteError(errorMessage, exitCode);
137
+ }
138
+
139
+ if (!trimmed) {
140
+ throw new CompleteError("complete() returned empty output");
141
+ }
142
+
143
+ return trimmed;
144
+ }
145
+
146
+ async plan(_options: PlanOptions): Promise<PlanResult> {
147
+ throw new Error("CodexAdapter.plan() not implemented");
148
+ }
149
+
150
+ async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
151
+ throw new Error("CodexAdapter.decompose() not implemented");
152
+ }
153
+ }
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
 
10
10
  import type { PidRegistry } from "../execution/pid-registry";
11
11
  import { getLogger } from "../logger";
12
+ import { resolveBalancedModelDef } from "./model-resolution";
12
13
  import type { AgentRunOptions } from "./types";
13
14
  import type { PlanOptions, PlanResult } from "./types-extended";
14
15
 
@@ -18,9 +19,14 @@ import type { PlanOptions, PlanResult } from "./types-extended";
18
19
  export function buildPlanCommand(binary: string, options: PlanOptions): string[] {
19
20
  const cmd = [binary, "--permission-mode", "plan"];
20
21
 
21
- // Add model if specified
22
- if (options.modelDef) {
23
- cmd.push("--model", options.modelDef.model);
22
+ // Add model if specified (explicit or resolved from config)
23
+ let modelDef = options.modelDef;
24
+ if (!modelDef && options.config) {
25
+ modelDef = resolveBalancedModelDef(options.config);
26
+ }
27
+
28
+ if (modelDef) {
29
+ cmd.push("--model", modelDef.model);
24
30
  }
25
31
 
26
32
  // Add dangerously-skip-permissions for automation
@@ -64,13 +70,24 @@ export async function runPlan(
64
70
  pidRegistry: PidRegistry,
65
71
  buildAllowedEnv: (options: AgentRunOptions) => Record<string, string | undefined>,
66
72
  ): Promise<PlanResult> {
73
+ const { resolveBalancedModelDef } = await import("./model-resolution");
74
+
67
75
  const cmd = buildPlanCommand(binary, options);
68
76
 
77
+ // Resolve model: explicit modelDef > config.models.balanced > throw
78
+ let modelDef = options.modelDef;
79
+ if (!modelDef) {
80
+ if (!options.config) {
81
+ throw new Error("runPlan() requires either modelDef or config with models.balanced configured");
82
+ }
83
+ modelDef = resolveBalancedModelDef(options.config);
84
+ }
85
+
69
86
  const envOptions: AgentRunOptions = {
70
87
  workdir: options.workdir,
71
- modelDef: options.modelDef || { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
88
+ modelDef,
72
89
  prompt: "",
73
- modelTier: "balanced",
90
+ modelTier: options.modelTier || "balanced",
74
91
  timeoutSeconds: 600,
75
92
  };
76
93
 
@@ -12,6 +12,7 @@ import type {
12
12
  AgentCapabilities,
13
13
  AgentResult,
14
14
  AgentRunOptions,
15
+ CompleteOptions,
15
16
  DecomposeOptions,
16
17
  DecomposeResult,
17
18
  InteractiveRunOptions,
@@ -19,6 +20,7 @@ import type {
19
20
  PlanResult,
20
21
  PtyHandle,
21
22
  } from "./types";
23
+ import { CompleteError } from "./types";
22
24
 
23
25
  /**
24
26
  * Maximum characters to capture from agent stdout.
@@ -42,6 +44,53 @@ const MAX_AGENT_STDERR_CHARS = 1000;
42
44
  */
43
45
  const SIGKILL_GRACE_PERIOD_MS = 5000;
44
46
 
47
+ /**
48
+ * Injectable dependencies for complete() — allows tests to intercept
49
+ * Bun.spawn calls and verify correct CLI args without the claude binary.
50
+ *
51
+ * @internal
52
+ */
53
+ export const _completeDeps = {
54
+ spawn(
55
+ cmd: string[],
56
+ opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
57
+ ): { stdout: ReadableStream<Uint8Array>; stderr: ReadableStream<Uint8Array>; exited: Promise<number>; pid: number } {
58
+ return Bun.spawn(cmd, opts) as unknown as {
59
+ stdout: ReadableStream<Uint8Array>;
60
+ stderr: ReadableStream<Uint8Array>;
61
+ exited: Promise<number>;
62
+ pid: number;
63
+ };
64
+ },
65
+ };
66
+
67
+ /**
68
+ * Injectable dependencies for decompose() — allows tests to intercept
69
+ * Bun.spawn calls and verify correct CLI args without the claude binary.
70
+ *
71
+ * @internal
72
+ */
73
+ export const _decomposeDeps = {
74
+ spawn(
75
+ cmd: string[],
76
+ opts: { cwd?: string; stdout: "pipe"; stderr: "inherit" | "pipe"; env?: Record<string, string | undefined> },
77
+ ): {
78
+ stdout: ReadableStream<Uint8Array>;
79
+ stderr?: ReadableStream<Uint8Array>;
80
+ exited: Promise<number>;
81
+ pid: number;
82
+ kill(signal?: NodeJS.Signals | number): void;
83
+ } {
84
+ return Bun.spawn(cmd, opts) as unknown as {
85
+ stdout: ReadableStream<Uint8Array>;
86
+ stderr?: ReadableStream<Uint8Array>;
87
+ exited: Promise<number>;
88
+ pid: number;
89
+ kill(signal?: NodeJS.Signals | number): void;
90
+ };
91
+ },
92
+ };
93
+
45
94
  /**
46
95
  * Injectable dependencies for runOnce() — allows tests to verify
47
96
  * that PID cleanup (unregister) always runs even if kill() throws.
@@ -295,34 +344,76 @@ export class ClaudeCodeAdapter implements AgentAdapter {
295
344
  };
296
345
  }
297
346
 
347
+ async complete(prompt: string, options?: CompleteOptions): Promise<string> {
348
+ // Build command: claude -p <prompt> [--model <model>] [--max-tokens <tokens>] [--output-format json]
349
+ const cmd = ["claude", "-p", prompt];
350
+
351
+ if (options?.model) {
352
+ cmd.push("--model", options.model);
353
+ }
354
+
355
+ if (options?.maxTokens !== undefined) {
356
+ cmd.push("--max-tokens", String(options.maxTokens));
357
+ }
358
+
359
+ if (options?.jsonMode) {
360
+ cmd.push("--output-format", "json");
361
+ }
362
+
363
+ const proc = _completeDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
364
+ const exitCode = await proc.exited;
365
+
366
+ // Read stdout and stderr for error messages
367
+ const stdout = await new Response(proc.stdout).text();
368
+ const stderr = await new Response(proc.stderr).text();
369
+ const trimmed = stdout.trim();
370
+
371
+ // Validate exit code and output
372
+ if (exitCode !== 0) {
373
+ const errorDetails = stderr.trim() || trimmed;
374
+ const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
375
+ throw new CompleteError(errorMessage, exitCode);
376
+ }
377
+
378
+ if (!trimmed) {
379
+ throw new CompleteError("complete() returned empty output");
380
+ }
381
+
382
+ return trimmed;
383
+ }
384
+
298
385
  async plan(options: PlanOptions): Promise<PlanResult> {
299
386
  const pidRegistry = this.getPidRegistry(options.workdir);
300
387
  return runPlan(this.binary, options, pidRegistry, this.buildAllowedEnv.bind(this));
301
388
  }
302
389
 
303
390
  async decompose(options: DecomposeOptions): Promise<DecomposeResult> {
391
+ const { resolveBalancedModelDef } = await import("./model-resolution");
392
+
304
393
  const prompt = buildDecomposePrompt(options);
305
394
 
306
- const cmd = [
307
- this.binary,
308
- "--model",
309
- options.modelDef?.model || "claude-sonnet-4-5",
310
- "--dangerously-skip-permissions",
311
- "-p",
312
- prompt,
313
- ];
395
+ // Resolve model: explicit modelDef > config.models.balanced > throw
396
+ let modelDef = options.modelDef;
397
+ if (!modelDef) {
398
+ if (!options.config) {
399
+ throw new Error("decompose() requires either modelDef or config with models.balanced configured");
400
+ }
401
+ modelDef = resolveBalancedModelDef(options.config);
402
+ }
403
+
404
+ const cmd = [this.binary, "--model", modelDef.model, "--dangerously-skip-permissions", "-p", prompt];
314
405
 
315
406
  const pidRegistry = this.getPidRegistry(options.workdir);
316
407
 
317
- const proc = Bun.spawn(cmd, {
408
+ const proc = _decomposeDeps.spawn(cmd, {
318
409
  cwd: options.workdir,
319
410
  stdout: "pipe",
320
411
  stderr: "inherit", // MEM-3: Inherit stderr to avoid blocking on unread pipe
321
412
  env: this.buildAllowedEnv({
322
413
  workdir: options.workdir,
323
- modelDef: options.modelDef || { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
414
+ modelDef,
324
415
  prompt: "",
325
- modelTier: "balanced",
416
+ modelTier: options.modelTier || "balanced",
326
417
  timeoutSeconds: 600,
327
418
  }),
328
419
  });
@@ -1,4 +1,5 @@
1
- export type { AgentAdapter, AgentCapabilities, AgentResult, AgentRunOptions } from "./types";
1
+ export type { AgentAdapter, AgentCapabilities, AgentResult, AgentRunOptions, CompleteOptions } from "./types";
2
+ export { CompleteError } from "./types";
2
3
  export { ClaudeCodeAdapter } from "./claude";
3
4
  export { getAllAgentNames, getAgent, getInstalledAgents, checkAgentHealth } from "./registry";
4
5
  export type { ModelCostRates, TokenUsage, CostEstimate, TokenUsageWithConfidence } from "./cost";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Model resolution utility — AA-006
3
+ *
4
+ * Resolves a ModelDef from config.models.balanced with fallback chain:
5
+ * config value -> adapter default -> throw if none configured
6
+ *
7
+ * Implementation placeholder — logic to be filled in by the implementer.
8
+ */
9
+
10
+ import { resolveModel } from "../config/schema";
11
+ import type { ModelDef, NaxConfig } from "../config/schema";
12
+
13
+ /**
14
+ * Resolve the balanced model definition from config, with optional adapter default fallback.
15
+ *
16
+ * Fallback chain:
17
+ * 1. config.models.balanced (object or string shorthand)
18
+ * 2. adapterDefault (if provided)
19
+ * 3. Throws if neither is configured
20
+ *
21
+ * @param config - Partial NaxConfig (models.balanced is read if present)
22
+ * @param adapterDefault - Optional adapter-level fallback ModelDef
23
+ * @returns Resolved ModelDef
24
+ * @throws Error if no balanced model is configured and no adapter default provided
25
+ */
26
+ export function resolveBalancedModelDef(
27
+ config: Pick<NaxConfig, "models"> | Partial<NaxConfig>,
28
+ adapterDefault?: ModelDef,
29
+ ): ModelDef {
30
+ const configWithModels = config as Pick<NaxConfig, "models">;
31
+ const models = configWithModels.models as Record<string, unknown> | undefined;
32
+ const balancedEntry = models?.balanced;
33
+
34
+ if (balancedEntry) {
35
+ return resolveModel(balancedEntry as string | ModelDef);
36
+ }
37
+
38
+ if (adapterDefault) {
39
+ return adapterDefault;
40
+ }
41
+
42
+ throw new Error("No balanced model configured in config.models.balanced and no adapter default provided");
43
+ }
@@ -4,13 +4,14 @@
4
4
  * Discovers and manages available coding agents.
5
5
  */
6
6
 
7
+ import { CodexAdapter } from "./adapters/codex";
7
8
  import { ClaudeCodeAdapter } from "./claude";
8
9
  import type { AgentAdapter } from "./types";
9
10
 
10
11
  /** All known agent adapters */
11
12
  export const ALL_AGENTS: AgentAdapter[] = [
12
13
  new ClaudeCodeAdapter(),
13
- // Future: new CodexAdapter(),
14
+ new CodexAdapter(),
14
15
  // Future: new OpenCodeAdapter(),
15
16
  // Future: new GeminiAdapter(),
16
17
  ];
@@ -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 } 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.
@@ -28,6 +28,8 @@ export interface PlanOptions {
28
28
  modelTier?: ModelTier;
29
29
  /** Resolved model definition */
30
30
  modelDef?: ModelDef;
31
+ /** Global config — used to resolve models.balanced when modelDef is absent */
32
+ config?: Partial<NaxConfig>;
31
33
  }
32
34
 
33
35
  /**
@@ -59,6 +61,8 @@ export interface DecomposeOptions {
59
61
  modelTier?: ModelTier;
60
62
  /** Resolved model definition */
61
63
  modelDef?: ModelDef;
64
+ /** Global config — used to resolve models.balanced when modelDef is absent */
65
+ config?: Partial<NaxConfig>;
62
66
  }
63
67
 
64
68
  /** A single classified user story from decompose result. */
@@ -73,6 +73,31 @@ export interface AgentCapabilities {
73
73
  readonly features: ReadonlySet<"tdd" | "review" | "refactor" | "batch">;
74
74
  }
75
75
 
76
+ /**
77
+ * Options for one-shot LLM completion calls.
78
+ */
79
+ export interface CompleteOptions {
80
+ /** Maximum tokens for the response */
81
+ maxTokens?: number;
82
+ /** Request JSON-formatted output (adds --output-format json) */
83
+ jsonMode?: boolean;
84
+ /** Override the model (adds --model flag) */
85
+ model?: string;
86
+ }
87
+
88
+ /**
89
+ * Typed error thrown when complete() fails due to non-zero exit or empty output.
90
+ */
91
+ export class CompleteError extends Error {
92
+ constructor(
93
+ message: string,
94
+ public readonly exitCode?: number,
95
+ ) {
96
+ super(message);
97
+ this.name = "CompleteError";
98
+ }
99
+ }
100
+
76
101
  /**
77
102
  * Agent adapter interface — one implementation per supported coding agent.
78
103
  *
@@ -104,6 +129,12 @@ export interface AgentAdapter {
104
129
  /** Run the agent in decompose mode to break spec into classified stories. */
105
130
  decompose(options: import("./types-extended").DecomposeOptions): Promise<import("./types-extended").DecomposeResult>;
106
131
 
132
+ /**
133
+ * Run a one-shot LLM call and return the plain text response.
134
+ * Uses claude -p CLI for non-interactive completions.
135
+ */
136
+ complete(prompt: string, options?: CompleteOptions): Promise<string>;
137
+
107
138
  /**
108
139
  * Run the agent in interactive PTY mode for TUI embedding.
109
140
  * This method is optional — only implemented by agents that support
@@ -5,13 +5,25 @@
5
5
  * Falls back to keyword matching if LLM call fails.
6
6
  */
7
7
 
8
- import { Anthropic } from "@anthropic-ai/sdk";
8
+ import type { AgentAdapter } from "../agents";
9
+ import { ClaudeCodeAdapter } from "../agents/claude";
9
10
  import type { NaxConfig } from "../config";
11
+ import { resolveModel } from "../config/schema";
10
12
  import { getLogger } from "../logger";
11
13
  import type { UserStory } from "../prd";
12
14
  import { classifyComplexity } from "../routing";
13
15
  import type { ClassificationResult, CodebaseScan, StoryClassification } from "./types";
14
16
 
17
+ /**
18
+ * Injectable dependencies for classifier — allows tests to mock adapter.complete()
19
+ * without needing the claude binary.
20
+ *
21
+ * @internal
22
+ */
23
+ export const _classifyDeps = {
24
+ adapter: new ClaudeCodeAdapter() as AgentAdapter,
25
+ };
26
+
15
27
  /**
16
28
  * Raw LLM classification item (before validation)
17
29
  */
@@ -92,37 +104,29 @@ async function classifyWithLLM(
92
104
  scan: CodebaseScan,
93
105
  config: NaxConfig,
94
106
  ): Promise<StoryClassification[]> {
95
- // Check API key
96
- const apiKey = process.env.ANTHROPIC_API_KEY;
97
- if (!apiKey) {
98
- throw new Error("ANTHROPIC_API_KEY not set");
107
+ // Check for required environment variables
108
+ if (!process.env.ANTHROPIC_API_KEY) {
109
+ throw new Error("ANTHROPIC_API_KEY environment variable not configured — cannot use LLM classification");
99
110
  }
100
111
 
101
- const client = new Anthropic({ apiKey });
102
-
103
112
  // Build prompt
104
- const prompt = buildClassificationPrompt(stories, scan, config);
113
+ const prompt = buildClassificationPrompt(stories, scan);
105
114
 
106
- // Make API call (use haiku for cheap classification)
107
- const response = await client.messages.create({
108
- model: "claude-haiku-4-20250514",
109
- max_tokens: 4096,
110
- messages: [
111
- {
112
- role: "user",
113
- content: prompt,
114
- },
115
- ],
116
- });
117
-
118
- // Extract text from response
119
- const textContent = response.content.find((c) => c.type === "text");
120
- if (!textContent || textContent.type !== "text") {
121
- throw new Error("No text response from LLM");
115
+ // Resolve model from config.models.fast
116
+ const fastModelEntry = config.models.fast;
117
+ if (!fastModelEntry) {
118
+ throw new Error("config.models.fast not configured");
122
119
  }
120
+ const modelDef = resolveModel(fastModelEntry);
121
+
122
+ // Make API call via adapter (use haiku for cheap classification)
123
+ const jsonText = await _classifyDeps.adapter.complete(prompt, {
124
+ jsonMode: true,
125
+ maxTokens: 4096,
126
+ model: modelDef.model,
127
+ });
123
128
 
124
129
  // Parse JSON response
125
- const jsonText = extractJSON(textContent.text);
126
130
  const parsed: unknown = JSON.parse(jsonText);
127
131
 
128
132
  // Validate structure
@@ -159,10 +163,9 @@ async function classifyWithLLM(
159
163
  *
160
164
  * @param stories - User stories to classify
161
165
  * @param scan - Codebase scan result
162
- * @param config - Ngent configuration
163
166
  * @returns Formatted prompt string
164
167
  */
165
- function buildClassificationPrompt(stories: UserStory[], scan: CodebaseScan, config: NaxConfig): string {
168
+ function buildClassificationPrompt(stories: UserStory[], scan: CodebaseScan): string {
166
169
  // Format codebase summary
167
170
  const codebaseSummary = `
168
171
  FILE TREE:
@@ -229,29 +232,6 @@ Consider:
229
232
  Respond with ONLY the JSON array.`;
230
233
  }
231
234
 
232
- /**
233
- * Extract JSON from LLM response (handles markdown code fences).
234
- *
235
- * @param text - LLM response text
236
- * @returns JSON string
237
- */
238
- function extractJSON(text: string): string {
239
- // Remove markdown code fences if present
240
- const jsonMatch = text.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
241
- if (jsonMatch) {
242
- return jsonMatch[1];
243
- }
244
-
245
- // Try to find JSON array directly
246
- const arrayMatch = text.match(/\[[\s\S]*\]/);
247
- if (arrayMatch) {
248
- return arrayMatch[0];
249
- }
250
-
251
- // Return as-is if no special formatting detected
252
- return text.trim();
253
- }
254
-
255
235
  /**
256
236
  * Validate complexity value from LLM response.
257
237
  *
@@ -233,7 +233,14 @@ async function reclassifyWithLLM(
233
233
  const modelTier = config.analyze.model;
234
234
  const modelDef = resolveModel(config.models[modelTier]);
235
235
 
236
- const result = await adapter.decompose({ specContent: storySpec, workdir, codebaseContext, modelTier, modelDef });
236
+ const result = await adapter.decompose({
237
+ specContent: storySpec,
238
+ workdir,
239
+ codebaseContext,
240
+ modelTier,
241
+ modelDef,
242
+ config,
243
+ });
237
244
 
238
245
  if (result.stories.length === 0) return story;
239
246
  const ds = result.stories[0];
@@ -115,7 +115,7 @@ async function decomposeLLM(
115
115
 
116
116
  const modelTier = config.analyze.model;
117
117
  const modelDef = resolveModel(config.models[modelTier]);
118
- const result = await adapter.decompose({ specContent, workdir, codebaseContext, modelTier, modelDef });
118
+ const result = await adapter.decompose({ specContent, workdir, codebaseContext, modelTier, modelDef, config });
119
119
 
120
120
  logger.info("cli", "[OK] Agent decompose complete", { storiesCount: result.stories.length });
121
121
 
package/src/cli/plan.ts CHANGED
@@ -91,6 +91,7 @@ export async function planCommand(
91
91
  inputFile: options.from,
92
92
  modelTier,
93
93
  modelDef,
94
+ config,
94
95
  };
95
96
 
96
97
  // Run agent in plan mode