@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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent launcher — spawn Claude CLI with streaming JSON output.
|
|
3
|
+
* Two exports:
|
|
4
|
+
* launchAgent(promptPath, opts) — low-level spawn + stream parse
|
|
5
|
+
* runOnboarding(promptPath, agentName) — high-level with UI (spinner + streambox)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { createSpinner } from '../ui/spinner.js';
|
|
11
|
+
import { createStreamBox } from '../ui/streambox.js';
|
|
12
|
+
import { titledBox } from '../ui/box.js';
|
|
13
|
+
import { style } from '../ui/colors.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Launch Claude with a prompt file, parse stream-json output.
|
|
17
|
+
* Pipes prompt via stdin to avoid OS arg-length limits.
|
|
18
|
+
* @param {string} promptPath — absolute path to the prompt .md file
|
|
19
|
+
* @param {object} opts
|
|
20
|
+
* @param {function} [opts.onLine] — called with each assistant text chunk
|
|
21
|
+
* @param {number} [opts.timeout] — ms before SIGTERM (default 120_000)
|
|
22
|
+
* @returns {Promise<{ sessionId: string|null, finalMessage: string, exitCode: number, stderrOutput: string, pid: number|null }>}
|
|
23
|
+
*/
|
|
24
|
+
export function launchAgent(promptPath, opts = {}) {
|
|
25
|
+
const { onLine = () => {}, timeout = 120_000 } = opts;
|
|
26
|
+
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let promptContent;
|
|
29
|
+
try {
|
|
30
|
+
promptContent = readFileSync(promptPath, 'utf-8');
|
|
31
|
+
} catch (_e) {
|
|
32
|
+
return reject(new Error(`Impossible de lire le prompt : ${promptPath}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let child;
|
|
36
|
+
try {
|
|
37
|
+
child = spawn('claude', [
|
|
38
|
+
'-p', promptContent,
|
|
39
|
+
'--output-format', 'stream-json',
|
|
40
|
+
'--verbose',
|
|
41
|
+
'--dangerously-skip-permissions',
|
|
42
|
+
], {
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
env: (() => { const e = { ...process.env }; delete e.CLAUDECODE; return e; })(),
|
|
45
|
+
});
|
|
46
|
+
} catch (_e) {
|
|
47
|
+
return reject(new Error('Impossible de lancer claude. Verifiez que Claude CLI est installe.'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let sessionId = null;
|
|
51
|
+
let finalMessage = '';
|
|
52
|
+
let lineBuffer = '';
|
|
53
|
+
let stderrOutput = '';
|
|
54
|
+
let settled = false;
|
|
55
|
+
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
child.kill('SIGTERM');
|
|
58
|
+
if (!settled) {
|
|
59
|
+
settled = true;
|
|
60
|
+
reject(new Error(`Timeout : l'agent n'a pas repondu en ${timeout / 1000}s`));
|
|
61
|
+
}
|
|
62
|
+
}, timeout);
|
|
63
|
+
|
|
64
|
+
child.on('error', (err) => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
if (settled) return;
|
|
67
|
+
settled = true;
|
|
68
|
+
if (err.code === 'ENOENT') {
|
|
69
|
+
reject(new Error('Claude CLI introuvable. Installez-le avec : npm i -g @anthropic-ai/claude-code'));
|
|
70
|
+
} else {
|
|
71
|
+
reject(err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.stderr.on('data', (chunk) => {
|
|
76
|
+
stderrOutput += chunk.toString();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
child.stdout.on('data', (chunk) => {
|
|
80
|
+
lineBuffer += chunk.toString();
|
|
81
|
+
const lines = lineBuffer.split('\n');
|
|
82
|
+
lineBuffer = lines.pop(); // keep incomplete last line
|
|
83
|
+
|
|
84
|
+
for (const raw of lines) {
|
|
85
|
+
if (!raw.trim()) continue;
|
|
86
|
+
try {
|
|
87
|
+
const evt = JSON.parse(raw);
|
|
88
|
+
if (evt.type === 'assistant' && evt.message?.content) {
|
|
89
|
+
for (const block of evt.message.content) {
|
|
90
|
+
if (block.type === 'text' && block.text) {
|
|
91
|
+
const textLines = block.text.split('\n').filter(Boolean);
|
|
92
|
+
for (const tl of textLines) onLine(tl);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (evt.type === 'result') {
|
|
97
|
+
sessionId = evt.session_id || null;
|
|
98
|
+
finalMessage = evt.result || '';
|
|
99
|
+
}
|
|
100
|
+
} catch (_e) {
|
|
101
|
+
/* fallback: non-JSON stream line */
|
|
102
|
+
onLine(raw.trim());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.on('close', (code) => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
resolve({ sessionId, finalMessage, exitCode: code ?? 1, stderrOutput, pid: child.pid ?? null });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* High-level onboarding: spinner + streambox(4) + build display box.
|
|
118
|
+
* Returns result data + displayBox string for the caller to print.
|
|
119
|
+
* @param {string} promptPath
|
|
120
|
+
* @param {string} agentName
|
|
121
|
+
* @returns {Promise<{ sessionId: string|null, finalMessage: string, exitCode: number, stderrOutput: string, pid: number|null, displayBox: string }>}
|
|
122
|
+
*/
|
|
123
|
+
export async function runOnboarding(promptPath, agentName) {
|
|
124
|
+
const spinner = createSpinner(`Onboarding ${style.bold(agentName)}...`);
|
|
125
|
+
const box = createStreamBox(4);
|
|
126
|
+
|
|
127
|
+
spinner.start();
|
|
128
|
+
process.stdout.write('\n');
|
|
129
|
+
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
result = await launchAgent(promptPath, {
|
|
133
|
+
onLine(line) {
|
|
134
|
+
box.push(style.dim(line));
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
box.clear();
|
|
139
|
+
spinner.fail(err.message);
|
|
140
|
+
|
|
141
|
+
const fallbackLines = [
|
|
142
|
+
`${style.red('Onboarding automatique echoue')} : ${err.message}`,
|
|
143
|
+
'',
|
|
144
|
+
'Lancez manuellement :',
|
|
145
|
+
` ${style.bold(`cat "${promptPath}" | claude -p -`)}`,
|
|
146
|
+
];
|
|
147
|
+
const displayBox = titledBox('Fallback', fallbackLines, { border: style.yellow });
|
|
148
|
+
|
|
149
|
+
return { sessionId: null, finalMessage: err.message, exitCode: 1, stderrOutput: '', pid: null, displayBox };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
box.clear();
|
|
153
|
+
|
|
154
|
+
const success = result.exitCode === 0;
|
|
155
|
+
if (success) {
|
|
156
|
+
spinner.stop(`Onboarding ${style.bold(agentName)} termine`);
|
|
157
|
+
} else {
|
|
158
|
+
spinner.fail(`Onboarding ${style.bold(agentName)} — exit code ${result.exitCode}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resultLines = [];
|
|
162
|
+
|
|
163
|
+
if (result.finalMessage) {
|
|
164
|
+
resultLines.push(...result.finalMessage.split('\n'));
|
|
165
|
+
} else if (result.stderrOutput) {
|
|
166
|
+
resultLines.push(style.red('stderr:'));
|
|
167
|
+
resultLines.push(...result.stderrOutput.split('\n').slice(0, 5));
|
|
168
|
+
} else if (!success) {
|
|
169
|
+
resultLines.push(`Le processus s'est termine avec le code ${result.exitCode}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (result.sessionId) {
|
|
173
|
+
resultLines.push('');
|
|
174
|
+
resultLines.push(`${style.bold('Session')} : ${style.nemesisAccent(result.sessionId)}`);
|
|
175
|
+
resultLines.push('');
|
|
176
|
+
resultLines.push(`Reprendre la session : ${style.bold(`nemesis run agent:${agentName}`)}`);
|
|
177
|
+
resultLines.push(`Envoyer un prompt : ${style.bold(`nemesis run agent:${agentName} --p "votre instruction"`)}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (resultLines.length === 0) {
|
|
181
|
+
resultLines.push(`Onboarding termine (exit ${result.exitCode})`);
|
|
182
|
+
resultLines.push(`Pas de session capturee.`);
|
|
183
|
+
resultLines.push('');
|
|
184
|
+
resultLines.push('Lancez manuellement :');
|
|
185
|
+
resultLines.push(` ${style.bold(`cat "${promptPath}" | claude -p -`)}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const boxTitle = result.sessionId ? 'Reponse agent' : 'Resultat onboarding';
|
|
189
|
+
const boxBorder = success ? style.nemesisAccent : style.yellow;
|
|
190
|
+
const displayBox = titledBox(boxTitle, resultLines, { border: boxBorder });
|
|
191
|
+
|
|
192
|
+
return { ...result, displayBox };
|
|
193
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit module — load templates, build prompts, save reports.
|
|
3
|
+
* Templates are JSON files in .nemesis/HCM/audit/ (or legacy .owner/HCM/audit/).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the audit directory for a project.
|
|
11
|
+
* Priority: hcm_dir/audit/, fallback: .owner/HCM/audit/ (legacy).
|
|
12
|
+
*/
|
|
13
|
+
function resolveAuditDir(project) {
|
|
14
|
+
const primary = join(project.hcm_dir, 'audit');
|
|
15
|
+
if (existsSync(primary) && readdirSync(primary).some(f => f.endsWith('.json'))) {
|
|
16
|
+
return primary;
|
|
17
|
+
}
|
|
18
|
+
// Legacy fallback
|
|
19
|
+
const legacy = join(project.root, '.owner', 'HCM', 'audit');
|
|
20
|
+
if (existsSync(legacy)) return legacy;
|
|
21
|
+
return primary; // default even if empty
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scan audit directory for template JSON files.
|
|
26
|
+
* Returns array of { id, name, description, path }.
|
|
27
|
+
*/
|
|
28
|
+
export function loadAuditTemplates(project) {
|
|
29
|
+
const auditDir = resolveAuditDir(project);
|
|
30
|
+
if (!existsSync(auditDir)) return [];
|
|
31
|
+
|
|
32
|
+
const files = readdirSync(auditDir).filter(f => f.endsWith('.json'));
|
|
33
|
+
const templates = [];
|
|
34
|
+
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(readFileSync(join(auditDir, file), 'utf-8'));
|
|
38
|
+
if (data.audit_meta?.audit_type_id) {
|
|
39
|
+
templates.push({
|
|
40
|
+
id: data.audit_meta.audit_type_id,
|
|
41
|
+
name: data.audit_meta.name || data.audit_meta.audit_type_id,
|
|
42
|
+
description: data.audit_meta.description || '',
|
|
43
|
+
path: join(auditDir, file),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch (_e) { /* fallback: skip malformed audit JSON */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return templates.sort((a, b) => a.id.localeCompare(b.id));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load a single audit template by ID.
|
|
54
|
+
* Returns the full parsed JSON or null if not found.
|
|
55
|
+
*/
|
|
56
|
+
export function loadTemplate(project, templateId) {
|
|
57
|
+
const templates = loadAuditTemplates(project);
|
|
58
|
+
const entry = templates.find(t => t.id === templateId);
|
|
59
|
+
if (!entry) return null;
|
|
60
|
+
return JSON.parse(readFileSync(entry.path, 'utf-8'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build a complete LLM prompt from an audit template and target definition.
|
|
65
|
+
* @param {object} template — parsed audit template JSON
|
|
66
|
+
* @param {object} cible — { type, path, nom, contexte, taille_equipe, stack }
|
|
67
|
+
* @returns {string} markdown prompt ready for LLM
|
|
68
|
+
*/
|
|
69
|
+
export function buildAuditPrompt(template, cible) {
|
|
70
|
+
const meta = template.audit_meta;
|
|
71
|
+
const methodo = template.methodologie;
|
|
72
|
+
const output = template.output;
|
|
73
|
+
|
|
74
|
+
const lines = [];
|
|
75
|
+
|
|
76
|
+
// Header
|
|
77
|
+
lines.push(`# ${meta.name}`);
|
|
78
|
+
lines.push('');
|
|
79
|
+
|
|
80
|
+
// Persona & posture
|
|
81
|
+
lines.push(`## Persona`);
|
|
82
|
+
lines.push(meta.persona);
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(`## Posture`);
|
|
85
|
+
lines.push(meta.posture);
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
// Target
|
|
89
|
+
lines.push('## Cible');
|
|
90
|
+
lines.push('');
|
|
91
|
+
if (cible.nom) lines.push(`- **Nom** : ${cible.nom}`);
|
|
92
|
+
if (cible.type) lines.push(`- **Type** : ${cible.type}`);
|
|
93
|
+
if (cible.path) lines.push(`- **Path** : ${cible.path}`);
|
|
94
|
+
if (cible.contexte) lines.push(`- **Contexte** : ${cible.contexte}`);
|
|
95
|
+
if (cible.taille_equipe) lines.push(`- **Taille equipe** : ${cible.taille_equipe}`);
|
|
96
|
+
if (cible.stack) lines.push(`- **Stack** : ${cible.stack}`);
|
|
97
|
+
lines.push('');
|
|
98
|
+
|
|
99
|
+
// Methodology
|
|
100
|
+
if (methodo?.analyse_domains) {
|
|
101
|
+
lines.push('## Methodologie');
|
|
102
|
+
lines.push('');
|
|
103
|
+
for (const domain of methodo.analyse_domains) {
|
|
104
|
+
lines.push(`### ${domain.titre}`);
|
|
105
|
+
const items = domain.points || domain.criteres || domain.inspections || domain.principes || domain.risques || [];
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
lines.push(`- ${item}`);
|
|
108
|
+
}
|
|
109
|
+
if (domain.verifications) {
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push('**Verifications :**');
|
|
112
|
+
for (const v of domain.verifications) {
|
|
113
|
+
lines.push(`- ${v}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Imperative rules
|
|
121
|
+
if (methodo?.regles_imperatives) {
|
|
122
|
+
lines.push('## Regles imperatives');
|
|
123
|
+
lines.push('');
|
|
124
|
+
for (const r of methodo.regles_imperatives) {
|
|
125
|
+
lines.push(`- ${r}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Style
|
|
131
|
+
if (methodo?.style) {
|
|
132
|
+
lines.push('## Style attendu');
|
|
133
|
+
lines.push('');
|
|
134
|
+
for (const s of methodo.style) {
|
|
135
|
+
lines.push(`- ${s}`);
|
|
136
|
+
}
|
|
137
|
+
lines.push('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Output format
|
|
141
|
+
if (output?.sections) {
|
|
142
|
+
lines.push('## Format de sortie attendu');
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(`Format : ${output.format || 'markdown'}`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('Sections attendues :');
|
|
147
|
+
for (const section of output.sections) {
|
|
148
|
+
lines.push(`1. **${section.titre}**`);
|
|
149
|
+
if (section.contenu) {
|
|
150
|
+
for (const c of section.contenu) {
|
|
151
|
+
lines.push(` - ${c}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (section.scoring?.dimensions) {
|
|
155
|
+
lines.push(` Dimensions (echelle ${section.scoring.echelle}) :`);
|
|
156
|
+
for (const d of section.scoring.dimensions) {
|
|
157
|
+
lines.push(` - ${d}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (section.par_domaine?.domaines) {
|
|
161
|
+
lines.push(` Domaines : ${section.par_domaine.domaines.join(', ')}`);
|
|
162
|
+
lines.push(` Structure : ${section.par_domaine.structure_champ.join(' | ')}`);
|
|
163
|
+
}
|
|
164
|
+
if (section.categories) {
|
|
165
|
+
lines.push(` Categories : ${section.categories.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
if (section.horizons) {
|
|
168
|
+
for (const h of section.horizons) {
|
|
169
|
+
lines.push(` - ${h.label} (${h.delai})`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Save an audit report to .nemesis/HCM/audit/reports/.
|
|
181
|
+
* @param {object} project
|
|
182
|
+
* @param {string} auditId — e.g. "AUDIT-TECHNIQUE"
|
|
183
|
+
* @param {string} content — markdown content
|
|
184
|
+
* @returns {string} path to saved report
|
|
185
|
+
*/
|
|
186
|
+
export function saveReport(project, auditId, content) {
|
|
187
|
+
const reportsDir = join(project.hcm_dir, 'audit', 'reports');
|
|
188
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
189
|
+
|
|
190
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
191
|
+
const filename = `${auditId}_${timestamp}.md`;
|
|
192
|
+
const filepath = join(reportsDir, filename);
|
|
193
|
+
writeFileSync(filepath, content, 'utf-8');
|
|
194
|
+
return filepath;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* List existing audit reports.
|
|
199
|
+
* Returns array of { filename, path }.
|
|
200
|
+
*/
|
|
201
|
+
export function listReports(project) {
|
|
202
|
+
const reportsDir = join(project.hcm_dir, 'audit', 'reports');
|
|
203
|
+
if (!existsSync(reportsDir)) return [];
|
|
204
|
+
|
|
205
|
+
return readdirSync(reportsDir)
|
|
206
|
+
.filter(f => f.endsWith('.md'))
|
|
207
|
+
.sort()
|
|
208
|
+
.reverse()
|
|
209
|
+
.map(f => ({ filename: f, path: join(reportsDir, f) }));
|
|
210
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { storeSecret, getSecret, removeSecret } from './secrets.js';
|
|
6
|
+
|
|
7
|
+
const CONNEXIONS_FILE = join(homedir(), '.nemesis', 'connexions.json');
|
|
8
|
+
|
|
9
|
+
export const PROVIDERS = [
|
|
10
|
+
{ id: 'anthropic', label: 'Anthropic', authTypes: ['api_key'] },
|
|
11
|
+
{ id: 'openai', label: 'OpenAI', authTypes: ['api_key'] },
|
|
12
|
+
{ id: 'google_cloud', label: 'Google Cloud', authTypes: ['api_key', 'token'] },
|
|
13
|
+
{ id: 'gemini_genai', label: 'Gemini GenAI', authTypes: ['api_key'] },
|
|
14
|
+
{ id: 'aws_bedrock', label: 'AWS Bedrock', authTypes: ['api_key'] },
|
|
15
|
+
{ id: 'ollama', label: 'Ollama', authTypes: ['none'] },
|
|
16
|
+
{ id: 'groq', label: 'Groq', authTypes: ['api_key'] },
|
|
17
|
+
{ id: 'custom', label: 'Custom', authTypes: ['api_key', 'token'] },
|
|
18
|
+
{ id: 'cli_tools', label: 'CLI Tools', authTypes: ['none'] },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function readConnexions() {
|
|
22
|
+
if (!existsSync(CONNEXIONS_FILE)) return { connexions: [] };
|
|
23
|
+
try { return JSON.parse(readFileSync(CONNEXIONS_FILE, 'utf-8')); }
|
|
24
|
+
catch (_e) { /* fallback: corrupted connexions file */ return { connexions: [] }; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writeConnexions(data) {
|
|
28
|
+
const dir = join(homedir(), '.nemesis');
|
|
29
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(CONNEXIONS_FILE, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addConnexion({ name, provider, authType, model, credential, endpoint, cliArgs }) {
|
|
34
|
+
const data = readConnexions();
|
|
35
|
+
const id = randomUUID();
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const connexion = {
|
|
38
|
+
id, name, provider, auth_type: authType,
|
|
39
|
+
model: model || null, endpoint: endpoint || null,
|
|
40
|
+
cli_args: cliArgs || null,
|
|
41
|
+
status: 'untested', last_tested: null, created_at: now,
|
|
42
|
+
};
|
|
43
|
+
data.connexions.push(connexion);
|
|
44
|
+
writeConnexions(data);
|
|
45
|
+
if (credential) storeSecret(id, credential);
|
|
46
|
+
return connexion;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getConnexion(id) {
|
|
50
|
+
return readConnexions().connexions.find(c => c.id === id) || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function listConnexions() {
|
|
54
|
+
return readConnexions().connexions;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function updateConnexionStatus(id, status, latency) {
|
|
58
|
+
const data = readConnexions();
|
|
59
|
+
const cx = data.connexions.find(c => c.id === id);
|
|
60
|
+
if (!cx) return false;
|
|
61
|
+
cx.status = status;
|
|
62
|
+
cx.last_tested = new Date().toISOString();
|
|
63
|
+
if (latency !== undefined) cx.latency = latency;
|
|
64
|
+
writeConnexions(data);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removeConnexion(id) {
|
|
69
|
+
const data = readConnexions();
|
|
70
|
+
const idx = data.connexions.findIndex(c => c.id === id);
|
|
71
|
+
if (idx === -1) return false;
|
|
72
|
+
data.connexions.splice(idx, 1);
|
|
73
|
+
writeConnexions(data);
|
|
74
|
+
removeSecret(id);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getConnexionCredential(id) {
|
|
79
|
+
return getSecret(id);
|
|
80
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createActor } from 'xstate';
|
|
2
|
+
import { flowmapMachine } from './machine.js';
|
|
3
|
+
import { saveSnapshot, loadSnapshot, listSnapshots } from './persistence.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create (or restore) a FLOWMAP actor for a mission.
|
|
7
|
+
* Auto-persists snapshots on every transition via subscribe.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} missionId
|
|
10
|
+
* @param {object} [opts]
|
|
11
|
+
* @param {string} [opts.root] - project root (default: cwd)
|
|
12
|
+
* @param {string} [opts.odmId]
|
|
13
|
+
* @param {string} [opts.projectId]
|
|
14
|
+
* @param {object} [opts.metadata]
|
|
15
|
+
* @returns {{ actor, snapshot }}
|
|
16
|
+
*/
|
|
17
|
+
export function createFlowmapActor(missionId, opts = {}) {
|
|
18
|
+
const { root = process.cwd(), odmId, projectId, metadata } = opts;
|
|
19
|
+
|
|
20
|
+
const existing = loadSnapshot(root, missionId);
|
|
21
|
+
|
|
22
|
+
const actor = createActor(flowmapMachine, {
|
|
23
|
+
input: { missionId, odmId, projectId, metadata },
|
|
24
|
+
...(existing ? { snapshot: existing } : {}),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Auto-persist on every transition
|
|
28
|
+
actor.subscribe((_snapshot) => {
|
|
29
|
+
saveSnapshot(root, missionId, actor.getPersistedSnapshot());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
actor.start();
|
|
33
|
+
|
|
34
|
+
return { actor, snapshot: actor.getSnapshot() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One-shot: create actor, send event, stop, return state.
|
|
39
|
+
* Adapted for CLI usage — no long-running actors.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} missionId
|
|
42
|
+
* @param {{ type: string, [key: string]: any }} event
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @returns {{ value, context }}
|
|
45
|
+
*/
|
|
46
|
+
export function sendFlowmapEvent(missionId, event, opts = {}) {
|
|
47
|
+
const { actor } = createFlowmapActor(missionId, opts);
|
|
48
|
+
|
|
49
|
+
actor.send(event);
|
|
50
|
+
const snapshot = actor.getSnapshot();
|
|
51
|
+
actor.stop();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
value: snapshot.value,
|
|
55
|
+
context: snapshot.context,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read-only: get the current state of a mission.
|
|
61
|
+
* Returns null if no snapshot exists.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} missionId
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @returns {{ value, context } | null}
|
|
66
|
+
*/
|
|
67
|
+
export function getCurrentState(missionId, opts = {}) {
|
|
68
|
+
const { root = process.cwd() } = opts;
|
|
69
|
+
|
|
70
|
+
const existing = loadSnapshot(root, missionId);
|
|
71
|
+
if (!existing) return null;
|
|
72
|
+
|
|
73
|
+
const actor = createActor(flowmapMachine, {
|
|
74
|
+
input: { missionId },
|
|
75
|
+
snapshot: existing,
|
|
76
|
+
});
|
|
77
|
+
actor.start();
|
|
78
|
+
|
|
79
|
+
const snapshot = actor.getSnapshot();
|
|
80
|
+
actor.stop();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
value: snapshot.value,
|
|
84
|
+
context: snapshot.context,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the transition history of a mission.
|
|
90
|
+
* Returns empty array if no snapshot exists.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} missionId
|
|
93
|
+
* @param {object} [opts]
|
|
94
|
+
* @returns {Array}
|
|
95
|
+
*/
|
|
96
|
+
export function getStateHistory(missionId, opts = {}) {
|
|
97
|
+
const state = getCurrentState(missionId, opts);
|
|
98
|
+
if (!state) return [];
|
|
99
|
+
return state.context.history;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List all missions that have a persisted state snapshot.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} [opts]
|
|
106
|
+
* @returns {string[]}
|
|
107
|
+
*/
|
|
108
|
+
export function listActiveMissions(opts = {}) {
|
|
109
|
+
const { root = process.cwd() } = opts;
|
|
110
|
+
return listSnapshots(root);
|
|
111
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-helpers.js — Pure functions for FLOWMAP display in CLI commands.
|
|
3
|
+
* Extracted for testability and reuse across status, project, odm, mission.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getCurrentState, getStateHistory, listActiveMissions } from './api.js';
|
|
7
|
+
import { STEP_METADATA } from './machine.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get a summary of all active missions with their FLOWMAP state.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} root - project root
|
|
13
|
+
* @returns {Array<{ missionId: string, state: string, phase: string, who: string, label: string }>}
|
|
14
|
+
*/
|
|
15
|
+
export function getFlowmapSummary(root) {
|
|
16
|
+
const missionIds = listActiveMissions({ root });
|
|
17
|
+
const results = [];
|
|
18
|
+
|
|
19
|
+
for (const missionId of missionIds) {
|
|
20
|
+
const current = getCurrentState(missionId, { root });
|
|
21
|
+
if (!current) continue;
|
|
22
|
+
|
|
23
|
+
const meta = STEP_METADATA[current.value];
|
|
24
|
+
if (!meta) continue;
|
|
25
|
+
|
|
26
|
+
results.push({
|
|
27
|
+
missionId,
|
|
28
|
+
state: current.value,
|
|
29
|
+
phase: meta.phase,
|
|
30
|
+
who: meta.who,
|
|
31
|
+
label: meta.label,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the FLOWMAP phase for an OdM based on its mission_id.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} odmMeta - odm.odm_meta object
|
|
42
|
+
* @param {string} root - project root
|
|
43
|
+
* @returns {string|null} - phase string or null
|
|
44
|
+
*/
|
|
45
|
+
export function getOdmFlowmapPhase(odmMeta, root) {
|
|
46
|
+
if (!odmMeta || !odmMeta.mission_id) return null;
|
|
47
|
+
|
|
48
|
+
const current = getCurrentState(odmMeta.mission_id, { root });
|
|
49
|
+
if (!current) return null;
|
|
50
|
+
|
|
51
|
+
const meta = STEP_METADATA[current.value];
|
|
52
|
+
return meta ? meta.phase : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get full FLOWMAP info for a specific mission.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} missionId
|
|
59
|
+
* @param {string} root - project root
|
|
60
|
+
* @returns {{ state: string, phase: string, who: string, label: string, history: Array } | null}
|
|
61
|
+
*/
|
|
62
|
+
export function getMissionFlowmapInfo(missionId, root) {
|
|
63
|
+
if (!missionId) return null;
|
|
64
|
+
|
|
65
|
+
const current = getCurrentState(missionId, { root });
|
|
66
|
+
if (!current) return null;
|
|
67
|
+
|
|
68
|
+
const meta = STEP_METADATA[current.value];
|
|
69
|
+
if (!meta) return null;
|
|
70
|
+
|
|
71
|
+
const history = getStateHistory(missionId, { root });
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
state: current.value,
|
|
75
|
+
phase: meta.phase,
|
|
76
|
+
who: meta.who,
|
|
77
|
+
label: meta.label,
|
|
78
|
+
history,
|
|
79
|
+
};
|
|
80
|
+
}
|