@liriraid/agentflow-ai 1.0.14 → 1.0.16

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
@@ -121,6 +121,8 @@ if (!fs.existsSync(CONFIG_FILE)) {
121
121
  const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
122
122
 
123
123
  const QUEUE_FILE = path.join(WORKSPACE, "QUEUE.md");
124
+ const INBOX_FILE = path.join(WORKSPACE, "INBOX.md");
125
+ const AWAY_MODE_FILE = path.join(WORKSPACE, ".away-mode");
124
126
  const LOG_DIR = path.join(WORKSPACE, "logs");
125
127
 
126
128
  const REPOS = config.repos || {};
@@ -129,6 +131,36 @@ const PROJECT_NAME = config.projectName || "Orchestrator Multi-Agents";
129
131
  const WORKSPACE_LANGUAGE = ["en", "es"].includes(config.workspaceLanguage)
130
132
  ? config.workspaceLanguage
131
133
  : "es";
134
+
135
+ // OpenAI model pricing ($ per 1M tokens) — update when prices change
136
+ const OPENAI_MODEL_PRICING = {
137
+ "gpt-4.1": { input: 2.0, output: 8.0 },
138
+ "gpt-4.1-mini": { input: 0.4, output: 1.6 },
139
+ "gpt-4.1-nano": { input: 0.1, output: 0.4 },
140
+ "gpt-4o": { input: 2.5, output: 10.0 },
141
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
142
+ "o4-mini": { input: 1.1, output: 4.4 },
143
+ "o3": { input: 10.0, output: 40.0 },
144
+ "o3-mini": { input: 1.1, output: 4.4 },
145
+ "o1": { input: 15.0, output: 60.0 },
146
+ "o1-mini": { input: 3.0, output: 12.0 },
147
+ "gpt-5": { input: 5.0, output: 20.0 },
148
+ "gpt-5.5": { input: 5.0, output: 20.0 },
149
+ "codex-mini-latest": { input: 1.5, output: 6.0 },
150
+ };
151
+
152
+ function calcOpenAICost(model, usage) {
153
+ if (!model || !usage) return null;
154
+ const modelLower = String(model).toLowerCase();
155
+ const key = Object.keys(OPENAI_MODEL_PRICING).find(k =>
156
+ modelLower === k || modelLower.startsWith(k) || modelLower.includes(k)
157
+ );
158
+ if (!key) return null;
159
+ const price = OPENAI_MODEL_PRICING[key];
160
+ const inputTokens = usage.input_tokens || usage.prompt_tokens || 0;
161
+ const outputTokens = usage.output_tokens || usage.completion_tokens || 0;
162
+ return (inputTokens * price.input + outputTokens * price.output) / 1_000_000;
163
+ }
132
164
  const TEXT = {
133
165
  es: {
134
166
  configExists:
@@ -160,6 +192,8 @@ const TEXT = {
160
192
  running: "EJECUTANDO",
161
193
  busy: "OCUPADO",
162
194
  idle: "EN ESPERA",
195
+ failed: "FALLÓ",
196
+ retrying: "REINTENTANDO",
163
197
  queueReloaded: (count) => `Cola recargada: ${count} tareas`,
164
198
  quitRequested: "Cierre solicitado desde Ink",
165
199
  starting: (name) => `${name} iniciando`,
@@ -173,6 +207,65 @@ const TEXT = {
173
207
  retryAt: (time, remaining) => `reintenta a las ${time} (${remaining} min)`,
174
208
  log: "REGISTRO",
175
209
  controls: "Seguir Pausa Recargar Quitar",
210
+ // QUEUE.md section headers
211
+ sectionPending: "## Pendientes",
212
+ sectionInProgress: "## En progreso",
213
+ sectionCompleted: "## Completadas",
214
+ // appendToAgent messages
215
+ agentTaskHeader: (id, title) => `=== ${id}: ${title} ===`,
216
+ agentCwd: (dir) => `CWD: ${dir}`,
217
+ agentCompleted: (elapsed, cost) => `=== COMPLETADA en ${elapsed}${cost} ===`,
218
+ agentFailed: (code, attempt) => `=== FALLÓ (salida ${code}, intento ${attempt}) ===`,
219
+ agentRateLimit: (resetStr) => `=== LÍMITE DE CUOTA (${resetStr}) ===`,
220
+ agentTimeout: "=== TIMEOUT ===",
221
+ agentDied: "=== PROCESS DIED ===",
222
+ agentReassigned: (agent, reason) => `=== REASIGNADA A ${agent} (${reason}) ===`,
223
+ // ag.lastLine state machine prefixes (used for status detection)
224
+ lastCompleted: (id) => `Última: ${id} completada`,
225
+ lastRetry: (id) => `REINTENTO: ${id}`,
226
+ lastLimit: (id, time) => `LÍMITE: ${id} (reintento a las ${time})`,
227
+ lastFailed: (id) => `FALLÓ: ${id}`,
228
+ // Fallback reasons
229
+ reasonQuota: "cuota o límite agotado",
230
+ reasonProvider: "proveedor o sesión no disponibles",
231
+ reasonNoWork: "el agente no trabajó nada (sin cambios)",
232
+ reasonPersistent: "fallo persistente",
233
+ // Log messages
234
+ logRateLimit: (agent, id, resetStr) => `${agent} alcanzó el límite en ${id} (${resetStr})`,
235
+ logFail: (agent, id, code, retries, max) => `${agent} falló ${id} (salida ${code}, ${retries}/${max})`,
236
+ logDone: (agent, id, elapsed, cost) => `${agent} completó ${id} en ${elapsed}${cost}`,
237
+ logFallback: (id, from, to, reason) => `${id} reasignada de ${from} a ${to} (${reason})`,
238
+ logReassignWarn: (id, agent) => `${id} reasignada a ${agent}, pero QUEUE.md no pudo actualizarse`,
239
+ logTimeout: (agent, id) => `${agent} timed out on ${id}`,
240
+ logUnknownAgent: (id, agent) => `${id} skipped — agente "${agent}" no definido en orchestrator.config.json`,
241
+ logPermanentFail: (id, retries) => `${id} falló definitivamente tras ${retries} intentos`,
242
+ logDied: (agent, id) => `${agent} terminó silenciosamente en ${id}`,
243
+ // STATUS.md
244
+ statusTitle: (ts) => `# Estado del Orquestador - ${ts}`,
245
+ statusProject: "**Proyecto:**",
246
+ statusState: "**Estado:**",
247
+ statusRunning: "EJECUTANDO",
248
+ statusPausedLabel: "PAUSADO",
249
+ statusActiveTime: "**Activo:**",
250
+ statusSectionAgents: "## Agentes",
251
+ statusSectionQueue: "## Cola",
252
+ statusPendingLabel: "Pendientes:",
253
+ statusCompletedLabel: "Completadas:",
254
+ statusInProgressLabel: "En progreso:",
255
+ statusInProgressHeader: "### En progreso",
256
+ statusAgentBusy: "🟡 OCUPADO",
257
+ statusAgentIdle: "⚪ EN ESPERA",
258
+ statusNoTask: "Sin tarea",
259
+ // INBOX.md
260
+ inboxDone: (ts, id, agent) => `## [${ts}] ${id} completada — ${agent}`,
261
+ inboxTaskLabel: "- **Tarea:**",
262
+ inboxDurationLabel: "- **Duración:**",
263
+ inboxReportLabel: "- **Reporte:**",
264
+ inboxActionLabel: (file) => `- **Acción:** Lee \`${file}\` y crea las siguientes TASKs si corresponde.`,
265
+ inboxFailed: (ts, id, from, to) => `## [${ts}] ${id} falló — ${from} → reasignada a ${to}`,
266
+ inboxReasonLabel: "- **Motivo:**",
267
+ inboxNewAgentLabel: "- **Nuevo agente:**",
268
+ inboxFailAction: "- **Acción:** La TUI reasignó automáticamente. Verifica en QUEUE.md o espera la siguiente notificación de completada.",
176
269
  },
177
270
  en: {
178
271
  configExists:
@@ -204,6 +297,8 @@ const TEXT = {
204
297
  running: "RUNNING",
205
298
  busy: "BUSY",
206
299
  idle: "IDLE",
300
+ failed: "FAILED",
301
+ retrying: "RETRYING",
207
302
  queueReloaded: (count) => `Queue reloaded: ${count} tasks`,
208
303
  quitRequested: "Quit requested from Ink",
209
304
  starting: (name) => `${name} starting`,
@@ -216,6 +311,65 @@ const TEXT = {
216
311
  retryAt: (time, remaining) => `retry at ${time} (${remaining} min)`,
217
312
  log: "LOG",
218
313
  controls: "Start Pause Reload Quit",
314
+ // QUEUE.md section headers
315
+ sectionPending: "## Pending",
316
+ sectionInProgress: "## In Progress",
317
+ sectionCompleted: "## Completed",
318
+ // appendToAgent messages
319
+ agentTaskHeader: (id, title) => `=== ${id}: ${title} ===`,
320
+ agentCwd: (dir) => `CWD: ${dir}`,
321
+ agentCompleted: (elapsed, cost) => `=== COMPLETED in ${elapsed}${cost} ===`,
322
+ agentFailed: (code, attempt) => `=== FAILED (exit ${code}, attempt ${attempt}) ===`,
323
+ agentRateLimit: (resetStr) => `=== QUOTA LIMIT (${resetStr}) ===`,
324
+ agentTimeout: "=== TIMEOUT ===",
325
+ agentDied: "=== PROCESS DIED ===",
326
+ agentReassigned: (agent, reason) => `=== REASSIGNED TO ${agent} (${reason}) ===`,
327
+ // ag.lastLine state machine prefixes
328
+ lastCompleted: (id) => `Last: ${id} completed`,
329
+ lastRetry: (id) => `RETRY: ${id}`,
330
+ lastLimit: (id, time) => `LIMIT: ${id} (retry at ${time})`,
331
+ lastFailed: (id) => `FAILED: ${id}`,
332
+ // Fallback reasons
333
+ reasonQuota: "quota or rate limit exhausted",
334
+ reasonProvider: "provider or session unavailable",
335
+ reasonNoWork: "agent did no real work (no changes)",
336
+ reasonPersistent: "persistent failure",
337
+ // Log messages
338
+ logRateLimit: (agent, id, resetStr) => `${agent} hit rate limit on ${id} (${resetStr})`,
339
+ logFail: (agent, id, code, retries, max) => `${agent} failed ${id} (exit ${code}, ${retries}/${max})`,
340
+ logDone: (agent, id, elapsed, cost) => `${agent} completed ${id} in ${elapsed}${cost}`,
341
+ logFallback: (id, from, to, reason) => `${id} reassigned from ${from} to ${to} (${reason})`,
342
+ logReassignWarn: (id, agent) => `${id} reassigned to ${agent}, but QUEUE.md could not be updated`,
343
+ logTimeout: (agent, id) => `${agent} timed out on ${id}`,
344
+ logUnknownAgent: (id, agent) => `${id} skipped — agent "${agent}" not in orchestrator.config.json`,
345
+ logPermanentFail: (id, retries) => `${id} permanently failed after ${retries} attempts`,
346
+ logDied: (agent, id) => `${agent} died silently on ${id}`,
347
+ // STATUS.md
348
+ statusTitle: (ts) => `# Orchestrator Status - ${ts}`,
349
+ statusProject: "**Project:**",
350
+ statusState: "**State:**",
351
+ statusRunning: "RUNNING",
352
+ statusPausedLabel: "PAUSED",
353
+ statusActiveTime: "**Active:**",
354
+ statusSectionAgents: "## Agents",
355
+ statusSectionQueue: "## Queue",
356
+ statusPendingLabel: "Pending:",
357
+ statusCompletedLabel: "Completed:",
358
+ statusInProgressLabel: "In progress:",
359
+ statusInProgressHeader: "### In progress",
360
+ statusAgentBusy: "🟡 BUSY",
361
+ statusAgentIdle: "⚪ IDLE",
362
+ statusNoTask: "No task",
363
+ // INBOX.md
364
+ inboxDone: (ts, id, agent) => `## [${ts}] ${id} completed — ${agent}`,
365
+ inboxTaskLabel: "- **Task:**",
366
+ inboxDurationLabel: "- **Duration:**",
367
+ inboxReportLabel: "- **Report:**",
368
+ inboxActionLabel: (file) => `- **Action:** Read \`${file}\` and create next TASKs if applicable.`,
369
+ inboxFailed: (ts, id, from, to) => `## [${ts}] ${id} failed — ${from} → reassigned to ${to}`,
370
+ inboxReasonLabel: "- **Reason:**",
371
+ inboxNewAgentLabel: "- **New agent:**",
372
+ inboxFailAction: "- **Action:** TUI reassigned automatically. Check QUEUE.md or wait for the next completion notification.",
219
373
  },
220
374
  };
221
375
  const L = TEXT[WORKSPACE_LANGUAGE];
@@ -279,6 +433,38 @@ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
279
433
  const LOCK_FILE = path.join(LOG_DIR, "orchestrator.lock");
280
434
  const STATE_FILE = path.join(LOG_DIR, "orchestrator-state.json");
281
435
  const CONTROL_FILE = path.join(LOG_DIR, "orchestrator-control.json");
436
+ const STATUS_FILE = path.join(WORKSPACE, "STATUS.md");
437
+
438
+ function updateStatusFile() {
439
+ const statusLines = [
440
+ L.statusTitle(timestamp()),
441
+ ``,
442
+ `${L.statusProject} ${PROJECT_NAME}`,
443
+ `${L.statusState} ${state.paused ? L.statusPausedLabel : L.statusRunning}`,
444
+ `${L.statusActiveTime} ${formatDuration(Math.round((Date.now() - state.startTime) / 1000))}`,
445
+ ``,
446
+ L.statusSectionAgents,
447
+ ``,
448
+ ];
449
+ for (const [name, ag] of Object.entries(state.agents)) {
450
+ const status = ag.status === "busy" ? L.statusAgentBusy : L.statusAgentIdle;
451
+ const task = ag.task ? `${ag.task.id}: ${ag.task.title}` : L.statusNoTask;
452
+ statusLines.push(`- **${name}**: ${status} - ${task}`);
453
+ }
454
+ statusLines.push("", L.statusSectionQueue);
455
+ statusLines.push("", `${L.statusPendingLabel} ${state.queue.length}`);
456
+ statusLines.push("", `${L.statusCompletedLabel} ${state.completed.length}`);
457
+ statusLines.push("", `${L.statusInProgressLabel} ${state.inProgress.length}`);
458
+ if (state.inProgress.length > 0) {
459
+ statusLines.push("", L.statusInProgressHeader);
460
+ for (const t of state.inProgress) {
461
+ statusLines.push(`- ${t.id}: ${t.title} (${t.agent})`);
462
+ }
463
+ }
464
+ try {
465
+ fs.writeFileSync(STATUS_FILE, statusLines.join("\n"), "utf-8");
466
+ } catch {}
467
+ }
282
468
 
283
469
  // Limpiar control.json orphan al iniciar (si el proceso anterior fechou mal)
284
470
  if (fs.existsSync(CONTROL_FILE)) {
@@ -373,6 +559,7 @@ for (const name of Object.keys(AGENTS)) {
373
559
  lastLine: "",
374
560
  exitCode: null,
375
561
  cost: null,
562
+ totalCost: 0,
376
563
  turns: 0,
377
564
  };
378
565
  }
@@ -486,6 +673,7 @@ function persistState() {
486
673
  lastLine: ag.lastLine,
487
674
  exitCode: ag.exitCode,
488
675
  cost: ag.cost,
676
+ totalCost: ag.totalCost || 0,
489
677
  turns: ag.turns,
490
678
  },
491
679
  ]),
@@ -611,18 +799,28 @@ function renderDashboard() {
611
799
  lines.push("");
612
800
 
613
801
  for (const [name, ag] of Object.entries(state.agents)) {
614
- const cfg = AGENTS[name];
615
- let status, detail;
802
+ const lastLine = ag.lastLine || "";
803
+ const isFailed = lastLine.startsWith("FALLÓ:") || lastLine.startsWith("FAILED:");
804
+ const isRetrying = lastLine.startsWith("REINTENTO:") || lastLine.startsWith("LÍMITE:") || lastLine.startsWith("RETRY:") || lastLine.startsWith("LIMIT:");
805
+ let status, detail, dot;
616
806
  if (ag.status === "busy") {
617
807
  status = `{yellow-fg}${L.busy}{/yellow-fg}`;
618
808
  detail = `${ag.task?.id || "?"} ${(ag.task?.title || "").slice(0, 35)} (${elapsedSince(ag.startTime)})`;
809
+ dot = "{green-fg}●{/green-fg}";
810
+ } else if (isFailed) {
811
+ status = `{red-fg}${L.failed}{/red-fg}`;
812
+ detail = lastLine;
813
+ dot = "{red-fg}✕{/red-fg}";
814
+ } else if (isRetrying) {
815
+ status = `{yellow-fg}${L.retrying}{/yellow-fg}`;
816
+ detail = lastLine;
817
+ dot = "{yellow-fg}⟳{/yellow-fg}";
619
818
  } else {
620
819
  status = `{gray-fg}${L.idle}{/gray-fg}`;
621
- detail = ag.lastLine || "";
820
+ detail = lastLine;
821
+ dot = "{gray-fg}○{/gray-fg}";
622
822
  }
623
- const dot =
624
- ag.status === "busy" ? "{green-fg}●{/green-fg}" : "{gray-fg}○{/gray-fg}";
625
- lines.push(` ${dot} {bold}${name}{/bold} ${status} ${detail}`);
823
+ lines.push(` ${dot} {bold}${name}{/bold} ${status} ${escBl(detail)}`);
626
824
  }
627
825
  lines.push("");
628
826
 
@@ -691,15 +889,25 @@ function renderDashboard() {
691
889
 
692
890
  for (const [name, ag] of Object.entries(state.agents)) {
693
891
  const box = agentBoxes[name];
892
+ const lastLine = ag.lastLine || "";
893
+ const isFailed = lastLine.startsWith("FALLÓ:") || lastLine.startsWith("FAILED:");
894
+ const isRetrying = lastLine.startsWith("REINTENTO:") || lastLine.startsWith("LÍMITE:") || lastLine.startsWith("RETRY:") || lastLine.startsWith("LIMIT:");
694
895
  if (ag.status === "busy") {
695
896
  box.style.border.fg = "yellow";
696
897
  box.setLabel(
697
- ` {bold}${escBl(name)}{/bold} {yellow-fg}OCUPADO{/yellow-fg} ${escBl(ag.task?.id || "")} `,
898
+ ` {bold}${escBl(name)}{/bold} {yellow-fg}${L.busy}{/yellow-fg} ${escBl(ag.task?.id || "")} `,
698
899
  );
900
+ } else if (isFailed) {
901
+ box.style.border.fg = "red";
902
+ box.setLabel(` {bold}${escBl(name)}{/bold} {red-fg}${L.failed}{/red-fg} `);
903
+ } else if (isRetrying) {
904
+ box.style.border.fg = "yellow";
905
+ box.setLabel(` {bold}${escBl(name)}{/bold} {yellow-fg}${L.retrying}{/yellow-fg} `);
699
906
  } else {
700
907
  box.style.border.fg = "gray";
908
+ const agCostLabel = ag.totalCost > 0 ? ` {gray-fg}$${ag.totalCost.toFixed(2)}{/gray-fg}` : "";
701
909
  box.setLabel(
702
- ` {bold}${escBl(name)}{/bold} {gray-fg}EN ESPERA{/gray-fg} `,
910
+ ` {bold}${escBl(name)}{/bold} {gray-fg}${L.idle}{/gray-fg}${agCostLabel} `,
703
911
  );
704
912
  }
705
913
  }
@@ -824,37 +1032,64 @@ function reloadQueue() {
824
1032
  // session can detect it on next interaction without Modo Ausencia active.
825
1033
  // ============================================================================
826
1034
  function writeInboxNotification(task, agentName, elapsed) {
827
- const inboxFile = path.join(WORKSPACE, "INBOX.md");
828
1035
  const progressFile = `progress/PROGRESS-${agentName}.md`;
829
1036
  const entry = [
830
1037
  ``,
831
- `## [${timestamp()}] ${task.id} completada — ${agentName}`,
1038
+ L.inboxDone(timestamp(), task.id, agentName),
832
1039
  ``,
833
- `- **Tarea:** ${task.title}`,
834
- `- **Duración:** ${formatDuration(elapsed)}`,
835
- `- **Reporte:** ${progressFile}`,
836
- `- **Acción:** Lee \`${progressFile}\` y crea las siguientes TASKs si corresponde.`,
1040
+ `${L.inboxTaskLabel} ${task.title}`,
1041
+ `${L.inboxDurationLabel} ${formatDuration(elapsed)}`,
1042
+ `${L.inboxReportLabel} ${progressFile}`,
1043
+ L.inboxActionLabel(progressFile),
837
1044
  ``,
838
1045
  ].join("\n");
839
1046
  try {
840
- fs.appendFileSync(inboxFile, entry, "utf-8");
1047
+ fs.appendFileSync(INBOX_FILE, entry, "utf-8");
841
1048
  } catch {}
842
1049
  }
843
1050
 
844
1051
  function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
845
- 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 {
857
- fs.appendFileSync(inboxFile, entry, "utf-8");
1063
+ fs.appendFileSync(INBOX_FILE, entry, "utf-8");
1064
+ } catch {}
1065
+ }
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");
858
1093
  } catch {}
859
1094
  }
860
1095
 
@@ -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,24 +1846,55 @@ 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
  }
1858
+
1859
+ // Notificar a Claude (sesión principal) cuando hay fallback
1860
+ notifyClaudeOfFallback(task, failedAgentName, targetAgent, reason);
1604
1861
  return true;
1605
1862
  }
1606
1863
 
1864
+ // ============================================================================
1865
+ // CLAUDE FALLBACK NOTIFIER — avisa a Claude principal cuando hay reasignación
1866
+ // ============================================================================
1867
+ function notifyClaudeOfFallback(task, fromAgent, toAgent, reason) {
1868
+ const lang = WORKSPACE_LANGUAGE;
1869
+ const prompt = lang === 'es'
1870
+ ? `⚠️ FALLBACK: La tarea "${task.id}: ${task.title}" falló en ${fromAgent} (${reason}) y fue reasignada a ${toAgent}.
1871
+
1872
+ Estado actual:
1873
+ - QUEUE.md tiene ahora la tarea asignada a ${toAgent}
1874
+ - El agente ${toAgent} está procediendo automáticamente
1875
+
1876
+ Acción: No necesitas hacer nada — solo toma nota del cambio. El orquestador将继续 automáticamente.
1877
+ Si quieres revisar el progreso, lee INBOX.md o STATUS.md.`
1878
+ : `⚠️ FALLBACK: Task "${task.id}: ${task.title}" failed on ${fromAgent} (${reason}) and was reassigned to ${toAgent}.
1879
+
1880
+ Current state:
1881
+ - QUEUE.md now has the task assigned to ${toAgent}
1882
+ - Agent ${toAgent} is proceeding automatically
1883
+
1884
+ Action: You don't need to do anything — just take note of the change. The orchestrator will continue automatically.
1885
+ If you want to check progress, read INBOX.md or STATUS.md.`;
1886
+
1887
+ const logPath = path.join(LOG_DIR, `fallback-notify-${Date.now()}.log`);
1888
+ try {
1889
+ const logFd = fs.openSync(logPath, 'a');
1890
+ const child = spawn('claude', ['-p', prompt, '--add-dir', WORKSPACE, '--dangerously-skip-permissions'], {
1891
+ cwd: WORKSPACE, stdio: ['ignore', logFd, logFd], shell: true, windowsHide: true, detached: true
1892
+ });
1893
+ fs.closeSync(logFd);
1894
+ child.unref();
1895
+ } catch {}
1896
+ }
1897
+
1607
1898
  // ============================================================================
1608
1899
  // KEYBOARD
1609
1900
  // ============================================================================
@@ -1612,23 +1903,11 @@ if (!CLI.headless && screen) {
1612
1903
  exitWithSummary();
1613
1904
  });
1614
1905
 
1615
- screen.key("s", () => {
1616
- if (state.paused) {
1617
- state.paused = false;
1618
- log("INFO", "Reanudado");
1619
- }
1620
- scheduleNext();
1621
- safeRenderDashboard();
1622
- });
1623
1906
  screen.key("p", () => {
1624
1907
  state.paused = !state.paused;
1625
1908
  log("INFO", state.paused ? L.paused : L.resumed);
1626
1909
  safeRenderDashboard();
1627
- });
1628
- screen.key("r", () => {
1629
- reloadQueue();
1630
- log("INFO", L.queueReloaded(state.queue.length));
1631
- safeRenderDashboard();
1910
+ updateStatusFile();
1632
1911
  });
1633
1912
  }
1634
1913
 
@@ -1641,6 +1920,7 @@ log("INFO", L.loadedCompleted(state.completed.length));
1641
1920
  reloadQueue();
1642
1921
  log("INFO", `${L.queue}: ${state.queue.length} ${L.tasks}`);
1643
1922
  renderDashboard();
1923
+ updateStatusFile();
1644
1924
  if (!state.paused) {
1645
1925
  scheduleNext();
1646
1926
  renderDashboard();
@@ -1651,19 +1931,249 @@ setInterval(() => {
1651
1931
  if (command) applyControlCommand(command);
1652
1932
  }, 1000);
1653
1933
 
1934
+ // Real-time queue detection via fs.watch — fires immediately when QUEUE.md changes
1935
+ // (e.g. Claude writes a new task). No more 30s delay.
1936
+ let _queueWatchDebounce = null;
1937
+ function startQueueWatcher() {
1938
+ if (!fs.existsSync(QUEUE_FILE)) return;
1939
+ try {
1940
+ const watcher = fs.watch(QUEUE_FILE, {persistent: false}, (eventType) => {
1941
+ if (eventType !== 'change') return;
1942
+ if (_queueWatchDebounce) clearTimeout(_queueWatchDebounce);
1943
+ _queueWatchDebounce = setTimeout(() => {
1944
+ const prevCount = state.queue.length;
1945
+ reloadQueue();
1946
+ if (!state.paused) scheduleNext();
1947
+ renderDashboard();
1948
+ if (state.queue.length > prevCount)
1949
+ log("INFO", WORKSPACE_LANGUAGE === "es"
1950
+ ? `Nueva tarea detectada en QUEUE.md`
1951
+ : `New task detected in QUEUE.md`);
1952
+ }, 400);
1953
+ });
1954
+ watcher.on('error', () => {});
1955
+ } catch {}
1956
+ }
1957
+ startQueueWatcher();
1958
+
1959
+ // Slow fallback (5 min) — only runs if there is actually pending work or busy agents
1960
+ // fs.watch handles real-time; this is just a safety net
1654
1961
  setInterval(() => {
1962
+ const busy = Object.values(state.agents).some(a => a.status === 'busy');
1963
+ if (state.paused || (state.queue.length === 0 && !busy)) return;
1655
1964
  reloadQueue();
1656
- if (!state.paused) scheduleNext();
1965
+ scheduleNext();
1657
1966
  renderDashboard();
1658
- }, POLL_INTERVAL_MS);
1967
+ }, 5 * 60 * 1000);
1968
+
1969
+ // ============================================================================
1970
+ // INBOX WATCHER — reacts immediately when a task completion is written to INBOX.md
1971
+ // Spawns headless Claude to check if a new implementation task needs to be created
1972
+ // ============================================================================
1973
+ let _inboxDebounce = null;
1974
+ let _lastInboxContent = '';
1975
+ let _inboxDispatching = false;
1976
+
1977
+ function dispatchInboxClaude() {
1978
+ if (_inboxDispatching) return;
1979
+ let content = '';
1980
+ try { content = fs.existsSync(INBOX_FILE) ? fs.readFileSync(INBOX_FILE, 'utf-8') : ''; } catch {}
1981
+ if (!content.trim() || content === _lastInboxContent) return;
1982
+
1983
+ _lastInboxContent = content;
1984
+ _inboxDispatching = true;
1985
+
1986
+ const lang = WORKSPACE_LANGUAGE;
1987
+ const prompt = lang === 'es'
1988
+ ? `Eres el orquestador de este workspace. Tu única misión ahora es procesar el INBOX.
1989
+
1990
+ Pasos:
1991
+ 1. Lee INBOX.md en ${WORKSPACE}
1992
+ 2. Lee QUEUE.md en ${WORKSPACE} para ver las tareas existentes (secciones Pendientes, En progreso, Completadas)
1993
+
1994
+ Si en INBOX.md hay análisis completados de un agente (especialmente OpenCode) que aún NO tienen su tarea de implementación en la sección ## Pendientes de QUEUE.md:
1995
+ - Determina el siguiente TASK ID disponible leyendo QUEUE.md
1996
+ - Crea la nueva TASK en QUEUE.md con el formato exacto:
1997
+ TASK-NNN | título corto | Codex | P1 | repo | descripción basada en el análisis
1998
+
1999
+ Si ya existe la tarea correspondiente, o el análisis no está completo, responde solo: "Sin acción necesaria."
2000
+
2001
+ Reglas: No hagas commit ni push. No analices código del proyecto. Solo lee INBOX.md y QUEUE.md, y edita QUEUE.md si hace falta.`
2002
+ : `You are the orchestrator for this workspace. Your only mission now is to process the INBOX.
2003
+
2004
+ Steps:
2005
+ 1. Read INBOX.md in ${WORKSPACE}
2006
+ 2. Read QUEUE.md in ${WORKSPACE} to see existing tasks (sections Pending, In Progress, Completed)
2007
+
2008
+ If INBOX.md contains completed analyses from an agent (especially OpenCode) that do NOT yet have a corresponding implementation task in the ## Pending section of QUEUE.md:
2009
+ - Determine the next available TASK ID by reading QUEUE.md
2010
+ - Create the new TASK in QUEUE.md with the exact format:
2011
+ TASK-NNN | short title | Codex | P1 | repo | description based on the analysis
2012
+
2013
+ If the corresponding task already exists, or the analysis is not complete, reply only: "No action needed."
2014
+
2015
+ Rules: Do not commit or push. Do not analyze project code. Only read INBOX.md and QUEUE.md, and edit QUEUE.md if necessary.`;
2016
+
2017
+ const logPath = path.join(LOG_DIR, `inbox-trigger-${Date.now()}.log`);
2018
+ try {
2019
+ const logFd = fs.openSync(logPath, 'a');
2020
+ const child = spawn('claude', [
2021
+ '-p', prompt,
2022
+ '--add-dir', WORKSPACE,
2023
+ '--dangerously-skip-permissions'
2024
+ ], {
2025
+ cwd: WORKSPACE,
2026
+ stdio: ['ignore', logFd, logFd],
2027
+ shell: true,
2028
+ windowsHide: true,
2029
+ detached: true
2030
+ });
2031
+ fs.closeSync(logFd);
2032
+ child.unref();
2033
+ log('INFO', lang === 'es'
2034
+ ? 'INBOX: Claude despachado para procesar notificación'
2035
+ : 'INBOX: Claude dispatched to process notification');
2036
+ } catch {}
2037
+ setTimeout(() => { _inboxDispatching = false; }, 3 * 60 * 1000);
2038
+ }
2039
+
2040
+ function startInboxWatcher() {
2041
+ if (!fs.existsSync(INBOX_FILE)) {
2042
+ try { fs.writeFileSync(INBOX_FILE, '', 'utf-8'); } catch {}
2043
+ }
2044
+ try {
2045
+ const watcher = fs.watch(INBOX_FILE, {persistent: false}, (eventType) => {
2046
+ if (eventType !== 'change') return;
2047
+ if (_inboxDebounce) clearTimeout(_inboxDebounce);
2048
+ _inboxDebounce = setTimeout(dispatchInboxClaude, 600);
2049
+ });
2050
+ watcher.on('error', () => {});
2051
+ } catch {}
2052
+ }
2053
+ startInboxWatcher();
2054
+
2055
+ // ============================================================================
2056
+ // AWAY MODE WATCHER — monitors .away-mode file; when active runs periodic
2057
+ // health checks via headless Claude; auto-deactivates when all tasks are done
2058
+ // ============================================================================
2059
+ let _awayModeTimer = null;
2060
+ let _awayModeActive = false;
2061
+
2062
+ function runAwayModeCheck() {
2063
+ if (!fs.existsSync(AWAY_MODE_FILE)) {
2064
+ deactivateAwayMode();
2065
+ return;
2066
+ }
2067
+
2068
+ const lang = WORKSPACE_LANGUAGE;
2069
+ const pendingTasks = state.queue.filter(t => !t.status || t.status === 'pending');
2070
+ const inProgressTasks = state.inProgress || [];
2071
+ const busy = Object.values(state.agents).some(a => a.status === 'busy');
2072
+ const completedCount = (state.completed || []).length;
2073
+ const hasWork = pendingTasks.length > 0 || inProgressTasks.length > 0 || busy;
2074
+
2075
+ if (!hasWork && completedCount > 0) {
2076
+ try { fs.unlinkSync(AWAY_MODE_FILE); } catch {}
2077
+ deactivateAwayMode();
2078
+
2079
+ const donePrompt = lang === 'es'
2080
+ ? `Modo Ausencia terminado. Todas las tareas se completaron mientras estabas ausente.\n\nLee QUEUE.md en ${WORKSPACE} y dame un resumen de todo lo que se logró durante la sesión.\nLuego dime si hay algo que podamos continuar o integrar a partir de lo que ya se hizo, o pregúntame qué quiero priorizar a continuación.`
2081
+ : `Away Mode ended. All tasks were completed while you were away.\n\nRead QUEUE.md in ${WORKSPACE} and give me a summary of everything accomplished during the session.\nThen tell me if there is anything we can continue or integrate from what was done, or ask me what I want to prioritize next.`;
2082
+
2083
+ const logPath = path.join(LOG_DIR, `away-done-${Date.now()}.log`);
2084
+ try {
2085
+ const logFd = fs.openSync(logPath, 'a');
2086
+ const child = spawn('claude', ['-p', donePrompt, '--add-dir', WORKSPACE, '--dangerously-skip-permissions'], {
2087
+ cwd: WORKSPACE, stdio: ['ignore', logFd, logFd], shell: true, windowsHide: true, detached: true
2088
+ });
2089
+ fs.closeSync(logFd);
2090
+ child.unref();
2091
+ log('INFO', lang === 'es' ? 'Modo Ausencia: todo completado — resumen final enviado.' : 'Away Mode: all done — final summary dispatched.');
2092
+ } catch {}
2093
+ return;
2094
+ }
2095
+
2096
+ if (!hasWork) return;
2097
+
2098
+ const lines = [];
2099
+ if (pendingTasks.length > 0) {
2100
+ lines.push(lang === 'es' ? `Tareas pendientes: ${pendingTasks.length}` : `Pending tasks: ${pendingTasks.length}`);
2101
+ pendingTasks.slice(0, 5).forEach(t => lines.push(` - ${t.id}: ${t.title}`));
2102
+ }
2103
+ if (inProgressTasks.length > 0) {
2104
+ lines.push(lang === 'es'
2105
+ ? `En progreso: ${inProgressTasks.map(t => `${t.id} (${t.agent})`).join(', ')}`
2106
+ : `In progress: ${inProgressTasks.map(t => `${t.id} (${t.agent})`).join(', ')}`);
2107
+ }
2108
+ const failedAgents = Object.entries(state.agents)
2109
+ .filter(([, a]) => /^(FALLÓ|FAILED):/.test(a.lastLine || ''))
2110
+ .map(([n, a]) => `${n}: ${a.lastLine}`);
2111
+ if (failedAgents.length > 0) {
2112
+ lines.push(lang === 'es' ? `Agentes con fallo: ${failedAgents.join(' | ')}` : `Failed agents: ${failedAgents.join(' | ')}`);
2113
+ }
2114
+ if (completedCount > 0) {
2115
+ lines.push(lang === 'es' ? `Completadas: ${completedCount}` : `Completed: ${completedCount}`);
2116
+ }
2117
+
2118
+ const stateCtx = lines.join('\n');
2119
+ const monitorPrompt = lang === 'es'
2120
+ ? `Modo Ausencia activo — revisión automática cada 5 minutos.\n\nEstado del orquestador:\n${stateCtx}\n\nInstrucciones:\n1. Lee INBOX.md — si hay análisis completados sin tarea de implementación en QUEUE.md, créala\n2. Lee QUEUE.md — si hay tareas fallidas no reasignadas, reasígnalas al siguiente agente\n3. Si hay tareas pendientes sin asignar a ningún agente (agent = >0 o vacío), asígnalas a un agente idle (Codex u OpenCode)\n4. Si hay agentes idle y tareas pendientes sin procesar, revisa bloqueos y resuélvelos\n5. Si todo avanza, no hagas nada y responde brevemente "TodoOK"\n\nNo hagas commit ni push. No inventes tareas nuevas.`
2121
+ : `Away Mode active — automatic check every 5 minutes.\n\nOrchestrator state:\n${stateCtx}\n\nInstructions:\n1. Read INBOX.md — if there are completed analyses without implementation tasks in QUEUE.md, create them\n2. Read QUEUE.md — if there are failed tasks not reassigned, reassign to next available agent\n3. If there are pending tasks with no agent assigned (agent = >0 or empty), assign them to an idle agent (Codex or OpenCode)\n4. If there are idle agents and pending tasks not being processed, check for blocking issues\n5. If everything is progressing, do nothing and respond briefly "AllGood"\n\nDo not commit or push. Do not invent new tasks.`;
2122
+
2123
+ const logPath = path.join(LOG_DIR, `away-check-${Date.now()}.log`);
2124
+ try {
2125
+ const logFd = fs.openSync(logPath, 'a');
2126
+ const child = spawn('claude', ['-p', monitorPrompt, '--add-dir', WORKSPACE, '--dangerously-skip-permissions'], {
2127
+ cwd: WORKSPACE, stdio: ['ignore', logFd, logFd], shell: true, windowsHide: true, detached: true
2128
+ });
2129
+ fs.closeSync(logFd);
2130
+ child.unref();
2131
+ log('INFO', lang === 'es' ? 'Modo Ausencia: revisión automática disparada.' : 'Away Mode: automatic check dispatched.');
2132
+ } catch {}
2133
+ }
2134
+
2135
+ function activateAwayMode() {
2136
+ if (_awayModeActive) return;
2137
+ _awayModeActive = true;
2138
+ log('INFO', WORKSPACE_LANGUAGE === 'es' ? 'Modo Ausencia activado.' : 'Away Mode activated.');
2139
+ runAwayModeCheck();
2140
+ _awayModeTimer = setInterval(runAwayModeCheck, 5 * 60 * 1000); // 5 minutos
2141
+ }
2142
+
2143
+ function deactivateAwayMode() {
2144
+ if (!_awayModeActive) return;
2145
+ _awayModeActive = false;
2146
+ if (_awayModeTimer) { clearInterval(_awayModeTimer); _awayModeTimer = null; }
2147
+ log('INFO', WORKSPACE_LANGUAGE === 'es' ? 'Modo Ausencia desactivado.' : 'Away Mode deactivated.');
2148
+ }
2149
+
2150
+ function startAwayModeWatcher() {
2151
+ if (fs.existsSync(AWAY_MODE_FILE)) activateAwayMode();
2152
+ try {
2153
+ const watcher = fs.watch(WORKSPACE, {persistent: false}, (eventType, filename) => {
2154
+ if (filename !== '.away-mode') return;
2155
+ if (fs.existsSync(AWAY_MODE_FILE)) {
2156
+ activateAwayMode();
2157
+ } else {
2158
+ deactivateAwayMode();
2159
+ }
2160
+ });
2161
+ watcher.on('error', () => {});
2162
+ } catch {}
2163
+ }
2164
+ startAwayModeWatcher();
2165
+
2166
+ setInterval(() => {
2167
+ updateStatusFile();
2168
+ }, 60000); // Update STATUS.md cada 60 segundos
1659
2169
  setInterval(() => {
1660
2170
  for (const [name, ag] of Object.entries(state.agents)) {
1661
2171
  if (ag.status !== "busy" || !ag.process) continue;
1662
2172
  try {
1663
2173
  process.kill(ag.process.pid, 0);
1664
2174
  } catch {
1665
- log("WARN", `${name} died silently on ${ag.task?.id}`);
1666
- appendToAgent(name, "{red-fg}=== PROCESS DIED ==={/red-fg}", true);
2175
+ log("WARN", L.logDied(name, ag.task?.id));
2176
+ appendToAgent(name, `{red-fg}${escBl(L.agentDied)}{/red-fg}`, true);
1667
2177
  ag.process = null;
1668
2178
  failTask(ag.task, name, -1);
1669
2179
  }