@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
package/lib/core/team.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { detectProject } from './project.js';
|
|
5
|
+
import { readRegistry, writeRegistry, addLane, removeLane, getLanes, getPM } from './registry.js';
|
|
6
|
+
import { loadTemplate, mergeTemplate } from './templates.js';
|
|
7
|
+
import { generateMemory, generateOnboardingPrompt, updateClaudeMd, getRepoName } from './generators.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Team Add — 9 steps from spec.
|
|
11
|
+
*/
|
|
12
|
+
export async function teamAdd(opts) {
|
|
13
|
+
const {
|
|
14
|
+
root = process.cwd(),
|
|
15
|
+
agentName,
|
|
16
|
+
role,
|
|
17
|
+
reportsTo,
|
|
18
|
+
profileId = null,
|
|
19
|
+
kairosData = null,
|
|
20
|
+
odmIds = [],
|
|
21
|
+
mcpServers = [],
|
|
22
|
+
environment = null,
|
|
23
|
+
leader = false,
|
|
24
|
+
} = opts;
|
|
25
|
+
|
|
26
|
+
const results = [];
|
|
27
|
+
|
|
28
|
+
// Step 1: Detect project
|
|
29
|
+
const project = detectProject(root);
|
|
30
|
+
if (!project) {
|
|
31
|
+
throw new Error('Aucun projet detecte. Lancez "nemesis init" d\'abord.');
|
|
32
|
+
}
|
|
33
|
+
results.push({ step: 1, label: 'Projet detecte', detail: project.id });
|
|
34
|
+
|
|
35
|
+
// Step 2: Profile Kairos
|
|
36
|
+
if (profileId) {
|
|
37
|
+
results.push({ step: 2, label: 'Profil Kairos', detail: profileId });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Step 3: Agent configured
|
|
41
|
+
results.push({ step: 3, label: 'Agent configure', detail: `${agentName} — ${role}` });
|
|
42
|
+
|
|
43
|
+
// Step 4: Create memory.md
|
|
44
|
+
const agentDir = join(root, '.nemesis', 'claude', agentName);
|
|
45
|
+
mkdirSync(agentDir, { recursive: true });
|
|
46
|
+
const memoryFile = join(agentDir, 'memory.md');
|
|
47
|
+
if (!existsSync(memoryFile)) {
|
|
48
|
+
writeFileSync(memoryFile, generateMemory(agentName, role, project), 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
results.push({ step: 4, label: 'memory.md cree', detail: `.nemesis/claude/${agentName}/memory.md` });
|
|
51
|
+
|
|
52
|
+
// Step 5: Create contributor card
|
|
53
|
+
const contribId = agentName.replace('Agent_', '').replace(/_/g, '-');
|
|
54
|
+
const contrib = mergeTemplate(loadTemplate('contributor', root), {
|
|
55
|
+
contributor_meta: { contributor_id: `CONTRIB-${contribId}`, kind: 'agent', project_id: project.id },
|
|
56
|
+
contributor_payload: {
|
|
57
|
+
identity: { display_name: agentName, role, organization: 'Arka Labs' },
|
|
58
|
+
expertise: { domains: [], description: role },
|
|
59
|
+
profiles: profileId ? [profileId] : [],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const contribFile = join(project.hcm_dir, 'contributors', `CONTRIB-${contribId}.json`);
|
|
63
|
+
writeFileSync(contribFile, JSON.stringify(contrib, null, 2) + '\n', 'utf-8');
|
|
64
|
+
results.push({ step: 5, label: 'Contributor card creee', detail: contribFile.replace(root + '/', '') });
|
|
65
|
+
|
|
66
|
+
// Step 6: Update registry
|
|
67
|
+
const registry = readRegistry(project.hcm_dir);
|
|
68
|
+
if (registry) {
|
|
69
|
+
addLane(registry, {
|
|
70
|
+
id: agentName, name: agentName, kind: 'agent', role,
|
|
71
|
+
reports_to: reportsTo || 'PM', reviews: [], executes_from: odmIds,
|
|
72
|
+
responsibilities: [role], memory_path: `.nemesis/claude/${agentName}/memory.md`,
|
|
73
|
+
mcp_servers: mcpServers,
|
|
74
|
+
leader,
|
|
75
|
+
});
|
|
76
|
+
writeRegistry(project.hcm_dir, registry);
|
|
77
|
+
results.push({ step: 6, label: 'Registre mis a jour', detail: `Lane ajoutee : ${agentName}` });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 7: Update CLAUDE.md
|
|
81
|
+
const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
|
|
82
|
+
if (existsSync(claudeMdPath)) {
|
|
83
|
+
updateClaudeMd(claudeMdPath, {
|
|
84
|
+
agentName, repo: getRepoName(root), role,
|
|
85
|
+
memoryPath: `${getRepoName(root)}/.nemesis/claude/${agentName}/memory.md`,
|
|
86
|
+
});
|
|
87
|
+
results.push({ step: 7, label: 'CLAUDE.md mis a jour', detail: 'Tableau + Workflow' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 8: Generate onboarding prompt
|
|
91
|
+
const onboardingDir = join(root, '.nemesis', 'onboarding');
|
|
92
|
+
mkdirSync(onboardingDir, { recursive: true });
|
|
93
|
+
const promptFile = join(onboardingDir, `prompt-${contribId}.md`);
|
|
94
|
+
// Build environment state for the prompt
|
|
95
|
+
const envState = environment || {
|
|
96
|
+
hcm: !!project.hcm_status?.connected,
|
|
97
|
+
files: [],
|
|
98
|
+
};
|
|
99
|
+
// Add files that were just created
|
|
100
|
+
if (existsSync(memoryFile)) envState.files = [...(envState.files || []), `.nemesis/claude/${agentName}/memory.md`];
|
|
101
|
+
if (existsSync(contribFile)) envState.files = [...(envState.files || []), contribFile.replace(root + '/', '')];
|
|
102
|
+
|
|
103
|
+
writeFileSync(promptFile, generateOnboardingPrompt({ agentName, role, profileId, kairosData, reportsTo, project, odmIds, environment: envState }), 'utf-8');
|
|
104
|
+
results.push({ step: 8, label: 'Prompt d\'onboarding genere', detail: `.nemesis/onboarding/prompt-${contribId}.md` });
|
|
105
|
+
|
|
106
|
+
// Step 9: Launch command
|
|
107
|
+
const launchCmd = `claude --prompt .nemesis/onboarding/prompt-${contribId}.md`;
|
|
108
|
+
results.push({ step: 9, label: 'Commande de lancement', detail: launchCmd });
|
|
109
|
+
|
|
110
|
+
return { project, agentName, results, launchCmd, promptPath: promptFile };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Team List — build org tree from registry.
|
|
115
|
+
*/
|
|
116
|
+
export function teamList(hcmDir) {
|
|
117
|
+
const registry = readRegistry(hcmDir);
|
|
118
|
+
if (!registry) return null;
|
|
119
|
+
|
|
120
|
+
const pm = getPM(registry);
|
|
121
|
+
const lanes = getLanes(registry);
|
|
122
|
+
|
|
123
|
+
const tree = {
|
|
124
|
+
label: `${pm?.name || 'PM'} (${pm?.role || 'PM'})`,
|
|
125
|
+
meta: pm?.kind || 'human',
|
|
126
|
+
children: [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const byReportsTo = {};
|
|
130
|
+
for (const lane of lanes) {
|
|
131
|
+
const parent = lane.reports_to || 'PM';
|
|
132
|
+
if (!byReportsTo[parent]) byReportsTo[parent] = [];
|
|
133
|
+
byReportsTo[parent].push(lane);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const topAgents = [
|
|
137
|
+
...(byReportsTo['PM'] || []),
|
|
138
|
+
...(byReportsTo[pm?.name] || []),
|
|
139
|
+
...(byReportsTo[pm?.id] || []),
|
|
140
|
+
];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const agent of topAgents) {
|
|
143
|
+
if (seen.has(agent.id)) continue;
|
|
144
|
+
seen.add(agent.id);
|
|
145
|
+
tree.children.push(buildAgentNode(agent, byReportsTo, seen));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const lane of lanes) {
|
|
149
|
+
if (!seen.has(lane.id)) {
|
|
150
|
+
tree.children.push(buildAgentNode(lane, byReportsTo, seen));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { tree, registry };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildAgentNode(agent, byReportsTo, seen) {
|
|
158
|
+
const node = { label: agent.name || agent.id, meta: agent.role, children: [] };
|
|
159
|
+
const subordinates = byReportsTo[agent.id] || byReportsTo[agent.name] || [];
|
|
160
|
+
for (const sub of subordinates) {
|
|
161
|
+
if (seen.has(sub.id)) continue;
|
|
162
|
+
seen.add(sub.id);
|
|
163
|
+
node.children.push(buildAgentNode(sub, byReportsTo, seen));
|
|
164
|
+
}
|
|
165
|
+
return node;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Team Inspect — detailed view of an agent.
|
|
170
|
+
*/
|
|
171
|
+
export function teamInspect(agentId, projectRoot) {
|
|
172
|
+
const { detectProject } = require_detect();
|
|
173
|
+
const project = detectProject(projectRoot);
|
|
174
|
+
if (!project) throw new Error('Aucun projet detecte.');
|
|
175
|
+
|
|
176
|
+
const registry = readRegistry(project.hcm_dir);
|
|
177
|
+
if (!registry) throw new Error('Registry introuvable.');
|
|
178
|
+
|
|
179
|
+
const lanes = getLanes(registry);
|
|
180
|
+
const lane = lanes.find(l => l.id === agentId || l.name === agentId);
|
|
181
|
+
if (!lane) throw new Error(`Agent ${agentId} non trouve dans le registre.`);
|
|
182
|
+
|
|
183
|
+
// Memory info
|
|
184
|
+
let memoryInfo = null;
|
|
185
|
+
if (lane.memory_path) {
|
|
186
|
+
const memPath = join(projectRoot, lane.memory_path);
|
|
187
|
+
if (existsSync(memPath)) {
|
|
188
|
+
const stat = statSync(memPath);
|
|
189
|
+
memoryInfo = {
|
|
190
|
+
path: lane.memory_path,
|
|
191
|
+
size: (stat.size / 1024).toFixed(1) + ' KB',
|
|
192
|
+
modified: stat.mtime.toISOString().split('T')[0],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Assigned OdMs
|
|
198
|
+
const odmDir = join(project.hcm_dir, 'odm');
|
|
199
|
+
let assignedOdms = [];
|
|
200
|
+
if (existsSync(odmDir)) {
|
|
201
|
+
assignedOdms = readdirSync(odmDir)
|
|
202
|
+
.filter(f => f.endsWith('.json') && !f.startsWith('legacy_'))
|
|
203
|
+
.map(f => {
|
|
204
|
+
try {
|
|
205
|
+
const data = JSON.parse(readFileSync(join(odmDir, f), 'utf-8'));
|
|
206
|
+
return data;
|
|
207
|
+
} catch (_e) { /* fallback: malformed OdM */ return null; }
|
|
208
|
+
})
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.filter(d => {
|
|
211
|
+
const assignee = d.odm_meta?.assigned_to?.actor_id || '';
|
|
212
|
+
return assignee === agentId || assignee === lane.name;
|
|
213
|
+
})
|
|
214
|
+
.map(d => ({
|
|
215
|
+
id: d.odm_meta?.odm_id || '?',
|
|
216
|
+
status: d.odm_meta?.status || '?',
|
|
217
|
+
title: d.odm_payload?.cadrage?.title || '',
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Associated CRs
|
|
222
|
+
const crDir = join(project.hcm_dir, 'cr');
|
|
223
|
+
let crs = [];
|
|
224
|
+
if (existsSync(crDir)) {
|
|
225
|
+
crs = readdirSync(crDir)
|
|
226
|
+
.filter(f => f.endsWith('.json'))
|
|
227
|
+
.map(f => {
|
|
228
|
+
try { return JSON.parse(readFileSync(join(crDir, f), 'utf-8')); } catch (_e) { /* fallback: malformed CR */ return null; }
|
|
229
|
+
})
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.filter(d => (d.metadata?.executed_by || '').includes(agentId))
|
|
232
|
+
.map(d => ({
|
|
233
|
+
id: d.metadata?.odm_id || '?',
|
|
234
|
+
title: d.title || '',
|
|
235
|
+
date: d.metadata?.date_execution || '',
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
agent: lane,
|
|
241
|
+
memory: memoryInfo,
|
|
242
|
+
odms: assignedOdms,
|
|
243
|
+
crs,
|
|
244
|
+
project: project.id,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Team Remove — archive agent (no deletion).
|
|
250
|
+
*/
|
|
251
|
+
export function teamRemove(agentId, projectRoot) {
|
|
252
|
+
const project = detectProject(projectRoot);
|
|
253
|
+
if (!project) throw new Error('Aucun projet detecte.');
|
|
254
|
+
|
|
255
|
+
const registry = readRegistry(project.hcm_dir);
|
|
256
|
+
if (!registry) throw new Error('Registry introuvable.');
|
|
257
|
+
|
|
258
|
+
// Remove from registry
|
|
259
|
+
removeLane(registry, agentId);
|
|
260
|
+
writeRegistry(project.hcm_dir, registry);
|
|
261
|
+
|
|
262
|
+
// Remove from CLAUDE.md (if present)
|
|
263
|
+
const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
|
|
264
|
+
if (existsSync(claudeMdPath)) {
|
|
265
|
+
let content = readFileSync(claudeMdPath, 'utf-8');
|
|
266
|
+
// Remove table row
|
|
267
|
+
const lines = content.split('\n');
|
|
268
|
+
const filtered = lines.filter(l => !l.includes(`**${agentId}**`));
|
|
269
|
+
// Remove workflow line
|
|
270
|
+
const finalLines = filtered.filter(l => !l.includes(`${agentId} :`));
|
|
271
|
+
writeFileSync(claudeMdPath, finalLines.join('\n'), 'utf-8');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Archive: rename memory.md → memory.md.archived (don't delete)
|
|
275
|
+
const memoryDir = join(projectRoot, '.nemesis', 'claude', agentId);
|
|
276
|
+
const memoryFile = join(memoryDir, 'memory.md');
|
|
277
|
+
if (existsSync(memoryFile)) {
|
|
278
|
+
renameSync(memoryFile, memoryFile + '.archived');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { agentId, archived: true };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Lazy import to avoid circular dependency
|
|
285
|
+
function require_detect() {
|
|
286
|
+
return { detectProject };
|
|
287
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const EMBEDDED_DIR = join(__dirname, '..', '..', 'templates');
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_MAP = {
|
|
9
|
+
'odm': 'template_ODM-NAME-000.json',
|
|
10
|
+
'cr': 'template_CR-ODM-NAME-000.exemple.json',
|
|
11
|
+
'mission-contract': 'template_MISSION_CONTRACT.json',
|
|
12
|
+
'registry': 'template_REGISTRY-PROJECT.json',
|
|
13
|
+
'decision': 'template_DEC-NAME-000.json',
|
|
14
|
+
'intervention': 'template_INTV-NAME-000.json',
|
|
15
|
+
'contributor': 'template_CONTRIB-NAME.json',
|
|
16
|
+
'transaction': 'template_TXN-NAME-000.json',
|
|
17
|
+
'project-context': 'project-context.json',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load a template by type.
|
|
22
|
+
* Priority: .nemesis/template/ > .owner/template/ (legacy) > embedded templates/
|
|
23
|
+
*/
|
|
24
|
+
export function loadTemplate(type, projectRoot = process.cwd()) {
|
|
25
|
+
const filename = TEMPLATE_MAP[type];
|
|
26
|
+
if (!filename) {
|
|
27
|
+
throw new Error(`Template inconnu : ${type}. Types valides : ${Object.keys(TEMPLATE_MAP).join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Priority 1: .nemesis/template/
|
|
31
|
+
const nemesisPath = join(projectRoot, '.nemesis', 'template', filename);
|
|
32
|
+
if (existsSync(nemesisPath)) {
|
|
33
|
+
return JSON.parse(readFileSync(nemesisPath, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Priority 2: .owner/template/ (legacy fallback)
|
|
37
|
+
const legacyPath = join(projectRoot, '.owner', 'template', filename);
|
|
38
|
+
if (existsSync(legacyPath)) {
|
|
39
|
+
return JSON.parse(readFileSync(legacyPath, 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Priority 3: embedded templates/
|
|
43
|
+
const embeddedPath = join(EMBEDDED_DIR, filename);
|
|
44
|
+
if (existsSync(embeddedPath)) {
|
|
45
|
+
return JSON.parse(readFileSync(embeddedPath, 'utf-8'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Template "${type}" introuvable (${filename})`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Deep merge values into a template.
|
|
53
|
+
*/
|
|
54
|
+
export function mergeTemplate(template, values) {
|
|
55
|
+
return deepMerge(structuredClone(template), values);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deepMerge(target, source) {
|
|
59
|
+
for (const key of Object.keys(source)) {
|
|
60
|
+
if (
|
|
61
|
+
source[key] !== null &&
|
|
62
|
+
typeof source[key] === 'object' &&
|
|
63
|
+
!Array.isArray(source[key]) &&
|
|
64
|
+
typeof target[key] === 'object' &&
|
|
65
|
+
!Array.isArray(target[key])
|
|
66
|
+
) {
|
|
67
|
+
deepMerge(target[key], source[key]);
|
|
68
|
+
} else {
|
|
69
|
+
target[key] = source[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List available template types.
|
|
77
|
+
*/
|
|
78
|
+
export function listTemplateTypes() {
|
|
79
|
+
return Object.keys(TEMPLATE_MAP);
|
|
80
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Runner — dispatch a prompt to an agent headless (non-interactive).
|
|
3
|
+
* The agent runs detached; its Stop hook writes the result to dispatch-results/.
|
|
4
|
+
* No blocking wait — the Leader polls for results via hooks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync, unlinkSync, openSync, closeSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { debug } from '../core/logger.js';
|
|
11
|
+
|
|
12
|
+
const PENDING_DIR = '.nemesis/kairos/pending-dispatch';
|
|
13
|
+
const RESULTS_DIR = '.nemesis/kairos/dispatch-results';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Launch an agent headless with a prompt. Returns immediately.
|
|
17
|
+
* The agent runs detached; its Stop hook will write the result.
|
|
18
|
+
* @param {object} agent — lane from registry (must have session_id)
|
|
19
|
+
* @param {string} prompt — formatted prompt text
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {string} opts.txnId — transaction ID for tracking
|
|
22
|
+
* @param {string} [opts.odmId] — OdM ID if dispatching an OdM
|
|
23
|
+
* @param {string} [opts.projectId] — project ID
|
|
24
|
+
* @param {string} opts.root — project root
|
|
25
|
+
* @returns {{ pid: number|null, txnId: string }}
|
|
26
|
+
*/
|
|
27
|
+
export async function launchHeadless(agent, prompt, opts = {}) {
|
|
28
|
+
const { txnId, odmId = '', missionId = '', projectId = '', root = process.cwd() } = opts;
|
|
29
|
+
|
|
30
|
+
if (!agent?.session_id) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Agent ${agent?.name || 'inconnu'} n'a pas de session_id. Lancez d'abord un onboarding.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!txnId) {
|
|
37
|
+
throw new Error('txnId requis pour le tracking du dispatch.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const launchedAt = new Date().toISOString();
|
|
41
|
+
|
|
42
|
+
// Write pending dispatch file — Stop hook will read it
|
|
43
|
+
createPendingDispatch(root, txnId, {
|
|
44
|
+
agentName: agent.name || agent.id,
|
|
45
|
+
agentSessionId: agent.session_id,
|
|
46
|
+
txnId,
|
|
47
|
+
odmId,
|
|
48
|
+
missionId,
|
|
49
|
+
projectId,
|
|
50
|
+
launchedAt,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Log file for diagnostics — captures stdout/stderr
|
|
54
|
+
const logDir = join(root, PENDING_DIR);
|
|
55
|
+
const logPath = join(logDir, `${txnId}.log`);
|
|
56
|
+
const logFd = openSync(logPath, 'w');
|
|
57
|
+
|
|
58
|
+
// Spawn detached — agent runs independently
|
|
59
|
+
// stdout/stderr → log file (not /dev/null) for debugging
|
|
60
|
+
const child = spawn('claude', [
|
|
61
|
+
'--resume', agent.session_id,
|
|
62
|
+
'-p', prompt,
|
|
63
|
+
'--dangerously-skip-permissions',
|
|
64
|
+
], {
|
|
65
|
+
stdio: ['ignore', logFd, logFd],
|
|
66
|
+
detached: true,
|
|
67
|
+
env: (() => { const e = { ...process.env }; delete e.CLAUDECODE; return e; })(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
child.unref();
|
|
71
|
+
closeSync(logFd);
|
|
72
|
+
|
|
73
|
+
// Patch pending-dispatch file with PID so dashboard can track liveness
|
|
74
|
+
if (child.pid) {
|
|
75
|
+
try {
|
|
76
|
+
const pendingPath = join(root, PENDING_DIR, `${txnId}.json`);
|
|
77
|
+
const pending = JSON.parse(readFileSync(pendingPath, 'utf-8'));
|
|
78
|
+
pending.pid = child.pid;
|
|
79
|
+
writeFileSync(pendingPath, JSON.stringify(pending, null, 2) + '\n', 'utf-8');
|
|
80
|
+
} catch (e) { debug(`launchHeadless PID patch: ${e.message}`); }
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const { registerPid } = await import('./pid-checker.js');
|
|
84
|
+
registerPid(root, agent.name || agent.id, child.pid, agent.session_id);
|
|
85
|
+
} catch (e) { debug(`launchHeadless registerPid: ${e.message}`); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { pid: child.pid ?? null, txnId };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a pending dispatch marker for the Stop hook to pick up.
|
|
93
|
+
* @param {string} root — project root
|
|
94
|
+
* @param {string} txnId
|
|
95
|
+
* @param {object} metadata — { agentName, agentSessionId, odmId, projectId, launchedAt }
|
|
96
|
+
*/
|
|
97
|
+
export function createPendingDispatch(root, txnId, metadata) {
|
|
98
|
+
const dir = join(root, PENDING_DIR);
|
|
99
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
const filepath = join(dir, `${txnId}.json`);
|
|
102
|
+
writeFileSync(filepath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find pending dispatch for a given agent (called by Stop hook).
|
|
107
|
+
* @param {string} root
|
|
108
|
+
* @param {string} agentName
|
|
109
|
+
* @returns {{ txnId: string, metadata: object }|null}
|
|
110
|
+
*/
|
|
111
|
+
export function findPendingDispatch(root, agentName) {
|
|
112
|
+
const dir = join(root, PENDING_DIR);
|
|
113
|
+
if (!existsSync(dir)) return null;
|
|
114
|
+
|
|
115
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
try {
|
|
118
|
+
const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
119
|
+
if (data.agentName === agentName) {
|
|
120
|
+
return { txnId: file.replace('.json', ''), metadata: data };
|
|
121
|
+
}
|
|
122
|
+
} catch (e) { debug(`findPendingDispatch ${file}: ${e.message}`); }
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Write dispatch result (called by Stop hook when agent finishes).
|
|
129
|
+
* @param {string} root
|
|
130
|
+
* @param {string} txnId
|
|
131
|
+
* @param {object} result — { agentName, odmId, response, exitCode, finishedAt }
|
|
132
|
+
*/
|
|
133
|
+
export function writeDispatchResult(root, txnId, result) {
|
|
134
|
+
const dir = join(root, RESULTS_DIR);
|
|
135
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
136
|
+
|
|
137
|
+
const filepath = join(dir, `${txnId}.json`);
|
|
138
|
+
writeFileSync(filepath, JSON.stringify(result, null, 2) + '\n', 'utf-8');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Consume pending dispatch file (cleanup after writing result).
|
|
143
|
+
* @param {string} root
|
|
144
|
+
* @param {string} txnId
|
|
145
|
+
*/
|
|
146
|
+
export function consumePendingDispatch(root, txnId) {
|
|
147
|
+
const filepath = join(root, PENDING_DIR, `${txnId}.json`);
|
|
148
|
+
try { unlinkSync(filepath); } catch (e) { debug(`consumePendingDispatch: ${e.message}`); }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read and consume all dispatch results (called by Leader polling).
|
|
153
|
+
* @param {string} root
|
|
154
|
+
* @returns {Array<{ txnId: string, result: object }>}
|
|
155
|
+
*/
|
|
156
|
+
export function consumeDispatchResults(root) {
|
|
157
|
+
const dir = join(root, RESULTS_DIR);
|
|
158
|
+
if (!existsSync(dir)) return [];
|
|
159
|
+
|
|
160
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
161
|
+
const results = [];
|
|
162
|
+
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const filepath = join(dir, file);
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
167
|
+
results.push({ txnId: file.replace('.json', ''), result: data });
|
|
168
|
+
unlinkSync(filepath); // consume
|
|
169
|
+
} catch (e) { debug(`consumeDispatchResults ${file}: ${e.message}`); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build a formatted dispatch prompt from an OdM or free document.
|
|
177
|
+
* @param {object|null} odm — full OdM JSON (null for free document)
|
|
178
|
+
* @param {object} opts
|
|
179
|
+
* @param {string} [opts.additionalContext] — extra leader instructions
|
|
180
|
+
* @param {string} [opts.documentBody] — free text (when odm is null)
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
export function buildDispatchPrompt(odm, opts = {}) {
|
|
184
|
+
const { additionalContext = '', documentBody = '' } = opts;
|
|
185
|
+
|
|
186
|
+
if (!odm) {
|
|
187
|
+
const parts = [
|
|
188
|
+
'# Document a traiter',
|
|
189
|
+
'',
|
|
190
|
+
documentBody,
|
|
191
|
+
'',
|
|
192
|
+
'## Instructions',
|
|
193
|
+
'Traite ce document et depose tes livrables dans .nemesis/HCM/.',
|
|
194
|
+
];
|
|
195
|
+
if (additionalContext) parts.push(additionalContext);
|
|
196
|
+
return parts.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Support both flat format (arka.odm.v1) and nested format (odm_meta/odm_payload)
|
|
200
|
+
const isFlat = !odm.odm_meta && (odm.odm_id || odm.title);
|
|
201
|
+
|
|
202
|
+
const odmId = isFlat ? odm.odm_id : (odm.odm_meta?.odm_id);
|
|
203
|
+
const title = isFlat ? odm.title : (odm.odm_payload?.cadrage?.title);
|
|
204
|
+
const summary = isFlat ? odm.summary : (odm.odm_payload?.cadrage?.summary);
|
|
205
|
+
const steps = isFlat ? (odm.steps || []) : (odm.odm_payload?.steps || []);
|
|
206
|
+
const deliverables = isFlat ? (odm.deliverables || []) : null;
|
|
207
|
+
const acceptance = isFlat ? (odm.acceptance_criteria || []) : null;
|
|
208
|
+
const rejection = isFlat
|
|
209
|
+
? (odm.rejection_criteria || [])
|
|
210
|
+
: (odm.odm_payload?.rejection_criteria || '');
|
|
211
|
+
const objectives = isFlat ? null : (odm.odm_payload?.cadrage?.objectives);
|
|
212
|
+
const inScope = isFlat ? null : (odm.odm_payload?.cadrage?.in_scope);
|
|
213
|
+
const outScope = isFlat ? null : (odm.odm_payload?.cadrage?.out_of_scope);
|
|
214
|
+
const outputs = isFlat ? null : (odm.odm_payload?.outputs_expected);
|
|
215
|
+
|
|
216
|
+
const parts = [
|
|
217
|
+
`# Assignation OdM : ${odmId || 'N/A'}`,
|
|
218
|
+
'',
|
|
219
|
+
'## Cadrage',
|
|
220
|
+
`- Titre : ${title || 'N/A'}`,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
if (summary) parts.push(`- Resume : ${summary}`);
|
|
224
|
+
if (objectives) parts.push(`- Objectifs : ${Array.isArray(objectives) ? objectives.join(', ') : objectives}`);
|
|
225
|
+
if (inScope) parts.push(`- Scope : ${Array.isArray(inScope) ? inScope.join(', ') : inScope}`);
|
|
226
|
+
if (outScope) parts.push(`- Hors scope : ${Array.isArray(outScope) ? outScope.join(', ') : outScope}`);
|
|
227
|
+
|
|
228
|
+
if (steps.length > 0) {
|
|
229
|
+
parts.push('', '## Steps');
|
|
230
|
+
steps.forEach((step, i) => {
|
|
231
|
+
parts.push(`${i + 1}. ${step.title || ''} — ${step.description || ''}`);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (deliverables && deliverables.length > 0) {
|
|
236
|
+
parts.push('', '## Livrables attendus');
|
|
237
|
+
deliverables.forEach(d => parts.push(`- ${d}`));
|
|
238
|
+
} else if (outputs?.items) {
|
|
239
|
+
parts.push('', '## Livrables attendus', Array.isArray(outputs.items) ? outputs.items.map(i => `- ${i}`).join('\n') : outputs.items);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (acceptance && acceptance.length > 0) {
|
|
243
|
+
parts.push('', '## Criteres d\'acceptation');
|
|
244
|
+
acceptance.forEach(a => parts.push(`- ${a}`));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (rejection) {
|
|
248
|
+
parts.push('', '## Criteres de rejet');
|
|
249
|
+
if (Array.isArray(rejection)) {
|
|
250
|
+
rejection.forEach(r => parts.push(`- ${r}`));
|
|
251
|
+
} else {
|
|
252
|
+
parts.push(rejection);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
parts.push('', '## Instructions');
|
|
257
|
+
parts.push('Execute cet OdM. Depose ton CR dans .nemesis/HCM/cr/ a la fin.');
|
|
258
|
+
if (additionalContext) parts.push(additionalContext);
|
|
259
|
+
|
|
260
|
+
return parts.join('\n');
|
|
261
|
+
}
|