@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.
Files changed (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +668 -0
  3. package/lib/core/agent-launcher.js +193 -0
  4. package/lib/core/audit.js +210 -0
  5. package/lib/core/connexions.js +80 -0
  6. package/lib/core/flowmap/api.js +111 -0
  7. package/lib/core/flowmap/cli-helpers.js +80 -0
  8. package/lib/core/flowmap/machine.js +281 -0
  9. package/lib/core/flowmap/persistence.js +83 -0
  10. package/lib/core/generators.js +183 -0
  11. package/lib/core/inbox.js +275 -0
  12. package/lib/core/logger.js +20 -0
  13. package/lib/core/mission.js +109 -0
  14. package/lib/core/notewriter/config.js +36 -0
  15. package/lib/core/notewriter/cr.js +237 -0
  16. package/lib/core/notewriter/log.js +112 -0
  17. package/lib/core/notewriter/notes.js +168 -0
  18. package/lib/core/notewriter/paths.js +45 -0
  19. package/lib/core/notewriter/reader.js +121 -0
  20. package/lib/core/notewriter/registry.js +80 -0
  21. package/lib/core/odm.js +191 -0
  22. package/lib/core/profile-picker.js +323 -0
  23. package/lib/core/project.js +287 -0
  24. package/lib/core/registry.js +129 -0
  25. package/lib/core/secrets.js +137 -0
  26. package/lib/core/services.js +45 -0
  27. package/lib/core/team.js +287 -0
  28. package/lib/core/templates.js +80 -0
  29. package/lib/kairos/agent-runner.js +261 -0
  30. package/lib/kairos/claude-invoker.js +90 -0
  31. package/lib/kairos/context-injector.js +331 -0
  32. package/lib/kairos/context-loader.js +108 -0
  33. package/lib/kairos/context-writer.js +45 -0
  34. package/lib/kairos/dispatcher-router.js +173 -0
  35. package/lib/kairos/dispatcher.js +139 -0
  36. package/lib/kairos/event-bus.js +287 -0
  37. package/lib/kairos/event-router.js +131 -0
  38. package/lib/kairos/flowmap-bridge.js +120 -0
  39. package/lib/kairos/hook-handlers.js +351 -0
  40. package/lib/kairos/hook-installer.js +207 -0
  41. package/lib/kairos/hook-prompts.js +54 -0
  42. package/lib/kairos/leader-rules.js +94 -0
  43. package/lib/kairos/pid-checker.js +108 -0
  44. package/lib/kairos/situation-detector.js +123 -0
  45. package/lib/sync/fallback-engine.js +97 -0
  46. package/lib/sync/hcm-client.js +170 -0
  47. package/lib/sync/health.js +47 -0
  48. package/lib/sync/llm-client.js +387 -0
  49. package/lib/sync/nemesis-client.js +379 -0
  50. package/lib/sync/service-session.js +74 -0
  51. package/lib/sync/sync-engine.js +178 -0
  52. package/lib/ui/box.js +104 -0
  53. package/lib/ui/brand.js +42 -0
  54. package/lib/ui/colors.js +57 -0
  55. package/lib/ui/dashboard.js +580 -0
  56. package/lib/ui/error-hints.js +49 -0
  57. package/lib/ui/format.js +61 -0
  58. package/lib/ui/menu.js +306 -0
  59. package/lib/ui/note-card.js +198 -0
  60. package/lib/ui/note-colors.js +26 -0
  61. package/lib/ui/note-detail.js +297 -0
  62. package/lib/ui/note-filters.js +252 -0
  63. package/lib/ui/note-views.js +283 -0
  64. package/lib/ui/prompt.js +81 -0
  65. package/lib/ui/spinner.js +139 -0
  66. package/lib/ui/streambox.js +46 -0
  67. package/lib/ui/table.js +42 -0
  68. package/lib/ui/tree.js +33 -0
  69. package/package.json +53 -0
  70. package/src/cli.js +457 -0
  71. package/src/commands/_helpers.js +119 -0
  72. package/src/commands/audit.js +187 -0
  73. package/src/commands/auth.js +316 -0
  74. package/src/commands/doctor.js +243 -0
  75. package/src/commands/hcm.js +147 -0
  76. package/src/commands/inbox.js +333 -0
  77. package/src/commands/init.js +160 -0
  78. package/src/commands/kairos.js +216 -0
  79. package/src/commands/kars.js +134 -0
  80. package/src/commands/mission.js +275 -0
  81. package/src/commands/notes.js +316 -0
  82. package/src/commands/notewriter.js +296 -0
  83. package/src/commands/odm.js +329 -0
  84. package/src/commands/orch.js +68 -0
  85. package/src/commands/project.js +123 -0
  86. package/src/commands/run.js +123 -0
  87. package/src/commands/services.js +705 -0
  88. package/src/commands/status.js +231 -0
  89. package/src/commands/team.js +572 -0
  90. package/src/config.js +84 -0
  91. package/src/index.js +5 -0
  92. package/templates/project-context.json +10 -0
  93. package/templates/template_CONTRIB-NAME.json +22 -0
  94. package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
  95. package/templates/template_DEC-NAME-000.json +18 -0
  96. package/templates/template_INTV-NAME-000.json +15 -0
  97. package/templates/template_MISSION_CONTRACT.json +46 -0
  98. package/templates/template_ODM-NAME-000.json +89 -0
  99. package/templates/template_REGISTRY-PROJECT.json +26 -0
  100. package/templates/template_TXN-NAME-000.json +24 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Dispatcher Router — execute routing decisions post-classification.
3
+ * V1: terminal display only. V2 will add webhook HCM + event-bus.
4
+ */
5
+
6
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { loadTemplate, mergeTemplate } from '../core/templates.js';
9
+ import { titledBox } from '../ui/box.js';
10
+ import { style } from '../ui/colors.js';
11
+
12
+ /**
13
+ * Display routing decisions in terminal.
14
+ * LEADER = green box, HUMAN = yellow box, NOISE = dim line.
15
+ * @param {Array} routing — array of { target, reason, priority, summary }
16
+ * @param {object} context — { agentName, odmId }
17
+ * @returns {{ displayed: string[], archived: string[], escalated: string[] }}
18
+ */
19
+ export function routeToTerminal(routing, _context = {}) {
20
+ const displayed = [];
21
+ const archived = [];
22
+ const escalated = [];
23
+
24
+ for (const entry of routing) {
25
+ const line = formatSingleRouting(entry);
26
+
27
+ switch (entry.target) {
28
+ case 'LEADER':
29
+ console.log('');
30
+ console.log(titledBox('Routing LEADER', [line], { border: style.green }));
31
+ displayed.push(entry.summary || entry.reason);
32
+ break;
33
+ case 'HUMAN':
34
+ console.log('');
35
+ console.log(titledBox('Routing HUMAN', [line], { border: style.yellow }));
36
+ escalated.push(entry.summary || entry.reason);
37
+ break;
38
+ case 'NOISE':
39
+ console.log(` ${style.dim(`NOISE ○ [${entry.priority}] ${entry.summary || entry.reason}`)}`);
40
+ archived.push(entry.summary || entry.reason);
41
+ break;
42
+ }
43
+ }
44
+
45
+ // V2: webhook HCM (Signal 2)
46
+ // V2: event-bus push vers Leader
47
+ // V2: notifications Slack/email pour HUMAN
48
+
49
+ return { displayed, archived, escalated };
50
+ }
51
+
52
+ /**
53
+ * Create a dispatch Transaction in .nemesis/HCM/transactions/.
54
+ * @param {object} opts — { from, to, odmId, projectId, root }
55
+ * @returns {{ filepath: string, txnId: string }}
56
+ */
57
+ export function createDispatchTransaction(opts = {}) {
58
+ const { from = 'PM', to, odmId = '', missionId = '', projectId = '', root = process.cwd() } = opts;
59
+
60
+ const txnId = `TXN-DISP-${Date.now()}`;
61
+ const txnDir = join(root, '.nemesis', 'HCM', 'transactions');
62
+
63
+ if (!existsSync(txnDir)) {
64
+ mkdirSync(txnDir, { recursive: true });
65
+ }
66
+
67
+ const refs = [];
68
+ if (odmId) refs.push(odmId);
69
+ if (missionId) refs.push(missionId);
70
+
71
+ const template = loadTemplate('transaction', root);
72
+ const txn = mergeTemplate(template, {
73
+ transaction_meta: {
74
+ txn_id: txnId,
75
+ project_id: projectId,
76
+ type: 'OdM_DISPATCH',
77
+ status: 'SENT',
78
+ created_at: new Date().toISOString(),
79
+ mission_id: missionId || undefined,
80
+ },
81
+ transaction_payload: {
82
+ from: { actor_type: 'human', actor_id: from },
83
+ to: { actor_type: 'agent', actor_id: to },
84
+ subject: `Dispatch OdM ${odmId}`,
85
+ refs,
86
+ },
87
+ });
88
+
89
+ const filepath = join(txnDir, `${txnId}.json`);
90
+ writeFileSync(filepath, JSON.stringify(txn, null, 2) + '\n', 'utf-8');
91
+
92
+ return { filepath, txnId };
93
+ }
94
+
95
+ /**
96
+ * Format routing decisions as display lines.
97
+ * @param {Array} routing
98
+ * @param {object} context
99
+ * @returns {string[]}
100
+ */
101
+ export function formatRoutingDisplay(routing, _context = {}) {
102
+ return routing.map(entry => formatSingleRouting(entry));
103
+ }
104
+
105
+ function formatSingleRouting(entry) {
106
+ const icons = { LEADER: '★', HUMAN: '⚠', NOISE: '○' };
107
+ const colors = { LEADER: style.green, HUMAN: style.yellow, NOISE: style.dim };
108
+ const icon = icons[entry.target] || '?';
109
+ const colorFn = colors[entry.target] || (v => v);
110
+ return colorFn(`${entry.target} ${icon} [${entry.priority}] ${entry.reason} — ${entry.summary}`);
111
+ }
112
+
113
+ /**
114
+ * Route classified dispatch results to files for inter-process consumption.
115
+ * LEADER → .nemesis/kairos/events/
116
+ * HUMAN → .nemesis/kairos/escalations/
117
+ * NOISE → .nemesis/kairos/noise/
118
+ *
119
+ * @param {Array} routing - array of { target, reason, priority, summary }
120
+ * @param {object} context - { agentName, odmId, projectId }
121
+ * @param {string} projectRoot
122
+ * @returns {{ events: string[], escalations: string[], noise: string[] }}
123
+ */
124
+ export function routeToFile(routing, context = {}, projectRoot = process.cwd()) {
125
+ const kairosDir = join(projectRoot, '.nemesis', 'kairos');
126
+ const events = [];
127
+ const escalations = [];
128
+ const noise = [];
129
+
130
+ for (const entry of routing) {
131
+ const ts = Date.now();
132
+ const agentId = context.agentName || 'unknown';
133
+ const payload = {
134
+ timestamp: new Date().toISOString(),
135
+ agentName: agentId,
136
+ odmId: context.odmId || '',
137
+ projectId: context.projectId || '',
138
+ target: entry.target,
139
+ priority: entry.priority,
140
+ reason: entry.reason,
141
+ summary: entry.summary,
142
+ };
143
+
144
+ switch (entry.target) {
145
+ case 'LEADER': {
146
+ const dir = join(kairosDir, 'events');
147
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
148
+ const filepath = join(dir, `${ts}-${agentId}.json`);
149
+ writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
150
+ events.push(filepath);
151
+ break;
152
+ }
153
+ case 'HUMAN': {
154
+ const dir = join(kairosDir, 'escalations');
155
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
156
+ const filepath = join(dir, `${ts}.json`);
157
+ writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
158
+ escalations.push(filepath);
159
+ break;
160
+ }
161
+ case 'NOISE': {
162
+ const dir = join(kairosDir, 'noise');
163
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
164
+ const filepath = join(dir, `${ts}.json`);
165
+ writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
166
+ noise.push(filepath);
167
+ break;
168
+ }
169
+ }
170
+ }
171
+
172
+ return { events, escalations, noise };
173
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Dispatcher — LLM classification of agent responses.
3
+ * Routes to LEADER, HUMAN, or NOISE.
4
+ */
5
+
6
+ import { debug } from '../core/logger.js';
7
+
8
+ export const ROUTING_TARGETS = { LEADER: 'LEADER', HUMAN: 'HUMAN', NOISE: 'NOISE' };
9
+
10
+ const VALID_TARGETS = new Set(Object.values(ROUTING_TARGETS));
11
+ const VALID_PRIORITIES = new Set(['HIGH', 'MEDIUM', 'LOW']);
12
+
13
+ /**
14
+ * Build the system prompt for the Dispatcher LLM.
15
+ * @returns {string}
16
+ */
17
+ export function buildDispatcherSystemPrompt() {
18
+ return `Tu es le Kairos Dispatcher. Classe la reponse d'un agent en 3 categories.
19
+
20
+ ## LEADER
21
+ Contient un livrable, CR, demande de validation, rapport d'avancement,
22
+ ou information que le Leader doit examiner.
23
+ Indices : fichiers deposes, references OdM, "voici le livrable", "CR depose".
24
+
25
+ ## HUMAN
26
+ Demande d'intervention humaine : decision hors perimetre, blocage non
27
+ resolvable, clarification PM, alerte securite.
28
+ Indices : "besoin d'arbitrage PM", "blocage", "question pour le PM".
29
+
30
+ ## NOISE
31
+ Accuse de reception, log intermediaire, confirmation standard, contenu vide.
32
+ Indices : reponse < 50 mots sans contenu technique, "OK", "compris".
33
+
34
+ ## Regles
35
+ 1. Doute LEADER/NOISE → LEADER
36
+ 2. Doute LEADER/HUMAN → HUMAN
37
+ 3. Un CR n'est JAMAIS NOISE
38
+ 4. Message > 200 mots technique = rarement NOISE
39
+
40
+ ## Format JSON strict
41
+ { "routing": [{ "target": "LEADER"|"HUMAN"|"NOISE", "reason": "...", "priority": "HIGH"|"MEDIUM"|"LOW", "summary": "..." }] }`;
42
+ }
43
+
44
+ /**
45
+ * Build the user prompt containing the agent response to classify.
46
+ * @param {string} agentResponse
47
+ * @param {object} context — { agentName, odmId, projectId }
48
+ * @returns {string}
49
+ */
50
+ export function buildDispatcherUserPrompt(agentResponse, context = {}) {
51
+ const parts = ['# Reponse agent a classifier'];
52
+ if (context.agentName) parts.push(`Agent : ${context.agentName}`);
53
+ if (context.odmId) parts.push(`OdM : ${context.odmId}`);
54
+ if (context.projectId) parts.push(`Projet : ${context.projectId}`);
55
+ parts.push('', '## Reponse', '', agentResponse);
56
+ return parts.join('\n');
57
+ }
58
+
59
+ /**
60
+ * Classify an agent response via LLM.
61
+ * @param {string} agentResponse
62
+ * @param {object} context — { agentName, odmId, projectId }
63
+ * @param {function} callLlm — async (systemPrompt, userPrompt) => string
64
+ * @returns {Promise<{ routing: Array, raw: string }>}
65
+ */
66
+ export async function classifyResponse(agentResponse, context, callLlm) {
67
+ const systemPrompt = buildDispatcherSystemPrompt();
68
+ const userPrompt = buildDispatcherUserPrompt(agentResponse, context);
69
+
70
+ let raw;
71
+ try {
72
+ raw = await callLlm(systemPrompt, userPrompt);
73
+ } catch (e) {
74
+ debug(`classifyResponse LLM: ${e.message}`);
75
+ return classifyFallback(agentResponse, context);
76
+ }
77
+
78
+ const parsed = parseJsonResponse(raw);
79
+ if (!parsed) return classifyFallback(agentResponse, context);
80
+
81
+ return { routing: parsed.routing, raw };
82
+ }
83
+
84
+ /**
85
+ * Graceful degradation: route everything to LEADER.
86
+ * @param {string} agentResponse
87
+ * @param {object} context
88
+ * @returns {{ routing: Array, raw: string }}
89
+ */
90
+ export function classifyFallback(agentResponse, _context = {}) {
91
+ const maxLen = 200;
92
+ const summary = agentResponse.length > maxLen
93
+ ? agentResponse.slice(0, maxLen) + '...'
94
+ : agentResponse;
95
+
96
+ return {
97
+ routing: [{
98
+ target: 'LEADER',
99
+ reason: 'Classification automatique (fallback)',
100
+ priority: 'MEDIUM',
101
+ summary,
102
+ }],
103
+ raw: '',
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Parse JSON from LLM response — direct or from markdown code block.
109
+ * @param {string} raw
110
+ * @returns {{ routing: Array }|null}
111
+ */
112
+ function parseJsonResponse(raw) {
113
+ // Try direct parse
114
+ let obj = tryParse(raw);
115
+ if (obj && validateRouting(obj)) return obj;
116
+
117
+ // Try extracting from markdown code block
118
+ const match = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
119
+ if (match) {
120
+ obj = tryParse(match[1].trim());
121
+ if (obj && validateRouting(obj)) return obj;
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ function tryParse(str) {
128
+ try { return JSON.parse(str); } catch (e) { debug(`tryParse: ${e.message}`); return null; }
129
+ }
130
+
131
+ function validateRouting(obj) {
132
+ if (!obj || !Array.isArray(obj.routing) || obj.routing.length === 0) return false;
133
+ return obj.routing.every(entry =>
134
+ VALID_TARGETS.has(entry.target) &&
135
+ typeof entry.reason === 'string' &&
136
+ VALID_PRIORITIES.has(entry.priority) &&
137
+ typeof entry.summary === 'string'
138
+ );
139
+ }
@@ -0,0 +1,287 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { debug } from '../core/logger.js';
5
+
6
+ const EVENT_BUS_DIR = '.nemesis/kairos/event-bus';
7
+
8
+ export const EVENT_TYPES = Object.freeze({
9
+ CR_DEPOSITED: 'cr_deposited',
10
+ TASK_ASSIGNED: 'task_assigned',
11
+ VALIDATION_REQUIRED: 'validation_required',
12
+ DISPATCH_RESULT: 'dispatch_result',
13
+ MESSAGE: 'message',
14
+ });
15
+
16
+ export const PRIORITIES = Object.freeze({
17
+ HIGH: 'HIGH',
18
+ MEDIUM: 'MEDIUM',
19
+ LOW: 'LOW',
20
+ });
21
+
22
+ const DEFAULT_TTL = 300; // seconds
23
+
24
+ /**
25
+ * Resolve queue directory for an agent.
26
+ */
27
+ function queueDir(projectRoot, agentId) {
28
+ return join(projectRoot, EVENT_BUS_DIR, agentId);
29
+ }
30
+
31
+ /**
32
+ * Generate a unique event ID.
33
+ */
34
+ function generateEventId() {
35
+ const ts = Date.now();
36
+ const rand = randomBytes(3).toString('hex');
37
+ return `EVT-${ts}-${rand}`;
38
+ }
39
+
40
+ /**
41
+ * Push an event to an agent's inbox queue.
42
+ * @param {string} projectRoot
43
+ * @param {object} opts
44
+ * @param {string} opts.target_agent_id
45
+ * @param {string} opts.event_type
46
+ * @param {object} [opts.payload]
47
+ * @param {string} [opts.priority]
48
+ * @param {number} [opts.ttl] - TTL in seconds
49
+ * @param {string} [opts.source_agent_id]
50
+ * @returns {{ eventId: string, filepath: string }}
51
+ */
52
+ export function pushEvent(projectRoot, opts) {
53
+ const {
54
+ target_agent_id,
55
+ event_type,
56
+ payload = {},
57
+ priority = PRIORITIES.MEDIUM,
58
+ ttl = DEFAULT_TTL,
59
+ source_agent_id = 'system',
60
+ } = opts;
61
+
62
+ if (!target_agent_id) throw new Error('target_agent_id requis');
63
+ if (!event_type) throw new Error('event_type requis');
64
+
65
+ const dir = queueDir(projectRoot, target_agent_id);
66
+ mkdirSync(dir, { recursive: true });
67
+
68
+ const eventId = generateEventId();
69
+ const now = new Date();
70
+ const expiresAt = new Date(now.getTime() + ttl * 1000);
71
+
72
+ const event = {
73
+ event_id: eventId,
74
+ event_type,
75
+ source_agent_id,
76
+ target_agent_id,
77
+ priority,
78
+ ttl,
79
+ created_at: now.toISOString(),
80
+ expires_at: expiresAt.toISOString(),
81
+ payload,
82
+ };
83
+
84
+ const filepath = join(dir, `${eventId}.json`);
85
+ writeFileSync(filepath, JSON.stringify(event, null, 2) + '\n', 'utf-8');
86
+
87
+ return { eventId, filepath };
88
+ }
89
+
90
+ /**
91
+ * Poll pending events for an agent (non-consuming read).
92
+ * @param {string} projectRoot
93
+ * @param {string} agentId
94
+ * @returns {Array<{ eventId: string, event: object, filepath: string }>}
95
+ */
96
+ export function pollEvents(projectRoot, agentId) {
97
+ const dir = queueDir(projectRoot, agentId);
98
+ if (!existsSync(dir)) return [];
99
+
100
+ const files = readdirSync(dir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
101
+ const results = [];
102
+
103
+ for (const file of files) {
104
+ const filepath = join(dir, file);
105
+ try {
106
+ const event = JSON.parse(readFileSync(filepath, 'utf-8'));
107
+ results.push({
108
+ eventId: event.event_id || file.replace('.json', ''),
109
+ event,
110
+ filepath,
111
+ });
112
+ } catch (e) { debug(`pollEvents ${file}: ${e.message}`); }
113
+ }
114
+
115
+ // Sort by priority (HIGH first) then by creation time
116
+ const priorityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 };
117
+ results.sort((a, b) => {
118
+ const pa = priorityOrder[a.event.priority] ?? 1;
119
+ const pb = priorityOrder[b.event.priority] ?? 1;
120
+ if (pa !== pb) return pa - pb;
121
+ return (a.event.created_at || '').localeCompare(b.event.created_at || '');
122
+ });
123
+
124
+ return results;
125
+ }
126
+
127
+ /**
128
+ * Consume (delete) a single event by ID.
129
+ * @param {string} projectRoot
130
+ * @param {string} agentId
131
+ * @param {string} eventId
132
+ */
133
+ export function consumeEvent(projectRoot, agentId, eventId) {
134
+ const dir = queueDir(projectRoot, agentId);
135
+ if (!existsSync(dir)) return;
136
+
137
+ const filepath = join(dir, `${eventId}.json`);
138
+ try { unlinkSync(filepath); } catch (e) { debug(`consumeEvent: ${e.message}`); }
139
+ }
140
+
141
+ /**
142
+ * Atomically consume all events for an agent — read + delete.
143
+ * @param {string} projectRoot
144
+ * @param {string} agentId
145
+ * @returns {Array<{ eventId: string, event: object }>}
146
+ */
147
+ export function consumeAllEvents(projectRoot, agentId) {
148
+ const dir = queueDir(projectRoot, agentId);
149
+ if (!existsSync(dir)) return [];
150
+
151
+ const files = readdirSync(dir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
152
+ const results = [];
153
+
154
+ for (const file of files) {
155
+ const filepath = join(dir, file);
156
+ try {
157
+ const event = JSON.parse(readFileSync(filepath, 'utf-8'));
158
+ results.push({
159
+ eventId: event.event_id || file.replace('.json', ''),
160
+ event,
161
+ });
162
+ unlinkSync(filepath);
163
+ } catch (e) { debug(`consumeAllEvents ${file}: ${e.message}`); }
164
+ }
165
+
166
+ // Sort by priority then time
167
+ const priorityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 };
168
+ results.sort((a, b) => {
169
+ const pa = priorityOrder[a.event.priority] ?? 1;
170
+ const pb = priorityOrder[b.event.priority] ?? 1;
171
+ if (pa !== pb) return pa - pb;
172
+ return (a.event.created_at || '').localeCompare(b.event.created_at || '');
173
+ });
174
+
175
+ return results;
176
+ }
177
+
178
+ /**
179
+ * List all queues with event counts.
180
+ * @param {string} projectRoot
181
+ * @returns {Array<{ agentId: string, count: number, oldest: string|null }>}
182
+ */
183
+ export function listQueues(projectRoot) {
184
+ const busDir = join(projectRoot, EVENT_BUS_DIR);
185
+ if (!existsSync(busDir)) return [];
186
+
187
+ const dirs = readdirSync(busDir, { withFileTypes: true })
188
+ .filter(d => d.isDirectory());
189
+
190
+ return dirs.map(d => {
191
+ const agentDir = join(busDir, d.name);
192
+ const files = readdirSync(agentDir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
193
+ let oldest = null;
194
+ for (const file of files) {
195
+ try {
196
+ const evt = JSON.parse(readFileSync(join(agentDir, file), 'utf-8'));
197
+ if (!oldest || evt.created_at < oldest) oldest = evt.created_at;
198
+ } catch (e) { debug(`listQueues ${file}: ${e.message}`); }
199
+ }
200
+ return { agentId: d.name, count: files.length, oldest };
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Clear all events in an agent's queue.
206
+ * @param {string} projectRoot
207
+ * @param {string} agentId
208
+ * @returns {number} Number of events cleared
209
+ */
210
+ export function clearQueue(projectRoot, agentId) {
211
+ const dir = queueDir(projectRoot, agentId);
212
+ if (!existsSync(dir)) return 0;
213
+
214
+ const files = readdirSync(dir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
215
+ let count = 0;
216
+ for (const file of files) {
217
+ try { unlinkSync(join(dir, file)); count++; } catch (e) { debug(`clearQueue: ${e.message}`); }
218
+ }
219
+ return count;
220
+ }
221
+
222
+ /**
223
+ * Format events as markdown for stdout injection in hooks.
224
+ * @param {Array<{ eventId: string, event: object }>} events
225
+ * @returns {string}
226
+ */
227
+ export function formatEventsForInjection(events) {
228
+ if (!events || events.length === 0) return '';
229
+
230
+ const lines = [
231
+ '',
232
+ `# KAIROS EVENT BUS — ${events.length} evenement(s) en attente`,
233
+ '',
234
+ ];
235
+
236
+ for (const { event } of events) {
237
+ lines.push(`## [${event.priority || 'MEDIUM'}] ${event.event_type}`);
238
+ lines.push(`De: ${event.source_agent_id || 'inconnu'} — ${event.created_at || ''}`);
239
+
240
+ if (event.payload) {
241
+ if (event.payload.summary) lines.push(`Resume: ${event.payload.summary}`);
242
+ if (event.payload.odmId) lines.push(`OdM: ${event.payload.odmId}`);
243
+ if (event.payload.cr_id) lines.push(`CR: ${event.payload.cr_id}`);
244
+ if (event.payload.message) lines.push(`Message: ${event.payload.message}`);
245
+ }
246
+ lines.push('');
247
+ }
248
+
249
+ lines.push('---');
250
+ lines.push('Traite ces evenements selon leur priorite.');
251
+ lines.push('');
252
+
253
+ return lines.join('\n');
254
+ }
255
+
256
+ /**
257
+ * Purge expired events across all queues.
258
+ * @param {string} projectRoot
259
+ * @returns {number} Number of events purged
260
+ */
261
+ export function purgeExpired(projectRoot) {
262
+ const busDir = join(projectRoot, EVENT_BUS_DIR);
263
+ if (!existsSync(busDir)) return 0;
264
+
265
+ const now = new Date();
266
+ let purged = 0;
267
+
268
+ const dirs = readdirSync(busDir, { withFileTypes: true })
269
+ .filter(d => d.isDirectory());
270
+
271
+ for (const d of dirs) {
272
+ const agentDir = join(busDir, d.name);
273
+ const files = readdirSync(agentDir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
274
+ for (const file of files) {
275
+ const filepath = join(agentDir, file);
276
+ try {
277
+ const evt = JSON.parse(readFileSync(filepath, 'utf-8'));
278
+ if (evt.expires_at && new Date(evt.expires_at) < now) {
279
+ unlinkSync(filepath);
280
+ purged++;
281
+ }
282
+ } catch (e) { debug(`purgeExpired ${file}: ${e.message}`); }
283
+ }
284
+ }
285
+
286
+ return purged;
287
+ }