@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,216 @@
|
|
|
1
|
+
import { installHooks, verifyHooks, removeHooks } from '../../lib/kairos/hook-installer.js';
|
|
2
|
+
import { handlePreHook, handleDynamicHook, handlePostHook, handlePreCompact } from '../../lib/kairos/hook-handlers.js';
|
|
3
|
+
import { titledBox } from '../../lib/ui/box.js';
|
|
4
|
+
import { style } from '../../lib/ui/colors.js';
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
nemesis kairos — Hooks & contexte agent
|
|
8
|
+
|
|
9
|
+
Usage: nemesis kairos <subcommand>
|
|
10
|
+
|
|
11
|
+
Subcommands:
|
|
12
|
+
hooks setup Installe les hooks Claude Code
|
|
13
|
+
hooks status Verifie les hooks
|
|
14
|
+
hooks remove Desinstalle les hooks
|
|
15
|
+
push Envoyer un evenement a un agent
|
|
16
|
+
events Lister les evenements en attente
|
|
17
|
+
clear Vider la queue d'un agent
|
|
18
|
+
-h, --help Aide
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export async function handler({ args, flags, config }) {
|
|
22
|
+
if (flags.help || args.includes('--help') || args.includes('-h')) {
|
|
23
|
+
console.log(HELP);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sub = args[0];
|
|
28
|
+
const sub2 = args[1];
|
|
29
|
+
|
|
30
|
+
// Public hooks subcommands
|
|
31
|
+
if (sub === 'hooks') {
|
|
32
|
+
switch (sub2) {
|
|
33
|
+
case 'setup': return handleHooksSetup(config);
|
|
34
|
+
case 'status': return handleHooksStatus(config);
|
|
35
|
+
case 'remove': return handleHooksRemove(config);
|
|
36
|
+
default:
|
|
37
|
+
console.log(' Usage: nemesis kairos hooks <setup|status|remove>');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Event bus subcommands
|
|
43
|
+
if (sub === 'push') return handlePush(args.slice(1), flags, config);
|
|
44
|
+
if (sub === 'events') return handleEvents(args.slice(1), flags, config);
|
|
45
|
+
if (sub === 'clear') return handleClear(args.slice(1), flags, config);
|
|
46
|
+
|
|
47
|
+
// Internal hook handlers (called by Claude Code hooks via stdin)
|
|
48
|
+
switch (sub) {
|
|
49
|
+
case 'pre-hook': return handlePreHook(config.cwd);
|
|
50
|
+
case 'dynamic-hook': return handleDynamicHook(config.cwd);
|
|
51
|
+
case 'post-hook': return handlePostHook(config.cwd);
|
|
52
|
+
case 'pre-compact': return handlePreCompact(config.cwd);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default
|
|
56
|
+
console.log(' Usage:');
|
|
57
|
+
console.log(' nemesis kairos hooks setup — Installe les hooks Claude Code');
|
|
58
|
+
console.log(' nemesis kairos hooks status — Verifie les hooks');
|
|
59
|
+
console.log(' nemesis kairos hooks remove — Desinstalle les hooks');
|
|
60
|
+
console.log(' nemesis kairos push — Envoyer un evenement');
|
|
61
|
+
console.log(' nemesis kairos events — Lister les evenements');
|
|
62
|
+
console.log(' nemesis kairos clear — Vider une queue');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleHooksSetup(config) {
|
|
66
|
+
const result = installHooks(config.cwd);
|
|
67
|
+
|
|
68
|
+
console.log(titledBox('Kairos Hooks — Installation', [
|
|
69
|
+
`Fichier : ${result.path}`,
|
|
70
|
+
result.merged
|
|
71
|
+
? 'Hooks merges avec la configuration existante.'
|
|
72
|
+
: 'Configuration creee.',
|
|
73
|
+
'',
|
|
74
|
+
'Hooks installes :',
|
|
75
|
+
' SessionStart → nemesis kairos pre-hook',
|
|
76
|
+
' UserPromptSubmit → nemesis kairos dynamic-hook',
|
|
77
|
+
' Stop → nemesis kairos post-hook',
|
|
78
|
+
' PreCompact → nemesis kairos pre-compact',
|
|
79
|
+
], { border: style.arkaRed }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleHooksStatus(config) {
|
|
83
|
+
const result = verifyHooks(config.cwd);
|
|
84
|
+
|
|
85
|
+
const icon = (ok) => ok ? style.green('✓') : style.red('✗');
|
|
86
|
+
const lines = [
|
|
87
|
+
`settings.json : ${result.settingsExists ? 'present' : style.red('ABSENT')}`,
|
|
88
|
+
'',
|
|
89
|
+
'Hooks :',
|
|
90
|
+
` ${icon(result.hooks.SessionStart)} SessionStart → pre-hook`,
|
|
91
|
+
` ${icon(result.hooks.UserPromptSubmit)} UserPromptSubmit → dynamic-hook`,
|
|
92
|
+
` ${icon(result.hooks.Stop)} Stop → post-hook`,
|
|
93
|
+
` ${icon(result.hooks.PreCompact)} PreCompact → pre-compact`,
|
|
94
|
+
'',
|
|
95
|
+
result.ok
|
|
96
|
+
? style.green('Tous les hooks sont installes.')
|
|
97
|
+
: style.yellow('ATTENTION : hooks manquants. Lancez nemesis kairos hooks setup.'),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
console.log(titledBox('Kairos Hooks — Status', lines, { border: style.arkaRed }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function handleHooksRemove(config) {
|
|
104
|
+
const result = removeHooks(config.cwd);
|
|
105
|
+
|
|
106
|
+
if (result.removed) {
|
|
107
|
+
console.log(' Hooks Kairos desinstalles.');
|
|
108
|
+
} else {
|
|
109
|
+
console.log(` Desinstallation impossible : ${result.reason}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Event Bus subcommands
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
function getFlag(args, flag) {
|
|
118
|
+
const idx = args.indexOf(flag);
|
|
119
|
+
if (idx === -1 || idx + 1 >= args.length) return null;
|
|
120
|
+
return args[idx + 1];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handlePush(args, flags, config) {
|
|
124
|
+
const to = getFlag(args, '--to');
|
|
125
|
+
const type = getFlag(args, '--type');
|
|
126
|
+
const priority = getFlag(args, '--priority') || 'MEDIUM';
|
|
127
|
+
const message = getFlag(args, '--message') || '';
|
|
128
|
+
|
|
129
|
+
if (!to || !type) {
|
|
130
|
+
console.log(' Usage: nemesis kairos push --to <agent> --type <event_type> [--priority HIGH|MEDIUM|LOW] [--message <text>]');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { EVENT_TYPES } = await import('../../lib/kairos/event-bus.js');
|
|
135
|
+
const validTypes = Object.values(EVENT_TYPES);
|
|
136
|
+
if (!validTypes.includes(type)) {
|
|
137
|
+
console.log(` Type invalide: ${type}`);
|
|
138
|
+
console.log(` Types valides: ${validTypes.join(', ')}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const validPriorities = ['HIGH', 'MEDIUM', 'LOW'];
|
|
143
|
+
if (!validPriorities.includes(priority.toUpperCase())) {
|
|
144
|
+
console.log(` Priorite invalide: ${priority}`);
|
|
145
|
+
console.log(` Priorites valides: ${validPriorities.join(', ')}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { routeEvent } = await import('../../lib/kairos/event-router.js');
|
|
150
|
+
const result = await routeEvent(config.cwd, {
|
|
151
|
+
target_agent_id: to,
|
|
152
|
+
event_type: type,
|
|
153
|
+
source_agent_id: 'cli',
|
|
154
|
+
priority: priority.toUpperCase(),
|
|
155
|
+
payload: message ? { message } : {},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (result.delivered) {
|
|
159
|
+
console.log(` ${style.green('✓')} Evenement ${type} envoye a ${to} (${result.method})`);
|
|
160
|
+
console.log(` ${style.dim(result.detail)}`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(` ${style.red('✗')} Echec: ${result.detail}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleEvents(args, flags, config) {
|
|
167
|
+
const agentFilter = getFlag(args, '--agent');
|
|
168
|
+
|
|
169
|
+
const { pollEvents, listQueues } = await import('../../lib/kairos/event-bus.js');
|
|
170
|
+
|
|
171
|
+
if (agentFilter) {
|
|
172
|
+
const events = pollEvents(config.cwd, agentFilter);
|
|
173
|
+
if (events.length === 0) {
|
|
174
|
+
console.log(`\n ${style.dim('Aucun evenement pour')} ${agentFilter}\n`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const lines = events.map(({ event }) =>
|
|
178
|
+
` [${event.priority}] ${event.event_type} de ${event.source_agent_id} (${event.created_at})`
|
|
179
|
+
);
|
|
180
|
+
console.log(titledBox(`Events — ${agentFilter}`, lines, { border: style.arkaRed }));
|
|
181
|
+
} else {
|
|
182
|
+
const queues = listQueues(config.cwd);
|
|
183
|
+
if (queues.length === 0) {
|
|
184
|
+
console.log(`\n ${style.dim('Aucune queue active.')}\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const lines = queues.map(q =>
|
|
188
|
+
` ${q.agentId} ${style.bold(String(q.count))} evt ${q.oldest ? style.dim(`depuis ${q.oldest}`) : ''}`
|
|
189
|
+
);
|
|
190
|
+
console.log(titledBox('Event Bus — Queues', lines, { border: style.arkaRed }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function handleClear(args, flags, config) {
|
|
195
|
+
const agent = getFlag(args, '--agent');
|
|
196
|
+
const all = args.includes('--all');
|
|
197
|
+
|
|
198
|
+
if (!agent && !all) {
|
|
199
|
+
console.log(' Usage: nemesis kairos clear --agent <name> [--all]');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const { clearQueue, listQueues } = await import('../../lib/kairos/event-bus.js');
|
|
204
|
+
|
|
205
|
+
if (all) {
|
|
206
|
+
const queues = listQueues(config.cwd);
|
|
207
|
+
let total = 0;
|
|
208
|
+
for (const q of queues) {
|
|
209
|
+
total += clearQueue(config.cwd, q.agentId);
|
|
210
|
+
}
|
|
211
|
+
console.log(` ${style.green('✓')} ${total} evenement(s) purge(s) sur ${queues.length} queue(s).`);
|
|
212
|
+
} else {
|
|
213
|
+
const count = clearQueue(config.cwd, agent);
|
|
214
|
+
console.log(` ${style.green('✓')} ${count} evenement(s) purge(s) pour ${agent}.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHcmClient } from '../../lib/sync/hcm-client.js';
|
|
2
|
+
import { interactiveMenu } from '../../lib/ui/menu.js';
|
|
3
|
+
import { createSpinner } from '../../lib/ui/spinner.js';
|
|
4
|
+
import { renderTable } from '../../lib/ui/table.js';
|
|
5
|
+
import { formatOutput } from '../../lib/ui/format.js';
|
|
6
|
+
import { style } from '../../lib/ui/colors.js';
|
|
7
|
+
import { getFlag, BACK_ITEM, HOME_ITEM } from './_helpers.js';
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
nemesis kars — Recherche profils Kairos
|
|
11
|
+
|
|
12
|
+
Usage: nemesis kars search [--sector <secteur>]
|
|
13
|
+
|
|
14
|
+
Subcommands:
|
|
15
|
+
search Recherche de profils par secteur
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--sector <s> Filtrer par secteur
|
|
19
|
+
-h, --help Aide
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export async function handler({ args, flags, _config }) {
|
|
23
|
+
if (flags.help || args.includes('--help') || args.includes('-h')) {
|
|
24
|
+
console.log(HELP);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let sub = args[0];
|
|
29
|
+
let subArgs = args.slice(1);
|
|
30
|
+
|
|
31
|
+
if (!sub) {
|
|
32
|
+
sub = await interactiveMenu([
|
|
33
|
+
{ label: 'search', value: 'search', description: 'Recherche de profils par secteur' },
|
|
34
|
+
BACK_ITEM,
|
|
35
|
+
HOME_ITEM,
|
|
36
|
+
], { title: 'nemesis kars' });
|
|
37
|
+
if (!sub || sub === '__back__') return '__back__';
|
|
38
|
+
if (sub === '__home__') return '__home__';
|
|
39
|
+
subArgs = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (sub) {
|
|
43
|
+
case 'search':
|
|
44
|
+
return handleSearch(subArgs, flags);
|
|
45
|
+
default:
|
|
46
|
+
console.error(`Sous-commande inconnue : ${sub}`);
|
|
47
|
+
console.log(HELP);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function handleSearch(args, flags) {
|
|
52
|
+
const client = createHcmClient();
|
|
53
|
+
let sector = getFlag(args, '--sector');
|
|
54
|
+
|
|
55
|
+
// Interactive: fetch all profiles to extract sectors, then let user pick
|
|
56
|
+
if (!sector) {
|
|
57
|
+
const spinner = createSpinner('Chargement des secteurs...');
|
|
58
|
+
spinner.start();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// q is required by the API — use 'a' to match all profiles
|
|
62
|
+
const all = await client.searchProfiles('a');
|
|
63
|
+
const profiles = Array.isArray(all) ? all : [];
|
|
64
|
+
const sectorSet = new Set();
|
|
65
|
+
for (const p of profiles) {
|
|
66
|
+
const sectors = Array.isArray(p.sectors) ? p.sectors : [];
|
|
67
|
+
for (const s of sectors) {
|
|
68
|
+
if (s) sectorSet.add(s);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const sectors = [...sectorSet].sort((a, b) => a.localeCompare(b, 'fr'));
|
|
72
|
+
spinner.stop(`${sectors.length} secteurs disponibles`);
|
|
73
|
+
|
|
74
|
+
if (sectors.length === 0) {
|
|
75
|
+
console.log(`\n ${style.yellow('⚠')} Aucun secteur disponible.\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const items = sectors.map(s => ({ label: s, value: s }));
|
|
80
|
+
sector = await interactiveMenu(items, {
|
|
81
|
+
title: 'Secteur',
|
|
82
|
+
maxVisible: 15,
|
|
83
|
+
});
|
|
84
|
+
if (!sector) return;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
spinner.fail('Erreur HCM');
|
|
87
|
+
console.error(` ${err.message}\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fetch profiles for selected sector
|
|
93
|
+
const spinner = createSpinner(`Recherche secteur : ${sector}...`);
|
|
94
|
+
spinner.start();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const results = await client.searchProfiles(null, sector);
|
|
98
|
+
const profiles = Array.isArray(results) ? results : [];
|
|
99
|
+
spinner.stop(`${profiles.length} profils trouves`);
|
|
100
|
+
|
|
101
|
+
if (flags.format === 'json') {
|
|
102
|
+
console.log(formatOutput(profiles, 'json'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (profiles.length === 0) {
|
|
107
|
+
console.log(`\n Aucun profil trouve pour le secteur "${sector}".\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const columns = [
|
|
112
|
+
{ key: 'id', label: 'ID' },
|
|
113
|
+
{ key: 'role', label: 'Role' },
|
|
114
|
+
{ key: 'keywords', label: 'Keywords' },
|
|
115
|
+
{ key: 'sectors', label: 'Sectors' },
|
|
116
|
+
{ key: 'score', label: 'Score' },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const rows = profiles.map(p => ({
|
|
120
|
+
id: p.id || '',
|
|
121
|
+
role: p.role || '',
|
|
122
|
+
keywords: Array.isArray(p.keywords) ? p.keywords.slice(0, 3).join(', ') : '',
|
|
123
|
+
sectors: Array.isArray(p.sectors) ? p.sectors.join(', ') : '',
|
|
124
|
+
score: p.score != null ? String(p.score) : '',
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(renderTable(columns, rows));
|
|
129
|
+
console.log('');
|
|
130
|
+
} catch (err) {
|
|
131
|
+
spinner.fail('Erreur HCM');
|
|
132
|
+
console.error(` ${err.message}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { initMission, listMissions, inspectMission, validateMission } from '../../lib/core/mission.js';
|
|
2
|
+
import { readRegistry, getLanes } from '../../lib/core/registry.js';
|
|
3
|
+
import { askText, askConfirm } from '../../lib/ui/prompt.js';
|
|
4
|
+
import { interactiveMenu } from '../../lib/ui/menu.js';
|
|
5
|
+
import { renderTable } from '../../lib/ui/table.js';
|
|
6
|
+
import { formatOutput } from '../../lib/ui/format.js';
|
|
7
|
+
import { style } from '../../lib/ui/colors.js';
|
|
8
|
+
import { ensureProject, pickFromList, nextId, BACK_ITEM, HOME_ITEM } from './_helpers.js';
|
|
9
|
+
import { getMissionFlowmapInfo } from '../../lib/core/flowmap/cli-helpers.js';
|
|
10
|
+
import { getCurrentState } from '../../lib/core/flowmap/api.js';
|
|
11
|
+
|
|
12
|
+
const HELP = `
|
|
13
|
+
nemesis mission — Mission Contracts
|
|
14
|
+
|
|
15
|
+
Usage: nemesis mission <subcommand> [options]
|
|
16
|
+
|
|
17
|
+
Subcommands:
|
|
18
|
+
init [id] Cree un Mission Contract
|
|
19
|
+
list Liste les Mission Contracts
|
|
20
|
+
inspect [id] Affiche le contenu complet d'un MCT
|
|
21
|
+
validate [id] Valide un MC (DRAFT → VALIDATED)
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
-h, --help Aide
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export async function handler({ args, flags, config }) {
|
|
28
|
+
if (flags.help || args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
console.log(HELP);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let sub = args[0];
|
|
34
|
+
let subArgs = args.slice(1);
|
|
35
|
+
|
|
36
|
+
if (!sub) {
|
|
37
|
+
sub = await interactiveMenu([
|
|
38
|
+
{ label: 'list', value: 'list', description: 'Liste les Mission Contracts' },
|
|
39
|
+
{ label: 'init', value: 'init', description: 'Cree un Mission Contract' },
|
|
40
|
+
{ label: 'inspect', value: 'inspect', description: 'Affiche le contenu d\'un MCT' },
|
|
41
|
+
{ label: 'validate', value: 'validate', description: 'Valide un MC (DRAFT → VALIDATED)' },
|
|
42
|
+
BACK_ITEM,
|
|
43
|
+
HOME_ITEM,
|
|
44
|
+
], { title: 'nemesis mission' });
|
|
45
|
+
if (!sub || sub === '__back__') return '__back__';
|
|
46
|
+
if (sub === '__home__') return '__home__';
|
|
47
|
+
subArgs = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
switch (sub) {
|
|
51
|
+
case 'init':
|
|
52
|
+
return handleInit(subArgs, flags, config);
|
|
53
|
+
case 'list':
|
|
54
|
+
return handleList(flags, config);
|
|
55
|
+
case 'inspect':
|
|
56
|
+
return handleInspect(subArgs, flags, config);
|
|
57
|
+
case 'validate':
|
|
58
|
+
return handleValidate(subArgs, flags, config);
|
|
59
|
+
default:
|
|
60
|
+
console.error(`Sous-commande inconnue : ${sub}`);
|
|
61
|
+
console.log(HELP);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleInit(args, flags, config) {
|
|
66
|
+
const project = await ensureProject(config, flags);
|
|
67
|
+
if (!project) return;
|
|
68
|
+
|
|
69
|
+
// Auto-generate ID if not provided
|
|
70
|
+
let id = args[0];
|
|
71
|
+
if (!id) {
|
|
72
|
+
const existing = listMissions(project.hcm_dir);
|
|
73
|
+
const prefix = `MCT-${project.id}`;
|
|
74
|
+
const suggested = nextId(prefix, existing.map(m => m.id));
|
|
75
|
+
id = await askText('ID du Mission Contract', suggested);
|
|
76
|
+
}
|
|
77
|
+
if (!id) return;
|
|
78
|
+
|
|
79
|
+
const title = await askText('Titre');
|
|
80
|
+
const summary = await askText('Resume (optionnel)', '');
|
|
81
|
+
|
|
82
|
+
const result = initMission(id, {
|
|
83
|
+
title, summary, root: config.cwd, projectId: project.id,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(`\n ${style.green('\u2713')} Mission Contract cree : ${style.bold(id)}`);
|
|
87
|
+
console.log(` ${style.dim('\u2192')} ${result.filepath.replace(config.cwd + '/', '')}`);
|
|
88
|
+
|
|
89
|
+
// Next steps
|
|
90
|
+
const registry = readRegistry(project.hcm_dir);
|
|
91
|
+
const lanes = registry ? getLanes(registry) : [];
|
|
92
|
+
|
|
93
|
+
console.log(`\n ${style.bold('Prochaines etapes')} :`);
|
|
94
|
+
console.log(' 1. Completez le workflow (gates, task_registry)');
|
|
95
|
+
console.log(` 2. Redigez les OdMs associes : ${style.dim('nemesis odm init')}`);
|
|
96
|
+
if (lanes.length === 0) {
|
|
97
|
+
console.log(` 3. ${style.yellow('Equipe vide')} — ajoutez un agent : ${style.dim('nemesis team add')}`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(` 3. Equipe : ${lanes.length} agent(s) disponible(s)`);
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
// Loop — propose to create another
|
|
104
|
+
const another = await askConfirm('Creer un autre Mission Contract ?', false);
|
|
105
|
+
if (another) return handleInit([], flags, config);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleList(flags, config) {
|
|
109
|
+
const project = await ensureProject(config, flags);
|
|
110
|
+
if (!project) return;
|
|
111
|
+
|
|
112
|
+
const missions = listMissions(project.hcm_dir);
|
|
113
|
+
|
|
114
|
+
if (flags.format === 'json') {
|
|
115
|
+
console.log(formatOutput(missions, 'json'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (missions.length === 0) {
|
|
120
|
+
console.log(`\n ${style.dim('Aucun Mission Contract.')}`);
|
|
121
|
+
const doCreate = await askConfirm('Creer un Mission Contract maintenant ?', true);
|
|
122
|
+
if (doCreate) {
|
|
123
|
+
return handleInit([], flags, config);
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Enrich with FLOWMAP state
|
|
130
|
+
const enriched = missions.map(m => {
|
|
131
|
+
const missionId = m.id;
|
|
132
|
+
let flowmap = '-';
|
|
133
|
+
try {
|
|
134
|
+
const fmState = getCurrentState(missionId, { root: config.cwd });
|
|
135
|
+
if (fmState) flowmap = fmState.value;
|
|
136
|
+
} catch { /* no-op */ }
|
|
137
|
+
return { ...m, flowmap };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(renderTable(
|
|
142
|
+
[
|
|
143
|
+
{ key: 'id', label: 'ID' },
|
|
144
|
+
{ key: 'title', label: 'Titre' },
|
|
145
|
+
{ key: 'status', label: 'Status' },
|
|
146
|
+
{ key: 'flowmap', label: 'FLOWMAP' },
|
|
147
|
+
],
|
|
148
|
+
enriched,
|
|
149
|
+
));
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleInspect(args, flags, config) {
|
|
154
|
+
const project = await ensureProject(config, flags);
|
|
155
|
+
if (!project) return;
|
|
156
|
+
|
|
157
|
+
let id = args[0];
|
|
158
|
+
|
|
159
|
+
// If no ID → picker from existing missions
|
|
160
|
+
if (!id) {
|
|
161
|
+
const missions = listMissions(project.hcm_dir);
|
|
162
|
+
if (missions.length === 0) {
|
|
163
|
+
console.log(`\n ${style.dim('Aucun Mission Contract a inspecter.')}`);
|
|
164
|
+
const doCreate = await askConfirm('Creer un Mission Contract maintenant ?', true);
|
|
165
|
+
if (doCreate) return handleInit([], flags, config);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
id = await pickFromList(missions, {
|
|
169
|
+
title: 'Quel Mission Contract inspecter ?',
|
|
170
|
+
labelFn: (m) => `${m.id} — ${m.title || style.dim('(sans titre)')} [${m.status}]`,
|
|
171
|
+
});
|
|
172
|
+
if (!id) return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data = inspectMission(id, project.hcm_dir);
|
|
176
|
+
|
|
177
|
+
if (flags.format === 'json') {
|
|
178
|
+
console.log(formatOutput(data, 'json'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Formatted view — consistent with odm inspect
|
|
183
|
+
const meta = data.contract_meta || {};
|
|
184
|
+
const payload = data.contract_payload || {};
|
|
185
|
+
const cadrage = payload.cadrage || {};
|
|
186
|
+
const workflow = payload.workflow || {};
|
|
187
|
+
const taskReg = payload.task_registry || {};
|
|
188
|
+
const tasks = Array.isArray(taskReg.tasks) ? taskReg.tasks : [];
|
|
189
|
+
|
|
190
|
+
console.log(`\n ${style.bold('Mission Contract')} : ${style.nemesisAccent(meta.id || id)}`);
|
|
191
|
+
console.log(` Status : ${meta.status || '?'}`);
|
|
192
|
+
console.log(` Owner : ${meta.owner || '?'}`);
|
|
193
|
+
console.log(` Projet : ${meta.project_id || '?'}`);
|
|
194
|
+
console.log(` Titre : ${cadrage.title || '?'}`);
|
|
195
|
+
if (cadrage.summary) {
|
|
196
|
+
console.log(` Resume : ${cadrage.summary}`);
|
|
197
|
+
}
|
|
198
|
+
console.log(` Cree le : ${meta.created_at ? meta.created_at.split('T')[0] : '?'}`);
|
|
199
|
+
|
|
200
|
+
// Workflow
|
|
201
|
+
const gates = workflow.gates || {};
|
|
202
|
+
const gateCount = Object.keys(gates).length;
|
|
203
|
+
if (gateCount > 0) {
|
|
204
|
+
console.log(`\n Workflow : ${gateCount} gate(s)`);
|
|
205
|
+
for (const [name, gate] of Object.entries(gates)) {
|
|
206
|
+
console.log(` ${style.dim('\u2192')} ${name} : ${gate.description || gate.status || '?'}`);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
console.log(`\n Workflow : ${style.dim('(a completer)')}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Task registry
|
|
213
|
+
if (tasks.length > 0) {
|
|
214
|
+
console.log(`\n Tasks : ${tasks.length} tache(s)`);
|
|
215
|
+
console.log(renderTable(
|
|
216
|
+
[
|
|
217
|
+
{ key: 'id', label: 'ID' },
|
|
218
|
+
{ key: 'title', label: 'Tache' },
|
|
219
|
+
{ key: 'status', label: 'Status' },
|
|
220
|
+
],
|
|
221
|
+
tasks,
|
|
222
|
+
));
|
|
223
|
+
} else {
|
|
224
|
+
console.log(` Tasks : ${style.dim('(aucune)')}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// FLOWMAP
|
|
228
|
+
const missionId = meta.mission_id || meta.id || id;
|
|
229
|
+
const fmInfo = getMissionFlowmapInfo(missionId, config.cwd);
|
|
230
|
+
if (fmInfo) {
|
|
231
|
+
console.log(`\n FLOWMAP : ${fmInfo.state} [${fmInfo.phase}] ${fmInfo.label}`);
|
|
232
|
+
if (fmInfo.history.length > 0) {
|
|
233
|
+
console.log('\n Historique recent :');
|
|
234
|
+
for (const h of fmInfo.history.slice(-5)) {
|
|
235
|
+
const ts = h.timestamp ? h.timestamp.replace('T', ' ').substring(0, 16) : '';
|
|
236
|
+
console.log(` - ${h.from || '?'} \u2192 ${h.to || '?'} ${h.event} ${style.dim(ts)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
console.log(`\n FLOWMAP : ${style.dim('(non initialise)')}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function handleValidate(args, flags, config) {
|
|
247
|
+
const project = await ensureProject(config, flags);
|
|
248
|
+
if (!project) return;
|
|
249
|
+
|
|
250
|
+
let id = args[0];
|
|
251
|
+
|
|
252
|
+
if (!id) {
|
|
253
|
+
const missions = listMissions(project.hcm_dir);
|
|
254
|
+
const drafts = missions.filter(m => m.status === 'DRAFT');
|
|
255
|
+
if (drafts.length === 0) {
|
|
256
|
+
console.log(`\n ${style.dim('Aucun Mission Contract en DRAFT a valider.')}\n`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
id = await pickFromList(drafts, {
|
|
260
|
+
title: 'Mission Contract a valider',
|
|
261
|
+
labelFn: (m) => `${m.id} — ${m.title || style.dim('(sans titre)')} [${m.status}]`,
|
|
262
|
+
});
|
|
263
|
+
if (!id) return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = validateMission(id, project.hcm_dir);
|
|
268
|
+
console.log(`\n ${style.green('\u2713')} Mission Contract ${style.bold(result.id)} → ${style.green('VALIDATED')}`);
|
|
269
|
+
console.log(`\n ${style.bold('Prochaines etapes')} :`);
|
|
270
|
+
console.log(` 1. Creer les OdMs associes : ${style.dim('nemesis odm init')}`);
|
|
271
|
+
console.log(` 2. Dispatcher aux agents : ${style.dim('nemesis odm set-status <ID> READY_TO_DISPATCH + nemesis orch')}\n`);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.log(`\n ${style.red('\u2717')} ${err.message}\n`);
|
|
274
|
+
}
|
|
275
|
+
}
|