@liriraid/agentflow-ai 1.0.13 → 1.0.15
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/agentflow.mjs +103 -3
- package/orchestrator.js +393 -113
- package/package.json +1 -1
- package/scripts/auto-trigger.js +114 -0
- package/scripts/monitor-check.js +172 -0
- package/src/ink/app.mjs +22 -14
- package/src/ink/index.mjs +23 -2
- package/templates/en/ORCHESTRATOR.md +53 -28
- package/templates/en/orchestrator.config.json +3 -3
- package/templates/es/ORCHESTRATOR.md +61 -22
- package/templates/es/orchestrator.config.json +5 -5
package/orchestrator.js
CHANGED
|
@@ -129,6 +129,36 @@ const PROJECT_NAME = config.projectName || "Orchestrator Multi-Agents";
|
|
|
129
129
|
const WORKSPACE_LANGUAGE = ["en", "es"].includes(config.workspaceLanguage)
|
|
130
130
|
? config.workspaceLanguage
|
|
131
131
|
: "es";
|
|
132
|
+
|
|
133
|
+
// OpenAI model pricing ($ per 1M tokens) — update when prices change
|
|
134
|
+
const OPENAI_MODEL_PRICING = {
|
|
135
|
+
"gpt-4.1": { input: 2.0, output: 8.0 },
|
|
136
|
+
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
|
|
137
|
+
"gpt-4.1-nano": { input: 0.1, output: 0.4 },
|
|
138
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
139
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
140
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
141
|
+
"o3": { input: 10.0, output: 40.0 },
|
|
142
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
143
|
+
"o1": { input: 15.0, output: 60.0 },
|
|
144
|
+
"o1-mini": { input: 3.0, output: 12.0 },
|
|
145
|
+
"gpt-5": { input: 5.0, output: 20.0 },
|
|
146
|
+
"gpt-5.5": { input: 5.0, output: 20.0 },
|
|
147
|
+
"codex-mini-latest": { input: 1.5, output: 6.0 },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function calcOpenAICost(model, usage) {
|
|
151
|
+
if (!model || !usage) return null;
|
|
152
|
+
const modelLower = String(model).toLowerCase();
|
|
153
|
+
const key = Object.keys(OPENAI_MODEL_PRICING).find(k =>
|
|
154
|
+
modelLower === k || modelLower.startsWith(k) || modelLower.includes(k)
|
|
155
|
+
);
|
|
156
|
+
if (!key) return null;
|
|
157
|
+
const price = OPENAI_MODEL_PRICING[key];
|
|
158
|
+
const inputTokens = usage.input_tokens || usage.prompt_tokens || 0;
|
|
159
|
+
const outputTokens = usage.output_tokens || usage.completion_tokens || 0;
|
|
160
|
+
return (inputTokens * price.input + outputTokens * price.output) / 1_000_000;
|
|
161
|
+
}
|
|
132
162
|
const TEXT = {
|
|
133
163
|
es: {
|
|
134
164
|
configExists:
|
|
@@ -160,6 +190,8 @@ const TEXT = {
|
|
|
160
190
|
running: "EJECUTANDO",
|
|
161
191
|
busy: "OCUPADO",
|
|
162
192
|
idle: "EN ESPERA",
|
|
193
|
+
failed: "FALLÓ",
|
|
194
|
+
retrying: "REINTENTANDO",
|
|
163
195
|
queueReloaded: (count) => `Cola recargada: ${count} tareas`,
|
|
164
196
|
quitRequested: "Cierre solicitado desde Ink",
|
|
165
197
|
starting: (name) => `${name} iniciando`,
|
|
@@ -170,10 +202,68 @@ const TEXT = {
|
|
|
170
202
|
empty: "(vacía)",
|
|
171
203
|
after: "después de",
|
|
172
204
|
quotaLimit: "LÍMITE DE CUOTA",
|
|
173
|
-
retryAt: (time, remaining) =>
|
|
174
|
-
`reintenta a las ${time} (${remaining} min)`,
|
|
205
|
+
retryAt: (time, remaining) => `reintenta a las ${time} (${remaining} min)`,
|
|
175
206
|
log: "REGISTRO",
|
|
176
207
|
controls: "Seguir Pausa Recargar Quitar",
|
|
208
|
+
// QUEUE.md section headers
|
|
209
|
+
sectionPending: "## Pendientes",
|
|
210
|
+
sectionInProgress: "## En progreso",
|
|
211
|
+
sectionCompleted: "## Completadas",
|
|
212
|
+
// appendToAgent messages
|
|
213
|
+
agentTaskHeader: (id, title) => `=== ${id}: ${title} ===`,
|
|
214
|
+
agentCwd: (dir) => `CWD: ${dir}`,
|
|
215
|
+
agentCompleted: (elapsed, cost) => `=== COMPLETADA en ${elapsed}${cost} ===`,
|
|
216
|
+
agentFailed: (code, attempt) => `=== FALLÓ (salida ${code}, intento ${attempt}) ===`,
|
|
217
|
+
agentRateLimit: (resetStr) => `=== LÍMITE DE CUOTA (${resetStr}) ===`,
|
|
218
|
+
agentTimeout: "=== TIMEOUT ===",
|
|
219
|
+
agentDied: "=== PROCESS DIED ===",
|
|
220
|
+
agentReassigned: (agent, reason) => `=== REASIGNADA A ${agent} (${reason}) ===`,
|
|
221
|
+
// ag.lastLine state machine prefixes (used for status detection)
|
|
222
|
+
lastCompleted: (id) => `Última: ${id} completada`,
|
|
223
|
+
lastRetry: (id) => `REINTENTO: ${id}`,
|
|
224
|
+
lastLimit: (id, time) => `LÍMITE: ${id} (reintento a las ${time})`,
|
|
225
|
+
lastFailed: (id) => `FALLÓ: ${id}`,
|
|
226
|
+
// Fallback reasons
|
|
227
|
+
reasonQuota: "cuota o límite agotado",
|
|
228
|
+
reasonProvider: "proveedor o sesión no disponibles",
|
|
229
|
+
reasonNoWork: "el agente no trabajó nada (sin cambios)",
|
|
230
|
+
reasonPersistent: "fallo persistente",
|
|
231
|
+
// Log messages
|
|
232
|
+
logRateLimit: (agent, id, resetStr) => `${agent} alcanzó el límite en ${id} (${resetStr})`,
|
|
233
|
+
logFail: (agent, id, code, retries, max) => `${agent} falló ${id} (salida ${code}, ${retries}/${max})`,
|
|
234
|
+
logDone: (agent, id, elapsed, cost) => `${agent} completó ${id} en ${elapsed}${cost}`,
|
|
235
|
+
logFallback: (id, from, to, reason) => `${id} reasignada de ${from} a ${to} (${reason})`,
|
|
236
|
+
logReassignWarn: (id, agent) => `${id} reasignada a ${agent}, pero QUEUE.md no pudo actualizarse`,
|
|
237
|
+
logTimeout: (agent, id) => `${agent} timed out on ${id}`,
|
|
238
|
+
logUnknownAgent: (id, agent) => `${id} skipped — agente "${agent}" no definido en orchestrator.config.json`,
|
|
239
|
+
logPermanentFail: (id, retries) => `${id} falló definitivamente tras ${retries} intentos`,
|
|
240
|
+
logDied: (agent, id) => `${agent} terminó silenciosamente en ${id}`,
|
|
241
|
+
// STATUS.md
|
|
242
|
+
statusTitle: (ts) => `# Estado del Orquestador - ${ts}`,
|
|
243
|
+
statusProject: "**Proyecto:**",
|
|
244
|
+
statusState: "**Estado:**",
|
|
245
|
+
statusRunning: "EJECUTANDO",
|
|
246
|
+
statusPausedLabel: "PAUSADO",
|
|
247
|
+
statusActiveTime: "**Activo:**",
|
|
248
|
+
statusSectionAgents: "## Agentes",
|
|
249
|
+
statusSectionQueue: "## Cola",
|
|
250
|
+
statusPendingLabel: "Pendientes:",
|
|
251
|
+
statusCompletedLabel: "Completadas:",
|
|
252
|
+
statusInProgressLabel: "En progreso:",
|
|
253
|
+
statusInProgressHeader: "### En progreso",
|
|
254
|
+
statusAgentBusy: "🟡 OCUPADO",
|
|
255
|
+
statusAgentIdle: "⚪ EN ESPERA",
|
|
256
|
+
statusNoTask: "Sin tarea",
|
|
257
|
+
// INBOX.md
|
|
258
|
+
inboxDone: (ts, id, agent) => `## [${ts}] ${id} completada — ${agent}`,
|
|
259
|
+
inboxTaskLabel: "- **Tarea:**",
|
|
260
|
+
inboxDurationLabel: "- **Duración:**",
|
|
261
|
+
inboxReportLabel: "- **Reporte:**",
|
|
262
|
+
inboxActionLabel: (file) => `- **Acción:** Lee \`${file}\` y crea las siguientes TASKs si corresponde.`,
|
|
263
|
+
inboxFailed: (ts, id, from, to) => `## [${ts}] ${id} falló — ${from} → reasignada a ${to}`,
|
|
264
|
+
inboxReasonLabel: "- **Motivo:**",
|
|
265
|
+
inboxNewAgentLabel: "- **Nuevo agente:**",
|
|
266
|
+
inboxFailAction: "- **Acción:** La TUI reasignó automáticamente. Verifica en QUEUE.md o espera la siguiente notificación de completada.",
|
|
177
267
|
},
|
|
178
268
|
en: {
|
|
179
269
|
configExists:
|
|
@@ -205,6 +295,8 @@ const TEXT = {
|
|
|
205
295
|
running: "RUNNING",
|
|
206
296
|
busy: "BUSY",
|
|
207
297
|
idle: "IDLE",
|
|
298
|
+
failed: "FAILED",
|
|
299
|
+
retrying: "RETRYING",
|
|
208
300
|
queueReloaded: (count) => `Queue reloaded: ${count} tasks`,
|
|
209
301
|
quitRequested: "Quit requested from Ink",
|
|
210
302
|
starting: (name) => `${name} starting`,
|
|
@@ -217,10 +309,79 @@ const TEXT = {
|
|
|
217
309
|
retryAt: (time, remaining) => `retry at ${time} (${remaining} min)`,
|
|
218
310
|
log: "LOG",
|
|
219
311
|
controls: "Start Pause Reload Quit",
|
|
312
|
+
// QUEUE.md section headers
|
|
313
|
+
sectionPending: "## Pending",
|
|
314
|
+
sectionInProgress: "## In Progress",
|
|
315
|
+
sectionCompleted: "## Completed",
|
|
316
|
+
// appendToAgent messages
|
|
317
|
+
agentTaskHeader: (id, title) => `=== ${id}: ${title} ===`,
|
|
318
|
+
agentCwd: (dir) => `CWD: ${dir}`,
|
|
319
|
+
agentCompleted: (elapsed, cost) => `=== COMPLETED in ${elapsed}${cost} ===`,
|
|
320
|
+
agentFailed: (code, attempt) => `=== FAILED (exit ${code}, attempt ${attempt}) ===`,
|
|
321
|
+
agentRateLimit: (resetStr) => `=== QUOTA LIMIT (${resetStr}) ===`,
|
|
322
|
+
agentTimeout: "=== TIMEOUT ===",
|
|
323
|
+
agentDied: "=== PROCESS DIED ===",
|
|
324
|
+
agentReassigned: (agent, reason) => `=== REASSIGNED TO ${agent} (${reason}) ===`,
|
|
325
|
+
// ag.lastLine state machine prefixes
|
|
326
|
+
lastCompleted: (id) => `Last: ${id} completed`,
|
|
327
|
+
lastRetry: (id) => `RETRY: ${id}`,
|
|
328
|
+
lastLimit: (id, time) => `LIMIT: ${id} (retry at ${time})`,
|
|
329
|
+
lastFailed: (id) => `FAILED: ${id}`,
|
|
330
|
+
// Fallback reasons
|
|
331
|
+
reasonQuota: "quota or rate limit exhausted",
|
|
332
|
+
reasonProvider: "provider or session unavailable",
|
|
333
|
+
reasonNoWork: "agent did no real work (no changes)",
|
|
334
|
+
reasonPersistent: "persistent failure",
|
|
335
|
+
// Log messages
|
|
336
|
+
logRateLimit: (agent, id, resetStr) => `${agent} hit rate limit on ${id} (${resetStr})`,
|
|
337
|
+
logFail: (agent, id, code, retries, max) => `${agent} failed ${id} (exit ${code}, ${retries}/${max})`,
|
|
338
|
+
logDone: (agent, id, elapsed, cost) => `${agent} completed ${id} in ${elapsed}${cost}`,
|
|
339
|
+
logFallback: (id, from, to, reason) => `${id} reassigned from ${from} to ${to} (${reason})`,
|
|
340
|
+
logReassignWarn: (id, agent) => `${id} reassigned to ${agent}, but QUEUE.md could not be updated`,
|
|
341
|
+
logTimeout: (agent, id) => `${agent} timed out on ${id}`,
|
|
342
|
+
logUnknownAgent: (id, agent) => `${id} skipped — agent "${agent}" not in orchestrator.config.json`,
|
|
343
|
+
logPermanentFail: (id, retries) => `${id} permanently failed after ${retries} attempts`,
|
|
344
|
+
logDied: (agent, id) => `${agent} died silently on ${id}`,
|
|
345
|
+
// STATUS.md
|
|
346
|
+
statusTitle: (ts) => `# Orchestrator Status - ${ts}`,
|
|
347
|
+
statusProject: "**Project:**",
|
|
348
|
+
statusState: "**State:**",
|
|
349
|
+
statusRunning: "RUNNING",
|
|
350
|
+
statusPausedLabel: "PAUSED",
|
|
351
|
+
statusActiveTime: "**Active:**",
|
|
352
|
+
statusSectionAgents: "## Agents",
|
|
353
|
+
statusSectionQueue: "## Queue",
|
|
354
|
+
statusPendingLabel: "Pending:",
|
|
355
|
+
statusCompletedLabel: "Completed:",
|
|
356
|
+
statusInProgressLabel: "In progress:",
|
|
357
|
+
statusInProgressHeader: "### In progress",
|
|
358
|
+
statusAgentBusy: "🟡 BUSY",
|
|
359
|
+
statusAgentIdle: "⚪ IDLE",
|
|
360
|
+
statusNoTask: "No task",
|
|
361
|
+
// INBOX.md
|
|
362
|
+
inboxDone: (ts, id, agent) => `## [${ts}] ${id} completed — ${agent}`,
|
|
363
|
+
inboxTaskLabel: "- **Task:**",
|
|
364
|
+
inboxDurationLabel: "- **Duration:**",
|
|
365
|
+
inboxReportLabel: "- **Report:**",
|
|
366
|
+
inboxActionLabel: (file) => `- **Action:** Read \`${file}\` and create next TASKs if applicable.`,
|
|
367
|
+
inboxFailed: (ts, id, from, to) => `## [${ts}] ${id} failed — ${from} → reassigned to ${to}`,
|
|
368
|
+
inboxReasonLabel: "- **Reason:**",
|
|
369
|
+
inboxNewAgentLabel: "- **New agent:**",
|
|
370
|
+
inboxFailAction: "- **Action:** TUI reassigned automatically. Check QUEUE.md or wait for the next completion notification.",
|
|
220
371
|
},
|
|
221
372
|
};
|
|
222
373
|
const L = TEXT[WORKSPACE_LANGUAGE];
|
|
223
374
|
|
|
375
|
+
let lastRenderTime = 0;
|
|
376
|
+
const RENDER_DEBOUNCE_MS = 500;
|
|
377
|
+
|
|
378
|
+
function safeRenderDashboard() {
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
if (now - lastRenderTime < RENDER_DEBOUNCE_MS) return;
|
|
381
|
+
lastRenderTime = now;
|
|
382
|
+
renderDashboard();
|
|
383
|
+
}
|
|
384
|
+
|
|
224
385
|
// CLI args
|
|
225
386
|
const argv = process.argv.slice(2);
|
|
226
387
|
const CLI = {
|
|
@@ -258,8 +419,7 @@ ${L.keyboard}:
|
|
|
258
419
|
const MAX_CONCURRENT = config.maxConcurrent || Object.keys(AGENTS).length;
|
|
259
420
|
const POLL_INTERVAL_MS = (config.pollIntervalSeconds || 30) * 1000;
|
|
260
421
|
const TASK_TIMEOUT_MS = (config.taskTimeoutMinutes || 30) * 60 * 1000;
|
|
261
|
-
const SKIP_PERMISSIONS =
|
|
262
|
-
process.env.SKIP_PERMISSIONS === "true" || CLI.yolo;
|
|
422
|
+
const SKIP_PERMISSIONS = process.env.SKIP_PERMISSIONS === "true" || CLI.yolo;
|
|
263
423
|
const PERMISSION_FLAGS = SKIP_PERMISSIONS
|
|
264
424
|
? ["--dangerously-skip-permissions"]
|
|
265
425
|
: ["--permission-mode", "default"];
|
|
@@ -271,6 +431,38 @@ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
|
271
431
|
const LOCK_FILE = path.join(LOG_DIR, "orchestrator.lock");
|
|
272
432
|
const STATE_FILE = path.join(LOG_DIR, "orchestrator-state.json");
|
|
273
433
|
const CONTROL_FILE = path.join(LOG_DIR, "orchestrator-control.json");
|
|
434
|
+
const STATUS_FILE = path.join(WORKSPACE, "STATUS.md");
|
|
435
|
+
|
|
436
|
+
function updateStatusFile() {
|
|
437
|
+
const statusLines = [
|
|
438
|
+
L.statusTitle(timestamp()),
|
|
439
|
+
``,
|
|
440
|
+
`${L.statusProject} ${PROJECT_NAME}`,
|
|
441
|
+
`${L.statusState} ${state.paused ? L.statusPausedLabel : L.statusRunning}`,
|
|
442
|
+
`${L.statusActiveTime} ${formatDuration(Math.round((Date.now() - state.startTime) / 1000))}`,
|
|
443
|
+
``,
|
|
444
|
+
L.statusSectionAgents,
|
|
445
|
+
``,
|
|
446
|
+
];
|
|
447
|
+
for (const [name, ag] of Object.entries(state.agents)) {
|
|
448
|
+
const status = ag.status === "busy" ? L.statusAgentBusy : L.statusAgentIdle;
|
|
449
|
+
const task = ag.task ? `${ag.task.id}: ${ag.task.title}` : L.statusNoTask;
|
|
450
|
+
statusLines.push(`- **${name}**: ${status} - ${task}`);
|
|
451
|
+
}
|
|
452
|
+
statusLines.push("", L.statusSectionQueue);
|
|
453
|
+
statusLines.push("", `${L.statusPendingLabel} ${state.queue.length}`);
|
|
454
|
+
statusLines.push("", `${L.statusCompletedLabel} ${state.completed.length}`);
|
|
455
|
+
statusLines.push("", `${L.statusInProgressLabel} ${state.inProgress.length}`);
|
|
456
|
+
if (state.inProgress.length > 0) {
|
|
457
|
+
statusLines.push("", L.statusInProgressHeader);
|
|
458
|
+
for (const t of state.inProgress) {
|
|
459
|
+
statusLines.push(`- ${t.id}: ${t.title} (${t.agent})`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
fs.writeFileSync(STATUS_FILE, statusLines.join("\n"), "utf-8");
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
274
466
|
|
|
275
467
|
// Limpiar control.json orphan al iniciar (si el proceso anterior fechou mal)
|
|
276
468
|
if (fs.existsSync(CONTROL_FILE)) {
|
|
@@ -365,6 +557,7 @@ for (const name of Object.keys(AGENTS)) {
|
|
|
365
557
|
lastLine: "",
|
|
366
558
|
exitCode: null,
|
|
367
559
|
cost: null,
|
|
560
|
+
totalCost: 0,
|
|
368
561
|
turns: 0,
|
|
369
562
|
};
|
|
370
563
|
}
|
|
@@ -401,7 +594,10 @@ const dashboard =
|
|
|
401
594
|
|
|
402
595
|
const agentNames = Object.keys(AGENTS);
|
|
403
596
|
const agentBoxes = {};
|
|
404
|
-
const panelWidth = Math.max(
|
|
597
|
+
const panelWidth = Math.max(
|
|
598
|
+
1,
|
|
599
|
+
Math.floor(100 / Math.max(1, agentNames.length)),
|
|
600
|
+
);
|
|
405
601
|
|
|
406
602
|
if (!CLI.headless && screen) {
|
|
407
603
|
agentNames.forEach((name, i) => {
|
|
@@ -475,12 +671,17 @@ function persistState() {
|
|
|
475
671
|
lastLine: ag.lastLine,
|
|
476
672
|
exitCode: ag.exitCode,
|
|
477
673
|
cost: ag.cost,
|
|
674
|
+
totalCost: ag.totalCost || 0,
|
|
478
675
|
turns: ag.turns,
|
|
479
676
|
},
|
|
480
677
|
]),
|
|
481
678
|
),
|
|
482
679
|
};
|
|
483
|
-
fs.writeFileSync(
|
|
680
|
+
fs.writeFileSync(
|
|
681
|
+
STATE_FILE,
|
|
682
|
+
JSON.stringify(snapshot, null, 2) + "\n",
|
|
683
|
+
"utf-8",
|
|
684
|
+
);
|
|
484
685
|
}
|
|
485
686
|
|
|
486
687
|
function consumeControlCommand() {
|
|
@@ -590,22 +791,34 @@ function renderDashboard() {
|
|
|
590
791
|
? `{yellow-fg}${L.paused}{/yellow-fg}`
|
|
591
792
|
: `{green-fg}${L.running}{/green-fg}`;
|
|
592
793
|
|
|
593
|
-
lines.push(
|
|
794
|
+
lines.push(
|
|
795
|
+
` ${datestamp()} ${timestamp()} ${WORKSPACE_LANGUAGE === "es" ? "activo" : "active"} ${up} ${cost} ${mode}`,
|
|
796
|
+
);
|
|
594
797
|
lines.push("");
|
|
595
798
|
|
|
596
799
|
for (const [name, ag] of Object.entries(state.agents)) {
|
|
597
|
-
const
|
|
598
|
-
|
|
800
|
+
const lastLine = ag.lastLine || "";
|
|
801
|
+
const isFailed = lastLine.startsWith("FALLÓ:") || lastLine.startsWith("FAILED:");
|
|
802
|
+
const isRetrying = lastLine.startsWith("REINTENTO:") || lastLine.startsWith("LÍMITE:") || lastLine.startsWith("RETRY:") || lastLine.startsWith("LIMIT:");
|
|
803
|
+
let status, detail, dot;
|
|
599
804
|
if (ag.status === "busy") {
|
|
600
805
|
status = `{yellow-fg}${L.busy}{/yellow-fg}`;
|
|
601
806
|
detail = `${ag.task?.id || "?"} ${(ag.task?.title || "").slice(0, 35)} (${elapsedSince(ag.startTime)})`;
|
|
807
|
+
dot = "{green-fg}●{/green-fg}";
|
|
808
|
+
} else if (isFailed) {
|
|
809
|
+
status = `{red-fg}${L.failed}{/red-fg}`;
|
|
810
|
+
detail = lastLine;
|
|
811
|
+
dot = "{red-fg}✕{/red-fg}";
|
|
812
|
+
} else if (isRetrying) {
|
|
813
|
+
status = `{yellow-fg}${L.retrying}{/yellow-fg}`;
|
|
814
|
+
detail = lastLine;
|
|
815
|
+
dot = "{yellow-fg}⟳{/yellow-fg}";
|
|
602
816
|
} else {
|
|
603
817
|
status = `{gray-fg}${L.idle}{/gray-fg}`;
|
|
604
|
-
detail =
|
|
818
|
+
detail = lastLine;
|
|
819
|
+
dot = "{gray-fg}○{/gray-fg}";
|
|
605
820
|
}
|
|
606
|
-
|
|
607
|
-
ag.status === "busy" ? "{green-fg}●{/green-fg}" : "{gray-fg}○{/gray-fg}";
|
|
608
|
-
lines.push(` ${dot} {bold}${name}{/bold} ${status} ${detail}`);
|
|
821
|
+
lines.push(` ${dot} {bold}${name}{/bold} ${status} ${escBl(detail)}`);
|
|
609
822
|
}
|
|
610
823
|
lines.push("");
|
|
611
824
|
|
|
@@ -668,23 +881,31 @@ function renderDashboard() {
|
|
|
668
881
|
lines.push(` {gray-fg}${escBl(entry)}{/gray-fg}`);
|
|
669
882
|
}
|
|
670
883
|
lines.push("");
|
|
671
|
-
lines.push(
|
|
672
|
-
` {cyan-fg}S{/cyan-fg} ${L.controls}`,
|
|
673
|
-
);
|
|
884
|
+
lines.push(` {cyan-fg}S{/cyan-fg} ${L.controls}`);
|
|
674
885
|
|
|
675
886
|
dashboard.setContent(lines.join("\n"));
|
|
676
887
|
|
|
677
888
|
for (const [name, ag] of Object.entries(state.agents)) {
|
|
678
889
|
const box = agentBoxes[name];
|
|
890
|
+
const lastLine = ag.lastLine || "";
|
|
891
|
+
const isFailed = lastLine.startsWith("FALLÓ:") || lastLine.startsWith("FAILED:");
|
|
892
|
+
const isRetrying = lastLine.startsWith("REINTENTO:") || lastLine.startsWith("LÍMITE:") || lastLine.startsWith("RETRY:") || lastLine.startsWith("LIMIT:");
|
|
679
893
|
if (ag.status === "busy") {
|
|
680
894
|
box.style.border.fg = "yellow";
|
|
681
895
|
box.setLabel(
|
|
682
|
-
` {bold}${escBl(name)}{/bold} {yellow-fg}
|
|
896
|
+
` {bold}${escBl(name)}{/bold} {yellow-fg}${L.busy}{/yellow-fg} ${escBl(ag.task?.id || "")} `,
|
|
683
897
|
);
|
|
898
|
+
} else if (isFailed) {
|
|
899
|
+
box.style.border.fg = "red";
|
|
900
|
+
box.setLabel(` {bold}${escBl(name)}{/bold} {red-fg}${L.failed}{/red-fg} `);
|
|
901
|
+
} else if (isRetrying) {
|
|
902
|
+
box.style.border.fg = "yellow";
|
|
903
|
+
box.setLabel(` {bold}${escBl(name)}{/bold} {yellow-fg}${L.retrying}{/yellow-fg} `);
|
|
684
904
|
} else {
|
|
685
905
|
box.style.border.fg = "gray";
|
|
906
|
+
const agCostLabel = ag.totalCost > 0 ? ` {gray-fg}$${ag.totalCost.toFixed(2)}{/gray-fg}` : "";
|
|
686
907
|
box.setLabel(
|
|
687
|
-
` {bold}${escBl(name)}{/bold} {gray-fg}
|
|
908
|
+
` {bold}${escBl(name)}{/bold} {gray-fg}${L.idle}{/gray-fg}${agCostLabel} `,
|
|
688
909
|
);
|
|
689
910
|
}
|
|
690
911
|
}
|
|
@@ -705,7 +926,10 @@ function parseQueue() {
|
|
|
705
926
|
section = "pending";
|
|
706
927
|
continue;
|
|
707
928
|
}
|
|
708
|
-
if (
|
|
929
|
+
if (
|
|
930
|
+
line.startsWith("## In Progress") ||
|
|
931
|
+
line.startsWith("## En progreso")
|
|
932
|
+
) {
|
|
709
933
|
section = "inprogress";
|
|
710
934
|
continue;
|
|
711
935
|
}
|
|
@@ -747,7 +971,10 @@ function parseCompletedFromFile() {
|
|
|
747
971
|
section = "pending";
|
|
748
972
|
continue;
|
|
749
973
|
}
|
|
750
|
-
if (
|
|
974
|
+
if (
|
|
975
|
+
line.startsWith("## In Progress") ||
|
|
976
|
+
line.startsWith("## En progreso")
|
|
977
|
+
) {
|
|
751
978
|
section = "inprogress";
|
|
752
979
|
continue;
|
|
753
980
|
}
|
|
@@ -807,12 +1034,12 @@ function writeInboxNotification(task, agentName, elapsed) {
|
|
|
807
1034
|
const progressFile = `progress/PROGRESS-${agentName}.md`;
|
|
808
1035
|
const entry = [
|
|
809
1036
|
``,
|
|
810
|
-
|
|
1037
|
+
L.inboxDone(timestamp(), task.id, agentName),
|
|
811
1038
|
``,
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1039
|
+
`${L.inboxTaskLabel} ${task.title}`,
|
|
1040
|
+
`${L.inboxDurationLabel} ${formatDuration(elapsed)}`,
|
|
1041
|
+
`${L.inboxReportLabel} ${progressFile}`,
|
|
1042
|
+
L.inboxActionLabel(progressFile),
|
|
816
1043
|
``,
|
|
817
1044
|
].join("\n");
|
|
818
1045
|
try {
|
|
@@ -824,12 +1051,12 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
|
|
|
824
1051
|
const inboxFile = path.join(WORKSPACE, "INBOX.md");
|
|
825
1052
|
const entry = [
|
|
826
1053
|
``,
|
|
827
|
-
|
|
1054
|
+
L.inboxFailed(timestamp(), task.id, failedAgent, newAgent),
|
|
828
1055
|
``,
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1056
|
+
`${L.inboxTaskLabel} ${task.title}`,
|
|
1057
|
+
`${L.inboxReasonLabel} ${reason}`,
|
|
1058
|
+
`${L.inboxNewAgentLabel} ${newAgent}`,
|
|
1059
|
+
L.inboxFailAction,
|
|
833
1060
|
``,
|
|
834
1061
|
].join("\n");
|
|
835
1062
|
try {
|
|
@@ -837,6 +1064,35 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
|
|
|
837
1064
|
} catch {}
|
|
838
1065
|
}
|
|
839
1066
|
|
|
1067
|
+
// GAP 2 — Move a task line from ## Pending to ## In Progress when it starts
|
|
1068
|
+
function moveTaskToInProgress(task) {
|
|
1069
|
+
if (!fs.existsSync(QUEUE_FILE)) return;
|
|
1070
|
+
try {
|
|
1071
|
+
const lines = fs.readFileSync(QUEUE_FILE, "utf-8").split("\n");
|
|
1072
|
+
const idMatcher = new RegExp(
|
|
1073
|
+
`^${task.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$|\\|)`,
|
|
1074
|
+
);
|
|
1075
|
+
// Remove from Pending (wherever it is) — keep line for insertion
|
|
1076
|
+
let taskLine = null;
|
|
1077
|
+
const without = lines.filter((l) => {
|
|
1078
|
+
if (idMatcher.test(l.trim())) {
|
|
1079
|
+
taskLine = l;
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
return true;
|
|
1083
|
+
});
|
|
1084
|
+
if (!taskLine) return; // already moved or not found
|
|
1085
|
+
// Find In Progress section and insert after its header
|
|
1086
|
+
const idx = without.findIndex(
|
|
1087
|
+
(l) =>
|
|
1088
|
+
l.trim().startsWith("## In Progress") ||
|
|
1089
|
+
l.trim().startsWith("## En progreso"),
|
|
1090
|
+
);
|
|
1091
|
+
if (idx >= 0) without.splice(idx + 1, 0, taskLine);
|
|
1092
|
+
fs.writeFileSync(QUEUE_FILE, without.join("\n"), "utf-8");
|
|
1093
|
+
} catch {}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
840
1096
|
// ============================================================================
|
|
841
1097
|
// BRIEF GENERATOR
|
|
842
1098
|
// ============================================================================
|
|
@@ -892,7 +1148,13 @@ function generateBrief(task) {
|
|
|
892
1148
|
}
|
|
893
1149
|
}
|
|
894
1150
|
|
|
895
|
-
const
|
|
1151
|
+
const hasBackend = REPOS.backend && fs.existsSync(REPOS.backend);
|
|
1152
|
+
const hasFrontend = REPOS.frontend && fs.existsSync(REPOS.frontend);
|
|
1153
|
+
const isSingleRepo = (hasBackend && hasFrontend &&
|
|
1154
|
+
path.resolve(REPOS.backend) === path.resolve(REPOS.frontend)) ||
|
|
1155
|
+
(!hasBackend && hasFrontend) || (hasBackend && !hasFrontend);
|
|
1156
|
+
const effectiveRepo = isSingleRepo ? "frontend" : (task.repo || agentCfg.defaultRepo);
|
|
1157
|
+
const repoDir = REPOS[effectiveRepo] || REPOS[task.repo] || REPOS[agentCfg.defaultRepo] || ".";
|
|
896
1158
|
const progressFile = path.join(
|
|
897
1159
|
WORKSPACE,
|
|
898
1160
|
"progress",
|
|
@@ -902,7 +1164,7 @@ function generateBrief(task) {
|
|
|
902
1164
|
return `
|
|
903
1165
|
# Agent: ${task.agent}
|
|
904
1166
|
# Task: ${task.id} — ${task.title}
|
|
905
|
-
# Repository: ${
|
|
1167
|
+
# Repository: ${effectiveRepo}
|
|
906
1168
|
# CWD: ${repoDir}
|
|
907
1169
|
# Priority: ${task.priority}
|
|
908
1170
|
# Workspace: ${WORKSPACE}
|
|
@@ -971,25 +1233,17 @@ function buildCliCommand(agentCfg, task, prompt) {
|
|
|
971
1233
|
case "codex":
|
|
972
1234
|
return {
|
|
973
1235
|
cmd: "codex",
|
|
974
|
-
args: [
|
|
975
|
-
"exec",
|
|
976
|
-
...(agentCfg.model ? ["--model", agentCfg.model] : []),
|
|
977
|
-
...(CLI.yolo ? ["--dangerously-bypass-approvals-and-sandbox"] : []),
|
|
978
|
-
"--add-dir",
|
|
979
|
-
WORKSPACE,
|
|
980
|
-
"-",
|
|
981
|
-
],
|
|
1236
|
+
args: ["exec", "--yolo", "--add-dir", WORKSPACE, "-"],
|
|
982
1237
|
};
|
|
983
1238
|
case "opencode":
|
|
984
1239
|
return {
|
|
985
1240
|
cmd: "opencode",
|
|
986
1241
|
args: [
|
|
987
1242
|
"run",
|
|
988
|
-
...(agentCfg.model ? ["--model", agentCfg.model] : []),
|
|
989
1243
|
"--format",
|
|
990
1244
|
"json",
|
|
991
1245
|
"--pure",
|
|
992
|
-
|
|
1246
|
+
"--dangerously-skip-permissions",
|
|
993
1247
|
],
|
|
994
1248
|
};
|
|
995
1249
|
case "gemini":
|
|
@@ -1023,7 +1277,7 @@ function buildCliCommand(agentCfg, task, prompt) {
|
|
|
1023
1277
|
cmd: "cmd",
|
|
1024
1278
|
args: [
|
|
1025
1279
|
"/c",
|
|
1026
|
-
`type "${promptFile}" | abacusai -p --output-format stream-json --permission-mode
|
|
1280
|
+
`type "${promptFile}" | abacusai -p --output-format stream-json --permission-mode yolo --dangerously-skip-permissions --auto-accept-edits --add-dir "${WORKSPACE}"`,
|
|
1027
1281
|
],
|
|
1028
1282
|
};
|
|
1029
1283
|
}
|
|
@@ -1031,7 +1285,7 @@ function buildCliCommand(agentCfg, task, prompt) {
|
|
|
1031
1285
|
cmd: "sh",
|
|
1032
1286
|
args: [
|
|
1033
1287
|
"-c",
|
|
1034
|
-
`cat "${promptFile}" | abacusai -p --output-format stream-json --permission-mode
|
|
1288
|
+
`cat "${promptFile}" | abacusai -p --output-format stream-json --permission-mode yolo --dangerously-skip-permissions --auto-accept-edits --add-dir "${WORKSPACE}"`,
|
|
1035
1289
|
],
|
|
1036
1290
|
};
|
|
1037
1291
|
}
|
|
@@ -1050,10 +1304,7 @@ function launchAgent(task) {
|
|
|
1050
1304
|
const agentCfg = AGENTS[agentName];
|
|
1051
1305
|
|
|
1052
1306
|
if (!ag || !agentCfg) {
|
|
1053
|
-
log(
|
|
1054
|
-
"ERROR",
|
|
1055
|
-
`Agente desconocido en QUEUE: "${agentName}" — no está definido en orchestrator.config.json`,
|
|
1056
|
-
);
|
|
1307
|
+
log("ERROR", L.logUnknownAgent(task.id, agentName));
|
|
1057
1308
|
failedTasks.set(task.id, MAX_RETRIES); // don't retry — config bug, not transient
|
|
1058
1309
|
return false;
|
|
1059
1310
|
}
|
|
@@ -1075,10 +1326,10 @@ function launchAgent(task) {
|
|
|
1075
1326
|
log("START", `${agentName} (${cliCmd}) → ${task.id}: ${task.title}`);
|
|
1076
1327
|
appendToAgent(
|
|
1077
1328
|
agentName,
|
|
1078
|
-
`{cyan-fg}
|
|
1329
|
+
`{cyan-fg}${escBl(L.agentTaskHeader(task.id, task.title))}{/cyan-fg}`,
|
|
1079
1330
|
true,
|
|
1080
1331
|
);
|
|
1081
|
-
appendToAgent(agentName, `{gray-fg}
|
|
1332
|
+
appendToAgent(agentName, `{gray-fg}${escBl(L.agentCwd(repoDir))}{/gray-fg}`, true);
|
|
1082
1333
|
appendToAgent(agentName, "", true);
|
|
1083
1334
|
|
|
1084
1335
|
try {
|
|
@@ -1093,8 +1344,8 @@ function launchAgent(task) {
|
|
|
1093
1344
|
proc.stdin.end();
|
|
1094
1345
|
|
|
1095
1346
|
const timeout = setTimeout(() => {
|
|
1096
|
-
log("WARN",
|
|
1097
|
-
appendToAgent(agentName,
|
|
1347
|
+
log("WARN", L.logTimeout(agentName, task.id));
|
|
1348
|
+
appendToAgent(agentName, `{red-fg}${escBl(L.agentTimeout)}{/red-fg}`, true);
|
|
1098
1349
|
try {
|
|
1099
1350
|
proc.kill("SIGTERM");
|
|
1100
1351
|
} catch {}
|
|
@@ -1140,8 +1391,22 @@ function launchAgent(task) {
|
|
|
1140
1391
|
}
|
|
1141
1392
|
}
|
|
1142
1393
|
}
|
|
1394
|
+
// Claude: result event with total_cost_usd
|
|
1143
1395
|
if (event.type === "result" && event.total_cost_usd)
|
|
1144
1396
|
ag.cost = event.total_cost_usd;
|
|
1397
|
+
// Codex: direct cost_usd field (any event)
|
|
1398
|
+
if (event.cost_usd != null && ag.cost == null)
|
|
1399
|
+
ag.cost = event.cost_usd;
|
|
1400
|
+
if (event.usage?.cost_usd != null && ag.cost == null)
|
|
1401
|
+
ag.cost = event.usage.cost_usd;
|
|
1402
|
+
// Codex: token-based cost calculation from usage event
|
|
1403
|
+
if (ag.cost == null && event.usage &&
|
|
1404
|
+
(event.type === "usage" || event.type === "result" || event.type === "response.completed")) {
|
|
1405
|
+
const usageObj = event.usage.usage || event.usage;
|
|
1406
|
+
const model = agentCfg.model || "";
|
|
1407
|
+
const calc = calcOpenAICost(model, usageObj);
|
|
1408
|
+
if (calc != null) ag.cost = calc;
|
|
1409
|
+
}
|
|
1145
1410
|
// OpenCode events
|
|
1146
1411
|
if (event.type === "text" && event.part?.text)
|
|
1147
1412
|
appendToAgent(agentName, event.part.text.slice(0, 120));
|
|
@@ -1211,6 +1476,7 @@ function launchAgent(task) {
|
|
|
1211
1476
|
ag.turns = 0;
|
|
1212
1477
|
task.status = "running";
|
|
1213
1478
|
state.inProgress.push(task);
|
|
1479
|
+
moveTaskToInProgress(task); // GAP 2: reflect in QUEUE.md
|
|
1214
1480
|
renderDashboard();
|
|
1215
1481
|
return true;
|
|
1216
1482
|
} catch (err) {
|
|
@@ -1232,7 +1498,10 @@ function completeTask(task, agentName) {
|
|
|
1232
1498
|
const elapsed = ag.startTime
|
|
1233
1499
|
? Math.round((Date.now() - ag.startTime) / 1000)
|
|
1234
1500
|
: 0;
|
|
1235
|
-
if (ag.cost)
|
|
1501
|
+
if (ag.cost) {
|
|
1502
|
+
state.totalCost += ag.cost;
|
|
1503
|
+
ag.totalCost = (ag.totalCost || 0) + ag.cost;
|
|
1504
|
+
}
|
|
1236
1505
|
task.status = "completed";
|
|
1237
1506
|
task.completedAt = timestamp();
|
|
1238
1507
|
task.elapsed = elapsed;
|
|
@@ -1240,21 +1509,18 @@ function completeTask(task, agentName) {
|
|
|
1240
1509
|
state.inProgress = state.inProgress.filter((t) => t.id !== task.id);
|
|
1241
1510
|
state.completed.push(task);
|
|
1242
1511
|
const costStr = ag.cost ? ` ($${ag.cost.toFixed(2)})` : "";
|
|
1243
|
-
log(
|
|
1244
|
-
"DONE",
|
|
1245
|
-
`${agentName} completó ${task.id} en ${formatDuration(elapsed)}${costStr}`,
|
|
1246
|
-
);
|
|
1512
|
+
log("DONE", L.logDone(agentName, task.id, formatDuration(elapsed), costStr));
|
|
1247
1513
|
appendToAgent(agentName, "", true);
|
|
1248
1514
|
appendToAgent(
|
|
1249
1515
|
agentName,
|
|
1250
|
-
`{green-fg}
|
|
1516
|
+
`{green-fg}${escBl(L.agentCompleted(formatDuration(elapsed), costStr))}{/green-fg}`,
|
|
1251
1517
|
true,
|
|
1252
1518
|
);
|
|
1253
1519
|
ag.status = "idle";
|
|
1254
1520
|
ag.task = null;
|
|
1255
1521
|
ag.process = null;
|
|
1256
1522
|
ag.startTime = null;
|
|
1257
|
-
ag.lastLine =
|
|
1523
|
+
ag.lastLine = L.lastCompleted(task.id);
|
|
1258
1524
|
updateQueueFile(task);
|
|
1259
1525
|
writeInboxNotification(task, agentName, elapsed);
|
|
1260
1526
|
scheduleNext();
|
|
@@ -1318,21 +1584,18 @@ function failTask(task, agentName, code) {
|
|
|
1318
1584
|
if (rl.isRateLimit) {
|
|
1319
1585
|
const resetStr = rl.resetsAt
|
|
1320
1586
|
? `resets ${rl.resetsAt.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true })}`
|
|
1321
|
-
: "
|
|
1322
|
-
log("RATE",
|
|
1587
|
+
: "retry in 10 min";
|
|
1588
|
+
log("RATE", L.logRateLimit(agentName, task.id, resetStr));
|
|
1323
1589
|
appendToAgent(
|
|
1324
1590
|
agentName,
|
|
1325
|
-
`{yellow-fg}
|
|
1591
|
+
`{yellow-fg}${escBl(L.agentRateLimit(resetStr))}{/yellow-fg}`,
|
|
1326
1592
|
true,
|
|
1327
1593
|
);
|
|
1328
1594
|
} else {
|
|
1329
|
-
log(
|
|
1330
|
-
"FAIL",
|
|
1331
|
-
`${agentName} falló ${task.id} (salida ${code}, ${retries}/${maxRetries})`,
|
|
1332
|
-
);
|
|
1595
|
+
log("FAIL", L.logFail(agentName, task.id, code, retries, maxRetries));
|
|
1333
1596
|
appendToAgent(
|
|
1334
1597
|
agentName,
|
|
1335
|
-
`{red-fg}
|
|
1598
|
+
`{red-fg}${escBl(L.agentFailed(code, retries))}{/red-fg}`,
|
|
1336
1599
|
true,
|
|
1337
1600
|
);
|
|
1338
1601
|
}
|
|
@@ -1346,29 +1609,31 @@ function failTask(task, agentName, code) {
|
|
|
1346
1609
|
["Codex", "OpenCode"].includes(agentName) &&
|
|
1347
1610
|
(failureFlags.exhaustedQuota ||
|
|
1348
1611
|
failureFlags.providerUnavailable ||
|
|
1612
|
+
failureFlags.noRealWork ||
|
|
1349
1613
|
retries >= maxRetries);
|
|
1350
1614
|
|
|
1351
1615
|
if (shouldFallback) {
|
|
1352
1616
|
const reason = failureFlags.exhaustedQuota
|
|
1353
|
-
?
|
|
1617
|
+
? L.reasonQuota
|
|
1354
1618
|
: failureFlags.providerUnavailable
|
|
1355
|
-
?
|
|
1356
|
-
:
|
|
1619
|
+
? L.reasonProvider
|
|
1620
|
+
: failureFlags.noRealWork
|
|
1621
|
+
? L.reasonNoWork
|
|
1622
|
+
: L.reasonPersistent;
|
|
1357
1623
|
if (tryFallbackToAlternative(task, agentName, reason)) {
|
|
1358
1624
|
writeInboxFailureNotification(task, agentName, task.agent, reason);
|
|
1359
1625
|
setTimeout(() => {
|
|
1360
1626
|
scheduleNext();
|
|
1361
|
-
|
|
1627
|
+
safeRenderDashboard();
|
|
1362
1628
|
}, 3000);
|
|
1363
|
-
renderDashboard();
|
|
1364
1629
|
return;
|
|
1365
1630
|
}
|
|
1366
1631
|
}
|
|
1367
1632
|
|
|
1368
1633
|
if (retries >= maxRetries) {
|
|
1369
1634
|
task.status = "failed";
|
|
1370
|
-
ag.lastLine =
|
|
1371
|
-
log("ERROR",
|
|
1635
|
+
ag.lastLine = L.lastFailed(task.id);
|
|
1636
|
+
log("ERROR", L.logPermanentFail(task.id, retries));
|
|
1372
1637
|
} else {
|
|
1373
1638
|
task.status = "pending";
|
|
1374
1639
|
let retryDelay = rl.isRateLimit
|
|
@@ -1384,15 +1649,15 @@ function failTask(task, agentName, code) {
|
|
|
1384
1649
|
hour12: true,
|
|
1385
1650
|
});
|
|
1386
1651
|
ag.lastLine = rl.isRateLimit
|
|
1387
|
-
?
|
|
1388
|
-
:
|
|
1652
|
+
? L.lastLimit(task.id, retryTime)
|
|
1653
|
+
: L.lastRetry(task.id);
|
|
1389
1654
|
if (rl.isRateLimit) rateLimitedAgents.set(agentName, task._retryAfter);
|
|
1390
1655
|
}
|
|
1391
1656
|
if (rl.isRateLimit && task._retryAfter) {
|
|
1392
1657
|
setTimeout(
|
|
1393
1658
|
() => {
|
|
1394
1659
|
scheduleNext();
|
|
1395
|
-
|
|
1660
|
+
safeRenderDashboard();
|
|
1396
1661
|
},
|
|
1397
1662
|
Math.max(
|
|
1398
1663
|
Math.min(task._retryAfter - Date.now() + 5000, 3600_000),
|
|
@@ -1402,10 +1667,9 @@ function failTask(task, agentName, code) {
|
|
|
1402
1667
|
} else {
|
|
1403
1668
|
setTimeout(() => {
|
|
1404
1669
|
scheduleNext();
|
|
1405
|
-
|
|
1670
|
+
safeRenderDashboard();
|
|
1406
1671
|
}, 3000);
|
|
1407
1672
|
}
|
|
1408
|
-
renderDashboard();
|
|
1409
1673
|
}
|
|
1410
1674
|
|
|
1411
1675
|
// ============================================================================
|
|
@@ -1442,12 +1706,18 @@ function scheduleNext() {
|
|
|
1442
1706
|
function updateQueueFile(completedTask) {
|
|
1443
1707
|
if (!fs.existsSync(QUEUE_FILE)) return;
|
|
1444
1708
|
const lines = fs.readFileSync(QUEUE_FILE, "utf-8").split("\n");
|
|
1445
|
-
//
|
|
1709
|
+
// Word-boundary match so TASK-1 does NOT also remove TASK-10, TASK-11, etc.
|
|
1446
1710
|
const idMatcher = new RegExp(
|
|
1447
1711
|
`^${completedTask.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$|\\|)`,
|
|
1448
1712
|
);
|
|
1713
|
+
// Remove task from both Pending and In Progress sections
|
|
1449
1714
|
const filtered = lines.filter((l) => !idMatcher.test(l.trim()));
|
|
1450
|
-
|
|
1715
|
+
// Find Completed section and insert entry
|
|
1716
|
+
const idx = filtered.findIndex(
|
|
1717
|
+
(l) =>
|
|
1718
|
+
l.trim().startsWith("## Completed") ||
|
|
1719
|
+
l.trim().startsWith("## Completadas"),
|
|
1720
|
+
);
|
|
1451
1721
|
if (idx >= 0)
|
|
1452
1722
|
filtered.splice(
|
|
1453
1723
|
idx + 1,
|
|
@@ -1491,6 +1761,14 @@ function detectSupportAgentFailure(agentName) {
|
|
|
1491
1761
|
}
|
|
1492
1762
|
|
|
1493
1763
|
const lower = content.toLowerCase();
|
|
1764
|
+
|
|
1765
|
+
const hasToolUses = content.includes("Write") || content.includes("Read") ||
|
|
1766
|
+
content.includes("Bash") || content.includes("Edit") || content.includes("ToolUse");
|
|
1767
|
+
const hasFilesModified = content.includes("files_modified") &&
|
|
1768
|
+
!content.includes("files_modified: list") && !content.includes("files_modified: none");
|
|
1769
|
+
const outputTooShort = ag.output && ag.output.length < 200;
|
|
1770
|
+
const noRealWork = outputTooShort && !hasToolUses && !hasFilesModified;
|
|
1771
|
+
|
|
1494
1772
|
return {
|
|
1495
1773
|
exhaustedQuota:
|
|
1496
1774
|
lower.includes("out of extra usage") ||
|
|
@@ -1498,7 +1776,9 @@ function detectSupportAgentFailure(agentName) {
|
|
|
1498
1776
|
lower.includes("quota") ||
|
|
1499
1777
|
lower.includes("rate_limit") ||
|
|
1500
1778
|
lower.includes("ratelimitexceeded") ||
|
|
1501
|
-
lower.includes("429")
|
|
1779
|
+
lower.includes("429") ||
|
|
1780
|
+
lower.includes("insufficient credits") ||
|
|
1781
|
+
lower.includes("no credits"),
|
|
1502
1782
|
providerUnavailable:
|
|
1503
1783
|
lower.includes("session expired") ||
|
|
1504
1784
|
lower.includes("authentication") ||
|
|
@@ -1511,21 +1791,29 @@ function detectSupportAgentFailure(agentName) {
|
|
|
1511
1791
|
lower.includes("timed out") ||
|
|
1512
1792
|
lower.includes("timeout") ||
|
|
1513
1793
|
lower.includes("network error"),
|
|
1794
|
+
noRealWork: noRealWork || lower.includes("no files") || lower.includes("nothing to"),
|
|
1514
1795
|
};
|
|
1515
1796
|
}
|
|
1516
1797
|
|
|
1517
1798
|
function getClaudeFallbackAgent(task) {
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
REPOS.frontend
|
|
1522
|
-
|
|
1523
|
-
) {
|
|
1799
|
+
const hasBackend = REPOS.backend && fs.existsSync(REPOS.backend);
|
|
1800
|
+
const hasFrontend = REPOS.frontend && fs.existsSync(REPOS.frontend);
|
|
1801
|
+
const isSameRepo = hasBackend && hasFrontend &&
|
|
1802
|
+
path.resolve(REPOS.backend) === path.resolve(REPOS.frontend);
|
|
1803
|
+
|
|
1804
|
+
if (isSameRepo || !hasFrontend) {
|
|
1805
|
+
if (AGENTS["Frontend"]?.cli === "claude") return "Frontend";
|
|
1806
|
+
if (AGENTS["Backend"]?.cli === "claude") return "Backend";
|
|
1807
|
+
}
|
|
1808
|
+
if (!hasBackend && hasFrontend) {
|
|
1524
1809
|
if (AGENTS["Frontend"]?.cli === "claude") return "Frontend";
|
|
1525
1810
|
}
|
|
1811
|
+
|
|
1526
1812
|
const preferred = task.repo === "frontend" ? "Frontend" : "Backend";
|
|
1527
1813
|
if (AGENTS[preferred]?.cli === "claude") return preferred;
|
|
1528
|
-
return
|
|
1814
|
+
return (
|
|
1815
|
+
Object.keys(AGENTS).find((name) => AGENTS[name]?.cli === "claude") || null
|
|
1816
|
+
);
|
|
1529
1817
|
}
|
|
1530
1818
|
|
|
1531
1819
|
function getAlternativeSupportAgent(failedAgentName) {
|
|
@@ -1546,7 +1834,9 @@ function tryFallbackToAlternative(task, failedAgentName, reason) {
|
|
|
1546
1834
|
!rateLimitedAgents.has(siblingAgent);
|
|
1547
1835
|
|
|
1548
1836
|
// Step 2: if sibling is also unavailable, fall back to Claude worker (prefer Frontend)
|
|
1549
|
-
const targetAgent = siblingAvailable
|
|
1837
|
+
const targetAgent = siblingAvailable
|
|
1838
|
+
? siblingAgent
|
|
1839
|
+
: getClaudeFallbackAgent(task);
|
|
1550
1840
|
if (!targetAgent || targetAgent === failedAgentName) return false;
|
|
1551
1841
|
|
|
1552
1842
|
const queueUpdated = updateQueueTaskAgent(task.id, targetAgent);
|
|
@@ -1556,14 +1846,14 @@ function tryFallbackToAlternative(task, failedAgentName, reason) {
|
|
|
1556
1846
|
failedTasks.set(task.id, 0);
|
|
1557
1847
|
state.queue.push(task);
|
|
1558
1848
|
|
|
1559
|
-
log("FALLBACK",
|
|
1849
|
+
log("FALLBACK", L.logFallback(task.id, failedAgentName, targetAgent, reason));
|
|
1560
1850
|
appendToAgent(
|
|
1561
1851
|
failedAgentName,
|
|
1562
|
-
`{yellow-fg}
|
|
1852
|
+
`{yellow-fg}${escBl(L.agentReassigned(targetAgent, reason))}{/yellow-fg}`,
|
|
1563
1853
|
true,
|
|
1564
1854
|
);
|
|
1565
1855
|
if (!queueUpdated) {
|
|
1566
|
-
log("WARN",
|
|
1856
|
+
log("WARN", L.logReassignWarn(task.id, targetAgent));
|
|
1567
1857
|
}
|
|
1568
1858
|
return true;
|
|
1569
1859
|
}
|
|
@@ -1576,23 +1866,11 @@ if (!CLI.headless && screen) {
|
|
|
1576
1866
|
exitWithSummary();
|
|
1577
1867
|
});
|
|
1578
1868
|
|
|
1579
|
-
screen.key("s", () => {
|
|
1580
|
-
if (state.paused) {
|
|
1581
|
-
state.paused = false;
|
|
1582
|
-
log("INFO", "Reanudado");
|
|
1583
|
-
}
|
|
1584
|
-
scheduleNext();
|
|
1585
|
-
renderDashboard();
|
|
1586
|
-
});
|
|
1587
1869
|
screen.key("p", () => {
|
|
1588
1870
|
state.paused = !state.paused;
|
|
1589
1871
|
log("INFO", state.paused ? L.paused : L.resumed);
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
screen.key("r", () => {
|
|
1593
|
-
reloadQueue();
|
|
1594
|
-
log("INFO", L.queueReloaded(state.queue.length));
|
|
1595
|
-
renderDashboard();
|
|
1872
|
+
safeRenderDashboard();
|
|
1873
|
+
updateStatusFile();
|
|
1596
1874
|
});
|
|
1597
1875
|
}
|
|
1598
1876
|
|
|
@@ -1601,13 +1879,11 @@ if (!CLI.headless && screen) {
|
|
|
1601
1879
|
// ============================================================================
|
|
1602
1880
|
log("INFO", L.starting(PROJECT_NAME));
|
|
1603
1881
|
state.completed = parseCompletedFromFile();
|
|
1604
|
-
log(
|
|
1605
|
-
"INFO",
|
|
1606
|
-
L.loadedCompleted(state.completed.length),
|
|
1607
|
-
);
|
|
1882
|
+
log("INFO", L.loadedCompleted(state.completed.length));
|
|
1608
1883
|
reloadQueue();
|
|
1609
1884
|
log("INFO", `${L.queue}: ${state.queue.length} ${L.tasks}`);
|
|
1610
1885
|
renderDashboard();
|
|
1886
|
+
updateStatusFile();
|
|
1611
1887
|
if (!state.paused) {
|
|
1612
1888
|
scheduleNext();
|
|
1613
1889
|
renderDashboard();
|
|
@@ -1623,14 +1899,18 @@ setInterval(() => {
|
|
|
1623
1899
|
if (!state.paused) scheduleNext();
|
|
1624
1900
|
renderDashboard();
|
|
1625
1901
|
}, POLL_INTERVAL_MS);
|
|
1902
|
+
|
|
1903
|
+
setInterval(() => {
|
|
1904
|
+
updateStatusFile();
|
|
1905
|
+
}, 60000); // Update STATUS.md cada 60 segundos
|
|
1626
1906
|
setInterval(() => {
|
|
1627
1907
|
for (const [name, ag] of Object.entries(state.agents)) {
|
|
1628
1908
|
if (ag.status !== "busy" || !ag.process) continue;
|
|
1629
1909
|
try {
|
|
1630
1910
|
process.kill(ag.process.pid, 0);
|
|
1631
1911
|
} catch {
|
|
1632
|
-
log("WARN",
|
|
1633
|
-
appendToAgent(name,
|
|
1912
|
+
log("WARN", L.logDied(name, ag.task?.id));
|
|
1913
|
+
appendToAgent(name, `{red-fg}${escBl(L.agentDied)}{/red-fg}`, true);
|
|
1634
1914
|
ag.process = null;
|
|
1635
1915
|
failTask(ag.task, name, -1);
|
|
1636
1916
|
}
|