@liriraid/agentflow-ai 1.0.14 → 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/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`,
@@ -173,6 +205,65 @@ const TEXT = {
173
205
  retryAt: (time, remaining) => `reintenta a las ${time} (${remaining} min)`,
174
206
  log: "REGISTRO",
175
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.",
176
267
  },
177
268
  en: {
178
269
  configExists:
@@ -204,6 +295,8 @@ const TEXT = {
204
295
  running: "RUNNING",
205
296
  busy: "BUSY",
206
297
  idle: "IDLE",
298
+ failed: "FAILED",
299
+ retrying: "RETRYING",
207
300
  queueReloaded: (count) => `Queue reloaded: ${count} tasks`,
208
301
  quitRequested: "Quit requested from Ink",
209
302
  starting: (name) => `${name} starting`,
@@ -216,6 +309,65 @@ const TEXT = {
216
309
  retryAt: (time, remaining) => `retry at ${time} (${remaining} min)`,
217
310
  log: "LOG",
218
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.",
219
371
  },
220
372
  };
221
373
  const L = TEXT[WORKSPACE_LANGUAGE];
@@ -279,6 +431,38 @@ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
279
431
  const LOCK_FILE = path.join(LOG_DIR, "orchestrator.lock");
280
432
  const STATE_FILE = path.join(LOG_DIR, "orchestrator-state.json");
281
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
+ }
282
466
 
283
467
  // Limpiar control.json orphan al iniciar (si el proceso anterior fechou mal)
284
468
  if (fs.existsSync(CONTROL_FILE)) {
@@ -373,6 +557,7 @@ for (const name of Object.keys(AGENTS)) {
373
557
  lastLine: "",
374
558
  exitCode: null,
375
559
  cost: null,
560
+ totalCost: 0,
376
561
  turns: 0,
377
562
  };
378
563
  }
@@ -486,6 +671,7 @@ function persistState() {
486
671
  lastLine: ag.lastLine,
487
672
  exitCode: ag.exitCode,
488
673
  cost: ag.cost,
674
+ totalCost: ag.totalCost || 0,
489
675
  turns: ag.turns,
490
676
  },
491
677
  ]),
@@ -611,18 +797,28 @@ function renderDashboard() {
611
797
  lines.push("");
612
798
 
613
799
  for (const [name, ag] of Object.entries(state.agents)) {
614
- const cfg = AGENTS[name];
615
- let status, detail;
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;
616
804
  if (ag.status === "busy") {
617
805
  status = `{yellow-fg}${L.busy}{/yellow-fg}`;
618
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}";
619
816
  } else {
620
817
  status = `{gray-fg}${L.idle}{/gray-fg}`;
621
- detail = ag.lastLine || "";
818
+ detail = lastLine;
819
+ dot = "{gray-fg}○{/gray-fg}";
622
820
  }
623
- const dot =
624
- ag.status === "busy" ? "{green-fg}●{/green-fg}" : "{gray-fg}○{/gray-fg}";
625
- lines.push(` ${dot} {bold}${name}{/bold} ${status} ${detail}`);
821
+ lines.push(` ${dot} {bold}${name}{/bold} ${status} ${escBl(detail)}`);
626
822
  }
627
823
  lines.push("");
628
824
 
@@ -691,15 +887,25 @@ function renderDashboard() {
691
887
 
692
888
  for (const [name, ag] of Object.entries(state.agents)) {
693
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:");
694
893
  if (ag.status === "busy") {
695
894
  box.style.border.fg = "yellow";
696
895
  box.setLabel(
697
- ` {bold}${escBl(name)}{/bold} {yellow-fg}OCUPADO{/yellow-fg} ${escBl(ag.task?.id || "")} `,
896
+ ` {bold}${escBl(name)}{/bold} {yellow-fg}${L.busy}{/yellow-fg} ${escBl(ag.task?.id || "")} `,
698
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} `);
699
904
  } else {
700
905
  box.style.border.fg = "gray";
906
+ const agCostLabel = ag.totalCost > 0 ? ` {gray-fg}$${ag.totalCost.toFixed(2)}{/gray-fg}` : "";
701
907
  box.setLabel(
702
- ` {bold}${escBl(name)}{/bold} {gray-fg}EN ESPERA{/gray-fg} `,
908
+ ` {bold}${escBl(name)}{/bold} {gray-fg}${L.idle}{/gray-fg}${agCostLabel} `,
703
909
  );
704
910
  }
705
911
  }
@@ -828,12 +1034,12 @@ function writeInboxNotification(task, agentName, elapsed) {
828
1034
  const progressFile = `progress/PROGRESS-${agentName}.md`;
829
1035
  const entry = [
830
1036
  ``,
831
- `## [${timestamp()}] ${task.id} completada — ${agentName}`,
1037
+ L.inboxDone(timestamp(), task.id, agentName),
832
1038
  ``,
833
- `- **Tarea:** ${task.title}`,
834
- `- **Duración:** ${formatDuration(elapsed)}`,
835
- `- **Reporte:** ${progressFile}`,
836
- `- **Acción:** Lee \`${progressFile}\` y crea las siguientes TASKs si corresponde.`,
1039
+ `${L.inboxTaskLabel} ${task.title}`,
1040
+ `${L.inboxDurationLabel} ${formatDuration(elapsed)}`,
1041
+ `${L.inboxReportLabel} ${progressFile}`,
1042
+ L.inboxActionLabel(progressFile),
837
1043
  ``,
838
1044
  ].join("\n");
839
1045
  try {
@@ -845,12 +1051,12 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
845
1051
  const inboxFile = path.join(WORKSPACE, "INBOX.md");
846
1052
  const entry = [
847
1053
  ``,
848
- `## [${timestamp()}] ${task.id} falló — ${failedAgent} → reasignada a ${newAgent}`,
1054
+ L.inboxFailed(timestamp(), task.id, failedAgent, newAgent),
849
1055
  ``,
850
- `- **Tarea:** ${task.title}`,
851
- `- **Motivo:** ${reason}`,
852
- `- **Nuevo agente:** ${newAgent}`,
853
- `- **Acción:** La TUI reasignó automáticamente. Verifica en QUEUE.md o espera la siguiente notificación de completada.`,
1056
+ `${L.inboxTaskLabel} ${task.title}`,
1057
+ `${L.inboxReasonLabel} ${reason}`,
1058
+ `${L.inboxNewAgentLabel} ${newAgent}`,
1059
+ L.inboxFailAction,
854
1060
  ``,
855
1061
  ].join("\n");
856
1062
  try {
@@ -858,6 +1064,35 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
858
1064
  } catch {}
859
1065
  }
860
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
+
861
1096
  // ============================================================================
862
1097
  // BRIEF GENERATOR
863
1098
  // ============================================================================
@@ -1069,10 +1304,7 @@ function launchAgent(task) {
1069
1304
  const agentCfg = AGENTS[agentName];
1070
1305
 
1071
1306
  if (!ag || !agentCfg) {
1072
- log(
1073
- "ERROR",
1074
- `Agente desconocido en QUEUE: "${agentName}" — no está definido en orchestrator.config.json`,
1075
- );
1307
+ log("ERROR", L.logUnknownAgent(task.id, agentName));
1076
1308
  failedTasks.set(task.id, MAX_RETRIES); // don't retry — config bug, not transient
1077
1309
  return false;
1078
1310
  }
@@ -1094,10 +1326,10 @@ function launchAgent(task) {
1094
1326
  log("START", `${agentName} (${cliCmd}) → ${task.id}: ${task.title}`);
1095
1327
  appendToAgent(
1096
1328
  agentName,
1097
- `{cyan-fg}=== ${escBl(task.id)}: ${escBl(task.title)} ==={/cyan-fg}`,
1329
+ `{cyan-fg}${escBl(L.agentTaskHeader(task.id, task.title))}{/cyan-fg}`,
1098
1330
  true,
1099
1331
  );
1100
- appendToAgent(agentName, `{gray-fg}CWD: ${escBl(repoDir)}{/gray-fg}`, true);
1332
+ appendToAgent(agentName, `{gray-fg}${escBl(L.agentCwd(repoDir))}{/gray-fg}`, true);
1101
1333
  appendToAgent(agentName, "", true);
1102
1334
 
1103
1335
  try {
@@ -1112,8 +1344,8 @@ function launchAgent(task) {
1112
1344
  proc.stdin.end();
1113
1345
 
1114
1346
  const timeout = setTimeout(() => {
1115
- log("WARN", `${agentName} timed out on ${task.id}`);
1116
- appendToAgent(agentName, "{red-fg}=== TIMEOUT ==={/red-fg}", true);
1347
+ log("WARN", L.logTimeout(agentName, task.id));
1348
+ appendToAgent(agentName, `{red-fg}${escBl(L.agentTimeout)}{/red-fg}`, true);
1117
1349
  try {
1118
1350
  proc.kill("SIGTERM");
1119
1351
  } catch {}
@@ -1159,8 +1391,22 @@ function launchAgent(task) {
1159
1391
  }
1160
1392
  }
1161
1393
  }
1394
+ // Claude: result event with total_cost_usd
1162
1395
  if (event.type === "result" && event.total_cost_usd)
1163
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
+ }
1164
1410
  // OpenCode events
1165
1411
  if (event.type === "text" && event.part?.text)
1166
1412
  appendToAgent(agentName, event.part.text.slice(0, 120));
@@ -1230,6 +1476,7 @@ function launchAgent(task) {
1230
1476
  ag.turns = 0;
1231
1477
  task.status = "running";
1232
1478
  state.inProgress.push(task);
1479
+ moveTaskToInProgress(task); // GAP 2: reflect in QUEUE.md
1233
1480
  renderDashboard();
1234
1481
  return true;
1235
1482
  } catch (err) {
@@ -1251,7 +1498,10 @@ function completeTask(task, agentName) {
1251
1498
  const elapsed = ag.startTime
1252
1499
  ? Math.round((Date.now() - ag.startTime) / 1000)
1253
1500
  : 0;
1254
- if (ag.cost) state.totalCost += ag.cost;
1501
+ if (ag.cost) {
1502
+ state.totalCost += ag.cost;
1503
+ ag.totalCost = (ag.totalCost || 0) + ag.cost;
1504
+ }
1255
1505
  task.status = "completed";
1256
1506
  task.completedAt = timestamp();
1257
1507
  task.elapsed = elapsed;
@@ -1259,21 +1509,18 @@ function completeTask(task, agentName) {
1259
1509
  state.inProgress = state.inProgress.filter((t) => t.id !== task.id);
1260
1510
  state.completed.push(task);
1261
1511
  const costStr = ag.cost ? ` ($${ag.cost.toFixed(2)})` : "";
1262
- log(
1263
- "DONE",
1264
- `${agentName} completó ${task.id} en ${formatDuration(elapsed)}${costStr}`,
1265
- );
1512
+ log("DONE", L.logDone(agentName, task.id, formatDuration(elapsed), costStr));
1266
1513
  appendToAgent(agentName, "", true);
1267
1514
  appendToAgent(
1268
1515
  agentName,
1269
- `{green-fg}=== COMPLETADA en ${formatDuration(elapsed)}${escBl(costStr)} ==={/green-fg}`,
1516
+ `{green-fg}${escBl(L.agentCompleted(formatDuration(elapsed), costStr))}{/green-fg}`,
1270
1517
  true,
1271
1518
  );
1272
1519
  ag.status = "idle";
1273
1520
  ag.task = null;
1274
1521
  ag.process = null;
1275
1522
  ag.startTime = null;
1276
- ag.lastLine = `Última: ${task.id} completada`;
1523
+ ag.lastLine = L.lastCompleted(task.id);
1277
1524
  updateQueueFile(task);
1278
1525
  writeInboxNotification(task, agentName, elapsed);
1279
1526
  scheduleNext();
@@ -1337,21 +1584,18 @@ function failTask(task, agentName, code) {
1337
1584
  if (rl.isRateLimit) {
1338
1585
  const resetStr = rl.resetsAt
1339
1586
  ? `resets ${rl.resetsAt.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true })}`
1340
- : "reintento en 10 min";
1341
- log("RATE", `${agentName} alcanzó el límite en ${task.id} (${resetStr})`);
1587
+ : "retry in 10 min";
1588
+ log("RATE", L.logRateLimit(agentName, task.id, resetStr));
1342
1589
  appendToAgent(
1343
1590
  agentName,
1344
- `{yellow-fg}=== LÍMITE DE CUOTA (${escBl(resetStr)}) ==={/yellow-fg}`,
1591
+ `{yellow-fg}${escBl(L.agentRateLimit(resetStr))}{/yellow-fg}`,
1345
1592
  true,
1346
1593
  );
1347
1594
  } else {
1348
- log(
1349
- "FAIL",
1350
- `${agentName} falló ${task.id} (salida ${code}, ${retries}/${maxRetries})`,
1351
- );
1595
+ log("FAIL", L.logFail(agentName, task.id, code, retries, maxRetries));
1352
1596
  appendToAgent(
1353
1597
  agentName,
1354
- `{red-fg}=== FALLÓ (salida ${code}, intento ${retries}) ==={/red-fg}`,
1598
+ `{red-fg}${escBl(L.agentFailed(code, retries))}{/red-fg}`,
1355
1599
  true,
1356
1600
  );
1357
1601
  }
@@ -1365,14 +1609,17 @@ function failTask(task, agentName, code) {
1365
1609
  ["Codex", "OpenCode"].includes(agentName) &&
1366
1610
  (failureFlags.exhaustedQuota ||
1367
1611
  failureFlags.providerUnavailable ||
1612
+ failureFlags.noRealWork ||
1368
1613
  retries >= maxRetries);
1369
1614
 
1370
1615
  if (shouldFallback) {
1371
1616
  const reason = failureFlags.exhaustedQuota
1372
- ? "cuota o límite agotado"
1617
+ ? L.reasonQuota
1373
1618
  : failureFlags.providerUnavailable
1374
- ? "proveedor o sesión no disponibles"
1375
- : "fallo persistente";
1619
+ ? L.reasonProvider
1620
+ : failureFlags.noRealWork
1621
+ ? L.reasonNoWork
1622
+ : L.reasonPersistent;
1376
1623
  if (tryFallbackToAlternative(task, agentName, reason)) {
1377
1624
  writeInboxFailureNotification(task, agentName, task.agent, reason);
1378
1625
  setTimeout(() => {
@@ -1385,8 +1632,8 @@ function failTask(task, agentName, code) {
1385
1632
 
1386
1633
  if (retries >= maxRetries) {
1387
1634
  task.status = "failed";
1388
- ag.lastLine = `FALLÓ: ${task.id}`;
1389
- log("ERROR", `${task.id} falló definitivamente tras ${retries} intentos`);
1635
+ ag.lastLine = L.lastFailed(task.id);
1636
+ log("ERROR", L.logPermanentFail(task.id, retries));
1390
1637
  } else {
1391
1638
  task.status = "pending";
1392
1639
  let retryDelay = rl.isRateLimit
@@ -1402,8 +1649,8 @@ function failTask(task, agentName, code) {
1402
1649
  hour12: true,
1403
1650
  });
1404
1651
  ag.lastLine = rl.isRateLimit
1405
- ? `LÍMITE: ${task.id} (reintento a las ${retryTime})`
1406
- : `REINTENTO: ${task.id}`;
1652
+ ? L.lastLimit(task.id, retryTime)
1653
+ : L.lastRetry(task.id);
1407
1654
  if (rl.isRateLimit) rateLimitedAgents.set(agentName, task._retryAfter);
1408
1655
  }
1409
1656
  if (rl.isRateLimit && task._retryAfter) {
@@ -1459,11 +1706,13 @@ function scheduleNext() {
1459
1706
  function updateQueueFile(completedTask) {
1460
1707
  if (!fs.existsSync(QUEUE_FILE)) return;
1461
1708
  const lines = fs.readFileSync(QUEUE_FILE, "utf-8").split("\n");
1462
- // Use a word-boundary match so TASK-1 does NOT also remove TASK-10, TASK-11, etc.
1709
+ // Word-boundary match so TASK-1 does NOT also remove TASK-10, TASK-11, etc.
1463
1710
  const idMatcher = new RegExp(
1464
1711
  `^${completedTask.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$|\\|)`,
1465
1712
  );
1713
+ // Remove task from both Pending and In Progress sections
1466
1714
  const filtered = lines.filter((l) => !idMatcher.test(l.trim()));
1715
+ // Find Completed section and insert entry
1467
1716
  const idx = filtered.findIndex(
1468
1717
  (l) =>
1469
1718
  l.trim().startsWith("## Completed") ||
@@ -1512,6 +1761,14 @@ function detectSupportAgentFailure(agentName) {
1512
1761
  }
1513
1762
 
1514
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
+
1515
1772
  return {
1516
1773
  exhaustedQuota:
1517
1774
  lower.includes("out of extra usage") ||
@@ -1519,7 +1776,9 @@ function detectSupportAgentFailure(agentName) {
1519
1776
  lower.includes("quota") ||
1520
1777
  lower.includes("rate_limit") ||
1521
1778
  lower.includes("ratelimitexceeded") ||
1522
- lower.includes("429"),
1779
+ lower.includes("429") ||
1780
+ lower.includes("insufficient credits") ||
1781
+ lower.includes("no credits"),
1523
1782
  providerUnavailable:
1524
1783
  lower.includes("session expired") ||
1525
1784
  lower.includes("authentication") ||
@@ -1532,6 +1791,7 @@ function detectSupportAgentFailure(agentName) {
1532
1791
  lower.includes("timed out") ||
1533
1792
  lower.includes("timeout") ||
1534
1793
  lower.includes("network error"),
1794
+ noRealWork: noRealWork || lower.includes("no files") || lower.includes("nothing to"),
1535
1795
  };
1536
1796
  }
1537
1797
 
@@ -1586,20 +1846,14 @@ function tryFallbackToAlternative(task, failedAgentName, reason) {
1586
1846
  failedTasks.set(task.id, 0);
1587
1847
  state.queue.push(task);
1588
1848
 
1589
- log(
1590
- "FALLBACK",
1591
- `${task.id} reasignada de ${failedAgentName} a ${targetAgent} (${reason})`,
1592
- );
1849
+ log("FALLBACK", L.logFallback(task.id, failedAgentName, targetAgent, reason));
1593
1850
  appendToAgent(
1594
1851
  failedAgentName,
1595
- `{yellow-fg}=== REASIGNADA A ${escBl(targetAgent)} (${escBl(reason)}) ==={/yellow-fg}`,
1852
+ `{yellow-fg}${escBl(L.agentReassigned(targetAgent, reason))}{/yellow-fg}`,
1596
1853
  true,
1597
1854
  );
1598
1855
  if (!queueUpdated) {
1599
- log(
1600
- "WARN",
1601
- `${task.id} reasignada a ${targetAgent}, pero QUEUE.md no pudo actualizarse`,
1602
- );
1856
+ log("WARN", L.logReassignWarn(task.id, targetAgent));
1603
1857
  }
1604
1858
  return true;
1605
1859
  }
@@ -1612,23 +1866,11 @@ if (!CLI.headless && screen) {
1612
1866
  exitWithSummary();
1613
1867
  });
1614
1868
 
1615
- screen.key("s", () => {
1616
- if (state.paused) {
1617
- state.paused = false;
1618
- log("INFO", "Reanudado");
1619
- }
1620
- scheduleNext();
1621
- safeRenderDashboard();
1622
- });
1623
1869
  screen.key("p", () => {
1624
1870
  state.paused = !state.paused;
1625
1871
  log("INFO", state.paused ? L.paused : L.resumed);
1626
1872
  safeRenderDashboard();
1627
- });
1628
- screen.key("r", () => {
1629
- reloadQueue();
1630
- log("INFO", L.queueReloaded(state.queue.length));
1631
- safeRenderDashboard();
1873
+ updateStatusFile();
1632
1874
  });
1633
1875
  }
1634
1876
 
@@ -1641,6 +1883,7 @@ log("INFO", L.loadedCompleted(state.completed.length));
1641
1883
  reloadQueue();
1642
1884
  log("INFO", `${L.queue}: ${state.queue.length} ${L.tasks}`);
1643
1885
  renderDashboard();
1886
+ updateStatusFile();
1644
1887
  if (!state.paused) {
1645
1888
  scheduleNext();
1646
1889
  renderDashboard();
@@ -1656,14 +1899,18 @@ setInterval(() => {
1656
1899
  if (!state.paused) scheduleNext();
1657
1900
  renderDashboard();
1658
1901
  }, POLL_INTERVAL_MS);
1902
+
1903
+ setInterval(() => {
1904
+ updateStatusFile();
1905
+ }, 60000); // Update STATUS.md cada 60 segundos
1659
1906
  setInterval(() => {
1660
1907
  for (const [name, ag] of Object.entries(state.agents)) {
1661
1908
  if (ag.status !== "busy" || !ag.process) continue;
1662
1909
  try {
1663
1910
  process.kill(ag.process.pid, 0);
1664
1911
  } catch {
1665
- log("WARN", `${name} died silently on ${ag.task?.id}`);
1666
- appendToAgent(name, "{red-fg}=== PROCESS DIED ==={/red-fg}", true);
1912
+ log("WARN", L.logDied(name, ag.task?.id));
1913
+ appendToAgent(name, `{red-fg}${escBl(L.agentDied)}{/red-fg}`, true);
1667
1914
  ag.process = null;
1668
1915
  failTask(ag.task, name, -1);
1669
1916
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liriraid/agentflow-ai",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Multi-agent workspace orchestrator with TUI. Coordinates AI coding agents over your real frontend and backend projects.",
5
5
  "author": "LiriRaid",
6
6
  "homepage": "https://github.com/LiriRaid/agentflow-ai#readme",