@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 +1 -100
- package/orchestrator.js +269 -6
- package/package.json +1 -5
- package/src/ink/index.mjs +13 -2
- package/templates/en/ORCHESTRATOR.md +6 -13
- package/templates/es/ORCHESTRATOR.md +6 -13
- package/scripts/auto-trigger.js +0 -114
- package/scripts/monitor-check.js +0 -172
- package/scripts/scaffold-agent-configs.mjs +0 -100
- package/scripts/scaffold-openspec-change.mjs +0 -84
- package/scripts/update-skill-registry.mjs +0 -174
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1965
|
+
scheduleNext();
|
|
1900
1966
|
renderDashboard();
|
|
1901
|
-
},
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
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
|
-
-
|
|
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
|
-
-
|
|
108
|
+
- Away Mode removes .away-mode automatically
|
|
110
109
|
- Away Mode deactivates by itself
|
|
111
|
-
- When you return and say "I'm back" →
|
|
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
|
|
47
|
-
3. **Verifica la automatización de
|
|
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
|
|
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
|
|
98
|
+
- El modo elimina .away-mode automáticamente
|
|
100
99
|
- Modo Ausencia se desactiva solo
|
|
101
|
-
- Cuando vuelvas y le digas "ya volví" →
|
|
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
|
package/scripts/auto-trigger.js
DELETED
|
@@ -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
|
-
});
|
package/scripts/monitor-check.js
DELETED
|
@@ -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));
|