@liriraid/agentflow-ai 1.0.15 → 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/bin/agentflow.mjs CHANGED
@@ -76,7 +76,6 @@ Uso / Usage:
76
76
  agentflow init-workspace <projectPath> [--workspace-name <name>] [--backend <path>] [--frontend <path>] [--lang <en|es>] [--force]
77
77
  agentflow tui [--paused] [--yolo]
78
78
  agentflow ink [--paused] [--yolo]
79
- agentflow schedule [--uninstall]
80
79
  agentflow skills:registry
81
80
  agentflow openspec:new <change-name>
82
81
  agentflow agent-config:init
@@ -86,8 +85,6 @@ Ejemplos / Examples:
86
85
  agentflow init-workspace C:/code/mi-proyecto --lang en
87
86
  agentflow tui --paused
88
87
  agentflow ink --yolo
89
- agentflow schedule
90
- agentflow schedule --uninstall
91
88
  `);
92
89
  }
93
90
 
@@ -281,100 +278,6 @@ async function initWorkspace(args) {
281
278
  console.log(TEXT[language].sibling);
282
279
  }
283
280
 
284
- async function scheduleScripts(args) {
285
- const {flags} = parseFlags(args);
286
- const uninstall = Boolean(flags.uninstall);
287
- const workspace = process.cwd();
288
- const scriptRoot = PACKAGE_ROOT;
289
- const autoTrigger = path.join(scriptRoot, 'scripts', 'auto-trigger.js');
290
- const monitorCheck = path.join(scriptRoot, 'scripts', 'monitor-check.js');
291
- const nodeExe = process.execPath;
292
-
293
- if (process.platform !== 'win32') {
294
- console.log('agentflow schedule only supports Windows Task Scheduler.');
295
- console.log('For macOS/Linux, add these to crontab manually:');
296
- console.log(` * * * * * ORCHESTRATOR_WORKSPACE="${workspace}" "${nodeExe}" "${autoTrigger}"`);
297
- console.log(` */5 * * * * ORCHESTRATOR_WORKSPACE="${workspace}" "${nodeExe}" "${monitorCheck}"`);
298
- return;
299
- }
300
-
301
- const {execSync} = await import('child_process');
302
- const env = `ORCHESTRATOR_WORKSPACE=${workspace}`;
303
-
304
- const tasks = [
305
- {
306
- name: 'agentflow-auto-trigger',
307
- script: autoTrigger,
308
- description: 'agentflow: check INBOX every 60s and trigger Claude',
309
- repetition: 'PT1M',
310
- duration: 'PT1M',
311
- },
312
- {
313
- name: 'agentflow-monitor-check',
314
- script: monitorCheck,
315
- description: 'agentflow: Away Mode monitor every 5 minutes',
316
- repetition: 'PT5M',
317
- duration: 'PT5M',
318
- },
319
- ];
320
-
321
- for (const t of tasks) {
322
- if (uninstall) {
323
- try {
324
- execSync(`schtasks /Delete /TN "${t.name}" /F`, {stdio: 'pipe'});
325
- console.log(`Removed: ${t.name}`);
326
- } catch {
327
- console.log(`Not found (already removed): ${t.name}`);
328
- }
329
- continue;
330
- }
331
- // Delete existing before recreating to avoid duplicates
332
- try { execSync(`schtasks /Delete /TN "${t.name}" /F`, {stdio: 'pipe'}); } catch {}
333
- const cmd = [
334
- 'schtasks /Create',
335
- `/TN "${t.name}"`,
336
- `/TR "${nodeExe} \\"${t.script}\\""`,
337
- '/SC MINUTE',
338
- `/MO ${t.name === 'agentflow-auto-trigger' ? 1 : 5}`,
339
- '/RU SYSTEM',
340
- `/F`,
341
- ].join(' ');
342
- try {
343
- execSync(cmd, {
344
- stdio: 'pipe',
345
- env: {...process.env, ORCHESTRATOR_WORKSPACE: workspace},
346
- });
347
- console.log(`Scheduled: ${t.name} (every ${t.name === 'agentflow-auto-trigger' ? '1' : '5'} min)`);
348
- } catch (err) {
349
- // schtasks /RU SYSTEM may fail without admin — fallback to current user
350
- const cmdUser = [
351
- 'schtasks /Create',
352
- `/TN "${t.name}"`,
353
- `/TR "cmd /c set ORCHESTRATOR_WORKSPACE=${workspace} && \\"${nodeExe}\\" \\"${t.script}\\""`,
354
- '/SC MINUTE',
355
- `/MO ${t.name === 'agentflow-auto-trigger' ? 1 : 5}`,
356
- '/F',
357
- ].join(' ');
358
- try {
359
- execSync(cmdUser, {stdio: 'inherit'});
360
- console.log(`Scheduled (current user): ${t.name}`);
361
- } catch (err2) {
362
- console.error(`Failed to schedule ${t.name}: ${err2.message}`);
363
- console.error('Run as Administrator or add manually to Task Scheduler.');
364
- }
365
- }
366
- }
367
-
368
- if (!uninstall) {
369
- console.log('');
370
- console.log(`Workspace: ${workspace}`);
371
- console.log('auto-trigger.js → every 1 min (checks INBOX, triggers Claude)');
372
- console.log('monitor-check.js → every 5 min (Away Mode monitor)');
373
- console.log('');
374
- console.log('To remove: agentflow schedule --uninstall');
375
- }
376
- }
377
-
378
281
  function runNodeScript(relativeScript, args = []) {
379
282
  const scriptPath = path.join(PACKAGE_ROOT, relativeScript);
380
283
  const child = spawn(process.execPath, [scriptPath, ...args], {
@@ -422,9 +325,7 @@ switch (command) {
422
325
  case 'agent-config:init':
423
326
  runNodeScript(path.join('scripts', 'scaffold-agent-configs.mjs'));
424
327
  break;
425
- case 'schedule':
426
- await scheduleScripts(argv.slice(1));
427
- break;
328
+ // schedule command removed - using fs.watch realtime instead
428
329
  default:
429
330
  console.error(TEXT.es.unknown(command));
430
331
  printHelp();
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 || {};
@@ -1030,7 +1032,6 @@ function reloadQueue() {
1030
1032
  // session can detect it on next interaction without Modo Ausencia active.
1031
1033
  // ============================================================================
1032
1034
  function writeInboxNotification(task, agentName, elapsed) {
1033
- const inboxFile = path.join(WORKSPACE, "INBOX.md");
1034
1035
  const progressFile = `progress/PROGRESS-${agentName}.md`;
1035
1036
  const entry = [
1036
1037
  ``,
@@ -1043,12 +1044,11 @@ function writeInboxNotification(task, agentName, elapsed) {
1043
1044
  ``,
1044
1045
  ].join("\n");
1045
1046
  try {
1046
- fs.appendFileSync(inboxFile, entry, "utf-8");
1047
+ fs.appendFileSync(INBOX_FILE, entry, "utf-8");
1047
1048
  } catch {}
1048
1049
  }
1049
1050
 
1050
1051
  function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
1051
- const inboxFile = path.join(WORKSPACE, "INBOX.md");
1052
1052
  const entry = [
1053
1053
  ``,
1054
1054
  L.inboxFailed(timestamp(), task.id, failedAgent, newAgent),
@@ -1060,7 +1060,7 @@ function writeInboxFailureNotification(task, failedAgent, newAgent, reason) {
1060
1060
  ``,
1061
1061
  ].join("\n");
1062
1062
  try {
1063
- fs.appendFileSync(inboxFile, entry, "utf-8");
1063
+ fs.appendFileSync(INBOX_FILE, entry, "utf-8");
1064
1064
  } catch {}
1065
1065
  }
1066
1066
 
@@ -1855,9 +1855,46 @@ function tryFallbackToAlternative(task, failedAgentName, reason) {
1855
1855
  if (!queueUpdated) {
1856
1856
  log("WARN", L.logReassignWarn(task.id, targetAgent));
1857
1857
  }
1858
+
1859
+ // Notificar a Claude (sesión principal) cuando hay fallback
1860
+ notifyClaudeOfFallback(task, failedAgentName, targetAgent, reason);
1858
1861
  return true;
1859
1862
  }
1860
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
+
1861
1898
  // ============================================================================
1862
1899
  // KEYBOARD
1863
1900
  // ============================================================================
@@ -1894,11 +1931,237 @@ setInterval(() => {
1894
1931
  if (command) applyControlCommand(command);
1895
1932
  }, 1000);
1896
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
1897
1961
  setInterval(() => {
1962
+ const busy = Object.values(state.agents).some(a => a.status === 'busy');
1963
+ if (state.paused || (state.queue.length === 0 && !busy)) return;
1898
1964
  reloadQueue();
1899
- if (!state.paused) scheduleNext();
1965
+ scheduleNext();
1900
1966
  renderDashboard();
1901
- }, 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();
1902
2165
 
1903
2166
  setInterval(() => {
1904
2167
  updateStatusFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liriraid/agentflow-ai",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
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",
@@ -22,7 +22,6 @@
22
22
  "bin",
23
23
  "src",
24
24
  "templates",
25
- "scripts",
26
25
  "orchestrator.js",
27
26
  "LICENSE"
28
27
  ],
@@ -31,9 +30,6 @@
31
30
  "start:paused": "node orchestrator.js --paused",
32
31
  "start:ink": "node src/ink/index.mjs",
33
32
  "start:ink:paused": "node src/ink/index.mjs --paused",
34
- "skills:registry": "node scripts/update-skill-registry.mjs",
35
- "openspec:new": "node scripts/scaffold-openspec-change.mjs",
36
- "agent-config:init": "node scripts/scaffold-agent-configs.mjs",
37
33
  "cli:help": "node bin/agentflow.mjs --help"
38
34
  },
39
35
  "keywords": [
package/src/ink/index.mjs CHANGED
@@ -369,7 +369,7 @@ function requestAction(action) {
369
369
  }
370
370
 
371
371
  function shutdown() {
372
- if (refreshTimer) clearInterval(refreshTimer);
372
+ if (refreshTimer) clearTimeout(refreshTimer);
373
373
  if (resizeTimer) clearTimeout(resizeTimer);
374
374
  if (spawnedEngine && !spawnedEngine.killed && quitRequested) {
375
375
  try {
@@ -383,10 +383,21 @@ function shutdown() {
383
383
  try { fs.unlinkSync(STATE_FILE); } catch {}
384
384
  }
385
385
 
386
+ function scheduleRefresh() {
387
+ const engineState = readEngineState();
388
+ const busy = engineState && Object.values(engineState.agents || {}).some(a => a.status === 'busy');
389
+ const hasWork = engineState && ((engineState.queue || []).length > 0 || (engineState.inProgress || []).length > 0);
390
+ const ms = (busy || hasWork) ? 1000 : 4000;
391
+ refreshTimer = setTimeout(() => {
392
+ refresh();
393
+ scheduleRefresh();
394
+ }, ms);
395
+ }
396
+
386
397
  function mount() {
387
398
  ensureEngine();
388
399
  refresh();
389
- refreshTimer = setInterval(refresh, 1000);
400
+ scheduleRefresh();
390
401
 
391
402
  if (process.stdout.isTTY) {
392
403
  process.stdout.on('resize', () => {
@@ -64,9 +64,7 @@ When the user says something like `Read ORCHESTRATOR.md and start`, do this:
64
64
 
65
65
  1. Read this file completely.
66
66
  2. Read `orchestrator.config.json` — identify the real project paths in `repos` (frontend, backend). Those are the paths where the worker agents operate.
67
- 3. **Verify script automation:** check if `logs/schedule-configured.json` exists.
68
- - If it does **NOT exist**: run `agentflow schedule` in the workspace directory to register `auto-trigger.js` (every 1 min) and `monitor-check.js` (every 5 min) in the task scheduler. Then create `logs/schedule-configured.json` with `{"configuredAt": "<date>"}`. Inform the user that automation is ready.
69
- - If it **already exists**: continue without doing anything.
67
+ 3. **Verify automation:** The orchestrator uses `fs.watch` (Node.js real-time watching). No Task Scheduler needed. The TUI stays running in a terminal and detects changes immediately.
70
68
  4. Read `<projectName>-plan.md`, `PLAN.md`, or `plan.md` if present.
71
69
  5. Read the newest `handoffs/HANDOFF-*.md` if the folder exists.
72
70
  6. **Read `INBOX.md` if it exists** — it contains automatic TUI notifications of completed tasks that require your attention (creating next TASKs, reading agent reports, etc.).
@@ -98,23 +96,18 @@ If the user says something like:
98
96
  echo away > .away-mode
99
97
  ```
100
98
 
101
- **The monitor-check.js script will run every 5 minutes** and check:
99
+ **Away Mode checks every 5 minutes** and will:
102
100
  - Completed tasks without follow-up
103
101
  - Failed tasks
104
102
  - Stuck tasks (>10 min)
105
- - And write to ACTIONS.md
103
+ - Pending tasks with no agent assigned → assign them automatically
104
+ - Write to ACTIONS.md
106
105
 
107
106
  **Auto-deactivate:**
108
107
  When there are NO pending tasks AND NO agents working AND all tasks are completed:
109
- - The script removes .away-mode automatically
108
+ - Away Mode removes .away-mode automatically
110
109
  - Away Mode deactivates by itself
111
- - When you return and say "I'm back" → Clayde responds normally
112
-
113
- **The monitor-check.js script will run every 5 minutes** and check:
114
- - Completed tasks without follow-up
115
- - Failed tasks
116
- - Stuck tasks (>10 min)
117
- - And write to ACTIONS.md so when you return, Claude reads it automatically
110
+ - When you return and say "I'm back" → Claude responds normally
118
111
 
119
112
  **Deactivate Away Mode:**
120
113
  ```bash
@@ -43,10 +43,8 @@ Cuando necesites entender el proyecto para planificar tareas, **lee archivos des
43
43
  ## Al iniciar la sesión — OBLIGATORIO
44
44
 
45
45
  1. Lee este archivo completo.
46
- 2. Lee `orchestrator.config.json` — identifica las rutas reales en `repos` (frontend, backend). Esas son las rutas del proyecto real donde trabajan los agentes.
47
- 3. **Verifica la automatización de scripts:** revisa si existe `logs/schedule-configured.json`.
48
- - Si **NO existe**: ejecuta `agentflow schedule` en el directorio del workspace para registrar `auto-trigger.js` (cada 1 min) y `monitor-check.js` (cada 5 min) en el programador de tareas. Luego crea `logs/schedule-configured.json` con `{"configuredAt": "<fecha>"}`. Informa al usuario que la automatización quedó lista.
49
- - Si **ya existe**: continúa sin hacer nada.
46
+ 2. Lee `orchestrator.config.json` — identifica las rutas reales en `repos` (frontend, backend). Esas son las rutas del proyecto real donde trabajan los agentees.
47
+ 3. **Verifica la automatización:** El orquestador usa `fs.watch` (realtime de Node.js). No necesita Task Scheduler. La TUI corre en una terminal y detecta cambios inmediatamente.
50
48
  4. Lee `<projectName>-plan.md` (o `PLAN.md` / `plan.md`) si existe; ese es el plan general.
51
49
  5. Lee el handoff más reciente en `handoffs/HANDOFF-*.md` si existe la carpeta.
52
50
  6. **Lee `INBOX.md` si existe** — contiene notificaciones automáticas del TUI de tasks completadas que requieren tu atención (crear siguientes TASKs, leer reportes de agentes, etc.).
@@ -88,23 +86,18 @@ Si el usuario dice explícitamente algo como:
88
86
  echo away > .away-mode
89
87
  ```
90
88
 
91
- **El script monitor-check.js se ejecutará cada 5 minutos** y revisará:
89
+ **El modo ausente hace revisión cada 5 minutos** y revisará:
92
90
  - Tareas completadas sin seguimiento
93
91
  - Tareas fallidas
94
92
  - Tareas atascadas (>10 min)
93
+ - Tareas pendientes sin agente asignado → las asignará automáticamente
95
94
  - Y escribirá en ACTIONS.md
96
95
 
97
96
  **Auto-desactivación:**
98
97
  Cuando NO hay tareas pendientes Y NO hay agentes trabajando Y todas las tareas están completadas:
99
- - El script elimina .away-mode automáticamente
98
+ - El modo elimina .away-mode automáticamente
100
99
  - Modo Ausencia se desactiva solo
101
- - Cuando vuelvas y le digas "ya volví" → Claade responde normalmente
102
-
103
- **El script monitor-check.js se ejecutará cada 5 minutos** y revisará:
104
- - Tareas completadas sin seguimiento
105
- - Tareas fallidas
106
- - Tareas atascadas (>10 min)
107
- - Y escribirá en ACTIONS.md para que cuando vuelvas, Claude lo lea automáticamente
100
+ - Cuando vuelvas y le digas "ya volví" → Claude responde normalmente
108
101
 
109
102
  **Desactivar Modo Ausencia:**
110
103
  ```bash
@@ -1,114 +0,0 @@
1
- #!/usr/bin/env node
2
- // ============================================================================
3
- // Auto-trigger script - Ejecutar cada 60 segundos desde Windows Task Scheduler
4
- // Detecta nuevo contenido en INBOX.md y dispara Claude headless para procesarlo
5
- // ============================================================================
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const { spawn } = require('child_process');
10
-
11
- const WORKSPACE = process.env.ORCHESTRATOR_WORKSPACE || process.cwd();
12
- const INBOX_FILE = path.join(WORKSPACE, 'INBOX.md');
13
- const QUEUE_FILE = path.join(WORKSPACE, 'QUEUE.md');
14
- const LAST_CHECK_FILE = path.join(WORKSPACE, 'logs', 'last-auto-check.json');
15
-
16
- function timestamp() {
17
- return new Date().toISOString().replace('T', ' ').slice(0, 19);
18
- }
19
-
20
- function detectLanguage() {
21
- if (!fs.existsSync(QUEUE_FILE)) return 'en';
22
- try {
23
- const content = fs.readFileSync(QUEUE_FILE, 'utf-8');
24
- return (content.includes('## Pendientes') || content.includes('## Completadas')) ? 'es' : 'en';
25
- } catch { return 'en'; }
26
- }
27
-
28
- const lang = detectLanguage();
29
-
30
- // Leer último hash guardado
31
- let lastCheck = { time: 0, inboxHash: '' };
32
- try {
33
- if (fs.existsSync(LAST_CHECK_FILE)) {
34
- lastCheck = JSON.parse(fs.readFileSync(LAST_CHECK_FILE, 'utf-8'));
35
- }
36
- } catch {}
37
-
38
- // Leer INBOX actual
39
- let inboxContent = '';
40
- let currentHash = '';
41
- try {
42
- if (fs.existsSync(INBOX_FILE)) {
43
- inboxContent = fs.readFileSync(INBOX_FILE, 'utf-8');
44
- currentHash = inboxContent.slice(0, 500);
45
- }
46
- } catch {}
47
-
48
- // Si no hay contenido o no cambió, salir
49
- if (!inboxContent.trim() || currentHash === lastCheck.inboxHash) {
50
- console.log(`[${timestamp()}] Sin cambios en INBOX. Nada que procesar.`);
51
- process.exit(0);
52
- }
53
-
54
- console.log(`[${timestamp()}] Nuevo contenido en INBOX detectado — disparando Claude...`);
55
-
56
- // Guardar hash para no relanzar en el próximo ciclo de 60s
57
- lastCheck = { time: Date.now(), inboxHash: currentHash };
58
- fs.mkdirSync(path.dirname(LAST_CHECK_FILE), { recursive: true });
59
- fs.writeFileSync(LAST_CHECK_FILE, JSON.stringify(lastCheck), 'utf-8');
60
-
61
- // Prompt para Claude headless — lee INBOX y crea la task de implementación si aplica
62
- const prompt = lang === 'es'
63
- ? `Eres el orquestador de este workspace. Tu única misión ahora es procesar el INBOX.
64
-
65
- Pasos:
66
- 1. Lee INBOX.md en ${WORKSPACE}
67
- 2. Lee QUEUE.md en ${WORKSPACE} para ver las tareas existentes (secciones Pendientes, En progreso, Completadas)
68
-
69
- 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:
70
- - Determina el siguiente TASK ID disponible leyendo QUEUE.md
71
- - Crea la nueva TASK en QUEUE.md con el formato exacto:
72
- TASK-NNN | título corto | Codex | P1 | repo | descripción basada en el análisis
73
-
74
- Si ya existe la tarea correspondiente, o el análisis no está completo, responde solo: "Sin acción necesaria."
75
-
76
- 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.`
77
- : `You are the orchestrator for this workspace. Your only mission now is to process the INBOX.
78
-
79
- Steps:
80
- 1. Read INBOX.md in ${WORKSPACE}
81
- 2. Read QUEUE.md in ${WORKSPACE} to see existing tasks (sections Pending, In Progress, Completed)
82
-
83
- 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:
84
- - Determine the next available TASK ID by reading QUEUE.md
85
- - Create the new TASK in QUEUE.md with the exact format:
86
- TASK-NNN | short title | Codex | P1 | repo | description based on the analysis
87
-
88
- If the corresponding task already exists, or the analysis is not complete, reply only: "No action needed."
89
-
90
- Rules: Do not commit or push. Do not analyze project code. Only read INBOX.md and QUEUE.md, and edit QUEUE.md if necessary.`;
91
-
92
- const claude = spawn('claude', [
93
- '-p', prompt,
94
- '--add-dir', WORKSPACE,
95
- '--dangerously-skip-permissions'
96
- ], {
97
- cwd: WORKSPACE,
98
- stdio: ['ignore', 'pipe', 'pipe'],
99
- shell: true
100
- });
101
-
102
- let output = '';
103
- claude.stdout.on('data', d => { output += d.toString(); });
104
- claude.stderr.on('data', d => { process.stderr.write(d); });
105
-
106
- claude.on('close', code => {
107
- const result = output.trim().slice(0, 300);
108
- console.log(`[${timestamp()}] Claude completó (exit ${code}): ${result}`);
109
- });
110
-
111
- claude.on('error', err => {
112
- console.error(`[${timestamp()}] Error al lanzar Claude: ${err.message}`);
113
- process.exit(1);
114
- });
@@ -1,172 +0,0 @@
1
- #!/usr/bin/env node
2
- // ============================================================================
3
- // Monitor script - Ejecutar cada 5 minutos desde Windows Task Scheduler
4
- // SOLO corre cuando Modo Ausencia está activado (.away-mode existe)
5
- // En cada revisión manda un prompt a Claude para que monitoree y tome decisiones
6
- // ============================================================================
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const { spawn } = require('child_process');
11
-
12
- const WORKSPACE = process.env.ORCHESTRATOR_WORKSPACE || process.cwd();
13
- const QUEUE_FILE = path.join(WORKSPACE, 'QUEUE.md');
14
- const INBOX_FILE = path.join(WORKSPACE, 'INBOX.md');
15
- const STATE_FILE = path.join(WORKSPACE, 'logs', 'orchestrator-state.json');
16
- const ACTIONS_FILE = path.join(WORKSPACE, 'ACTIONS.md');
17
- const AWAY_MODE_FILE = path.join(WORKSPACE, '.away-mode');
18
-
19
- // Si Away Mode no está activado, salir inmediatamente
20
- if (!fs.existsSync(AWAY_MODE_FILE)) {
21
- console.log('Monitor: Away Mode no activado. Saltando.');
22
- process.exit(0);
23
- }
24
-
25
- function timestamp() {
26
- return new Date().toISOString().replace('T', ' ').slice(0, 19);
27
- }
28
-
29
- function detectLanguage() {
30
- if (!fs.existsSync(QUEUE_FILE)) return 'en';
31
- try {
32
- const content = fs.readFileSync(QUEUE_FILE, 'utf-8');
33
- return (content.includes('## Pendientes') || content.includes('## Completadas')) ? 'es' : 'en';
34
- } catch { return 'en'; }
35
- }
36
-
37
- const lang = detectLanguage();
38
-
39
- function readQueue() {
40
- if (!fs.existsSync(QUEUE_FILE)) return { pending: [], inProgress: [], completed: [] };
41
- const result = { pending: [], inProgress: [], completed: [] };
42
- let section = '';
43
- for (const line of fs.readFileSync(QUEUE_FILE, 'utf-8').split('\n')) {
44
- const trimmed = line.trim();
45
- if (trimmed.startsWith('## Pending') || trimmed.startsWith('## Pendientes')) { section = 'pending'; continue; }
46
- if (trimmed.startsWith('## In Progress') || trimmed.startsWith('## En progreso')) { section = 'inProgress'; continue; }
47
- if (trimmed.startsWith('## Completed') || trimmed.startsWith('## Completadas')) { section = 'completed'; continue; }
48
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('>')) continue;
49
- if (section && trimmed.includes('|')) result[section].push(trimmed);
50
- }
51
- return result;
52
- }
53
-
54
- function readState() {
55
- if (!fs.existsSync(STATE_FILE)) return null;
56
- try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); }
57
- catch { return null; }
58
- }
59
-
60
- function launchClaude(prompt) {
61
- const claude = spawn('claude', [
62
- '-p', prompt,
63
- '--add-dir', WORKSPACE,
64
- '--dangerously-skip-permissions'
65
- ], {
66
- cwd: WORKSPACE,
67
- stdio: 'inherit',
68
- shell: true
69
- });
70
- claude.on('error', err => console.error(`[${timestamp()}] Error lanzando Claude: ${err.message}`));
71
- return claude;
72
- }
73
-
74
- // ============================================================================
75
- // RECOPILAR ESTADO ACTUAL
76
- // ============================================================================
77
- const queue = readQueue();
78
- const state = readState();
79
-
80
- const busyAgents = Object.values(state?.agents || {}).filter(a => a.status === 'busy').length;
81
- const failedAgents = Object.entries(state?.agents || {})
82
- .filter(([, ag]) => (ag.lastLine || '').startsWith('FALLÓ:') || (ag.lastLine || '').startsWith('FAILED:'))
83
- .map(([name, ag]) => `${name}: ${ag.lastLine}`);
84
- const retryingAgents = Object.entries(state?.agents || {})
85
- .filter(([, ag]) => {
86
- const ll = ag.lastLine || '';
87
- return ll.startsWith('REINTENTO:') || ll.startsWith('LÍMITE:') || ll.startsWith('RETRY:');
88
- })
89
- .map(([name, ag]) => `${name}: ${ag.lastLine}`);
90
-
91
- const hasWork = queue.pending.length > 0 || (state?.inProgress?.length || 0) > 0 || busyAgents > 0;
92
-
93
- console.log(`[${timestamp()}] Monitor Modo Ausencia:`);
94
- console.log(` Pendientes: ${queue.pending.length} | En progreso: ${state?.inProgress?.length || 0} | Completadas: ${queue.completed.length}`);
95
- console.log(` Agentes ocupados: ${busyAgents} | Fallidos: ${failedAgents.length} | Reintentando: ${retryingAgents.length}`);
96
-
97
- // ============================================================================
98
- // SI NO HAY TRABAJO → DESACTIVAR Y DAR RESUMEN FINAL
99
- // ============================================================================
100
- if (!hasWork && queue.completed.length > 0) {
101
- console.log(` -> Sin trabajo pendiente. Desactivando Modo Ausencia.`);
102
-
103
- try { fs.unlinkSync(AWAY_MODE_FILE); } catch {}
104
- try { if (fs.existsSync(ACTIONS_FILE)) fs.unlinkSync(ACTIONS_FILE); } catch {}
105
-
106
- const donePrompt = lang === 'es'
107
- ? `Modo Ausencia terminado. Todas las tareas se completaron mientras estabas ausente.
108
-
109
- Lee QUEUE.md en ${WORKSPACE} y dame un resumen de todo lo que se logró durante la sesión.
110
- Luego 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.`
111
- : `Away Mode ended. All tasks were completed while you were away.
112
-
113
- Read QUEUE.md in ${WORKSPACE} and give me a summary of everything accomplished during the session.
114
- Then tell me if there is anything we can continue or integrate from what was done, or ask me what I want to prioritize next.`;
115
-
116
- launchClaude(donePrompt);
117
- process.exit(0);
118
- }
119
-
120
- // ============================================================================
121
- // HAY TRABAJO → MANDAR PROMPT A CLAUDE PARA QUE MONITOREE Y TOME DECISIONES
122
- // ============================================================================
123
-
124
- // Construir contexto de estado para incluir en el prompt
125
- const stateLines = [];
126
- if (queue.pending.length > 0) {
127
- stateLines.push(`Tareas pendientes en cola: ${queue.pending.length}`);
128
- queue.pending.slice(0, 5).forEach(t => stateLines.push(` - ${t.split('|').slice(0, 3).join('|').trim()}`));
129
- }
130
- if (state?.inProgress?.length > 0) {
131
- stateLines.push(`Tareas en progreso: ${state.inProgress.map(t => `${t.id} (${t.agent})`).join(', ')}`);
132
- }
133
- if (failedAgents.length > 0) {
134
- stateLines.push(`Agentes con fallo: ${failedAgents.join(' | ')}`);
135
- }
136
- if (retryingAgents.length > 0) {
137
- stateLines.push(`Agentes reintentando: ${retryingAgents.join(' | ')}`);
138
- }
139
- if (queue.completed.length > 0) {
140
- stateLines.push(`Tareas completadas hasta ahora: ${queue.completed.length}`);
141
- }
142
-
143
- const stateContext = stateLines.join('\n');
144
-
145
- const monitorPrompt = lang === 'es'
146
- ? `Modo Ausencia activo — revisión automática cada 5 minutos.
147
-
148
- Estado actual del orquestador:
149
- ${stateContext}
150
-
151
- Instrucciones:
152
- 1. Lee INBOX.md en ${WORKSPACE} — si hay análisis completados de agentes que aún no tienen su tarea de implementación en QUEUE.md, crea la TASK correspondiente
153
- 2. Lee QUEUE.md en ${WORKSPACE} — si hay tareas fallidas que la TUI no pudo reasignar automáticamente (marcadas como failed), reasígnalas manualmente al siguiente agente disponible
154
- 3. Si hay agentes idle y tareas pendientes que no se están procesando, revisa si hay un problema de dependencias o bloqueo y resuélvelo
155
- 4. Si detectas que el trabajo avanza normalmente, no hagas nada y responde brevemente el estado
156
-
157
- No hagas commit ni push. No inventes tareas nuevas fuera del alcance actual.`
158
- : `Away Mode active — automatic check every 5 minutes.
159
-
160
- Current orchestrator state:
161
- ${stateContext}
162
-
163
- Instructions:
164
- 1. Read INBOX.md in ${WORKSPACE} — if there are completed agent analyses without a corresponding implementation task in QUEUE.md, create the TASK
165
- 2. Read QUEUE.md in ${WORKSPACE} — if there are failed tasks that the TUI could not auto-reassign (marked as failed), manually reassign to the next available agent
166
- 3. If there are idle agents and pending tasks not being processed, check for dependency or blocking issues and resolve them
167
- 4. If work is progressing normally, do nothing and briefly report the status
168
-
169
- Do not commit or push. Do not invent new tasks outside the current scope.`;
170
-
171
- console.log(` -> Disparando prompt a Claude para monitoreo...`);
172
- launchClaude(monitorPrompt);
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
-
6
- const ROOT = process.cwd();
7
- const CONFIG_FILE = path.join(ROOT, 'orchestrator.config.json');
8
- const CONFIG = fs.existsSync(CONFIG_FILE)
9
- ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
10
- : {};
11
- const LANGUAGE = CONFIG.workspaceLanguage === 'en' ? 'en' : 'es';
12
-
13
- const CONTENT = {
14
- es: {
15
- claude: [
16
- '# Claude Local Config',
17
- '',
18
- 'Esta carpeta contiene la configuración local del proyecto para Claude.',
19
- '',
20
- '- `skills/` guarda skills propias del repo',
21
- '- `CLAUDE.md` en la raíz define el routing del proyecto',
22
- '- esta capa local debe priorizarse sobre configuración global del usuario'
23
- ],
24
- codex: [
25
- '# Codex Local Config',
26
- '',
27
- 'Esta carpeta reserva la configuración local del proyecto para Codex.',
28
- '',
29
- '- hoy se usa como base reusable del proyecto',
30
- '- mañana puede alojar prompts, perfiles, reglas o plugins locales',
31
- '- no debe depender solo de configuración global del usuario'
32
- ],
33
- opencode: [
34
- '# OpenCode Local Config',
35
- '',
36
- 'Esta carpeta reserva la configuración local del proyecto para OpenCode.',
37
- '',
38
- '- hoy se usa como base reusable del proyecto',
39
- '- mañana puede alojar reglas, prompts o convenciones específicas',
40
- '- no debe depender solo de configuración global del usuario'
41
- ],
42
- done: 'Configuración local por agente creada o verificada.'
43
- },
44
- en: {
45
- claude: [
46
- '# Claude Local Config',
47
- '',
48
- 'This folder contains project-local Claude configuration.',
49
- '',
50
- '- `skills/` stores repo-specific skills',
51
- '- root `CLAUDE.md` defines project routing',
52
- '- this local layer should take priority over global user config'
53
- ],
54
- codex: [
55
- '# Codex Local Config',
56
- '',
57
- 'This folder reserves project-local configuration for Codex.',
58
- '',
59
- '- today it is used as the reusable local project base',
60
- '- later it can hold prompts, profiles, rules, or local plugins',
61
- '- it should not depend only on global user config'
62
- ],
63
- opencode: [
64
- '# OpenCode Local Config',
65
- '',
66
- 'This folder reserves project-local configuration for OpenCode.',
67
- '',
68
- '- today it is used as the reusable local project base',
69
- '- later it can hold rules, prompts, or specific conventions',
70
- '- it should not depend only on global user config'
71
- ],
72
- done: 'Local agent configuration created or verified.'
73
- }
74
- };
75
- const L = CONTENT[LANGUAGE];
76
-
77
- const files = [
78
- [
79
- '.claude/README.md',
80
- L.claude.join('\n')
81
- ],
82
- [
83
- '.codex/README.md',
84
- L.codex.join('\n')
85
- ],
86
- [
87
- '.opencode/README.md',
88
- L.opencode.join('\n')
89
- ]
90
- ];
91
-
92
- for (const [relativePath, content] of files) {
93
- const absolutePath = path.join(ROOT, relativePath);
94
- fs.mkdirSync(path.dirname(absolutePath), {recursive: true});
95
- if (!fs.existsSync(absolutePath)) {
96
- fs.writeFileSync(absolutePath, `${content}\n`, 'utf8');
97
- }
98
- }
99
-
100
- console.log(L.done);
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
-
6
- const ROOT = process.cwd();
7
- const CHANGE_NAME = process.argv[2];
8
- const CONFIG_FILE = path.join(ROOT, 'orchestrator.config.json');
9
- const CONFIG = fs.existsSync(CONFIG_FILE)
10
- ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
11
- : {};
12
- const LANGUAGE = CONFIG.workspaceLanguage === 'en' ? 'en' : 'es';
13
- const TEXT = {
14
- es: {
15
- usage: 'Uso: npm run openspec:new -- <change-name>',
16
- readmeIntro: 'Este change fue generado desde las plantillas locales de `openspec/`.',
17
- created: base => `OpenSpec change creado en ${base}`
18
- },
19
- en: {
20
- usage: 'Usage: npm run openspec:new -- <change-name>',
21
- readmeIntro: 'This change was generated from the local `openspec/` templates.',
22
- created: base => `OpenSpec change created at ${base}`
23
- }
24
- };
25
- const L = TEXT[LANGUAGE];
26
-
27
- if (!CHANGE_NAME) {
28
- console.error(L.usage);
29
- process.exit(1);
30
- }
31
-
32
- const base = path.join(ROOT, 'openspec', 'changes', CHANGE_NAME);
33
- const templates = path.join(ROOT, 'openspec', 'templates');
34
-
35
- const files = [
36
- ['proposal.md', 'proposal.md'],
37
- ['design.md', 'design.md'],
38
- ['tasks.md', 'tasks.md'],
39
- ['verify-report.md', 'verify-report.md'],
40
- ['archive-report.md', 'archive-report.md'],
41
- ['.openspec.yaml', 'change-metadata.yaml']
42
- ];
43
-
44
- fs.mkdirSync(base, {recursive: true});
45
- const specsDir = path.join(base, 'specs');
46
- fs.mkdirSync(specsDir, {recursive: true});
47
-
48
- for (const [targetName, templateName] of files) {
49
- const target = path.join(base, targetName);
50
- if (fs.existsSync(target)) continue;
51
- const template = fs.readFileSync(path.join(templates, templateName), 'utf8');
52
- const content = template.replaceAll('<change-name>', CHANGE_NAME);
53
- fs.writeFileSync(target, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
54
- }
55
-
56
- const specTemplate = fs.readFileSync(path.join(templates, 'spec.md'), 'utf8');
57
- const specTarget = path.join(specsDir, 'spec.md');
58
- if (!fs.existsSync(specTarget)) {
59
- const specContent = specTemplate.replaceAll('<change-name>', CHANGE_NAME);
60
- fs.writeFileSync(specTarget, specContent.endsWith('\n') ? specContent : `${specContent}\n`, 'utf8');
61
- }
62
-
63
- const readme = [
64
- `# ${CHANGE_NAME}`,
65
- '',
66
- L.readmeIntro,
67
- '',
68
- '## Files',
69
- '',
70
- '- `proposal.md`',
71
- '- `design.md`',
72
- '- `tasks.md`',
73
- '- `verify-report.md`',
74
- '- `archive-report.md`',
75
- '- `.openspec.yaml`',
76
- '- `specs/spec.md`'
77
- ].join('\n');
78
-
79
- const changeReadme = path.join(base, 'README.md');
80
- if (!fs.existsSync(changeReadme)) {
81
- fs.writeFileSync(changeReadme, `${readme}\n`, 'utf8');
82
- }
83
-
84
- console.log(L.created(base));
@@ -1,174 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
-
6
- const ROOT = process.cwd();
7
- const SKILLS_DIR = path.join(ROOT, '.claude', 'skills');
8
- const OUTPUT_DIR = path.join(ROOT, '.atl');
9
- const OUTPUT_FILE = path.join(OUTPUT_DIR, 'skill-registry.md');
10
- const CONFIG_FILE = path.join(ROOT, 'orchestrator.config.json');
11
- const CONFIG = fs.existsSync(CONFIG_FILE)
12
- ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
13
- : {};
14
- const LANGUAGE = CONFIG.workspaceLanguage === 'en' ? 'en' : 'es';
15
- const TEXT = {
16
- es: {
17
- fallbackRule: 'Usa esta skill solo para el propósito definido en su descripción.',
18
- entryPoint: 'Punto de entrada de la sesión del orquestador',
19
- intro:
20
- '**Project-local only.** Este registry prioriza las skills dentro de `./.claude/skills/` para evitar depender de instalaciones globales como `gentle-ai`.',
21
- noSkills: '| manual | none | _No hay skills locales todavía_ |',
22
- noRegistered: 'No hay skills locales registradas todavía.',
23
- noConventions: '| none | none | No se encontraron archivos de convención |',
24
- policy: [
25
- 'Prioriza siempre skills locales de `./.claude/skills/`.',
26
- 'No dependas de `~/.claude/skills/` para el funcionamiento principal del orquestador.',
27
- 'Si una skill global existe con el mismo nombre, la local del proyecto gana.',
28
- 'Regenera este archivo después de crear, borrar o cambiar skills locales.'
29
- ],
30
- updated: file => `Skill registry actualizado en ${file}`
31
- },
32
- en: {
33
- fallbackRule: 'Use this skill only for the purpose defined in its description.',
34
- entryPoint: 'Orchestrator session entry point',
35
- intro:
36
- '**Project-local only.** This registry prioritizes skills inside `./.claude/skills/` so the workflow does not depend on global installations such as `gentle-ai`.',
37
- noSkills: '| manual | none | _No local skills yet_ |',
38
- noRegistered: 'No local skills are registered yet.',
39
- noConventions: '| none | none | No convention files found |',
40
- policy: [
41
- 'Always prefer local skills from `./.claude/skills/`.',
42
- 'Do not depend on `~/.claude/skills/` for the main orchestrator workflow.',
43
- 'If a global skill has the same name as a project-local skill, the local skill wins.',
44
- 'Regenerate this file after creating, deleting, or changing local skills.'
45
- ],
46
- updated: file => `Skill registry updated at ${file}`
47
- }
48
- };
49
- const L = TEXT[LANGUAGE];
50
-
51
- const CONVENTION_FILES = [
52
- 'AGENTS.md',
53
- 'agents.md',
54
- 'CLAUDE.md',
55
- 'ORCHESTRATOR.md',
56
- 'PROJECT.md',
57
- 'README.md'
58
- ];
59
-
60
- function ensureDir(dir) {
61
- fs.mkdirSync(dir, {recursive: true});
62
- }
63
-
64
- function listSkillFiles(dir) {
65
- if (!fs.existsSync(dir)) return [];
66
- const entries = fs.readdirSync(dir, {withFileTypes: true});
67
- const files = [];
68
- for (const entry of entries) {
69
- if (!entry.isDirectory()) continue;
70
- const name = entry.name;
71
- if (name === '_shared') continue;
72
- const file = path.join(dir, name, 'SKILL.md');
73
- if (fs.existsSync(file)) files.push(file);
74
- }
75
- return files.sort((a, b) => a.localeCompare(b));
76
- }
77
-
78
- function extractName(content, fallback) {
79
- const match = content.match(/^\s*name:\s*(.+)$/im);
80
- return match ? match[1].trim().replace(/^['"]|['"]$/g, '') : fallback;
81
- }
82
-
83
- function extractTrigger(content = '') {
84
- const triggerLine = content.match(/^Trigger:\s*(.+)$/im);
85
- if (triggerLine) return triggerLine[1].trim();
86
- const descriptionTrigger = content.match(/Trigger:\s*([^.\n]+)/im);
87
- return descriptionTrigger ? descriptionTrigger[1].trim() : 'manual';
88
- }
89
-
90
- function compactRules(content) {
91
- const lines = content
92
- .split('\n')
93
- .map(line => line.trim())
94
- .filter(Boolean);
95
-
96
- const bullets = [];
97
- for (const line of lines) {
98
- if (line.startsWith('- ') || line.startsWith('* ')) {
99
- bullets.push(line.replace(/^[-*]\s*/, ''));
100
- }
101
- }
102
-
103
- const selected = bullets.slice(0, 8);
104
- return selected.length > 0
105
- ? selected
106
- : [L.fallbackRule];
107
- }
108
-
109
- function toPosix(filePath) {
110
- return filePath.replaceAll('\\', '/');
111
- }
112
-
113
- function buildRegistry() {
114
- const skillFiles = listSkillFiles(SKILLS_DIR);
115
- const skills = skillFiles.map(file => {
116
- const content = fs.readFileSync(file, 'utf8');
117
- return {
118
- name: extractName(content, path.basename(path.dirname(file))),
119
- trigger: extractTrigger(content),
120
- path: toPosix(path.relative(ROOT, file)),
121
- rules: compactRules(content)
122
- };
123
- });
124
-
125
- const conventions = CONVENTION_FILES.filter(file => fs.existsSync(path.join(ROOT, file))).map(
126
- file => ({
127
- file,
128
- path: toPosix(file),
129
- notes: file === 'ORCHESTRATOR.md' ? L.entryPoint : ''
130
- })
131
- );
132
-
133
- const registry = [
134
- '# Skill Registry',
135
- '',
136
- L.intro,
137
- '',
138
- '## User Skills',
139
- '',
140
- '| Trigger | Skill | Path |',
141
- '|---------|-------|------|',
142
- ...(skills.length > 0
143
- ? skills.map(skill => `| ${skill.trigger.replaceAll('|', '\\|')} | ${skill.name} | \`${skill.path}\` |`)
144
- : [L.noSkills]),
145
- '',
146
- '## Compact Rules',
147
- '',
148
- ...(skills.length > 0
149
- ? skills.flatMap(skill => [
150
- `### ${skill.name}`,
151
- ...skill.rules.map(rule => `- ${rule}`),
152
- ''
153
- ])
154
- : [L.noRegistered, '']),
155
- '## Project Conventions',
156
- '',
157
- '| File | Path | Notes |',
158
- '|------|------|-------|',
159
- ...(conventions.length > 0
160
- ? conventions.map(item => `| ${item.file} | \`${item.path}\` | ${item.notes || ''} |`)
161
- : [L.noConventions]),
162
- '',
163
- '## Resolution Policy',
164
- '',
165
- ...L.policy.map(rule => `- ${rule}`)
166
- ];
167
-
168
- return registry.join('\n');
169
- }
170
-
171
- ensureDir(OUTPUT_DIR);
172
- const registry = buildRegistry();
173
- fs.writeFileSync(OUTPUT_FILE, `${registry}\n`, 'utf8');
174
- console.log(L.updated(OUTPUT_FILE));