agent-rev 0.1.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/dist/api/qwen.d.ts +12 -0
- package/dist/api/qwen.js +150 -0
- package/dist/commands/auth.d.ts +31 -0
- package/dist/commands/auth.js +255 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +42 -0
- package/dist/commands/models.d.ts +2 -0
- package/dist/commands/models.js +27 -0
- package/dist/commands/repl.d.ts +29 -0
- package/dist/commands/repl.js +1167 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +52 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +353 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +46 -0
- package/dist/core/engine.d.ts +36 -0
- package/dist/core/engine.js +905 -0
- package/dist/core/prompts.d.ts +11 -0
- package/dist/core/prompts.js +126 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +171 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +120 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +58 -0
- package/dist/ui/input.d.ts +24 -0
- package/dist/ui/input.js +244 -0
- package/dist/ui/theme.d.ts +99 -0
- package/dist/ui/theme.js +307 -0
- package/dist/utils/config.d.ts +38 -0
- package/dist/utils/config.js +40 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.js +37 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/qwen-auth.d.ts +12 -0
- package/dist/utils/qwen-auth.js +250 -0
- package/dist/utils/sessions.d.ts +17 -0
- package/dist/utils/sessions.js +46 -0
- package/package.json +44 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as readline from 'readline';
|
|
5
|
+
import { CLI_REGISTRY } from '../types.js';
|
|
6
|
+
import { writeJson, readJson, fileExists, writeFile, readFile } from '../utils/fs.js';
|
|
7
|
+
import { log } from '../utils/logger.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { QWEN_AGENT_HOME } from '../utils/qwen-auth.js';
|
|
10
|
+
import { callQwenAPI } from '../utils/qwen-auth.js';
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
/** Parse --help output to detect current flags */
|
|
13
|
+
function detectCliFlags(cliName) {
|
|
14
|
+
try {
|
|
15
|
+
const help = execSync(`${cliName} --help 2>&1`, { encoding: 'utf-8', timeout: 10000 });
|
|
16
|
+
const result = {};
|
|
17
|
+
// Detect model flag
|
|
18
|
+
const modelFlagMatch = help.match(/--model\s/);
|
|
19
|
+
if (modelFlagMatch)
|
|
20
|
+
result.modelFlag = '--model';
|
|
21
|
+
const mFlagMatch = help.match(/(?:^|\s)-m\s/);
|
|
22
|
+
if (mFlagMatch && !result.modelFlag)
|
|
23
|
+
result.modelFlag = '-m';
|
|
24
|
+
// Detect extra flags — look for common patterns
|
|
25
|
+
const extraCandidates = [];
|
|
26
|
+
const flagPatterns = [
|
|
27
|
+
'--yolo', '--dangerously-skip-permissions', '--yes', '--auto',
|
|
28
|
+
'--output-format', '-o', '--json',
|
|
29
|
+
];
|
|
30
|
+
for (const fp of flagPatterns) {
|
|
31
|
+
if (help.includes(fp))
|
|
32
|
+
extraCandidates.push(fp);
|
|
33
|
+
}
|
|
34
|
+
if (extraCandidates.length > 0)
|
|
35
|
+
result.extraFlags = extraCandidates.join(' ');
|
|
36
|
+
// Detect prompt/positional flag
|
|
37
|
+
if (help.includes('--prompt'))
|
|
38
|
+
result.promptFlag = '--prompt';
|
|
39
|
+
else if (help.includes('-p'))
|
|
40
|
+
result.promptFlag = '-p';
|
|
41
|
+
else if (help.includes('-p '))
|
|
42
|
+
result.promptFlag = '-p';
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Rebuild a CLI command from scratch using detected flags */
|
|
50
|
+
function rebuildCmd(cliName, model, flags) {
|
|
51
|
+
const info = CLI_REGISTRY[cliName];
|
|
52
|
+
if (!info)
|
|
53
|
+
return `${cliName} ${flags.modelFlag || '--model'} ${model}`;
|
|
54
|
+
const modelFlag = flags.modelFlag || info.modelFlag;
|
|
55
|
+
const extra = flags.extraFlags || info.extraFlags;
|
|
56
|
+
const promptFlag = flags.promptFlag || info.promptFlag;
|
|
57
|
+
if (cliName === 'codex')
|
|
58
|
+
return `codex exec -m ${model}`;
|
|
59
|
+
if (cliName === 'cn')
|
|
60
|
+
return `cn --auto -p`;
|
|
61
|
+
if (cliName === 'opencode')
|
|
62
|
+
return `opencode run ${modelFlag ? `${modelFlag} ${model}` : model}`;
|
|
63
|
+
const extraStr = extra ? ` ${extra}` : '';
|
|
64
|
+
const promptStr = promptFlag ? ` ${promptFlag}` : '';
|
|
65
|
+
return `${info.command} ${modelFlag} ${model}${extraStr}${promptStr}`.trim();
|
|
66
|
+
}
|
|
67
|
+
async function ask(prompt, rl, fi) {
|
|
68
|
+
if (fi) {
|
|
69
|
+
fi.println(chalk.cyan(` ${prompt}`));
|
|
70
|
+
const answer = await fi.readLine();
|
|
71
|
+
// Echo user response right-aligned (chat style, per line)
|
|
72
|
+
const userLines = answer.trim().split('\n').filter(l => l.trim());
|
|
73
|
+
for (let i = 0; i < userLines.length; i++) {
|
|
74
|
+
const prefix = (i === 0) ? chalk.dim('usuario: ') : ' ';
|
|
75
|
+
const msg = `${prefix}${chalk.white(userLines[i])}`;
|
|
76
|
+
const rawLen = msg.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
77
|
+
const padding = ' '.repeat(Math.max(0, fi.cols - rawLen - 2));
|
|
78
|
+
fi.println(`${padding}${msg}`);
|
|
79
|
+
}
|
|
80
|
+
return answer;
|
|
81
|
+
}
|
|
82
|
+
if (rl) {
|
|
83
|
+
return new Promise((resolve) => rl.question(prompt, resolve));
|
|
84
|
+
}
|
|
85
|
+
const tempRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
86
|
+
return new Promise((resolve) => tempRl.question(prompt, (a) => { tempRl.close(); resolve(a); }));
|
|
87
|
+
}
|
|
88
|
+
function runCli(cmd, prompt, timeoutMs = 600000, envOverride) {
|
|
89
|
+
// Si el comando es "qwen-direct", usar la API directa sin CLI
|
|
90
|
+
if (cmd.startsWith('qwen-direct')) {
|
|
91
|
+
const model = cmd.includes('-m') ? cmd.split('-m')[1]?.trim().split(/\s/)[0] : 'coder-model';
|
|
92
|
+
return callQwenAPI(prompt, model)
|
|
93
|
+
.then(output => ({ output, exitCode: 0 }))
|
|
94
|
+
.catch(err => ({ output: `Error: ${err.message}`, exitCode: 1 }));
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const parts = cmd.trim().split(/\s+/);
|
|
98
|
+
let fullCmd;
|
|
99
|
+
let fullArgs;
|
|
100
|
+
if (cmd.includes('codex exec')) {
|
|
101
|
+
fullCmd = 'codex';
|
|
102
|
+
const rest = parts.slice(1);
|
|
103
|
+
fullArgs = rest.filter((p) => p !== '-p' && p !== '--prompt').concat([prompt]);
|
|
104
|
+
}
|
|
105
|
+
else if (cmd.includes('opencode run')) {
|
|
106
|
+
// opencode run -m model "message" — strip any -p/--prompt flags (opencode uses -p for password)
|
|
107
|
+
fullCmd = 'opencode';
|
|
108
|
+
const rest = parts.slice(1).filter((p) => p !== '-p' && p !== '--prompt');
|
|
109
|
+
fullArgs = rest.concat([prompt]);
|
|
110
|
+
}
|
|
111
|
+
else if (parts[0] === 'opencode' && !cmd.includes('run')) {
|
|
112
|
+
// opencode -m model → needs "run" subcommand, strip -p (opencode uses -p for password)
|
|
113
|
+
fullCmd = 'opencode';
|
|
114
|
+
const rest = parts.slice(1).filter((p) => p !== '-p' && p !== '--prompt');
|
|
115
|
+
fullArgs = ['run'].concat(rest).concat([prompt]);
|
|
116
|
+
}
|
|
117
|
+
else if (parts.includes('-p')) {
|
|
118
|
+
fullCmd = parts[0];
|
|
119
|
+
fullArgs = parts.slice(1).filter((p) => p !== '-p' && p !== '--prompt').concat(['-p', prompt]);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
fullCmd = parts[0];
|
|
123
|
+
fullArgs = parts.slice(1).concat([prompt]);
|
|
124
|
+
}
|
|
125
|
+
// Configurar entorno: si es qwen, usar QWEN_CODE_HOME separado
|
|
126
|
+
const baseEnv = process.env;
|
|
127
|
+
const finalEnv = {};
|
|
128
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
129
|
+
if (value !== undefined) {
|
|
130
|
+
finalEnv[key] = value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (fullCmd === 'qwen' && envOverride?.QWEN_CODE_HOME) {
|
|
134
|
+
finalEnv.QWEN_CODE_HOME = envOverride.QWEN_CODE_HOME;
|
|
135
|
+
}
|
|
136
|
+
const child = spawn(fullCmd, fullArgs, {
|
|
137
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
138
|
+
timeout: timeoutMs,
|
|
139
|
+
env: finalEnv,
|
|
140
|
+
});
|
|
141
|
+
let output = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
child.stdout?.on('data', (d) => { output += d.toString(); });
|
|
144
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
145
|
+
child.on('close', (code) => { resolve({ output: output + stderr, exitCode: code || 0 }); });
|
|
146
|
+
child.on('error', reject);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function extractTokens(output) {
|
|
150
|
+
const u = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 };
|
|
151
|
+
const im = output.match(/"input_tokens":\s*(\d+)/);
|
|
152
|
+
const om = output.match(/"output_tokens":\s*(\d+)/);
|
|
153
|
+
const cm = output.match(/"cache_read_input_tokens":\s*(\d+)/);
|
|
154
|
+
if (im)
|
|
155
|
+
u.input_tokens = parseInt(im[1]);
|
|
156
|
+
if (om)
|
|
157
|
+
u.output_tokens = parseInt(om[1]);
|
|
158
|
+
if (cm)
|
|
159
|
+
u.cache_read_input_tokens = parseInt(cm[1]);
|
|
160
|
+
return u;
|
|
161
|
+
}
|
|
162
|
+
/** Extract the final text response from a CLI output (handles qwen/claude streaming JSON events) */
|
|
163
|
+
function extractCliText(output) {
|
|
164
|
+
const trimmed = output.trim();
|
|
165
|
+
// Try to parse as a JSON array of events (qwen/claude streaming format)
|
|
166
|
+
try {
|
|
167
|
+
const events = JSON.parse(trimmed);
|
|
168
|
+
if (Array.isArray(events)) {
|
|
169
|
+
// Look for result event first
|
|
170
|
+
for (const item of events) {
|
|
171
|
+
if (item.type === 'result' && typeof item.result === 'string')
|
|
172
|
+
return item.result;
|
|
173
|
+
}
|
|
174
|
+
// Fallback: collect all assistant text content
|
|
175
|
+
const texts = [];
|
|
176
|
+
for (const item of events) {
|
|
177
|
+
if (item.type === 'assistant' && Array.isArray(item.message?.content)) {
|
|
178
|
+
for (const c of item.message.content) {
|
|
179
|
+
if (c.type === 'text' && typeof c.text === 'string')
|
|
180
|
+
texts.push(c.text);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (texts.length > 0)
|
|
185
|
+
return texts.join('\n');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
return trimmed;
|
|
190
|
+
}
|
|
191
|
+
function extractJson(text) {
|
|
192
|
+
let c = text.trim();
|
|
193
|
+
if (c.startsWith('```')) {
|
|
194
|
+
c = c.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
|
|
195
|
+
}
|
|
196
|
+
const first = c.indexOf('{');
|
|
197
|
+
const last = c.lastIndexOf('}');
|
|
198
|
+
if (first === -1 || last === -1)
|
|
199
|
+
throw new Error('No JSON found');
|
|
200
|
+
return JSON.parse(c.slice(first, last + 1));
|
|
201
|
+
}
|
|
202
|
+
export class AgentEngine {
|
|
203
|
+
config;
|
|
204
|
+
projectDir;
|
|
205
|
+
coordinatorCmd;
|
|
206
|
+
rl;
|
|
207
|
+
fi;
|
|
208
|
+
totalTokens = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 };
|
|
209
|
+
phaseTokens = [];
|
|
210
|
+
constructor(config, projectDir, coordinatorCmd, rl, fi) {
|
|
211
|
+
this.config = config;
|
|
212
|
+
this.projectDir = projectDir;
|
|
213
|
+
this.coordinatorCmd = coordinatorCmd || '';
|
|
214
|
+
this.rl = rl;
|
|
215
|
+
this.fi = fi;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* FASE 0 — Clarificacion con el programador.
|
|
219
|
+
* El coordinador (CLI activo, ej: Qwen) conversa con el usuario
|
|
220
|
+
* hasta tener toda la info necesaria. Retorna la tarea refinada.
|
|
221
|
+
*/
|
|
222
|
+
async runClarification(initialTask) {
|
|
223
|
+
if (!this.coordinatorCmd) {
|
|
224
|
+
return initialTask;
|
|
225
|
+
}
|
|
226
|
+
log.section('FASE 0 — Clarificacion');
|
|
227
|
+
log.info('El coordinador va a conversar con vos para entender la tarea.');
|
|
228
|
+
log.info('Cuando el coordinador tenga claro el objetivo, te va a pedir confirmación para lanzar el plan.');
|
|
229
|
+
log.divider();
|
|
230
|
+
const context = await this.buildCoordinatorContext();
|
|
231
|
+
let conversationHistory = `TAREA INICIAL: ${initialTask}`;
|
|
232
|
+
// Loop de clarificacion
|
|
233
|
+
while (true) {
|
|
234
|
+
const prompt = `Sos el COORDINADOR de un equipo multi-agente de desarrollo.
|
|
235
|
+
Tu trabajo es ENTENDER lo que el programador necesita haciendo PREGUNTAS si es necesario.
|
|
236
|
+
|
|
237
|
+
CONTEXTO DEL PROYECTO:
|
|
238
|
+
${context}
|
|
239
|
+
|
|
240
|
+
CONVERSACION PREVIA:
|
|
241
|
+
${conversationHistory}
|
|
242
|
+
|
|
243
|
+
INSTRUCCIONES:
|
|
244
|
+
- Habla de forma NATURAL, como un compañero de equipo.
|
|
245
|
+
- Si necesitas mas info, hace UNA pregunta clara y espera la respuesta.
|
|
246
|
+
- Si ya entendiste el objetivo, decilo claramente y pregunta: "¿Confirmás para lanzar el orchestrator?"
|
|
247
|
+
- NO uses JSON, habla normalmente.
|
|
248
|
+
- Sé breve y directo.`;
|
|
249
|
+
log.info('Coordinador analizando...');
|
|
250
|
+
// Usar credenciales corporativas si es qwen
|
|
251
|
+
const envOverride = {};
|
|
252
|
+
const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
|
|
253
|
+
const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
|
|
254
|
+
if (this.coordinatorCmd.startsWith('qwen')) {
|
|
255
|
+
// Check if corporate credentials exist
|
|
256
|
+
let corporateCredsContent = null;
|
|
257
|
+
try {
|
|
258
|
+
corporateCredsContent = await fs.readFile(corporateCreds, 'utf-8');
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
console.log(chalk.red('\n ✗ No hay credenciales corporativas de Qwen.'));
|
|
262
|
+
console.log(chalk.yellow(' Ejecutá /login para autenticarte.\n'));
|
|
263
|
+
return initialTask;
|
|
264
|
+
}
|
|
265
|
+
// Backup personal creds
|
|
266
|
+
let personalBackup = null;
|
|
267
|
+
try {
|
|
268
|
+
personalBackup = await fs.readFile(personalCreds, 'utf-8');
|
|
269
|
+
}
|
|
270
|
+
catch { }
|
|
271
|
+
// Copy corporate creds to personal location
|
|
272
|
+
await fs.writeFile(personalCreds, corporateCredsContent);
|
|
273
|
+
const res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
|
|
274
|
+
// Restore personal creds
|
|
275
|
+
if (personalBackup) {
|
|
276
|
+
await fs.writeFile(personalCreds, personalBackup);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// No personal backup, remove the file
|
|
280
|
+
await fs.unlink(personalCreds).catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
// Extraer texto legible del output (Qwen CLI devuelve JSON con eventos)
|
|
283
|
+
let responseText = res.output.trim();
|
|
284
|
+
try {
|
|
285
|
+
const json = JSON.parse(res.output);
|
|
286
|
+
if (Array.isArray(json)) {
|
|
287
|
+
// Buscar el resultado final
|
|
288
|
+
for (const item of json) {
|
|
289
|
+
if (item.type === 'result' && typeof item.result === 'string') {
|
|
290
|
+
responseText = item.result;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
if (item.type === 'assistant' && item.message?.content?.length > 0) {
|
|
294
|
+
const textContent = item.message.content.find((c) => c.type === 'text');
|
|
295
|
+
if (textContent?.text) {
|
|
296
|
+
responseText = textContent.text;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// No es JSON, usar el output directo
|
|
305
|
+
}
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(chalk.cyan(' Coordinador:'));
|
|
308
|
+
console.log(chalk.white(` ${responseText}`));
|
|
309
|
+
console.log('');
|
|
310
|
+
// Verificar si el coordinador quiere lanzar el orchestrator EXPLICITAMENTE
|
|
311
|
+
// Solo si pregunta "confirmás" o similar
|
|
312
|
+
const lower = responseText.toLowerCase();
|
|
313
|
+
if ((lower.includes('confirm') || lower.includes('procedo') || lower.includes('lanzo el plan')) &&
|
|
314
|
+
(lower.includes('orchestrator') || lower.includes('plan'))) {
|
|
315
|
+
const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl, this.fi);
|
|
316
|
+
if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 's') {
|
|
317
|
+
return conversationHistory;
|
|
318
|
+
}
|
|
319
|
+
const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl, this.fi);
|
|
320
|
+
conversationHistory += `\nPROGRAMADOR: ${correction}`;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
// Si no, seguir conversando
|
|
324
|
+
const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
|
|
325
|
+
conversationHistory += `\nPROGRAMADOR: ${answer}`;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// Para otros CLIs, usar normalmente
|
|
329
|
+
const res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
|
|
330
|
+
// Extraer texto legible del output
|
|
331
|
+
let responseText = res.output.trim();
|
|
332
|
+
try {
|
|
333
|
+
const json = JSON.parse(res.output);
|
|
334
|
+
if (Array.isArray(json)) {
|
|
335
|
+
for (const item of json) {
|
|
336
|
+
if (item.type === 'result' && typeof item.result === 'string') {
|
|
337
|
+
responseText = item.result;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch { }
|
|
344
|
+
console.log('');
|
|
345
|
+
console.log(chalk.cyan(' Coordinador:'));
|
|
346
|
+
console.log(chalk.white(` ${responseText}`));
|
|
347
|
+
console.log('');
|
|
348
|
+
const lower = responseText.toLowerCase();
|
|
349
|
+
if ((lower.includes('confirm') || lower.includes('procedo')) &&
|
|
350
|
+
(lower.includes('orchestrator') || lower.includes('plan'))) {
|
|
351
|
+
const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl, this.fi);
|
|
352
|
+
if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 's') {
|
|
353
|
+
return conversationHistory;
|
|
354
|
+
}
|
|
355
|
+
const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl, this.fi);
|
|
356
|
+
conversationHistory += `\nPROGRAMADOR: ${correction}`;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
|
|
360
|
+
conversationHistory += `\nPROGRAMADOR: ${answer}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async runWithFallback(roleName, prompt, phaseName) {
|
|
365
|
+
const roleMap = {
|
|
366
|
+
orchestrator: 'orchestrator',
|
|
367
|
+
implementor: 'implementor',
|
|
368
|
+
reviewer: 'reviewer',
|
|
369
|
+
};
|
|
370
|
+
const key = roleMap[roleName];
|
|
371
|
+
const role = this.config.roles[key];
|
|
372
|
+
if (!role)
|
|
373
|
+
throw new Error(`Role "${roleName}" not configured`);
|
|
374
|
+
const trackTokens = (output, cli, model) => {
|
|
375
|
+
const tokens = extractTokens(output);
|
|
376
|
+
this.totalTokens.input_tokens += tokens.input_tokens;
|
|
377
|
+
this.totalTokens.output_tokens += tokens.output_tokens;
|
|
378
|
+
this.totalTokens.cache_read_input_tokens += tokens.cache_read_input_tokens;
|
|
379
|
+
if (phaseName) {
|
|
380
|
+
this.phaseTokens.push({
|
|
381
|
+
phase: phaseName,
|
|
382
|
+
cli,
|
|
383
|
+
model,
|
|
384
|
+
role: roleName,
|
|
385
|
+
input: tokens.input_tokens,
|
|
386
|
+
output: tokens.output_tokens,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
// Inject role-specific system prompt
|
|
391
|
+
const rolePrompt = this.buildRolePrompt(roleName, prompt);
|
|
392
|
+
/** Try a cmd, and if it fails, auto-detect flags from --help and retry */
|
|
393
|
+
const tryWithAutoRepair = async (cliName, model, currentCmd) => {
|
|
394
|
+
try {
|
|
395
|
+
const result = await runCli(currentCmd, rolePrompt);
|
|
396
|
+
if (result.exitCode !== 0) {
|
|
397
|
+
const detail = result.output.trim().slice(0, 500);
|
|
398
|
+
throw new Error(`${cliName} exited with code ${result.exitCode}${detail ? `\n${detail}` : ''}`);
|
|
399
|
+
}
|
|
400
|
+
return result.output;
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
log.warn(`${cliName} failed, detecting flags from --help: ${err.message}`);
|
|
404
|
+
const detected = detectCliFlags(cliName);
|
|
405
|
+
if (Object.keys(detected).length === 0) {
|
|
406
|
+
log.warn(`Could not detect flags for ${cliName}`);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const newCmd = rebuildCmd(cliName, model, detected);
|
|
410
|
+
log.info(`Rebuilt command: ${newCmd}`);
|
|
411
|
+
// Update config so next time we use the new flags
|
|
412
|
+
const roleKey = key;
|
|
413
|
+
if (cliName === role.cli && model === role.model) {
|
|
414
|
+
// Update primary role cmd
|
|
415
|
+
;
|
|
416
|
+
role.cmd = newCmd;
|
|
417
|
+
}
|
|
418
|
+
else if (role.fallback && cliName === role.fallback.cli && model === role.fallback.model) {
|
|
419
|
+
;
|
|
420
|
+
role.fallback.cmd = newCmd;
|
|
421
|
+
}
|
|
422
|
+
else if (this.config.fallback_global && cliName === this.config.fallback_global.cli && model === this.config.fallback_global.model) {
|
|
423
|
+
;
|
|
424
|
+
this.config.fallback_global.cmd = newCmd;
|
|
425
|
+
}
|
|
426
|
+
// Persist to .agent/config.json
|
|
427
|
+
try {
|
|
428
|
+
const configPath = path.join(this.projectDir, '.agent', 'config.json');
|
|
429
|
+
const savedConfig = await readJson(configPath);
|
|
430
|
+
// Update the specific role's cmd
|
|
431
|
+
if (savedConfig.roles && savedConfig.roles[roleKey]) {
|
|
432
|
+
if (cliName === savedConfig.roles[roleKey].cli && model === savedConfig.roles[roleKey].model) {
|
|
433
|
+
savedConfig.roles[roleKey].cmd = newCmd;
|
|
434
|
+
if (savedConfig.roles[roleKey].fallback?.cli === role.fallback?.cli) {
|
|
435
|
+
savedConfig.roles[roleKey].fallback.cmd = role.fallback?.cmd || '';
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else if (role.fallback && cliName === savedConfig.roles[roleKey].fallback?.cli) {
|
|
439
|
+
savedConfig.roles[roleKey].fallback.cmd = newCmd;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (this.config.fallback_global && savedConfig.fallback_global) {
|
|
443
|
+
if (cliName === savedConfig.fallback_global.cli) {
|
|
444
|
+
savedConfig.fallback_global.cmd = newCmd;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
await writeJson(configPath, savedConfig);
|
|
448
|
+
log.ok(`Updated .agent/config.json with new flags for ${cliName}`);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Config file might not exist yet
|
|
452
|
+
}
|
|
453
|
+
// Retry with new command
|
|
454
|
+
try {
|
|
455
|
+
const result = await runCli(newCmd, rolePrompt);
|
|
456
|
+
if (result.exitCode !== 0) {
|
|
457
|
+
const detail = result.output.trim().slice(0, 500);
|
|
458
|
+
throw new Error(`${cliName} (repaired) exited with code ${result.exitCode}${detail ? `\n${detail}` : ''}`);
|
|
459
|
+
}
|
|
460
|
+
log.ok(`${cliName} succeeded with repaired flags`);
|
|
461
|
+
return result.output;
|
|
462
|
+
}
|
|
463
|
+
catch (retryErr) {
|
|
464
|
+
log.warn(`Repaired ${cliName} also failed: ${retryErr.message}`);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
// Try primary
|
|
470
|
+
log.info(`Launching ${roleName}: ${role.cli} (${role.model})`);
|
|
471
|
+
const primaryResult = await tryWithAutoRepair(role.cli, role.model, role.cmd);
|
|
472
|
+
if (primaryResult !== null) {
|
|
473
|
+
trackTokens(primaryResult, role.cli, role.model);
|
|
474
|
+
return primaryResult;
|
|
475
|
+
}
|
|
476
|
+
// Try individual fallback
|
|
477
|
+
if (role.fallback) {
|
|
478
|
+
log.warn(`Trying individual fallback for ${roleName}: ${role.fallback.cli} (${role.fallback.model})`);
|
|
479
|
+
const fallbackResult = await tryWithAutoRepair(role.fallback.cli, role.fallback.model, role.fallback.cmd);
|
|
480
|
+
if (fallbackResult !== null) {
|
|
481
|
+
trackTokens(fallbackResult, role.fallback.cli, role.fallback.model);
|
|
482
|
+
return fallbackResult;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Try global fallback
|
|
486
|
+
if (this.config.fallback_global) {
|
|
487
|
+
const fb = this.config.fallback_global;
|
|
488
|
+
log.warn(`Trying global fallback: ${fb.cli} (${fb.model})`);
|
|
489
|
+
const globalResult = await tryWithAutoRepair(fb.cli, fb.model, fb.cmd);
|
|
490
|
+
if (globalResult !== null) {
|
|
491
|
+
trackTokens(globalResult, fb.cli, fb.model);
|
|
492
|
+
return globalResult;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// All options exhausted
|
|
496
|
+
throw new Error(`${roleName} failed: primary + individual fallback + global fallback all failed`);
|
|
497
|
+
}
|
|
498
|
+
buildRolePrompt(roleName, taskPrompt) {
|
|
499
|
+
// Role preambles are minimal — each role gets only what it needs.
|
|
500
|
+
// The actual task context is in taskPrompt.
|
|
501
|
+
const preambles = {
|
|
502
|
+
orchestrator: `ROL: ORCHESTRATOR — solo planifica, nunca implementa.
|
|
503
|
+
REGLA: Tu unica salida debe ser el JSON del plan. Sin explicaciones, sin texto extra.`,
|
|
504
|
+
implementor: `ROL: IMPLEMENTOR — solo ejecuta los steps del plan usando tus herramientas.
|
|
505
|
+
REGLA: Usa tus herramientas (read_file, write_file, run_shell_command, etc.) para ejecutar cada step.
|
|
506
|
+
REGLA: NO tomes decisiones de arquitectura. NO modifiques el plan.
|
|
507
|
+
REGLA: Al terminar, responde SOLO con el JSON de progreso indicado.`,
|
|
508
|
+
reviewer: `ROL: REVIEWER — solo valida, nunca modifica codigo.
|
|
509
|
+
REGLA: Usa tus herramientas (read_file, grep_search, etc.) para leer los archivos y verificar criterios.
|
|
510
|
+
REGLA: NO modifiques archivos. NO implementes correcciones.
|
|
511
|
+
REGLA: Al terminar, responde SOLO con el JSON de resultado indicado.`,
|
|
512
|
+
};
|
|
513
|
+
const preamble = preambles[roleName] || '';
|
|
514
|
+
return preamble ? `${preamble}\n\n${taskPrompt}` : taskPrompt;
|
|
515
|
+
}
|
|
516
|
+
async generateTaskId(task) {
|
|
517
|
+
const today = new Date().toISOString().split('T')[0];
|
|
518
|
+
const short = task.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 30);
|
|
519
|
+
let num = 1;
|
|
520
|
+
while (true) {
|
|
521
|
+
const id = `${today}_${String(num).padStart(3, '0')}_${short}`;
|
|
522
|
+
const dir = path.join(this.projectDir, '.agent', 'tasks', id);
|
|
523
|
+
if (!(await fileExists(dir)))
|
|
524
|
+
return id;
|
|
525
|
+
num++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** Context for the ORCHESTRATOR only — planning-relevant files */
|
|
529
|
+
async buildOrchestratorContext() {
|
|
530
|
+
let ctx = `Project: ${this.config.project}\nStack: ${this.config.stack}\n`;
|
|
531
|
+
ctx += `Approved dirs: ${this.config.structure.approved_dirs.join(', ')}\n`;
|
|
532
|
+
ctx += `Forbidden dirs: ${this.config.structure.forbidden_dirs.join(', ')}\n`;
|
|
533
|
+
// Only planning-relevant docs — no architecture.md (that's built by previous tasks)
|
|
534
|
+
const planningFiles = ['.agent/INDEX.md', '.agent/rules/structure.md', '.agent/rules/patterns.md'];
|
|
535
|
+
for (const f of planningFiles) {
|
|
536
|
+
const fp = path.join(this.projectDir, f);
|
|
537
|
+
if (await fileExists(fp)) {
|
|
538
|
+
const content = await readFile(fp);
|
|
539
|
+
ctx += `\n--- ${f} ---\n${content.slice(0, 5000)}\n`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return ctx;
|
|
543
|
+
}
|
|
544
|
+
/** Minimal context for COORDINATOR clarification only */
|
|
545
|
+
async buildCoordinatorContext() {
|
|
546
|
+
let ctx = `Project: ${this.config.project}\nStack: ${this.config.stack}\n`;
|
|
547
|
+
const indexPath = path.join(this.projectDir, '.agent', 'INDEX.md');
|
|
548
|
+
if (await fileExists(indexPath)) {
|
|
549
|
+
const content = await readFile(indexPath);
|
|
550
|
+
ctx += `\n--- INDEX ---\n${content.slice(0, 2000)}\n`;
|
|
551
|
+
}
|
|
552
|
+
return ctx;
|
|
553
|
+
}
|
|
554
|
+
async runOrchestrator(task) {
|
|
555
|
+
const taskId = await this.generateTaskId(task);
|
|
556
|
+
const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
|
|
557
|
+
log.phase(1, 'Planificacion', this.config.roles.orchestrator.cli, this.config.roles.orchestrator.model);
|
|
558
|
+
const context = await this.buildOrchestratorContext();
|
|
559
|
+
const prompt = `TAREA: ${task}
|
|
560
|
+
TASK_ID: ${taskId}
|
|
561
|
+
DIRECTORIO_TRABAJO: ${this.projectDir}
|
|
562
|
+
|
|
563
|
+
CONTEXTO DEL PROYECTO:
|
|
564
|
+
${context}
|
|
565
|
+
|
|
566
|
+
INSTRUCCIONES:
|
|
567
|
+
1. Analiza la tarea y el contexto del proyecto.
|
|
568
|
+
2. Descompone en steps concretos y verificables.
|
|
569
|
+
3. Define acceptance_criteria medibles (cada criterio debe poder verificarse leyendo un archivo).
|
|
570
|
+
4. Responde SOLO con este JSON exacto, sin texto adicional:
|
|
571
|
+
|
|
572
|
+
{"plan":{"task_id":"${taskId}","description":"${task}","steps":[{"num":1,"description":"descripcion concreta del paso","files":["archivo/a/crear.ts"],"status":"pending"}],"acceptance_criteria":["criterio verificable"],"deliberation":{"needed":false,"question":""}}}`;
|
|
573
|
+
const res = await this.runWithFallback('orchestrator', prompt, 'Planificacion');
|
|
574
|
+
// Extract text content (handles streaming CLI formats like qwen/opencode)
|
|
575
|
+
const text = extractCliText(res);
|
|
576
|
+
try {
|
|
577
|
+
const json = extractJson(text);
|
|
578
|
+
const plan = (json.plan || json);
|
|
579
|
+
// Validate required fields
|
|
580
|
+
if (!plan.task_id || !Array.isArray(plan.steps) || !Array.isArray(plan.acceptance_criteria)) {
|
|
581
|
+
throw new Error('Missing required fields in plan');
|
|
582
|
+
}
|
|
583
|
+
// Engine constructs progress — orchestrator is never responsible for this
|
|
584
|
+
const progress = {
|
|
585
|
+
task_id: taskId,
|
|
586
|
+
created_at: new Date().toISOString(),
|
|
587
|
+
steps: plan.steps.map((s) => ({ num: s.num, description: s.description, files: s.files || [], status: 'pending' })),
|
|
588
|
+
status: 'planned',
|
|
589
|
+
};
|
|
590
|
+
await writeJson(path.join(taskDir, 'plan.json'), plan);
|
|
591
|
+
await writeJson(path.join(taskDir, 'progress.json'), progress);
|
|
592
|
+
await writeFile(path.join(taskDir, 'request.md'), `# Request\n\n${task}\n`);
|
|
593
|
+
log.ok('plan.json generated');
|
|
594
|
+
log.ok('progress.json generated');
|
|
595
|
+
log.ok('request.md generated');
|
|
596
|
+
log.info(`Steps: ${plan.steps.length}`);
|
|
597
|
+
for (const step of plan.steps) {
|
|
598
|
+
log.info(` Step ${step.num}: ${step.description}`);
|
|
599
|
+
}
|
|
600
|
+
log.info(`Acceptance criteria: ${plan.acceptance_criteria.length}`);
|
|
601
|
+
for (const ac of plan.acceptance_criteria) {
|
|
602
|
+
log.info(` - ${ac}`);
|
|
603
|
+
}
|
|
604
|
+
if (plan.deliberation?.needed)
|
|
605
|
+
log.warn(`Deliberation needed: ${plan.deliberation.question}`);
|
|
606
|
+
return { taskId, plan };
|
|
607
|
+
}
|
|
608
|
+
catch (e) {
|
|
609
|
+
log.error('Failed to parse orchestrator response');
|
|
610
|
+
log.error(`Detail: ${e.message}`);
|
|
611
|
+
log.error('Raw output (first 500 chars):');
|
|
612
|
+
log.info(text.slice(0, 500));
|
|
613
|
+
throw new Error(`Invalid orchestrator response: ${e.message}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async runDeliberation(taskId, plan) {
|
|
617
|
+
if (!this.config.deliberation || !plan.deliberation.needed)
|
|
618
|
+
return;
|
|
619
|
+
const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
|
|
620
|
+
const debateDir = path.join(taskDir, 'debate');
|
|
621
|
+
const maxRounds = this.config.deliberation.max_rounds;
|
|
622
|
+
const question = plan.deliberation.question;
|
|
623
|
+
const pRole = this.config.deliberation.proposer;
|
|
624
|
+
const cRole = this.config.deliberation.critic;
|
|
625
|
+
log.section('Deliberation');
|
|
626
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
627
|
+
log.info(`Round ${round}/${maxRounds}`);
|
|
628
|
+
const propPrompt = `TAREA: ${plan.description}\nPREGUNTA: ${question}\nGenera propuesta tecnica. Responde SOLO JSON: {"proposal":"...","justification":"..."}`;
|
|
629
|
+
const propRes = await runCli(pRole.cmd, propPrompt);
|
|
630
|
+
await writeFile(path.join(debateDir, `round-${round}-proposal.md`), propRes.output);
|
|
631
|
+
const critPrompt = `PROPUESTA: ${propRes.output}\nAnaliza y responde SOLO JSON: {"analysis":"...","verdict":"ACCEPT"}`;
|
|
632
|
+
const critRes = await runCli(cRole.cmd, critPrompt);
|
|
633
|
+
await writeFile(path.join(debateDir, `round-${round}-critique.md`), critRes.output);
|
|
634
|
+
if (critRes.output.includes('ACCEPT')) {
|
|
635
|
+
log.ok(`Accepted in round ${round}`);
|
|
636
|
+
plan.deliberation.resolved = true;
|
|
637
|
+
plan.deliberation.rounds = round;
|
|
638
|
+
await writeJson(path.join(taskDir, 'plan.json'), plan);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
plan.deliberation.resolved = true;
|
|
643
|
+
plan.deliberation.rounds = maxRounds;
|
|
644
|
+
await writeJson(path.join(taskDir, 'plan.json'), plan);
|
|
645
|
+
}
|
|
646
|
+
async runImplementor(taskId, plan) {
|
|
647
|
+
const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
|
|
648
|
+
log.phase(2, 'Implementacion', this.config.roles.implementor.cli, this.config.roles.implementor.model);
|
|
649
|
+
const structurePath = path.join(this.projectDir, '.agent', 'rules', 'structure.md');
|
|
650
|
+
let structureRules = '';
|
|
651
|
+
if (await fileExists(structurePath)) {
|
|
652
|
+
structureRules = await readFile(structurePath);
|
|
653
|
+
}
|
|
654
|
+
// Build steps list — implementor gets ONLY steps and structure rules (no acceptance_criteria, no deliberation)
|
|
655
|
+
const stepsText = plan.steps.map((s) => `Step ${s.num}: ${s.description}${s.files?.length ? `\n Archivos: ${s.files.join(', ')}` : ''}`).join('\n');
|
|
656
|
+
const prompt = `TAREA: ${plan.description}
|
|
657
|
+
DIRECTORIO_TRABAJO: ${this.projectDir}
|
|
658
|
+
|
|
659
|
+
STEPS A EJECUTAR EN ORDEN:
|
|
660
|
+
${stepsText}
|
|
661
|
+
|
|
662
|
+
${structureRules ? `REGLAS DE ESTRUCTURA:\n${structureRules}\n` : ''}
|
|
663
|
+
INSTRUCCIONES:
|
|
664
|
+
1. Ejecuta cada step EN ORDEN usando tus herramientas (read_file, write_file, run_shell_command, etc.)
|
|
665
|
+
2. Para cada step: lee lo necesario, luego crea/modifica los archivos indicados.
|
|
666
|
+
3. NO tomes decisiones de arquitectura fuera del plan.
|
|
667
|
+
4. Al terminar todos los steps, responde SOLO con este JSON:
|
|
668
|
+
|
|
669
|
+
{"status":"completed","steps_done":[1,2,3],"notes":"resumen breve de lo que se hizo"}`;
|
|
670
|
+
const res = await this.runWithFallback('implementor', prompt, 'Implementacion');
|
|
671
|
+
// Extract text from streaming CLI output (qwen/claude return event arrays)
|
|
672
|
+
const text = extractCliText(res);
|
|
673
|
+
let progress;
|
|
674
|
+
try {
|
|
675
|
+
const json = extractJson(text);
|
|
676
|
+
progress = (json.progress || json);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// CLI executed the task but didn't return structured JSON — build synthetic progress
|
|
680
|
+
log.warn('Implementor did not return JSON progress — building from plan');
|
|
681
|
+
const now = new Date().toISOString();
|
|
682
|
+
progress = {
|
|
683
|
+
task_id: taskId,
|
|
684
|
+
created_at: now,
|
|
685
|
+
status: 'completed',
|
|
686
|
+
steps: plan.steps.map((s) => ({ ...s, status: 'completed', completed_at: now })),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
await writeJson(path.join(taskDir, 'progress.json'), progress);
|
|
690
|
+
log.ok('progress.json updated');
|
|
691
|
+
return progress;
|
|
692
|
+
}
|
|
693
|
+
async runReviewer(taskId, plan, progress) {
|
|
694
|
+
const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
|
|
695
|
+
log.phase(3, 'Review', this.config.roles.reviewer.cli, this.config.roles.reviewer.model);
|
|
696
|
+
// Collect all files the implementor should have created/modified
|
|
697
|
+
const targetFiles = [...new Set(plan.steps.flatMap((s) => s.files || []))];
|
|
698
|
+
const prompt = `TAREA: ${plan.description}
|
|
699
|
+
DIRECTORIO_TRABAJO: ${this.projectDir}
|
|
700
|
+
|
|
701
|
+
ACCEPTANCE CRITERIA A VERIFICAR:
|
|
702
|
+
${plan.acceptance_criteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}
|
|
703
|
+
|
|
704
|
+
ARCHIVOS A REVISAR:
|
|
705
|
+
${targetFiles.length ? targetFiles.map((f) => `- ${f}`).join('\n') : '(revisar archivos creados por el implementor segun los steps)'}
|
|
706
|
+
|
|
707
|
+
STEPS EJECUTADOS:
|
|
708
|
+
${plan.steps.map((s) => `Step ${s.num}: ${s.description}`).join('\n')}
|
|
709
|
+
|
|
710
|
+
INSTRUCCIONES:
|
|
711
|
+
1. Usa read_file/grep_search para leer cada archivo listado en ARCHIVOS A REVISAR.
|
|
712
|
+
2. Para cada criterio de ACCEPTANCE CRITERIA: verifica si se cumple en el codigo real leido.
|
|
713
|
+
3. Un criterio es PASS solo si hay evidencia directa en los archivos leidos. Si no pudiste leer el archivo, es FAIL.
|
|
714
|
+
4. NO modifiques nada. NO implementes correcciones.
|
|
715
|
+
5. Al terminar, responde SOLO con este JSON:
|
|
716
|
+
|
|
717
|
+
{"result":{"verdict":"PASS","criteria_results":[{"criteria":"criterio exacto","status":"PASS","notes":"evidencia encontrada"}],"explanation":"resumen"}}`;
|
|
718
|
+
const res = await this.runWithFallback('reviewer', prompt, 'Review');
|
|
719
|
+
// Extract text from streaming CLI output
|
|
720
|
+
const text = extractCliText(res);
|
|
721
|
+
let verdict = 'PASS';
|
|
722
|
+
let md = `# Result - ${taskId}\n\n`;
|
|
723
|
+
try {
|
|
724
|
+
const json = extractJson(text);
|
|
725
|
+
const result = json.result || json;
|
|
726
|
+
verdict = result.verdict || 'PASS';
|
|
727
|
+
const criteriaResults = result.criteria_results || [];
|
|
728
|
+
md += `**Verdict: ${verdict}**\n\n## Criteria\n\n`;
|
|
729
|
+
for (const cr of criteriaResults) {
|
|
730
|
+
const icon = cr.status === 'PASS' ? '✓' : '✗';
|
|
731
|
+
md += `${icon} ${cr.criteria}${cr.notes ? ` — ${cr.notes}` : ''}\n`;
|
|
732
|
+
}
|
|
733
|
+
md += `\n## Explanation\n\n${result.explanation || ''}\n`;
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
// Reviewer ran but didn't return structured JSON — use raw text as explanation
|
|
737
|
+
log.warn('Reviewer did not return JSON — using raw output');
|
|
738
|
+
// Check if text contains FAIL/PASS keywords
|
|
739
|
+
if (text.toLowerCase().includes('fail'))
|
|
740
|
+
verdict = 'FAIL';
|
|
741
|
+
md += `**Verdict: ${verdict}**\n\n## Review\n\n${text}\n`;
|
|
742
|
+
}
|
|
743
|
+
await writeFile(path.join(taskDir, 'result.md'), md);
|
|
744
|
+
progress.validation = { status: verdict, reviewed_at: new Date().toISOString(), reviewer: this.config.roles.reviewer.model };
|
|
745
|
+
await writeJson(path.join(taskDir, 'progress.json'), progress);
|
|
746
|
+
log.verdict(verdict);
|
|
747
|
+
return { verdict };
|
|
748
|
+
}
|
|
749
|
+
async runFullCycle(task) {
|
|
750
|
+
// Header is now shown by the REPL before the first user message
|
|
751
|
+
// ══════════════════════════════════════════════════
|
|
752
|
+
// FASE 0 — Clarificacion (Coordinador ↔ Programador)
|
|
753
|
+
// ══════════════════════════════════════════════════
|
|
754
|
+
const refinedTask = await this.runClarification(task);
|
|
755
|
+
// ══════════════════════════════════════════════════
|
|
756
|
+
// FASE 1 — Planificacion (Orchestrator)
|
|
757
|
+
// ══════════════════════════════════════════════════
|
|
758
|
+
let taskId;
|
|
759
|
+
let plan;
|
|
760
|
+
while (true) {
|
|
761
|
+
try {
|
|
762
|
+
log.section('FASE 1 — Planificacion');
|
|
763
|
+
const result = await this.runOrchestrator(refinedTask);
|
|
764
|
+
taskId = result.taskId;
|
|
765
|
+
plan = result.plan;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
log.error(`Orchestrator fallo: ${err.message}`);
|
|
770
|
+
console.log('');
|
|
771
|
+
const action = await ask(' Que hacemos? (r=reintentar / s=saltar / c=cancelar): ', this.rl, this.fi);
|
|
772
|
+
if (action.toLowerCase() === 'r')
|
|
773
|
+
continue;
|
|
774
|
+
if (action.toLowerCase() === 's') {
|
|
775
|
+
log.info('Planificacion saltada.');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
log.info('Ciclo cancelado.');
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Deliberation opcional
|
|
783
|
+
if (plan.deliberation.needed && this.config.deliberation) {
|
|
784
|
+
const a = await ask('\n El plan requiere deliberacion. Ejecutar? (y/N): ', this.rl, this.fi);
|
|
785
|
+
if (a.toLowerCase() === 'y')
|
|
786
|
+
await this.runDeliberation(taskId, plan);
|
|
787
|
+
}
|
|
788
|
+
// ══════════════════════════════════════════════════
|
|
789
|
+
// COORDINADOR: Mostrar plan y preguntar
|
|
790
|
+
// ══════════════════════════════════════════════════
|
|
791
|
+
log.divider();
|
|
792
|
+
log.ok(`Plan generado: .agent/tasks/${taskId}/plan.json`);
|
|
793
|
+
log.info(`Steps: ${plan.steps.length} | Criteria: ${plan.acceptance_criteria.length}`);
|
|
794
|
+
const confirm1 = await ask('\n El plan esta listo. Ejecutar implementador? (y/N): ', this.rl, this.fi);
|
|
795
|
+
if (confirm1.toLowerCase() !== 'y') {
|
|
796
|
+
log.info('Implementacion cancelada por el usuario.');
|
|
797
|
+
log.info(`Plan guardado en: .agent/tasks/${taskId}/`);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
// ══════════════════════════════════════════════════
|
|
801
|
+
// FASE 2 — Implementacion (Implementor)
|
|
802
|
+
// ══════════════════════════════════════════════════
|
|
803
|
+
let progress;
|
|
804
|
+
while (true) {
|
|
805
|
+
try {
|
|
806
|
+
log.section('FASE 2 — Implementacion');
|
|
807
|
+
progress = await this.runImplementor(taskId, plan);
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
log.error(`Implementor fallo: ${err.message}`);
|
|
812
|
+
console.log('');
|
|
813
|
+
const action = await ask(' Que hacemos? (r=reintentar / f=feedback / c=cancelar): ', this.rl, this.fi);
|
|
814
|
+
if (action.toLowerCase() === 'r')
|
|
815
|
+
continue;
|
|
816
|
+
if (action.toLowerCase() === 'f') {
|
|
817
|
+
const feedback = await ask(' Tu feedback: ', this.rl, this.fi);
|
|
818
|
+
log.info(`Feedback registrado: ${feedback}`);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
log.info('Ciclo cancelado.');
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// ══════════════════════════════════════════════════
|
|
826
|
+
// COORDINADOR: Verificar y preguntar
|
|
827
|
+
// ══════════════════════════════════════════════════
|
|
828
|
+
log.divider();
|
|
829
|
+
log.ok('Implementacion completada.');
|
|
830
|
+
const confirm2 = await ask('\n Ejecutar reviewer para validar? (y/N): ', this.rl, this.fi);
|
|
831
|
+
if (confirm2.toLowerCase() !== 'y') {
|
|
832
|
+
log.info('Review cancelado por el usuario.');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
// ══════════════════════════════════════════════════
|
|
836
|
+
// FASE 3 — Review (Reviewer)
|
|
837
|
+
// ══════════════════════════════════════════════════
|
|
838
|
+
let review;
|
|
839
|
+
while (true) {
|
|
840
|
+
try {
|
|
841
|
+
log.section('FASE 3 — Review');
|
|
842
|
+
review = await this.runReviewer(taskId, plan, progress);
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
log.error(`Reviewer fallo: ${err.message}`);
|
|
847
|
+
const action = await ask(' Que hacemos? (r=reintentar / c=cancelar): ', this.rl, this.fi);
|
|
848
|
+
if (action.toLowerCase() === 'r')
|
|
849
|
+
continue;
|
|
850
|
+
log.info('Review cancelado.');
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Si FAIL, volver al programador
|
|
855
|
+
if (review.verdict === 'FAIL') {
|
|
856
|
+
log.warn('Review resulto FAIL.');
|
|
857
|
+
const action = await ask(' Que hacemos? (r=re-implementar / c=cancelar): ', this.rl, this.fi);
|
|
858
|
+
if (action.toLowerCase() === 'r') {
|
|
859
|
+
const feedback = await ask(' Feedback adicional para el implementor: ', this.rl, this.fi);
|
|
860
|
+
// TODO: re-run implementor with feedback
|
|
861
|
+
log.info(`Feedback: ${feedback}`);
|
|
862
|
+
log.info('Re-implementacion pendiente — usa /run impl para continuar.');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// ══════════════════════════════════════════════════
|
|
866
|
+
// FASE 4 — Reporte final (Coordinador)
|
|
867
|
+
// ══════════════════════════════════════════════════
|
|
868
|
+
log.section('FASE 4 — Reporte Final');
|
|
869
|
+
log.header(`TAREA: ${taskId}`);
|
|
870
|
+
log.verdict(review.verdict);
|
|
871
|
+
// Tabla de tokens por fase
|
|
872
|
+
if (this.phaseTokens.length > 0) {
|
|
873
|
+
console.log('');
|
|
874
|
+
console.log(' Tokens por fase:');
|
|
875
|
+
console.log(' ┌────────────────────┬────────────┬────────────────┬────────────┬──────────┬──────────┐');
|
|
876
|
+
console.log(' │ Fase │ CLI │ Modelo │ Rol │ Input │ Output │');
|
|
877
|
+
console.log(' ├────────────────────┼────────────┼────────────────┼────────────┼──────────┼──────────┤');
|
|
878
|
+
for (const pt of this.phaseTokens) {
|
|
879
|
+
const phase = pt.phase.padEnd(18);
|
|
880
|
+
const cli = pt.cli.padEnd(10);
|
|
881
|
+
const model = pt.model.padEnd(14);
|
|
882
|
+
const role = pt.role.padEnd(10);
|
|
883
|
+
const input = String(pt.input).padStart(8);
|
|
884
|
+
const output = String(pt.output).padStart(8);
|
|
885
|
+
console.log(` │ ${phase} │ ${cli} │ ${model} │ ${role} │ ${input} │ ${output} │`);
|
|
886
|
+
}
|
|
887
|
+
console.log(' ├────────────────────┼────────────┼────────────────┼────────────┼──────────┼──────────┤');
|
|
888
|
+
const totalIn = String(this.totalTokens.input_tokens).padStart(8);
|
|
889
|
+
const totalOut = String(this.totalTokens.output_tokens).padStart(8);
|
|
890
|
+
console.log(` │ ${'TOTAL'.padEnd(18)} │ ${''.padEnd(10)} │ ${''.padEnd(14)} │ ${''.padEnd(10)} │ ${totalIn} │ ${totalOut} │`);
|
|
891
|
+
console.log(' └────────────────────┴────────────┴────────────────┴────────────┴──────────┴──────────┘');
|
|
892
|
+
}
|
|
893
|
+
// Generar tokens.md
|
|
894
|
+
const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
|
|
895
|
+
let tokensMd = `# Tokens - ${taskId}\n\n| Fase | CLI | Modelo | Rol | Input | Output |\n|------|-----|--------|-----|-------|--------|\n`;
|
|
896
|
+
for (const pt of this.phaseTokens) {
|
|
897
|
+
tokensMd += `| ${pt.phase} | ${pt.cli} | ${pt.model} | ${pt.role} | ${pt.input} | ${pt.output} |\n`;
|
|
898
|
+
}
|
|
899
|
+
tokensMd += `| **TOTAL** | | | | **${this.totalTokens.input_tokens}** | **${this.totalTokens.output_tokens}** |\n`;
|
|
900
|
+
await writeFile(path.join(taskDir, 'tokens.md'), tokensMd);
|
|
901
|
+
log.divider();
|
|
902
|
+
log.info(`Archivos: .agent/tasks/${taskId}/`);
|
|
903
|
+
log.info(' plan.json | progress.json | result.md | tokens.md | request.md');
|
|
904
|
+
}
|
|
905
|
+
}
|