karajan-code 1.35.0 → 1.36.1
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/README.md +136 -19
- package/bin/kj-tail +294 -41
- package/docs/README.es.md +125 -22
- package/package.json +2 -1
- package/src/agents/aider-agent.js +16 -9
- package/src/agents/base-agent.js +15 -0
- package/src/agents/claude-agent.js +51 -6
- package/src/agents/codex-agent.js +35 -13
- package/src/agents/gemini-agent.js +17 -9
- package/src/agents/model-registry.js +8 -7
- package/src/agents/opencode-agent.js +17 -10
- package/src/commands/doctor.js +98 -0
- package/src/orchestrator/solomon-escalation.js +11 -2
- package/src/roles/solomon-role.js +17 -1
- package/src/utils/budget.js +12 -8
- package/src/utils/model-selector.js +3 -3
- package/src/utils/stall-detector.js +5 -5
- package/templates/kj.config.yml +3 -0
package/docs/README.es.md
CHANGED
|
@@ -29,8 +29,9 @@ En lugar de ejecutar un agente de IA y revisar manualmente su output, `kj` encad
|
|
|
29
29
|
|
|
30
30
|
**Caracteristicas principales:**
|
|
31
31
|
- **Pipeline multi-agente** con 11 roles configurables
|
|
32
|
-
- **
|
|
33
|
-
- **Servidor MCP** con
|
|
32
|
+
- **5 agentes de IA soportados**: Claude, Codex, Gemini, Aider, OpenCode
|
|
33
|
+
- **Servidor MCP** con 20 herramientas — usa `kj` desde Claude, Codex o cualquier host compatible con MCP sin salir de tu agente. [Ver configuracion MCP](#servidor-mcp)
|
|
34
|
+
- **Bootstrap obligatorio** — valida prerequisitos del entorno (git, remote, config, agentes, SonarQube) antes de cada ejecucion. Si algo falta, para con instrucciones claras
|
|
34
35
|
- **TDD obligatorio** — se exigen cambios en tests cuando se modifican ficheros fuente
|
|
35
36
|
- **Integracion con SonarQube** — analisis estatico con quality gates (requiere [Docker](#requisitos))
|
|
36
37
|
- **Perfiles de revision** — standard, strict, relaxed, paranoid
|
|
@@ -128,33 +129,135 @@ Guias completas: [`docs/multi-instance.md`](multi-instance.md) | [`docs/install-
|
|
|
128
129
|
|
|
129
130
|
`kj init` auto-detecta los agentes instalados. Si solo hay uno disponible, se asigna a todos los roles automaticamente.
|
|
130
131
|
|
|
131
|
-
##
|
|
132
|
+
## Tres formas de usar Karajan
|
|
133
|
+
|
|
134
|
+
Karajan instala **tres comandos**: `kj`, `kj-tail` y `karajan-mcp`.
|
|
135
|
+
|
|
136
|
+
### 1. CLI — Directamente desde terminal
|
|
132
137
|
|
|
133
138
|
```bash
|
|
134
|
-
# Ejecutar una tarea con defaults (claude=coder, codex=reviewer, TDD)
|
|
135
139
|
kj run "Implementar autenticacion de usuario con JWT"
|
|
136
|
-
|
|
137
|
-
# Solo coder (sin revision)
|
|
138
140
|
kj code "Anadir validacion de inputs al formulario de registro"
|
|
139
|
-
|
|
140
|
-
# Solo reviewer (revisar diff actual)
|
|
141
141
|
kj review "Revisar los cambios de autenticacion"
|
|
142
|
+
kj plan "Refactorizar la capa de base de datos"
|
|
143
|
+
```
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
kj plan "Refactorizar la capa de base de datos para usar connection pooling"
|
|
145
|
+
### 2. MCP — Dentro de tu agente de IA
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
El caso de uso principal. Karajan corre como servidor MCP dentro de Claude Code, Codex o Gemini. El agente tiene acceso a 20 herramientas (`kj_run`, `kj_code`, `kj_review`, etc.) y delega el trabajo pesado al pipeline de Karajan.
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Tu → Claude Code → kj_run (via MCP) → triage → coder → sonar → reviewer → tester → security
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**El problema**: cuando Karajan corre dentro de un agente de IA, pierdes visibilidad. El agente te muestra el resultado final, pero no las etapas del pipeline, iteraciones o decisiones de Solomon en tiempo real.
|
|
154
|
+
|
|
155
|
+
### 3. kj-tail — Monitorizar desde otro terminal
|
|
156
|
+
|
|
157
|
+
**La herramienta companera.** Abre un segundo terminal en el **mismo directorio del proyecto** donde esta trabajando tu agente de IA:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
kj-tail
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Veras la salida del pipeline en vivo — etapas, resultados, iteraciones, errores — tal como ocurren.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
kj-tail # Seguir pipeline en tiempo real (por defecto)
|
|
167
|
+
kj-tail -v # Verbose: incluir heartbeats de agente y presupuesto
|
|
168
|
+
kj-tail -t # Mostrar timestamps
|
|
169
|
+
kj-tail -s # Snapshot: mostrar log actual y salir
|
|
170
|
+
kj-tail -n 50 # Mostrar ultimas 50 lineas y seguir
|
|
171
|
+
kj-tail --help # Todas las opciones
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> **Importante**: `kj-tail` debe ejecutarse desde el mismo directorio donde el agente de IA esta trabajando. Lee `<proyecto>/.kj/run.log`, que se crea cuando Karajan arranca un pipeline via MCP.
|
|
175
|
+
|
|
176
|
+
**Flujo tipico:**
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌──────────────────────────┐ ┌──────────────────────────┐
|
|
180
|
+
│ Terminal 1 │ │ Terminal 2 │
|
|
181
|
+
│ │ │ │
|
|
182
|
+
│ $ claude │ │ $ kj-tail │
|
|
183
|
+
│ > implementa la │ │ │
|
|
184
|
+
│ siguiente tarea │ │ ├─ 📋 Triage: medium │
|
|
185
|
+
│ prioritaria │ │ ├─ 🔬 Researcher ✅ │
|
|
186
|
+
│ │ │ ├─ 🧠 Planner ✅ │
|
|
187
|
+
│ (Claude llama a kj_run │ │ ├─ 🔨 Coder ✅ │
|
|
188
|
+
│ via MCP — solo ves │ │ ├─ 🔍 Sonar: OK │
|
|
189
|
+
│ el resultado final) │ │ ├─ 👁️ Reviewer ❌ │
|
|
190
|
+
│ │ │ ├─ ⚖️ Solomon: 2 cond. │
|
|
191
|
+
│ │ │ ├─ 🔨 Coder (iter 2) ✅ │
|
|
192
|
+
│ │ │ ├─ ✅ Review: APPROVED │
|
|
193
|
+
│ │ │ ├─ 🧪 Tester: passed │
|
|
194
|
+
│ │ │ └─ 🏁 Result: APPROVED │
|
|
195
|
+
└──────────────────────────┘ └──────────────────────────┘
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Ejemplo con pipeline completo** — tarea compleja con todos los roles:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
┌─ Terminal 1 ─────────────────────────────────────────────────────────────────┐
|
|
202
|
+
│ │
|
|
203
|
+
│ $ claude │
|
|
204
|
+
│ │
|
|
205
|
+
│ > Construye una API REST para un sistema de reservas. Requisitos: │
|
|
206
|
+
│ > - Express + TypeScript con validacion Zod en cada endpoint │
|
|
207
|
+
│ > - Endpoints: POST /bookings, GET /bookings/:id, │
|
|
208
|
+
│ > PATCH /bookings/:id/cancel │
|
|
209
|
+
│ > - Una reserva tiene: id, guestName, roomType (standard|suite|penthouse), │
|
|
210
|
+
│ > checkIn, checkOut, status (confirmed|cancelled) │
|
|
211
|
+
│ > - Validar: checkOut posterior a checkIn, sin fechas pasadas, │
|
|
212
|
+
│ > roomType debe ser un valor valido del enum │
|
|
213
|
+
│ > - Cancelar devuelve 409 si ya esta cancelada │
|
|
214
|
+
│ > - Usa TDD. Ejecutalo con Karajan con architect y planner activos, │
|
|
215
|
+
│ > modo paranoid. Coder claude, reviewer codex. │
|
|
216
|
+
│ │
|
|
217
|
+
│ Claude llama a kj_run via MCP con: │
|
|
218
|
+
│ --enable-architect --enable-researcher --enable-planner --mode paranoid │
|
|
219
|
+
│ │
|
|
220
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
221
|
+
|
|
222
|
+
┌─ Terminal 2: kj-tail ────────────────────────────────────────────────────────┐
|
|
223
|
+
│ │
|
|
224
|
+
│ kj-tail v1.36.1 — .kj/run.log │
|
|
225
|
+
│ │
|
|
226
|
+
│ ├─ 📋 Triage: medium (sw) — activando researcher, architect, planner │
|
|
227
|
+
│ ├─ ⚙️ Preflight passed — all checks OK │
|
|
228
|
+
│ ├─ 🔬 Researcher: 8 ficheros, 3 patrones, 5 restricciones │
|
|
229
|
+
│ ├─ 🏗️ Architect: diseno 3 capas (routes → service → validators) │
|
|
230
|
+
│ ├─ 🧠 Planner: 6 pasos — tests primero, luego rutas, servicio, validadores │
|
|
231
|
+
│ │ │
|
|
232
|
+
│ ▶ Iteracion 1/5 │
|
|
233
|
+
│ ├─ 🔨 Coder (claude): 3 endpoints + 18 tests │
|
|
234
|
+
│ ├─ 📋 TDD: PASS (3 src, 2 test) │
|
|
235
|
+
│ ├─ 🔍 Sonar: Quality gate OK │
|
|
236
|
+
│ ├─ 👁️ Reviewer (codex): REJECTED (2 blocking) │
|
|
237
|
+
│ │ "Falta 404 para GET booking inexistente" │
|
|
238
|
+
│ │ "Endpoint cancel sin test de idempotencia" │
|
|
239
|
+
│ ├─ ⚖️ Solomon: approve_with_conditions (2 condiciones) │
|
|
240
|
+
│ │ "Anadir respuesta 404 y test para GET /bookings/:id con id desconocido" │
|
|
241
|
+
│ │ "Anadir test: cancelar reserva ya cancelada devuelve 409, no 500" │
|
|
242
|
+
│ │ │
|
|
243
|
+
│ ▶ Iteracion 2/5 │
|
|
244
|
+
│ ├─ 🔨 Coder (claude): corregido — 22 tests │
|
|
245
|
+
│ ├─ 📋 TDD: PASS │
|
|
246
|
+
│ ├─ 🔍 Sonar: OK │
|
|
247
|
+
│ ├─ 👁️ Reviewer (codex): APPROVED │
|
|
248
|
+
│ ├─ 🧪 Tester: passed — cobertura 94%, 22 tests │
|
|
249
|
+
│ ├─ 🔒 Security: passed — 0 criticos, 1 bajo (helmet recomendado) │
|
|
250
|
+
│ ├─ 📊 Audit: CERTIFIED (3 advertencias) │
|
|
251
|
+
│ │ │
|
|
252
|
+
│ 🏁 Resultado: APPROVED │
|
|
253
|
+
│ 🔬 Investigacion: 8 ficheros, 3 patrones │
|
|
254
|
+
│ 🗺 Plan: 6 pasos (tests primero) │
|
|
255
|
+
│ 🧪 Cobertura: 94%, 22 tests │
|
|
256
|
+
│ 🔒 Seguridad: OK │
|
|
257
|
+
│ 🔍 Sonar: OK │
|
|
258
|
+
│ 💰 Presupuesto: $0.42 (claude: $0.38, codex: $0.04) │
|
|
259
|
+
│ │
|
|
260
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
158
261
|
```
|
|
159
262
|
|
|
160
263
|
## Comandos CLI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.1",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"bin": {
|
|
40
40
|
"kj": "bin/kj.js",
|
|
41
|
+
"kj-tail": "bin/kj-tail",
|
|
41
42
|
"karajan-mcp": "bin/karajan-mcp.js"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
@@ -5,21 +5,28 @@ import { resolveBin } from "./resolve-bin.js";
|
|
|
5
5
|
export class AiderAgent extends BaseAgent {
|
|
6
6
|
async runTask(task) {
|
|
7
7
|
const role = task.role || "coder";
|
|
8
|
-
const args = ["--yes", "--message", task.prompt];
|
|
9
8
|
const model = this.getRoleModel(role);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
|
|
9
|
+
const result = await this._exec(task, model);
|
|
10
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
11
|
+
this.logger?.warn(`Aider model "${model}" not supported — retrying with agent default`);
|
|
12
|
+
return this._exec(task, null);
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
async reviewTask(task) {
|
|
20
18
|
const role = task.role || "reviewer";
|
|
21
|
-
const args = ["--yes", "--message", task.prompt];
|
|
22
19
|
const model = this.getRoleModel(role);
|
|
20
|
+
const result = await this._exec(task, model);
|
|
21
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
22
|
+
this.logger?.warn(`Aider model "${model}" not supported — retrying with agent default`);
|
|
23
|
+
return this._exec(task, null);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _exec(task, model) {
|
|
29
|
+
const args = ["--yes", "--message", task.prompt];
|
|
23
30
|
if (model) args.push("--model", model);
|
|
24
31
|
const res = await runCommand(resolveBin("aider"), args, {
|
|
25
32
|
onOutput: task.onOutput,
|
package/src/agents/base-agent.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
const MODEL_NOT_SUPPORTED_PATTERNS = [
|
|
2
|
+
/model.{0,30}is not supported/i,
|
|
3
|
+
/model.{0,30}not available/i,
|
|
4
|
+
/model.{0,30}does not exist/i,
|
|
5
|
+
/unsupported model/i,
|
|
6
|
+
/invalid model/i,
|
|
7
|
+
/model_not_found/i
|
|
8
|
+
];
|
|
9
|
+
|
|
1
10
|
export class BaseAgent {
|
|
2
11
|
constructor(name, config, logger) {
|
|
3
12
|
this.name = name;
|
|
@@ -24,4 +33,10 @@ export class BaseAgent {
|
|
|
24
33
|
if (role === "reviewer") return false;
|
|
25
34
|
return Boolean(this.config?.coder_options?.auto_approve);
|
|
26
35
|
}
|
|
36
|
+
|
|
37
|
+
isModelNotSupportedError(result) {
|
|
38
|
+
const text = [result?.error, result?.output, result?.stderr, result?.stdout]
|
|
39
|
+
.filter(Boolean).join("\n");
|
|
40
|
+
return MODEL_NOT_SUPPORTED_PATTERNS.some(re => re.test(text));
|
|
41
|
+
}
|
|
27
42
|
}
|
|
@@ -35,6 +35,29 @@ function collectAssistantText(obj) {
|
|
|
35
35
|
.map(block => block.text);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Extract usage metrics from stream-json/json NDJSON output.
|
|
40
|
+
* Looks for the "result" line which contains total_cost_usd,
|
|
41
|
+
* usage.input_tokens/output_tokens, and modelUsage.
|
|
42
|
+
* Returns an object with tokens_in, tokens_out, cost_usd, model or null if not found.
|
|
43
|
+
*/
|
|
44
|
+
export function extractUsageFromStreamJson(raw) {
|
|
45
|
+
const lines = (raw || "").split("\n").filter(Boolean);
|
|
46
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
47
|
+
const obj = tryParseJson(lines[i]);
|
|
48
|
+
if (!obj || obj.type !== "result") continue;
|
|
49
|
+
|
|
50
|
+
const tokens_in = obj.usage?.input_tokens ?? 0;
|
|
51
|
+
const tokens_out = obj.usage?.output_tokens ?? 0;
|
|
52
|
+
const cost_usd = obj.total_cost_usd ?? undefined;
|
|
53
|
+
const modelUsage = obj.modelUsage;
|
|
54
|
+
const model = modelUsage ? Object.keys(modelUsage)[0] || null : null;
|
|
55
|
+
|
|
56
|
+
return { tokens_in, tokens_out, cost_usd, model };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
/**
|
|
39
62
|
* Extract the final text result from stream-json NDJSON output.
|
|
40
63
|
* Each line is a JSON object. We collect assistant text content from
|
|
@@ -135,8 +158,28 @@ const ALLOWED_TOOLS = [
|
|
|
135
158
|
export class ClaudeAgent extends BaseAgent {
|
|
136
159
|
async runTask(task) {
|
|
137
160
|
const role = task.role || "coder";
|
|
138
|
-
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS];
|
|
139
161
|
const model = this.getRoleModel(role);
|
|
162
|
+
const result = await this._runTaskExec(task, model, role);
|
|
163
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
164
|
+
this.logger?.warn(`Claude model "${model}" not supported — retrying with agent default`);
|
|
165
|
+
return this._runTaskExec(task, null, role);
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async reviewTask(task) {
|
|
171
|
+
const role = task.role || "reviewer";
|
|
172
|
+
const model = this.getRoleModel(role);
|
|
173
|
+
const result = await this._reviewTaskExec(task, model);
|
|
174
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
175
|
+
this.logger?.warn(`Claude model "${model}" not supported — retrying with agent default`);
|
|
176
|
+
return this._reviewTaskExec(task, null);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async _runTaskExec(task, model, role) {
|
|
182
|
+
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS];
|
|
140
183
|
if (model) args.push("--model", model);
|
|
141
184
|
|
|
142
185
|
// Use stream-json when onOutput is provided to get real-time feedback
|
|
@@ -150,7 +193,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
150
193
|
}));
|
|
151
194
|
const raw = pickOutput(res);
|
|
152
195
|
const output = extractTextFromStreamJson(raw);
|
|
153
|
-
|
|
196
|
+
const usage = extractUsageFromStreamJson(raw);
|
|
197
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
154
198
|
}
|
|
155
199
|
|
|
156
200
|
// Without streaming, use json output to get structured response via stderr
|
|
@@ -158,12 +202,12 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
158
202
|
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts());
|
|
159
203
|
const raw = pickOutput(res);
|
|
160
204
|
const output = extractTextFromStreamJson(raw);
|
|
161
|
-
|
|
205
|
+
const usage = extractUsageFromStreamJson(raw);
|
|
206
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
162
207
|
}
|
|
163
208
|
|
|
164
|
-
async
|
|
209
|
+
async _reviewTaskExec(task, model) {
|
|
165
210
|
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS, "--output-format", "stream-json", "--verbose"];
|
|
166
|
-
const model = this.getRoleModel(task.role || "reviewer");
|
|
167
211
|
if (model) args.push("--model", model);
|
|
168
212
|
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
169
213
|
onOutput: task.onOutput,
|
|
@@ -171,6 +215,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
171
215
|
timeout: task.timeoutMs
|
|
172
216
|
}));
|
|
173
217
|
const raw = pickOutput(res);
|
|
174
|
-
|
|
218
|
+
const usage = extractUsageFromStreamJson(raw);
|
|
219
|
+
return { ok: res.exitCode === 0, output: raw, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
175
220
|
}
|
|
176
221
|
}
|
|
@@ -2,27 +2,48 @@ import { BaseAgent } from "./base-agent.js";
|
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Extract token usage from Codex CLI stdout.
|
|
7
|
+
* Codex prints "tokens used\n<number>" at the end, where number may have comma separators.
|
|
8
|
+
* Returns { tokens_out } with the total token count, or null if not found.
|
|
9
|
+
* Since Codex doesn't split input/output, we assign the total to tokens_out
|
|
10
|
+
* as a conservative estimate for cost calculation.
|
|
11
|
+
*/
|
|
12
|
+
export function extractCodexTokens(stdout) {
|
|
13
|
+
const match = (stdout || "").match(/tokens?\s+used\s*\n\s*([\d,]+)/i);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const total = Number(match[1].replace(/,/g, ""));
|
|
16
|
+
if (!Number.isFinite(total) || total <= 0) return null;
|
|
17
|
+
return { tokens_in: 0, tokens_out: total };
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export class CodexAgent extends BaseAgent {
|
|
6
21
|
async runTask(task) {
|
|
7
22
|
const role = task.role || "coder";
|
|
8
|
-
const args = ["exec"];
|
|
9
23
|
const model = this.getRoleModel(role);
|
|
10
|
-
|
|
11
|
-
if (this.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
timeout: task.timeoutMs,
|
|
17
|
-
input: task.prompt
|
|
18
|
-
});
|
|
19
|
-
return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
|
|
24
|
+
const result = await this._exec(task, model, role);
|
|
25
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
26
|
+
this.logger?.warn(`Codex model "${model}" not supported — retrying with agent default`);
|
|
27
|
+
return this._exec(task, null, role);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
async reviewTask(task) {
|
|
33
|
+
const role = task.role || "reviewer";
|
|
34
|
+
const model = this.getRoleModel(role);
|
|
35
|
+
const result = await this._exec(task, model, role);
|
|
36
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
37
|
+
this.logger?.warn(`Codex model "${model}" not supported — retrying with agent default`);
|
|
38
|
+
return this._exec(task, null, role);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async _exec(task, model, role) {
|
|
23
44
|
const args = ["exec"];
|
|
24
|
-
const model = this.getRoleModel(task.role || "reviewer");
|
|
25
45
|
if (model) args.push("--model", model);
|
|
46
|
+
if (role !== "reviewer" && this.isAutoApproveEnabled(role)) args.push("--full-auto");
|
|
26
47
|
args.push("-");
|
|
27
48
|
const res = await runCommand(resolveBin("codex"), args, {
|
|
28
49
|
onOutput: task.onOutput,
|
|
@@ -30,6 +51,7 @@ export class CodexAgent extends BaseAgent {
|
|
|
30
51
|
timeout: task.timeoutMs,
|
|
31
52
|
input: task.prompt
|
|
32
53
|
});
|
|
33
|
-
|
|
54
|
+
const usage = extractCodexTokens(res.stdout);
|
|
55
|
+
return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode, ...usage };
|
|
34
56
|
}
|
|
35
57
|
}
|
|
@@ -5,21 +5,29 @@ import { resolveBin } from "./resolve-bin.js";
|
|
|
5
5
|
export class GeminiAgent extends BaseAgent {
|
|
6
6
|
async runTask(task) {
|
|
7
7
|
const role = task.role || "coder";
|
|
8
|
-
const args = ["-p", task.prompt];
|
|
9
8
|
const model = this.getRoleModel(role);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
|
|
9
|
+
const result = await this._exec(task, model, "run");
|
|
10
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
11
|
+
this.logger?.warn(`Gemini model "${model}" not supported — retrying with agent default`);
|
|
12
|
+
return this._exec(task, null, "run");
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
async reviewTask(task) {
|
|
20
18
|
const role = task.role || "reviewer";
|
|
21
|
-
const args = ["-p", task.prompt, "--output-format", "json"];
|
|
22
19
|
const model = this.getRoleModel(role);
|
|
20
|
+
const result = await this._exec(task, model, "review");
|
|
21
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
22
|
+
this.logger?.warn(`Gemini model "${model}" not supported — retrying with agent default`);
|
|
23
|
+
return this._exec(task, null, "review");
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _exec(task, model, mode) {
|
|
29
|
+
const args = ["-p", task.prompt];
|
|
30
|
+
if (mode === "review") args.push("--output-format", "json");
|
|
23
31
|
if (model) args.push("--model", model);
|
|
24
32
|
const res = await runCommand(resolveBin("gemini"), args, {
|
|
25
33
|
onOutput: task.onOutput,
|
|
@@ -49,15 +49,16 @@ export function buildDefaultPricingTable() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Auto-register built-in models
|
|
52
|
+
// Names must match what each CLI accepts as --model argument
|
|
52
53
|
registerModel("claude", { provider: "anthropic", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
53
|
-
registerModel("
|
|
54
|
-
registerModel("
|
|
55
|
-
registerModel("
|
|
54
|
+
registerModel("sonnet", { provider: "anthropic", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
55
|
+
registerModel("opus", { provider: "anthropic", pricing: { input_per_million: 15, output_per_million: 75 } });
|
|
56
|
+
registerModel("haiku", { provider: "anthropic", pricing: { input_per_million: 0.25, output_per_million: 1.25 } });
|
|
56
57
|
registerModel("codex", { provider: "openai", pricing: { input_per_million: 1.5, output_per_million: 4 } });
|
|
57
|
-
registerModel("
|
|
58
|
-
registerModel("
|
|
58
|
+
registerModel("o4-mini", { provider: "openai", pricing: { input_per_million: 1.5, output_per_million: 4 } });
|
|
59
|
+
registerModel("o3", { provider: "openai", pricing: { input_per_million: 10, output_per_million: 40 } });
|
|
59
60
|
registerModel("gemini", { provider: "google", pricing: { input_per_million: 1.25, output_per_million: 5 } });
|
|
60
|
-
registerModel("gemini
|
|
61
|
-
registerModel("gemini
|
|
61
|
+
registerModel("gemini-2.5-pro", { provider: "google", pricing: { input_per_million: 1.25, output_per_million: 5 } });
|
|
62
|
+
registerModel("gemini-2.0-flash", { provider: "google", pricing: { input_per_million: 0.075, output_per_million: 0.3 } });
|
|
62
63
|
registerModel("aider", { provider: "aider", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
63
64
|
registerModel("opencode", { provider: "opencode", pricing: { input_per_million: 0, output_per_million: 0 } });
|
|
@@ -5,22 +5,29 @@ import { resolveBin } from "./resolve-bin.js";
|
|
|
5
5
|
export class OpenCodeAgent extends BaseAgent {
|
|
6
6
|
async runTask(task) {
|
|
7
7
|
const role = task.role || "coder";
|
|
8
|
-
const args = ["run"];
|
|
9
8
|
const model = this.getRoleModel(role);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
17
|
-
return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
|
|
9
|
+
const result = await this._exec(task, model, false);
|
|
10
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
11
|
+
this.logger?.warn(`OpenCode model "${model}" not supported — retrying with agent default`);
|
|
12
|
+
return this._exec(task, null, false);
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
async reviewTask(task) {
|
|
21
18
|
const role = task.role || "reviewer";
|
|
22
|
-
const args = ["run", "--format", "json"];
|
|
23
19
|
const model = this.getRoleModel(role);
|
|
20
|
+
const result = await this._exec(task, model, true);
|
|
21
|
+
if (!result.ok && model && this.isModelNotSupportedError(result)) {
|
|
22
|
+
this.logger?.warn(`OpenCode model "${model}" not supported — retrying with agent default`);
|
|
23
|
+
return this._exec(task, null, true);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _exec(task, model, jsonFormat) {
|
|
29
|
+
const args = ["run"];
|
|
30
|
+
if (jsonFormat) args.push("--format", "json");
|
|
24
31
|
if (model) args.push("--model", model);
|
|
25
32
|
args.push(task.prompt);
|
|
26
33
|
const res = await runCommand(resolveBin("opencode"), args, {
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { runCommand } from "../utils/process.js";
|
|
5
7
|
import { exists } from "../utils/fs.js";
|
|
@@ -239,6 +241,101 @@ async function checkRuleFiles(config) {
|
|
|
239
241
|
];
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Detect duplicate TOML table headers (e.g. [mcp_servers."karajan-mcp"] appearing twice).
|
|
246
|
+
* Full TOML parsing would require a dependency — this catches the most common config error.
|
|
247
|
+
*/
|
|
248
|
+
function findDuplicateTomlKeys(content) {
|
|
249
|
+
const tableHeaders = [];
|
|
250
|
+
const duplicates = [];
|
|
251
|
+
for (const line of content.split("\n")) {
|
|
252
|
+
const match = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
253
|
+
if (match) {
|
|
254
|
+
const key = match[1].trim();
|
|
255
|
+
if (tableHeaders.includes(key)) {
|
|
256
|
+
duplicates.push(key);
|
|
257
|
+
} else {
|
|
258
|
+
tableHeaders.push(key);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return duplicates;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function checkAgentConfigs() {
|
|
266
|
+
const checks = [];
|
|
267
|
+
const home = os.homedir();
|
|
268
|
+
|
|
269
|
+
// Claude: ~/.claude.json
|
|
270
|
+
const claudeJsonPath = path.join(home, ".claude.json");
|
|
271
|
+
try {
|
|
272
|
+
const raw = await fs.readFile(claudeJsonPath, "utf8");
|
|
273
|
+
JSON.parse(raw);
|
|
274
|
+
checks.push({ name: "agent-config:claude", label: "Agent config: claude (~/.claude.json)", ok: true, detail: "Valid JSON", fix: null });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (err.code === "ENOENT") {
|
|
277
|
+
// File doesn't exist — not an error, Claude may not be configured
|
|
278
|
+
} else {
|
|
279
|
+
checks.push({
|
|
280
|
+
name: "agent-config:claude",
|
|
281
|
+
label: "Agent config: claude (~/.claude.json)",
|
|
282
|
+
ok: false,
|
|
283
|
+
detail: `Invalid JSON: ${err.message.split("\n")[0]}`,
|
|
284
|
+
fix: "Fix the JSON syntax in ~/.claude.json. Common issues: trailing commas, missing quotes."
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Codex: ~/.codex/config.toml
|
|
290
|
+
const codexTomlPath = path.join(home, ".codex", "config.toml");
|
|
291
|
+
try {
|
|
292
|
+
const raw = await fs.readFile(codexTomlPath, "utf8");
|
|
293
|
+
const duplicates = findDuplicateTomlKeys(raw);
|
|
294
|
+
if (duplicates.length > 0) {
|
|
295
|
+
checks.push({
|
|
296
|
+
name: "agent-config:codex",
|
|
297
|
+
label: "Agent config: codex (~/.codex/config.toml)",
|
|
298
|
+
ok: false,
|
|
299
|
+
detail: `Duplicate TOML keys: ${duplicates.join(", ")}`,
|
|
300
|
+
fix: `Remove duplicate entries in ~/.codex/config.toml: ${duplicates.join(", ")}`
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
checks.push({ name: "agent-config:codex", label: "Agent config: codex (~/.codex/config.toml)", ok: true, detail: "Valid TOML (no duplicate keys)", fix: null });
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err.code !== "ENOENT") {
|
|
307
|
+
checks.push({
|
|
308
|
+
name: "agent-config:codex",
|
|
309
|
+
label: "Agent config: codex (~/.codex/config.toml)",
|
|
310
|
+
ok: false,
|
|
311
|
+
detail: `Cannot read: ${err.message.split("\n")[0]}`,
|
|
312
|
+
fix: "Check file permissions on ~/.codex/config.toml"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// KJ config: ~/.karajan/kj.config.yml (validate YAML)
|
|
318
|
+
const kjConfigPath = getConfigPath();
|
|
319
|
+
try {
|
|
320
|
+
const raw = await fs.readFile(kjConfigPath, "utf8");
|
|
321
|
+
const yaml = await import("js-yaml");
|
|
322
|
+
yaml.default.load(raw);
|
|
323
|
+
checks.push({ name: "agent-config:karajan", label: "Agent config: karajan (kj.config.yml)", ok: true, detail: "Valid YAML", fix: null });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (err.code !== "ENOENT") {
|
|
326
|
+
checks.push({
|
|
327
|
+
name: "agent-config:karajan",
|
|
328
|
+
label: "Agent config: karajan (kj.config.yml)",
|
|
329
|
+
ok: false,
|
|
330
|
+
detail: `Invalid YAML: ${err.message.split("\n")[0]}`,
|
|
331
|
+
fix: `Fix YAML syntax in ${kjConfigPath}. Run 'kj init' to regenerate if needed.`
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return checks;
|
|
337
|
+
}
|
|
338
|
+
|
|
242
339
|
export async function runChecks({ config }) {
|
|
243
340
|
const checks = [];
|
|
244
341
|
|
|
@@ -260,6 +357,7 @@ export async function runChecks({ config }) {
|
|
|
260
357
|
checks.push(...await checkBecariaInfra(config));
|
|
261
358
|
}
|
|
262
359
|
|
|
360
|
+
checks.push(...await checkAgentConfigs());
|
|
263
361
|
checks.push(...await checkRuleFiles(config));
|
|
264
362
|
checks.push(await checkRtk());
|
|
265
363
|
|