@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.
- package/bin/nax.ts +18 -9
- package/dist/nax.js +1934 -1138
- package/package.json +1 -2
- package/src/agents/adapters/aider.ts +135 -0
- package/src/agents/adapters/codex.ts +153 -0
- package/src/agents/adapters/gemini.ts +177 -0
- package/src/agents/adapters/opencode.ts +106 -0
- package/src/agents/claude-plan.ts +22 -5
- package/src/agents/claude.ts +102 -11
- package/src/agents/index.ts +4 -1
- package/src/agents/model-resolution.ts +43 -0
- package/src/agents/registry.ts +8 -3
- package/src/agents/types-extended.ts +5 -1
- package/src/agents/types.ts +31 -0
- package/src/agents/version-detection.ts +109 -0
- package/src/analyze/classifier.ts +30 -50
- package/src/cli/agents.ts +87 -0
- package/src/cli/analyze-parser.ts +8 -1
- package/src/cli/analyze.ts +1 -1
- package/src/cli/config.ts +28 -14
- package/src/cli/generate.ts +1 -1
- package/src/cli/index.ts +1 -0
- package/src/cli/plan.ts +1 -0
- package/src/config/types.ts +3 -1
- package/src/context/generator.ts +4 -0
- package/src/context/generators/codex.ts +28 -0
- package/src/context/generators/gemini.ts +28 -0
- package/src/context/types.ts +1 -1
- package/src/interaction/init.ts +8 -7
- package/src/interaction/plugins/auto.ts +41 -25
- package/src/pipeline/stages/execution.ts +2 -39
- package/src/pipeline/stages/routing.ts +12 -3
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.ts +4 -2
- package/src/plugins/plugin-logger.ts +41 -0
- package/src/plugins/types.ts +50 -1
- package/src/precheck/checks-agents.ts +63 -0
- package/src/precheck/checks-blockers.ts +37 -1
- package/src/precheck/checks.ts +4 -0
- package/src/precheck/index.ts +4 -2
- package/src/routing/router.ts +1 -0
- package/src/routing/strategies/llm.ts +53 -36
- package/src/routing/strategy.ts +3 -0
- package/src/tdd/rectification-gate.ts +25 -1
- package/src/tdd/session-runner.ts +18 -49
- package/src/tdd/verdict.ts +135 -7
- package/src/utils/git.ts +49 -0
- package/src/verification/rectification-loop.ts +14 -1
package/src/agents/claude.ts
CHANGED
|
@@ -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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
options.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 =
|
|
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
|
|
414
|
+
modelDef,
|
|
324
415
|
prompt: "",
|
|
325
|
-
modelTier: "balanced",
|
|
416
|
+
modelTier: options.modelTier || "balanced",
|
|
326
417
|
timeoutSeconds: 600,
|
|
327
418
|
}),
|
|
328
419
|
});
|
package/src/agents/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/agents/registry.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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. */
|
package/src/agents/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
113
|
+
const prompt = buildClassificationPrompt(stories, scan);
|
|
105
114
|
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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({
|
|
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];
|
package/src/cli/analyze.ts
CHANGED
|
@@ -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
|
|