@nathapp/nax 0.34.0 → 0.36.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 (48) hide show
  1. package/bin/nax.ts +18 -9
  2. package/dist/nax.js +1934 -1138
  3. package/package.json +1 -2
  4. package/src/agents/adapters/aider.ts +135 -0
  5. package/src/agents/adapters/codex.ts +153 -0
  6. package/src/agents/adapters/gemini.ts +177 -0
  7. package/src/agents/adapters/opencode.ts +106 -0
  8. package/src/agents/claude-plan.ts +22 -5
  9. package/src/agents/claude.ts +102 -11
  10. package/src/agents/index.ts +4 -1
  11. package/src/agents/model-resolution.ts +43 -0
  12. package/src/agents/registry.ts +8 -3
  13. package/src/agents/types-extended.ts +5 -1
  14. package/src/agents/types.ts +31 -0
  15. package/src/agents/version-detection.ts +109 -0
  16. package/src/analyze/classifier.ts +30 -50
  17. package/src/cli/agents.ts +87 -0
  18. package/src/cli/analyze-parser.ts +8 -1
  19. package/src/cli/analyze.ts +1 -1
  20. package/src/cli/config.ts +28 -14
  21. package/src/cli/generate.ts +1 -1
  22. package/src/cli/index.ts +1 -0
  23. package/src/cli/plan.ts +1 -0
  24. package/src/config/types.ts +3 -1
  25. package/src/context/generator.ts +4 -0
  26. package/src/context/generators/codex.ts +28 -0
  27. package/src/context/generators/gemini.ts +28 -0
  28. package/src/context/types.ts +1 -1
  29. package/src/interaction/init.ts +8 -7
  30. package/src/interaction/plugins/auto.ts +41 -25
  31. package/src/pipeline/stages/execution.ts +2 -39
  32. package/src/pipeline/stages/routing.ts +12 -3
  33. package/src/plugins/index.ts +2 -0
  34. package/src/plugins/loader.ts +4 -2
  35. package/src/plugins/plugin-logger.ts +41 -0
  36. package/src/plugins/types.ts +50 -1
  37. package/src/precheck/checks-agents.ts +63 -0
  38. package/src/precheck/checks-blockers.ts +37 -1
  39. package/src/precheck/checks.ts +4 -0
  40. package/src/precheck/index.ts +4 -2
  41. package/src/routing/router.ts +1 -0
  42. package/src/routing/strategies/llm.ts +53 -36
  43. package/src/routing/strategy.ts +3 -0
  44. package/src/tdd/rectification-gate.ts +25 -1
  45. package/src/tdd/session-runner.ts +18 -49
  46. package/src/tdd/verdict.ts +135 -7
  47. package/src/utils/git.ts +49 -0
  48. package/src/verification/rectification-loop.ts +14 -1
@@ -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";
@@ -11,3 +12,5 @@ export {
11
12
  formatCostWithConfidence,
12
13
  } from "./cost";
13
14
  export { validateAgentForTier, validateAgentFeature, describeAgentCapabilities } from "./validation";
15
+ export type { AgentVersionInfo } from "./version-detection";
16
+ export { getAgentVersion, getAgentVersions } from "./version-detection";
@@ -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,15 +4,20 @@
4
4
  * Discovers and manages available coding agents.
5
5
  */
6
6
 
7
+ import { AiderAdapter } from "./adapters/aider";
8
+ import { CodexAdapter } from "./adapters/codex";
9
+ import { GeminiAdapter } from "./adapters/gemini";
10
+ import { OpenCodeAdapter } from "./adapters/opencode";
7
11
  import { ClaudeCodeAdapter } from "./claude";
8
12
  import type { AgentAdapter } from "./types";
9
13
 
10
14
  /** All known agent adapters */
11
15
  export const ALL_AGENTS: AgentAdapter[] = [
12
16
  new ClaudeCodeAdapter(),
13
- // Future: new CodexAdapter(),
14
- // Future: new OpenCodeAdapter(),
15
- // Future: new GeminiAdapter(),
17
+ new CodexAdapter(),
18
+ new OpenCodeAdapter(),
19
+ new GeminiAdapter(),
20
+ new AiderAdapter(),
16
21
  ];
17
22
 
18
23
  /** Get all registered agent names */
@@ -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
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Agent version detection utilities
3
+ *
4
+ * Extracts version information from installed agent binaries
5
+ * by running `<agent> --version` and parsing the output.
6
+ */
7
+
8
+ import { getInstalledAgents } from "./registry";
9
+ import type { AgentAdapter } from "./types";
10
+
11
+ /**
12
+ * Information about an installed agent including its version
13
+ */
14
+ export interface AgentVersionInfo {
15
+ /** Agent name (e.g., "codex", "aider") */
16
+ name: string;
17
+ /** Human-readable display name */
18
+ displayName: string;
19
+ /** Agent version or null if not installed/unable to detect */
20
+ version: string | null;
21
+ /** Whether the agent binary is installed */
22
+ installed: boolean;
23
+ }
24
+
25
+ /**
26
+ * Dependency injection for testability
27
+ */
28
+ export const _versionDetectionDeps = {
29
+ spawn(
30
+ cmd: string[],
31
+ opts: { stdout: "pipe"; stderr: "pipe" },
32
+ ): {
33
+ stdout: ReadableStream<Uint8Array>;
34
+ stderr: ReadableStream<Uint8Array>;
35
+ exited: Promise<number>;
36
+ } {
37
+ return Bun.spawn(cmd, opts) as unknown as {
38
+ stdout: ReadableStream<Uint8Array>;
39
+ stderr: ReadableStream<Uint8Array>;
40
+ exited: Promise<number>;
41
+ };
42
+ },
43
+ };
44
+
45
+ /**
46
+ * Get version for a single agent binary
47
+ *
48
+ * Runs `<agent> --version` and extracts version string.
49
+ * Returns null if agent not found or version detection fails.
50
+ */
51
+ export async function getAgentVersion(binaryName: string): Promise<string | null> {
52
+ try {
53
+ const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
54
+ stdout: "pipe",
55
+ stderr: "pipe",
56
+ });
57
+
58
+ const exitCode = await proc.exited;
59
+ if (exitCode !== 0) {
60
+ return null;
61
+ }
62
+
63
+ const stdout = await new Response(proc.stdout).text();
64
+ const versionLine = stdout.trim().split("\n")[0];
65
+
66
+ // Extract version from common formats:
67
+ // "tool version 1.2.3"
68
+ // "v1.2.3"
69
+ // "1.2.3"
70
+ const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
71
+ if (versionMatch) {
72
+ return versionMatch[0];
73
+ }
74
+
75
+ // If no version pattern matched, return the first line as-is
76
+ return versionLine || null;
77
+ } catch {
78
+ // Bun.spawn throws ENOENT if binary not found
79
+ return null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get version information for all configured agents
85
+ *
86
+ * Returns list of agents with their installation status and version info.
87
+ */
88
+ export async function getAgentVersions(): Promise<AgentVersionInfo[]> {
89
+ const agents = await getInstalledAgents();
90
+ const agentsByName = new Map(agents.map((a) => [a.name, a]));
91
+
92
+ // Import ALL_AGENTS to include non-installed ones
93
+ const { ALL_AGENTS } = await import("./registry");
94
+
95
+ const versions = await Promise.all(
96
+ ALL_AGENTS.map(async (agent: AgentAdapter): Promise<AgentVersionInfo> => {
97
+ const version = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
98
+
99
+ return {
100
+ name: agent.name,
101
+ displayName: agent.displayName,
102
+ version,
103
+ installed: agentsByName.has(agent.name),
104
+ };
105
+ }),
106
+ );
107
+
108
+ return versions;
109
+ }
@@ -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
  *
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Agents Command
3
+ *
4
+ * Lists available agents with their binary paths, versions, and health status.
5
+ */
6
+
7
+ import { ALL_AGENTS } from "../agents/registry";
8
+ import { getAgentVersion } from "../agents/version-detection";
9
+ import type { NaxConfig } from "../config/schema";
10
+
11
+ /**
12
+ * List all agents with status, version, and capabilities.
13
+ *
14
+ * @param config - nax configuration
15
+ * @param _workdir - Working directory (for consistency with other commands)
16
+ */
17
+ export async function agentsListCommand(config: NaxConfig, _workdir: string): Promise<void> {
18
+ // Get version info for all agents
19
+ const agentVersions = await Promise.all(
20
+ ALL_AGENTS.map(async (agent) => ({
21
+ name: agent.name,
22
+ displayName: agent.displayName,
23
+ binary: agent.binary,
24
+ version: await getAgentVersion(agent.binary),
25
+ installed: await agent.isInstalled(),
26
+ capabilities: agent.capabilities,
27
+ isDefault: config.autoMode.defaultAgent === agent.name,
28
+ })),
29
+ );
30
+
31
+ // Build table rows
32
+ const rows = agentVersions.map((info) => {
33
+ const status = info.installed ? "installed" : "unavailable";
34
+ const versionStr = info.version || "-";
35
+ const defaultMarker = info.isDefault ? " (default)" : "";
36
+
37
+ return {
38
+ name: info.displayName + defaultMarker,
39
+ status,
40
+ version: versionStr,
41
+ binary: info.binary,
42
+ tiers: info.capabilities.supportedTiers.join(", "),
43
+ };
44
+ });
45
+
46
+ if (rows.length === 0) {
47
+ console.log("No agents available.");
48
+ return;
49
+ }
50
+
51
+ // Calculate column widths
52
+ const widths = {
53
+ name: Math.max(5, ...rows.map((r) => r.name.length)),
54
+ status: Math.max(6, ...rows.map((r) => r.status.length)),
55
+ version: Math.max(7, ...rows.map((r) => r.version.length)),
56
+ binary: Math.max(6, ...rows.map((r) => r.binary.length)),
57
+ tiers: Math.max(5, ...rows.map((r) => r.tiers.length)),
58
+ };
59
+
60
+ // Display table
61
+ console.log("\nAvailable Agents:\n");
62
+ console.log(
63
+ `${pad("Agent", widths.name)} ${pad("Status", widths.status)} ${pad("Version", widths.version)} ${pad("Binary", widths.binary)} ${pad("Tiers", widths.tiers)}`,
64
+ );
65
+ console.log(
66
+ `${"-".repeat(widths.name)} ${"-".repeat(widths.status)} ${"-".repeat(widths.version)} ${"-".repeat(widths.binary)} ${"-".repeat(widths.tiers)}`,
67
+ );
68
+
69
+ for (const row of rows) {
70
+ console.log(
71
+ `${pad(row.name, widths.name)} ${pad(row.status, widths.status)} ${pad(row.version, widths.version)} ${pad(row.binary, widths.binary)} ${pad(row.tiers, widths.tiers)}`,
72
+ );
73
+ }
74
+
75
+ console.log();
76
+ }
77
+
78
+ /**
79
+ * Pad string to width.
80
+ *
81
+ * @param str - String to pad
82
+ * @param width - Target width
83
+ * @returns Padded string
84
+ */
85
+ function pad(str: string, width: number): string {
86
+ return str.padEnd(width);
87
+ }
@@ -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