@arka-labs/nemesis 1.2.0
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/LICENSE +201 -0
- package/README.md +668 -0
- package/lib/core/agent-launcher.js +193 -0
- package/lib/core/audit.js +210 -0
- package/lib/core/connexions.js +80 -0
- package/lib/core/flowmap/api.js +111 -0
- package/lib/core/flowmap/cli-helpers.js +80 -0
- package/lib/core/flowmap/machine.js +281 -0
- package/lib/core/flowmap/persistence.js +83 -0
- package/lib/core/generators.js +183 -0
- package/lib/core/inbox.js +275 -0
- package/lib/core/logger.js +20 -0
- package/lib/core/mission.js +109 -0
- package/lib/core/notewriter/config.js +36 -0
- package/lib/core/notewriter/cr.js +237 -0
- package/lib/core/notewriter/log.js +112 -0
- package/lib/core/notewriter/notes.js +168 -0
- package/lib/core/notewriter/paths.js +45 -0
- package/lib/core/notewriter/reader.js +121 -0
- package/lib/core/notewriter/registry.js +80 -0
- package/lib/core/odm.js +191 -0
- package/lib/core/profile-picker.js +323 -0
- package/lib/core/project.js +287 -0
- package/lib/core/registry.js +129 -0
- package/lib/core/secrets.js +137 -0
- package/lib/core/services.js +45 -0
- package/lib/core/team.js +287 -0
- package/lib/core/templates.js +80 -0
- package/lib/kairos/agent-runner.js +261 -0
- package/lib/kairos/claude-invoker.js +90 -0
- package/lib/kairos/context-injector.js +331 -0
- package/lib/kairos/context-loader.js +108 -0
- package/lib/kairos/context-writer.js +45 -0
- package/lib/kairos/dispatcher-router.js +173 -0
- package/lib/kairos/dispatcher.js +139 -0
- package/lib/kairos/event-bus.js +287 -0
- package/lib/kairos/event-router.js +131 -0
- package/lib/kairos/flowmap-bridge.js +120 -0
- package/lib/kairos/hook-handlers.js +351 -0
- package/lib/kairos/hook-installer.js +207 -0
- package/lib/kairos/hook-prompts.js +54 -0
- package/lib/kairos/leader-rules.js +94 -0
- package/lib/kairos/pid-checker.js +108 -0
- package/lib/kairos/situation-detector.js +123 -0
- package/lib/sync/fallback-engine.js +97 -0
- package/lib/sync/hcm-client.js +170 -0
- package/lib/sync/health.js +47 -0
- package/lib/sync/llm-client.js +387 -0
- package/lib/sync/nemesis-client.js +379 -0
- package/lib/sync/service-session.js +74 -0
- package/lib/sync/sync-engine.js +178 -0
- package/lib/ui/box.js +104 -0
- package/lib/ui/brand.js +42 -0
- package/lib/ui/colors.js +57 -0
- package/lib/ui/dashboard.js +580 -0
- package/lib/ui/error-hints.js +49 -0
- package/lib/ui/format.js +61 -0
- package/lib/ui/menu.js +306 -0
- package/lib/ui/note-card.js +198 -0
- package/lib/ui/note-colors.js +26 -0
- package/lib/ui/note-detail.js +297 -0
- package/lib/ui/note-filters.js +252 -0
- package/lib/ui/note-views.js +283 -0
- package/lib/ui/prompt.js +81 -0
- package/lib/ui/spinner.js +139 -0
- package/lib/ui/streambox.js +46 -0
- package/lib/ui/table.js +42 -0
- package/lib/ui/tree.js +33 -0
- package/package.json +53 -0
- package/src/cli.js +457 -0
- package/src/commands/_helpers.js +119 -0
- package/src/commands/audit.js +187 -0
- package/src/commands/auth.js +316 -0
- package/src/commands/doctor.js +243 -0
- package/src/commands/hcm.js +147 -0
- package/src/commands/inbox.js +333 -0
- package/src/commands/init.js +160 -0
- package/src/commands/kairos.js +216 -0
- package/src/commands/kars.js +134 -0
- package/src/commands/mission.js +275 -0
- package/src/commands/notes.js +316 -0
- package/src/commands/notewriter.js +296 -0
- package/src/commands/odm.js +329 -0
- package/src/commands/orch.js +68 -0
- package/src/commands/project.js +123 -0
- package/src/commands/run.js +123 -0
- package/src/commands/services.js +705 -0
- package/src/commands/status.js +231 -0
- package/src/commands/team.js +572 -0
- package/src/config.js +84 -0
- package/src/index.js +5 -0
- package/templates/project-context.json +10 -0
- package/templates/template_CONTRIB-NAME.json +22 -0
- package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
- package/templates/template_DEC-NAME-000.json +18 -0
- package/templates/template_INTV-NAME-000.json +15 -0
- package/templates/template_MISSION_CONTRACT.json +46 -0
- package/templates/template_ODM-NAME-000.json +89 -0
- package/templates/template_REGISTRY-PROJECT.json +26 -0
- package/templates/template_TXN-NAME-000.json +24 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration dashboard + daemon — poll-based TUI that monitors AND orchestrates dispatches.
|
|
3
|
+
* Actively dispatches READY_TO_DISPATCH OdMs and routes results via routing.chain.
|
|
4
|
+
* All data from filesystem, zero network.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, readFileSync, existsSync, statSync, unlinkSync, appendFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
10
|
+
import { style } from './colors.js';
|
|
11
|
+
import { titledBox } from './box.js';
|
|
12
|
+
import { readRegistry, getLanes } from '../../lib/core/registry.js';
|
|
13
|
+
import { launchHeadless, buildDispatchPrompt } from '../kairos/agent-runner.js';
|
|
14
|
+
import { createDispatchTransaction } from '../kairos/dispatcher-router.js';
|
|
15
|
+
import { debug } from '../core/logger.js';
|
|
16
|
+
import { listOdm, inspectOdm, setOdmStatus, assignOdm } from '../core/odm.js';
|
|
17
|
+
import { pushEvent, EVENT_TYPES } from '../kairos/event-bus.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// States
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export const STATES = {
|
|
24
|
+
IDLE: 'IDLE',
|
|
25
|
+
DISPATCH_ACTIVE: 'DISPATCH_ACTIVE',
|
|
26
|
+
MULTI_DISPATCH: 'MULTI_DISPATCH',
|
|
27
|
+
ESCALATION: 'ESCALATION',
|
|
28
|
+
DELIVERY: 'DELIVERY',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Data loaders (filesystem)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function readJsonDir(dir) {
|
|
36
|
+
if (!existsSync(dir)) return [];
|
|
37
|
+
return readdirSync(dir)
|
|
38
|
+
.filter(f => f.endsWith('.json'))
|
|
39
|
+
.map(f => {
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(readFileSync(join(dir, f), 'utf-8'));
|
|
42
|
+
return { file: f, ...data };
|
|
43
|
+
} catch (e) { debug(`readJsonDir ${f}: ${e.message}`); return null; }
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readLogTail(logPath, lines = 5) {
|
|
49
|
+
if (!existsSync(logPath)) return [];
|
|
50
|
+
try {
|
|
51
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
52
|
+
return content.split('\n').filter(Boolean).slice(-lines);
|
|
53
|
+
} catch (e) { debug(`readLogTail: ${e.message}`); return []; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// detectState — determine current orchestration state
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export function detectState(root) {
|
|
61
|
+
const kairosDir = join(root, '.nemesis', 'kairos');
|
|
62
|
+
const pending = readJsonDir(join(kairosDir, 'pending-dispatch'));
|
|
63
|
+
const results = readJsonDir(join(kairosDir, 'dispatch-results'));
|
|
64
|
+
const escalations = readJsonDir(join(kairosDir, 'escalations'));
|
|
65
|
+
const events = readJsonDir(join(kairosDir, 'events'));
|
|
66
|
+
|
|
67
|
+
if (escalations.length > 0) {
|
|
68
|
+
return { state: STATES.ESCALATION, pending, results, escalations, events };
|
|
69
|
+
}
|
|
70
|
+
if (pending.length > 1) {
|
|
71
|
+
return { state: STATES.MULTI_DISPATCH, pending, results, escalations, events };
|
|
72
|
+
}
|
|
73
|
+
if (pending.length === 1) {
|
|
74
|
+
return { state: STATES.DISPATCH_ACTIVE, pending, results, escalations, events };
|
|
75
|
+
}
|
|
76
|
+
if (results.length > 0) {
|
|
77
|
+
return { state: STATES.DELIVERY, pending, results, escalations, events };
|
|
78
|
+
}
|
|
79
|
+
return { state: STATES.IDLE, pending, results, escalations, events };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// checkAgentActivity — probe PID + log for liveness
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export function checkAgentActivity(dispatchEntry, root) {
|
|
87
|
+
const pid = dispatchEntry.pid;
|
|
88
|
+
let alive = false;
|
|
89
|
+
if (pid) {
|
|
90
|
+
try { process.kill(pid, 0); alive = true; } catch { alive = false; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const logFile = dispatchEntry.file?.replace('.json', '.log');
|
|
94
|
+
const logPath = logFile
|
|
95
|
+
? join(root, '.nemesis', 'kairos', 'pending-dispatch', logFile)
|
|
96
|
+
: null;
|
|
97
|
+
const logLines = logPath ? readLogTail(logPath) : [];
|
|
98
|
+
|
|
99
|
+
let status = 'dead';
|
|
100
|
+
if (alive) {
|
|
101
|
+
status = 'active';
|
|
102
|
+
} else if (logLines.length > 0) {
|
|
103
|
+
// Check if log was written recently (within 60s)
|
|
104
|
+
if (logPath && existsSync(logPath)) {
|
|
105
|
+
const stat = statSync(logPath);
|
|
106
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
107
|
+
status = ageMs < 60_000 ? 'active' : 'silent';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { alive, status, logLines, pid };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Relative time
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export function relativeTime(isoString) {
|
|
119
|
+
if (!isoString) return '';
|
|
120
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
121
|
+
if (diff < 0) return 'maintenant';
|
|
122
|
+
const secs = Math.floor(diff / 1000);
|
|
123
|
+
if (secs < 60) return `il y a ${secs}s`;
|
|
124
|
+
const mins = Math.floor(secs / 60);
|
|
125
|
+
if (mins < 60) return `il y a ${mins}m`;
|
|
126
|
+
const hours = Math.floor(mins / 60);
|
|
127
|
+
if (hours < 24) return `il y a ${hours}h`;
|
|
128
|
+
const days = Math.floor(hours / 24);
|
|
129
|
+
return `il y a ${days}j`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Render functions (one per state)
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
function renderIdle(data, registry) {
|
|
137
|
+
const lanes = registry ? getLanes(registry) : [];
|
|
138
|
+
const lines = [
|
|
139
|
+
`${style.dim('Aucun dispatch en cours.')}`,
|
|
140
|
+
'',
|
|
141
|
+
`Agents enregistres : ${lanes.length}`,
|
|
142
|
+
];
|
|
143
|
+
if (data.results.length > 0) {
|
|
144
|
+
lines.push(`Resultats disponibles : ${data.results.length}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(style.dim('Les OdMs READY_TO_DISPATCH sont dispatches automatiquement.'));
|
|
148
|
+
return titledBox('Orchestration \u2014 IDLE', lines, { border: style.dim });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderDispatchActive(data, registry, root) {
|
|
152
|
+
const d = data.pending[0];
|
|
153
|
+
const activity = checkAgentActivity(d, root);
|
|
154
|
+
|
|
155
|
+
const statusIcon = activity.status === 'active'
|
|
156
|
+
? style.green('\u25CF')
|
|
157
|
+
: activity.status === 'silent'
|
|
158
|
+
? style.yellow('\u25CF')
|
|
159
|
+
: style.red('\u25CF');
|
|
160
|
+
|
|
161
|
+
const agentLabel = d.to || d.agent || d.agentName || '?';
|
|
162
|
+
const txnLabel = d.txnId || d.txn_id || d.file?.replace('.json', '') || '';
|
|
163
|
+
const timeLabel = d.created_at || d.dispatched_at || d.launchedAt;
|
|
164
|
+
|
|
165
|
+
const lines = [
|
|
166
|
+
`Agent : ${style.bold(agentLabel)} ${statusIcon} ${activity.status}`,
|
|
167
|
+
`OdM : ${d.odmId || d.odm_id || 'N/A'}`,
|
|
168
|
+
`TXN : ${style.dim(txnLabel)}`,
|
|
169
|
+
`PID : ${activity.pid || 'N/A'}`,
|
|
170
|
+
`Demarre : ${relativeTime(timeLabel)}`,
|
|
171
|
+
'',
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// OdM steps if available
|
|
175
|
+
if (d.steps && Array.isArray(d.steps)) {
|
|
176
|
+
lines.push(style.bold('Etapes :'));
|
|
177
|
+
for (const step of d.steps) {
|
|
178
|
+
const icon = step.status === 'done' ? style.green('\u2713') : '\u25CB';
|
|
179
|
+
lines.push(` ${icon} ${step.label || step.name || '?'}`);
|
|
180
|
+
}
|
|
181
|
+
lines.push('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Last log lines
|
|
185
|
+
if (activity.logLines.length > 0) {
|
|
186
|
+
lines.push(style.bold('Derniere activite :'));
|
|
187
|
+
for (const line of activity.logLines.slice(-3)) {
|
|
188
|
+
lines.push(` ${style.dim(line)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return titledBox('Orchestration \u2014 DISPATCH ACTIF', lines, { border: style.nemesisAccent });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderMultiDispatch(data, registry, root) {
|
|
196
|
+
const lines = [`${data.pending.length} dispatches en cours`, ''];
|
|
197
|
+
|
|
198
|
+
for (const d of data.pending) {
|
|
199
|
+
const activity = checkAgentActivity(d, root);
|
|
200
|
+
const statusIcon = activity.status === 'active'
|
|
201
|
+
? style.green('\u25CF')
|
|
202
|
+
: activity.status === 'silent'
|
|
203
|
+
? style.yellow('\u25CF')
|
|
204
|
+
: style.red('\u25CF');
|
|
205
|
+
|
|
206
|
+
lines.push(`${statusIcon} ${style.bold(d.to || d.agent || d.agentName || '?')} \u2014 OdM ${d.odmId || d.odm_id || 'N/A'} ${relativeTime(d.created_at || d.dispatched_at || d.launchedAt)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return titledBox('Orchestration \u2014 MULTI-DISPATCH', lines, { border: style.nemesisAccent });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderEscalation(data) {
|
|
213
|
+
const esc = data.escalations[0];
|
|
214
|
+
const lines = [
|
|
215
|
+
`${style.red('\u26A0')} ${style.bold('ESCALADE')}`,
|
|
216
|
+
'',
|
|
217
|
+
`Agent : ${style.bold(esc.from || esc.agent || '?')}`,
|
|
218
|
+
`Type : ${esc.type || esc.escalation_type || 'unknown'}`,
|
|
219
|
+
`Message : ${esc.message || esc.reason || '(pas de message)'}`,
|
|
220
|
+
`Date : ${relativeTime(esc.created_at || esc.timestamp)}`,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
if (data.escalations.length > 1) {
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push(style.dim(`+ ${data.escalations.length - 1} autre(s) escalade(s)`));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return titledBox('Orchestration \u2014 ESCALADE', lines, { border: style.red });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderDelivery(data) {
|
|
232
|
+
const lines = [`${data.results.length} resultat(s) disponible(s)`, ''];
|
|
233
|
+
|
|
234
|
+
for (const r of data.results.slice(0, 5)) {
|
|
235
|
+
// Consider result successful if: explicit success flag, or response exists (agent delivered something)
|
|
236
|
+
const isSuccess = r.status === 'success' || r.success || (r.response && r.exitCode !== 1);
|
|
237
|
+
const icon = isSuccess ? style.green('\u2713') : style.red('\u2717');
|
|
238
|
+
const agentLabel = r.from || r.agent || r.agentName || '?';
|
|
239
|
+
lines.push(`${icon} ${style.bold(agentLabel)} \u2014 OdM ${r.odmId || r.odm_id || 'N/A'} ${relativeTime(r.completed_at || r.finishedAt || r.created_at)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (data.results.length > 5) {
|
|
243
|
+
lines.push(style.dim(` + ${data.results.length - 5} de plus`));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return titledBox('Orchestration \u2014 LIVRAISON', lines, { border: style.green });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// renderDashboard — single frame
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
export function renderDashboard(root, _opts = {}) {
|
|
254
|
+
const hcmDir = join(root, '.nemesis', 'HCM');
|
|
255
|
+
let registry = null;
|
|
256
|
+
if (existsSync(join(hcmDir, 'project'))) {
|
|
257
|
+
registry = readRegistry(hcmDir);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const data = detectState(root);
|
|
261
|
+
let output;
|
|
262
|
+
|
|
263
|
+
switch (data.state) {
|
|
264
|
+
case STATES.ESCALATION: output = renderEscalation(data); break;
|
|
265
|
+
case STATES.MULTI_DISPATCH: output = renderMultiDispatch(data, registry, root); break;
|
|
266
|
+
case STATES.DISPATCH_ACTIVE: output = renderDispatchActive(data, registry, root); break;
|
|
267
|
+
case STATES.DELIVERY: output = renderDelivery(data); break;
|
|
268
|
+
default: output = renderIdle(data, registry); break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Orchestrator daemon — dispatch + routing + Leader notifications
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function orchLog(msg) {
|
|
279
|
+
try { appendFileSync('/tmp/nemesis-orch.log', `[${new Date().toISOString()}] ${msg}\n`); } catch {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function notifyLeader(root, leader, payload) {
|
|
283
|
+
try {
|
|
284
|
+
pushEvent(root, {
|
|
285
|
+
target_agent_id: leader.id || leader.name,
|
|
286
|
+
event_type: EVENT_TYPES.DISPATCH_RESULT,
|
|
287
|
+
source_agent_id: 'orchestrator',
|
|
288
|
+
priority: 'HIGH',
|
|
289
|
+
payload,
|
|
290
|
+
});
|
|
291
|
+
} catch (e) { debug(`notifyLeader: ${e.message}`); }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* One orchestration cycle: dispatch READY_TO_DISPATCH OdMs + route results via chain.
|
|
296
|
+
* Returns an array of action strings for display.
|
|
297
|
+
* @param {string} root — project root
|
|
298
|
+
* @returns {Promise<string[]>}
|
|
299
|
+
*/
|
|
300
|
+
export async function orchestrate(root) {
|
|
301
|
+
const hcmDir = join(root, '.nemesis', 'HCM');
|
|
302
|
+
if (!existsSync(join(hcmDir, 'project'))) return [];
|
|
303
|
+
|
|
304
|
+
const registry = readRegistry(hcmDir);
|
|
305
|
+
if (!registry) return [];
|
|
306
|
+
|
|
307
|
+
const lanes = getLanes(registry);
|
|
308
|
+
const leader = lanes.find(l => l.leader);
|
|
309
|
+
const actions = [];
|
|
310
|
+
|
|
311
|
+
// --- S3: Auto-dispatch OdMs with READY_TO_DISPATCH ---
|
|
312
|
+
const odms = listOdm(hcmDir);
|
|
313
|
+
const readyOdms = odms.filter(o => o.status === 'READY_TO_DISPATCH');
|
|
314
|
+
|
|
315
|
+
for (const odmSummary of readyOdms) {
|
|
316
|
+
const assignee = odmSummary.assignee;
|
|
317
|
+
const agent = lanes.find(l =>
|
|
318
|
+
(l.name === assignee || l.id === assignee) && l.session_id
|
|
319
|
+
);
|
|
320
|
+
if (!agent) {
|
|
321
|
+
orchLog(`Skip ${odmSummary.id}: agent ${assignee} not found or no session`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
setOdmStatus(odmSummary.id, 'DISPATCHING', hcmDir);
|
|
327
|
+
|
|
328
|
+
const { odm } = inspectOdm(odmSummary.id, hcmDir);
|
|
329
|
+
const prompt = buildDispatchPrompt(odm);
|
|
330
|
+
const missionId = odm.odm_meta?.mission_id || odm.odm_meta?.contract_id || '';
|
|
331
|
+
const projectId = odm.odm_meta?.project_id || '';
|
|
332
|
+
|
|
333
|
+
const txnResult = createDispatchTransaction({
|
|
334
|
+
from: 'orchestrator',
|
|
335
|
+
to: agent.name,
|
|
336
|
+
odmId: odmSummary.id,
|
|
337
|
+
missionId,
|
|
338
|
+
projectId,
|
|
339
|
+
root,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await launchHeadless(agent, prompt, {
|
|
343
|
+
txnId: txnResult.txnId,
|
|
344
|
+
odmId: odmSummary.id,
|
|
345
|
+
missionId,
|
|
346
|
+
projectId,
|
|
347
|
+
root,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
setOdmStatus(odmSummary.id, 'ASSIGNED', hcmDir);
|
|
351
|
+
|
|
352
|
+
const action = `Dispatch ${odmSummary.id} → ${agent.name}`;
|
|
353
|
+
actions.push(action);
|
|
354
|
+
orchLog(action);
|
|
355
|
+
|
|
356
|
+
// S5: Notify Leader
|
|
357
|
+
if (leader) {
|
|
358
|
+
notifyLeader(root, leader, {
|
|
359
|
+
summary: action,
|
|
360
|
+
odmId: odmSummary.id,
|
|
361
|
+
agent_from: 'orchestrator',
|
|
362
|
+
agent_to: agent.name,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
// Revert on error: DISPATCHING → READY_TO_DISPATCH
|
|
367
|
+
try { setOdmStatus(odmSummary.id, 'READY_TO_DISPATCH', hcmDir); } catch (e) { orchLog(`revert status ${odmSummary.id}: ${e.message}`); }
|
|
368
|
+
const action = `ERREUR ${odmSummary.id}: ${err.message}`;
|
|
369
|
+
actions.push(action);
|
|
370
|
+
orchLog(action);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// --- S4: Route dispatch results via routing.chain ---
|
|
375
|
+
// Read results WITHOUT consuming — only delete after successful routing
|
|
376
|
+
const resultsDir = join(root, '.nemesis', 'kairos', 'dispatch-results');
|
|
377
|
+
const resultFiles = existsSync(resultsDir)
|
|
378
|
+
? readdirSync(resultsDir).filter(f => f.endsWith('.json'))
|
|
379
|
+
: [];
|
|
380
|
+
|
|
381
|
+
for (const file of resultFiles) {
|
|
382
|
+
const filepath = join(resultsDir, file);
|
|
383
|
+
let result;
|
|
384
|
+
try {
|
|
385
|
+
result = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
386
|
+
} catch (e) { debug(`orchestrate result parse ${file}: ${e.message}`); continue; }
|
|
387
|
+
|
|
388
|
+
const _txnId = file.replace('.json', '');
|
|
389
|
+
const odmId = result.odmId;
|
|
390
|
+
if (!odmId) { try { unlinkSync(filepath); } catch {} continue; }
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const { odm } = inspectOdm(odmId, hcmDir);
|
|
394
|
+
const chain = odm.odm_payload?.cadrage?.routing?.chain;
|
|
395
|
+
const currentAssignee = odm.odm_meta?.assigned_to?.actor_id;
|
|
396
|
+
|
|
397
|
+
if (!chain || chain.length === 0) {
|
|
398
|
+
// No chain → mark completed
|
|
399
|
+
try { setOdmStatus(odmId, 'COMPLETED', hcmDir); } catch (e) { orchLog(`setOdmStatus ${odmId}: ${e.message}`); }
|
|
400
|
+
const action = `Completed ${odmId} (no chain)`;
|
|
401
|
+
actions.push(action);
|
|
402
|
+
orchLog(action);
|
|
403
|
+
|
|
404
|
+
if (leader) {
|
|
405
|
+
notifyLeader(root, leader, {
|
|
406
|
+
summary: action,
|
|
407
|
+
odmId,
|
|
408
|
+
agent_from: currentAssignee || result.agentName,
|
|
409
|
+
agent_to: 'COMPLETED',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// Find current position in chain
|
|
414
|
+
const currentIdx = chain.indexOf(currentAssignee);
|
|
415
|
+
const nextIdx = currentIdx + 1;
|
|
416
|
+
|
|
417
|
+
if (nextIdx < chain.length) {
|
|
418
|
+
// Route to next agent in chain
|
|
419
|
+
const nextAgent = chain[nextIdx];
|
|
420
|
+
assignOdm(odmId, nextAgent, hcmDir);
|
|
421
|
+
setOdmStatus(odmId, 'READY_TO_DISPATCH', hcmDir);
|
|
422
|
+
|
|
423
|
+
const action = `Route ${odmId}: ${currentAssignee} → ${nextAgent}`;
|
|
424
|
+
actions.push(action);
|
|
425
|
+
orchLog(action);
|
|
426
|
+
|
|
427
|
+
if (leader) {
|
|
428
|
+
notifyLeader(root, leader, {
|
|
429
|
+
summary: action,
|
|
430
|
+
odmId,
|
|
431
|
+
agent_from: currentAssignee,
|
|
432
|
+
agent_to: nextAgent,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
// End of chain → completed
|
|
437
|
+
try { setOdmStatus(odmId, 'COMPLETED', hcmDir); } catch (e) { orchLog(`setOdmStatus ${odmId}: ${e.message}`); }
|
|
438
|
+
const action = `Completed ${odmId} (chain done)`;
|
|
439
|
+
actions.push(action);
|
|
440
|
+
orchLog(action);
|
|
441
|
+
|
|
442
|
+
if (leader) {
|
|
443
|
+
notifyLeader(root, leader, {
|
|
444
|
+
summary: action,
|
|
445
|
+
odmId,
|
|
446
|
+
agent_from: currentAssignee,
|
|
447
|
+
agent_to: 'COMPLETED',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Only consume AFTER successful routing
|
|
454
|
+
try { unlinkSync(filepath); } catch (e) { debug(`unlinkSync result ${file}: ${e.message}`); }
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// DO NOT delete — result preserved for retry
|
|
457
|
+
orchLog(`Route error ${odmId}: ${err.message}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return actions;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// startDashboard — poll loop with keypress handler + orchestrator daemon
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
export function startDashboard(root, opts = {}) {
|
|
469
|
+
const { pollInterval = 2000 } = opts;
|
|
470
|
+
const { stdin, stdout } = process;
|
|
471
|
+
|
|
472
|
+
let lastOutput = '';
|
|
473
|
+
let running = true;
|
|
474
|
+
let orchestrating = false;
|
|
475
|
+
const recentActions = [];
|
|
476
|
+
|
|
477
|
+
function render() {
|
|
478
|
+
const output = renderDashboard(root);
|
|
479
|
+
// Build full output including orchestrator actions
|
|
480
|
+
const actionLines = recentActions.slice(-5).map(a => ` ${style.dim('• ' + a)}`).join('\n');
|
|
481
|
+
const fullOutput = output + (actionLines ? '\n' + actionLines : '');
|
|
482
|
+
|
|
483
|
+
if (fullOutput === lastOutput) return;
|
|
484
|
+
|
|
485
|
+
// Clear screen and move cursor to top
|
|
486
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
487
|
+
stdout.write(output);
|
|
488
|
+
stdout.write('\n');
|
|
489
|
+
|
|
490
|
+
// Display recent orchestrator actions
|
|
491
|
+
if (recentActions.length > 0) {
|
|
492
|
+
stdout.write(`\n ${style.bold('Orchestrateur :')}\n`);
|
|
493
|
+
for (const action of recentActions.slice(-5)) {
|
|
494
|
+
stdout.write(` ${style.dim('• ' + action)}\n`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const data = detectState(root);
|
|
499
|
+
// Action bar
|
|
500
|
+
const actions = [];
|
|
501
|
+
if (data.state === STATES.DISPATCH_ACTIVE || data.state === STATES.MULTI_DISPATCH) {
|
|
502
|
+
actions.push('r actualiser');
|
|
503
|
+
}
|
|
504
|
+
actions.push('q quitter');
|
|
505
|
+
stdout.write(`\n ${style.dim(actions.join(' | '))}\n`);
|
|
506
|
+
|
|
507
|
+
lastOutput = fullOutput;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Initial render
|
|
511
|
+
render();
|
|
512
|
+
|
|
513
|
+
// Poll interval with orchestrator
|
|
514
|
+
const timer = setInterval(async () => {
|
|
515
|
+
if (!running) return;
|
|
516
|
+
// Orchestrate: dispatch + route (with guard against overlapping)
|
|
517
|
+
if (!orchestrating) {
|
|
518
|
+
orchestrating = true;
|
|
519
|
+
try {
|
|
520
|
+
const actions = await orchestrate(root);
|
|
521
|
+
if (actions.length > 0) {
|
|
522
|
+
recentActions.push(...actions);
|
|
523
|
+
// Keep only last 20 actions
|
|
524
|
+
if (recentActions.length > 20) recentActions.splice(0, recentActions.length - 20);
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
orchLog(`orchestrate error: ${err.message}`);
|
|
528
|
+
}
|
|
529
|
+
orchestrating = false;
|
|
530
|
+
}
|
|
531
|
+
render();
|
|
532
|
+
}, pollInterval);
|
|
533
|
+
|
|
534
|
+
// Setup keypress
|
|
535
|
+
if (stdin.isTTY) {
|
|
536
|
+
emitKeypressEvents(stdin);
|
|
537
|
+
stdin.setRawMode(true);
|
|
538
|
+
stdin.resume();
|
|
539
|
+
stdin.setEncoding('utf8');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function cleanup() {
|
|
543
|
+
running = false;
|
|
544
|
+
clearInterval(timer);
|
|
545
|
+
if (stdin.isTTY) {
|
|
546
|
+
stdin.removeListener('keypress', onKey);
|
|
547
|
+
stdin.setRawMode(false);
|
|
548
|
+
stdin.pause();
|
|
549
|
+
}
|
|
550
|
+
// Restore cursor
|
|
551
|
+
stdout.write('\x1b[?25h');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function onKey(ch, key) {
|
|
555
|
+
if (!key) return;
|
|
556
|
+
if (key.ctrl && key.name === 'c') {
|
|
557
|
+
cleanup();
|
|
558
|
+
process.exit(0);
|
|
559
|
+
}
|
|
560
|
+
if (ch === 'q') {
|
|
561
|
+
cleanup();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (ch === 'r') {
|
|
565
|
+
lastOutput = '';
|
|
566
|
+
render();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (stdin.isTTY) {
|
|
571
|
+
stdin.on('keypress', onKey);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Cleanup on signals
|
|
575
|
+
const onSignal = () => { cleanup(); process.exit(0); };
|
|
576
|
+
process.on('SIGINT', onSignal);
|
|
577
|
+
process.on('SIGTERM', onSignal);
|
|
578
|
+
|
|
579
|
+
return { stop: cleanup };
|
|
580
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error hints — structured error messages with actionable guidance.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { style } from './colors.js';
|
|
6
|
+
|
|
7
|
+
export const COMMON_HINTS = {
|
|
8
|
+
REGISTRY_NOT_FOUND: {
|
|
9
|
+
hint: 'Le registre projet est introuvable.',
|
|
10
|
+
command: 'nemesis init',
|
|
11
|
+
},
|
|
12
|
+
AGENT_NOT_FOUND: {
|
|
13
|
+
hint: 'Cet agent n\'existe pas dans le registre.',
|
|
14
|
+
command: 'nemesis team list',
|
|
15
|
+
},
|
|
16
|
+
ODM_NOT_FOUND: {
|
|
17
|
+
hint: 'Aucun OdM correspondant.',
|
|
18
|
+
command: 'nemesis odm init',
|
|
19
|
+
},
|
|
20
|
+
HCM_UNREACHABLE: {
|
|
21
|
+
hint: 'Le HCM est injoignable.',
|
|
22
|
+
command: 'nemesis auth login',
|
|
23
|
+
},
|
|
24
|
+
NO_AGENTS: {
|
|
25
|
+
hint: 'Aucun agent enregistre dans le projet.',
|
|
26
|
+
command: 'nemesis team add',
|
|
27
|
+
},
|
|
28
|
+
AGENT_NO_SESSION: {
|
|
29
|
+
hint: 'Cet agent n\'a pas de session active.',
|
|
30
|
+
command: 'nemesis team panneau <agent>',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format an error with optional hint and suggested command.
|
|
36
|
+
* @param {string} message — main error message
|
|
37
|
+
* @param {{ hint?: string, command?: string }} opts
|
|
38
|
+
* @returns {string} formatted multi-line error string
|
|
39
|
+
*/
|
|
40
|
+
export function formatError(message, opts = {}) {
|
|
41
|
+
const lines = [` ${style.red('\u2717')} ${message}`];
|
|
42
|
+
if (opts.hint) {
|
|
43
|
+
lines.push(` ${style.dim('\u2192')} ${opts.hint}`);
|
|
44
|
+
}
|
|
45
|
+
if (opts.command) {
|
|
46
|
+
lines.push(` ${style.dim('\u2192 Essayez :')} ${style.bold(opts.command)}`);
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
package/lib/ui/format.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatter — JSON / text / yaml based on --format flag.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function formatOutput(data, format = 'text') {
|
|
6
|
+
switch (format) {
|
|
7
|
+
case 'json':
|
|
8
|
+
return JSON.stringify(data, null, 2);
|
|
9
|
+
case 'yaml':
|
|
10
|
+
return toYaml(data);
|
|
11
|
+
case 'text':
|
|
12
|
+
default:
|
|
13
|
+
if (typeof data === 'string') return data;
|
|
14
|
+
return JSON.stringify(data, null, 2);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toYaml(obj, indent = 0) {
|
|
19
|
+
const prefix = ' '.repeat(indent);
|
|
20
|
+
const lines = [];
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(obj)) {
|
|
23
|
+
for (const item of obj) {
|
|
24
|
+
if (typeof item === 'object' && item !== null) {
|
|
25
|
+
lines.push(`${prefix}-`);
|
|
26
|
+
lines.push(toYaml(item, indent + 1));
|
|
27
|
+
} else {
|
|
28
|
+
lines.push(`${prefix}- ${formatScalar(item)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
32
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
33
|
+
if (val === null || val === undefined) {
|
|
34
|
+
lines.push(`${prefix}${key}: null`);
|
|
35
|
+
} else if (typeof val === 'object') {
|
|
36
|
+
lines.push(`${prefix}${key}:`);
|
|
37
|
+
lines.push(toYaml(val, indent + 1));
|
|
38
|
+
} else {
|
|
39
|
+
lines.push(`${prefix}${key}: ${formatScalar(val)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
lines.push(`${prefix}${formatScalar(obj)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatScalar(val) {
|
|
50
|
+
if (typeof val === 'string') {
|
|
51
|
+
if (val.includes('\n') || val.includes(':') || val.includes('#')) {
|
|
52
|
+
return `"${val.replace(/"/g, '\\"')}"`;
|
|
53
|
+
}
|
|
54
|
+
return val;
|
|
55
|
+
}
|
|
56
|
+
return String(val);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function print(data, format = 'text') {
|
|
60
|
+
console.log(formatOutput(data, format));
|
|
61
|
+
}
|