@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aider Agent Adapter — implements AgentAdapter interface
|
|
3
|
+
*
|
|
4
|
+
* Provides uniform interface for spawning Aider agent processes,
|
|
5
|
+
* supporting one-shot completions in headless mode.
|
|
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 other adapters
|
|
23
|
+
// These are replaced in unit tests to intercept Bun.spawn and Bun.which calls.
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const _aiderCompleteDeps = {
|
|
27
|
+
which(name: string): string | null {
|
|
28
|
+
return Bun.which(name);
|
|
29
|
+
},
|
|
30
|
+
spawn(
|
|
31
|
+
cmd: string[],
|
|
32
|
+
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
33
|
+
): {
|
|
34
|
+
stdout: ReadableStream<Uint8Array>;
|
|
35
|
+
stderr: ReadableStream<Uint8Array>;
|
|
36
|
+
exited: Promise<number>;
|
|
37
|
+
pid: number;
|
|
38
|
+
} {
|
|
39
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
40
|
+
stdout: ReadableStream<Uint8Array>;
|
|
41
|
+
stderr: ReadableStream<Uint8Array>;
|
|
42
|
+
exited: Promise<number>;
|
|
43
|
+
pid: number;
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// AiderAdapter implementation
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum characters to capture from agent stdout.
|
|
54
|
+
*/
|
|
55
|
+
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
56
|
+
|
|
57
|
+
export class AiderAdapter implements AgentAdapter {
|
|
58
|
+
readonly name = "aider";
|
|
59
|
+
readonly displayName = "Aider";
|
|
60
|
+
readonly binary = "aider";
|
|
61
|
+
|
|
62
|
+
readonly capabilities: AgentCapabilities = {
|
|
63
|
+
supportedTiers: ["balanced"],
|
|
64
|
+
maxContextTokens: 20_000,
|
|
65
|
+
features: new Set<"tdd" | "review" | "refactor" | "batch">(["refactor"]),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async isInstalled(): Promise<boolean> {
|
|
69
|
+
const path = _aiderCompleteDeps.which("aider");
|
|
70
|
+
return path !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
buildCommand(options: AgentRunOptions): string[] {
|
|
74
|
+
return ["aider", "--message", options.prompt, "--yes"];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
78
|
+
const cmd = this.buildCommand(options);
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
|
|
81
|
+
const proc = _aiderCompleteDeps.spawn(cmd, {
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "inherit",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const exitCode = await proc.exited;
|
|
87
|
+
const stdout = await new Response(proc.stdout).text();
|
|
88
|
+
const durationMs = Date.now() - startTime;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
success: exitCode === 0,
|
|
92
|
+
exitCode,
|
|
93
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
94
|
+
rateLimited: false,
|
|
95
|
+
durationMs,
|
|
96
|
+
estimatedCost: 0,
|
|
97
|
+
pid: proc.pid,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async complete(prompt: string, options?: CompleteOptions): Promise<string> {
|
|
102
|
+
const cmd = ["aider", "--message", prompt, "--yes"];
|
|
103
|
+
|
|
104
|
+
if (options?.model) {
|
|
105
|
+
cmd.push("--model", options.model);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const proc = _aiderCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
109
|
+
const exitCode = await proc.exited;
|
|
110
|
+
|
|
111
|
+
const stdout = await new Response(proc.stdout).text();
|
|
112
|
+
const stderr = await new Response(proc.stderr).text();
|
|
113
|
+
const trimmed = stdout.trim();
|
|
114
|
+
|
|
115
|
+
if (exitCode !== 0) {
|
|
116
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
117
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
118
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!trimmed) {
|
|
122
|
+
throw new CompleteError("complete() returned empty output");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return trimmed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async plan(_options: PlanOptions): Promise<PlanResult> {
|
|
129
|
+
throw new Error("AiderAdapter.plan() not implemented");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
|
|
133
|
+
throw new Error("AiderAdapter.decompose() not implemented");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Agent Adapter — implements AgentAdapter interface
|
|
3
|
+
*
|
|
4
|
+
* Provides uniform interface for spawning Gemini CLI processes,
|
|
5
|
+
* supporting one-shot completions via 'gemini -p' and Google auth detection.
|
|
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 — follows the _deps pattern
|
|
23
|
+
// Replaced in unit tests to intercept Bun.spawn/Bun.which calls.
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const _geminiRunDeps = {
|
|
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 _geminiCompleteDeps = {
|
|
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
|
+
// GeminiAdapter implementation
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
74
|
+
|
|
75
|
+
export class GeminiAdapter implements AgentAdapter {
|
|
76
|
+
readonly name = "gemini";
|
|
77
|
+
readonly displayName = "Gemini CLI";
|
|
78
|
+
readonly binary = "gemini";
|
|
79
|
+
|
|
80
|
+
readonly capabilities: AgentCapabilities = {
|
|
81
|
+
supportedTiers: ["fast", "balanced", "powerful"],
|
|
82
|
+
maxContextTokens: 1_000_000,
|
|
83
|
+
features: new Set<"tdd" | "review" | "refactor" | "batch">(["tdd", "review", "refactor"]),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
async isInstalled(): Promise<boolean> {
|
|
87
|
+
const path = _geminiRunDeps.which("gemini");
|
|
88
|
+
if (path === null) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check Google auth — run 'gemini' with a flag that shows auth status
|
|
93
|
+
try {
|
|
94
|
+
const proc = _geminiRunDeps.spawn(["gemini", "--version"], {
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const exitCode = await proc.exited;
|
|
100
|
+
if (exitCode !== 0) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stdout = await new Response(proc.stdout).text();
|
|
105
|
+
const lowerOut = stdout.toLowerCase();
|
|
106
|
+
|
|
107
|
+
// If output explicitly says "not logged in", auth has failed
|
|
108
|
+
if (lowerOut.includes("not logged in")) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
buildCommand(options: AgentRunOptions): string[] {
|
|
119
|
+
return ["gemini", "-p", options.prompt];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
123
|
+
const cmd = this.buildCommand(options);
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
|
|
126
|
+
const proc = _geminiRunDeps.spawn(cmd, {
|
|
127
|
+
cwd: options.workdir,
|
|
128
|
+
stdout: "pipe",
|
|
129
|
+
stderr: "inherit",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const exitCode = await proc.exited;
|
|
133
|
+
const stdout = await new Response(proc.stdout).text();
|
|
134
|
+
const durationMs = Date.now() - startTime;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
success: exitCode === 0,
|
|
138
|
+
exitCode,
|
|
139
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
140
|
+
rateLimited: false,
|
|
141
|
+
durationMs,
|
|
142
|
+
estimatedCost: 0,
|
|
143
|
+
pid: proc.pid,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
|
|
148
|
+
const cmd = ["gemini", "-p", prompt];
|
|
149
|
+
|
|
150
|
+
const proc = _geminiCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
151
|
+
const exitCode = await proc.exited;
|
|
152
|
+
|
|
153
|
+
const stdout = await new Response(proc.stdout).text();
|
|
154
|
+
const stderr = await new Response(proc.stderr).text();
|
|
155
|
+
const trimmed = stdout.trim();
|
|
156
|
+
|
|
157
|
+
if (exitCode !== 0) {
|
|
158
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
159
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
160
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!trimmed) {
|
|
164
|
+
throw new CompleteError("complete() returned empty output");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return trimmed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async plan(_options: PlanOptions): Promise<PlanResult> {
|
|
171
|
+
throw new Error("GeminiAdapter.plan() not implemented");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
|
|
175
|
+
throw new Error("GeminiAdapter.decompose() not implemented");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Agent Adapter — implements AgentAdapter interface
|
|
3
|
+
*
|
|
4
|
+
* Provides uniform interface for spawning OpenCode agent processes,
|
|
5
|
+
* supporting one-shot completions.
|
|
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 _opencodeCompleteDeps = {
|
|
27
|
+
which(name: string): string | null {
|
|
28
|
+
return Bun.which(name);
|
|
29
|
+
},
|
|
30
|
+
spawn(
|
|
31
|
+
cmd: string[],
|
|
32
|
+
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
33
|
+
): {
|
|
34
|
+
stdout: ReadableStream<Uint8Array>;
|
|
35
|
+
stderr: ReadableStream<Uint8Array>;
|
|
36
|
+
exited: Promise<number>;
|
|
37
|
+
pid: number;
|
|
38
|
+
} {
|
|
39
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
40
|
+
stdout: ReadableStream<Uint8Array>;
|
|
41
|
+
stderr: ReadableStream<Uint8Array>;
|
|
42
|
+
exited: Promise<number>;
|
|
43
|
+
pid: number;
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// OpenCodeAdapter implementation
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export class OpenCodeAdapter implements AgentAdapter {
|
|
53
|
+
readonly name = "opencode";
|
|
54
|
+
readonly displayName = "OpenCode";
|
|
55
|
+
readonly binary = "opencode";
|
|
56
|
+
|
|
57
|
+
readonly capabilities: AgentCapabilities = {
|
|
58
|
+
supportedTiers: ["fast", "balanced"],
|
|
59
|
+
maxContextTokens: 8_000,
|
|
60
|
+
features: new Set<"tdd" | "review" | "refactor" | "batch">(["tdd", "refactor"]),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async isInstalled(): Promise<boolean> {
|
|
64
|
+
const path = _opencodeCompleteDeps.which("opencode");
|
|
65
|
+
return path !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
buildCommand(_options: AgentRunOptions): string[] {
|
|
69
|
+
throw new Error("OpenCodeAdapter.buildCommand() not implemented");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async run(_options: AgentRunOptions): Promise<AgentResult> {
|
|
73
|
+
throw new Error("OpenCodeAdapter.run() not implemented");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
|
|
77
|
+
const cmd = ["opencode", "--prompt", prompt];
|
|
78
|
+
|
|
79
|
+
const proc = _opencodeCompleteDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
80
|
+
const exitCode = await proc.exited;
|
|
81
|
+
|
|
82
|
+
const stdout = await new Response(proc.stdout).text();
|
|
83
|
+
const stderr = await new Response(proc.stderr).text();
|
|
84
|
+
const trimmed = stdout.trim();
|
|
85
|
+
|
|
86
|
+
if (exitCode !== 0) {
|
|
87
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
88
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
89
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!trimmed) {
|
|
93
|
+
throw new CompleteError("complete() returned empty output");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async plan(_options: PlanOptions): Promise<PlanResult> {
|
|
100
|
+
throw new Error("OpenCodeAdapter.plan() not implemented");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async decompose(_options: DecomposeOptions): Promise<DecomposeResult> {
|
|
104
|
+
throw new Error("OpenCodeAdapter.decompose() not implemented");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
|
88
|
+
modelDef,
|
|
72
89
|
prompt: "",
|
|
73
|
-
modelTier: "balanced",
|
|
90
|
+
modelTier: options.modelTier || "balanced",
|
|
74
91
|
timeoutSeconds: 600,
|
|
75
92
|
};
|
|
76
93
|
|