@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/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(1, Math.floor(100 / Math.max(1, agentNames.length)));
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(STATE_FILE, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
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(` ${datestamp()} ${timestamp()} ${WORKSPACE_LANGUAGE === "es" ? "activo" : "active"} ${up} ${cost} ${mode}`);
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 cfg = AGENTS[name];
598
- 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;
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 = ag.lastLine || "";
818
+ detail = lastLine;
819
+ dot = "{gray-fg}○{/gray-fg}";
605
820
  }
606
- const dot =
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}OCUPADO{/yellow-fg} ${escBl(ag.task?.id || "")} `,
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}EN ESPERA{/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 (line.startsWith("## In Progress") || line.startsWith("## En progreso")) {
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 (line.startsWith("## In Progress") || line.startsWith("## En progreso")) {
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
- `## [${timestamp()}] ${task.id} completada — ${agentName}`,
1037
+ L.inboxDone(timestamp(), task.id, agentName),
811
1038
  ``,
812
- `- **Tarea:** ${task.title}`,
813
- `- **Duración:** ${formatDuration(elapsed)}`,
814
- `- **Reporte:** ${progressFile}`,
815
- `- **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),
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
- `## [${timestamp()}] ${task.id} falló — ${failedAgent} → reasignada a ${newAgent}`,
1054
+ L.inboxFailed(timestamp(), task.id, failedAgent, newAgent),
828
1055
  ``,
829
- `- **Tarea:** ${task.title}`,
830
- `- **Motivo:** ${reason}`,
831
- `- **Nuevo agente:** ${newAgent}`,
832
- `- **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,
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 repoDir = REPOS[task.repo] || REPOS[agentCfg.defaultRepo] || ".";
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: ${task.repo}
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
- ...(CLI.yolo ? ["--dangerously-skip-permissions"] : []),
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 ${CLI.yolo ? "yolo" : "default"} ${CLI.yolo ? "--dangerously-skip-permissions --auto-accept-edits" : ""} --add-dir "${WORKSPACE}"`,
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 ${CLI.yolo ? "yolo" : "default"} ${CLI.yolo ? "--dangerously-skip-permissions --auto-accept-edits" : ""} --add-dir "${WORKSPACE}"`,
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}=== ${escBl(task.id)}: ${escBl(task.title)} ==={/cyan-fg}`,
1329
+ `{cyan-fg}${escBl(L.agentTaskHeader(task.id, task.title))}{/cyan-fg}`,
1079
1330
  true,
1080
1331
  );
1081
- appendToAgent(agentName, `{gray-fg}CWD: ${escBl(repoDir)}{/gray-fg}`, true);
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", `${agentName} timed out on ${task.id}`);
1097
- 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);
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) state.totalCost += 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}=== COMPLETADA en ${formatDuration(elapsed)}${escBl(costStr)} ==={/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 = `Última: ${task.id} completada`;
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
- : "reintento en 10 min";
1322
- 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));
1323
1589
  appendToAgent(
1324
1590
  agentName,
1325
- `{yellow-fg}=== LÍMITE DE CUOTA (${escBl(resetStr)}) ==={/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}=== FALLÓ (salida ${code}, intento ${retries}) ==={/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
- ? "cuota o límite agotado"
1617
+ ? L.reasonQuota
1354
1618
  : failureFlags.providerUnavailable
1355
- ? "proveedor o sesión no disponibles"
1356
- : "fallo persistente";
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
- renderDashboard();
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 = `FALLÓ: ${task.id}`;
1371
- log("ERROR", `${task.id} falló definitivamente tras ${retries} intentos`);
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
- ? `LÍMITE: ${task.id} (reintento a las ${retryTime})`
1388
- : `REINTENTO: ${task.id}`;
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
- renderDashboard();
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
- renderDashboard();
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
- // 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.
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
- const idx = filtered.findIndex((l) => l.trim().startsWith("## Completed") || l.trim().startsWith("## Completadas"));
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
- // If both repos point to the same resolved path (frontend-only project), always prefer Frontend
1519
- if (
1520
- REPOS.backend &&
1521
- REPOS.frontend &&
1522
- path.resolve(REPOS.backend) === path.resolve(REPOS.frontend)
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 Object.keys(AGENTS).find((name) => AGENTS[name]?.cli === "claude") || null;
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 ? siblingAgent : getClaudeFallbackAgent(task);
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", `${task.id} reasignada de ${failedAgentName} a ${targetAgent} (${reason})`);
1849
+ log("FALLBACK", L.logFallback(task.id, failedAgentName, targetAgent, reason));
1560
1850
  appendToAgent(
1561
1851
  failedAgentName,
1562
- `{yellow-fg}=== REASIGNADA A ${escBl(targetAgent)} (${escBl(reason)}) ==={/yellow-fg}`,
1852
+ `{yellow-fg}${escBl(L.agentReassigned(targetAgent, reason))}{/yellow-fg}`,
1563
1853
  true,
1564
1854
  );
1565
1855
  if (!queueUpdated) {
1566
- log("WARN", `${task.id} reasignada a ${targetAgent}, pero QUEUE.md no pudo actualizarse`);
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
- renderDashboard();
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", `${name} died silently on ${ag.task?.id}`);
1633
- 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);
1634
1914
  ag.process = null;
1635
1915
  failTask(ag.task, name, -1);
1636
1916
  }