@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,90 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { ALL_HOOKS, getHookStatus } from './hook-installer.js';
|
|
3
|
+
import { generateActivationPrompt, generateDeactivationPrompt } from './hook-prompts.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Invoke `claude -p` with a prompt and capture stdout.
|
|
7
|
+
* Uses spawnSync to avoid hanging on inherited stdio pipes.
|
|
8
|
+
* @param {string} prompt
|
|
9
|
+
* @param {object} options - { cwd, timeout }
|
|
10
|
+
* @returns {{ ok: boolean, output?: string, error?: string }}
|
|
11
|
+
*/
|
|
12
|
+
export function invokeClaudeP(prompt, options = {}) {
|
|
13
|
+
const bin = process.env.CLAUDE_BIN || 'claude';
|
|
14
|
+
const timeout = options.timeout || 60000;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const result = spawnSync(bin, ['-p', prompt, '--dangerously-skip-permissions'], {
|
|
18
|
+
cwd: options.cwd || process.cwd(),
|
|
19
|
+
timeout,
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
if (result.error.code === 'ENOENT') {
|
|
26
|
+
return { ok: false, error: 'claude CLI introuvable' };
|
|
27
|
+
}
|
|
28
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
29
|
+
return { ok: false, error: `Timeout (${timeout}ms)` };
|
|
30
|
+
}
|
|
31
|
+
return { ok: false, error: result.error.message };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.status !== 0) {
|
|
35
|
+
return { ok: false, error: result.stderr || `Exit code ${result.status}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { ok: true, output: result.stdout };
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return { ok: false, error: err.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Activate a hook via claude -p.
|
|
46
|
+
* @param {string} projectRoot
|
|
47
|
+
* @param {string} hookEvent - e.g. 'SessionStart'
|
|
48
|
+
* @returns {{ ok: boolean, verified: boolean, error?: string }}
|
|
49
|
+
*/
|
|
50
|
+
export function activateHook(projectRoot, hookEvent) {
|
|
51
|
+
const meta = ALL_HOOKS[hookEvent];
|
|
52
|
+
if (!meta) return { ok: false, verified: false, error: `Hook inconnu: ${hookEvent}` };
|
|
53
|
+
|
|
54
|
+
const prompt = generateActivationPrompt(hookEvent, meta.command);
|
|
55
|
+
const result = invokeClaudeP(prompt, { cwd: projectRoot });
|
|
56
|
+
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
return { ok: false, verified: false, error: result.error };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const confirmed = (result.output || '').includes('HOOK_ACTIVATED_OK');
|
|
62
|
+
const status = getHookStatus(projectRoot);
|
|
63
|
+
const verified = status[hookEvent]?.active || false;
|
|
64
|
+
|
|
65
|
+
return { ok: confirmed || verified, verified, error: confirmed ? null : 'Confirmation absente dans la reponse' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Deactivate a hook via claude -p.
|
|
70
|
+
* @param {string} projectRoot
|
|
71
|
+
* @param {string} hookEvent - e.g. 'SessionStart'
|
|
72
|
+
* @returns {{ ok: boolean, verified: boolean, error?: string }}
|
|
73
|
+
*/
|
|
74
|
+
export function deactivateHook(projectRoot, hookEvent) {
|
|
75
|
+
const meta = ALL_HOOKS[hookEvent];
|
|
76
|
+
if (!meta) return { ok: false, verified: false, error: `Hook inconnu: ${hookEvent}` };
|
|
77
|
+
|
|
78
|
+
const prompt = generateDeactivationPrompt(hookEvent, meta.command);
|
|
79
|
+
const result = invokeClaudeP(prompt, { cwd: projectRoot });
|
|
80
|
+
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
return { ok: false, verified: false, error: result.error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const confirmed = (result.output || '').includes('HOOK_DEACTIVATED_OK');
|
|
86
|
+
const status = getHookStatus(projectRoot);
|
|
87
|
+
const verified = !status[hookEvent]?.active;
|
|
88
|
+
|
|
89
|
+
return { ok: confirmed || verified, verified, error: confirmed ? null : 'Confirmation absente dans la reponse' };
|
|
90
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Injector — unified context builder for SessionStart / PreCompact / situational updates.
|
|
3
|
+
*
|
|
4
|
+
* Single canal (stdout), driven by situation via client.navigate(), zero duplication.
|
|
5
|
+
*
|
|
6
|
+
* Leader receives: B00 (gouvernance) + situational bloc + dispatch/validation ops + recent memory
|
|
7
|
+
* Expert receives: B00 (gouvernance) + situational bloc + recent memory
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import {
|
|
13
|
+
LEADER_DISPATCH,
|
|
14
|
+
LEADER_VALIDATION,
|
|
15
|
+
LEADER_INTERDICTIONS,
|
|
16
|
+
} from './leader-rules.js';
|
|
17
|
+
import { debug } from '../core/logger.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get recent notes for an agent, sorted by mtime desc.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} root - project root
|
|
23
|
+
* @param {string} agentName - e.g. "Agent_Leader_-_Architecte"
|
|
24
|
+
* @param {number} [limit=2] - max notes to return
|
|
25
|
+
* @returns {Array<{ id: string, content: string, timestamp: string }>}
|
|
26
|
+
*/
|
|
27
|
+
export function getRecentNotes(root, agentName, limit = 2) {
|
|
28
|
+
const notesBase = join(root, '.nemesis', 'claude', agentName, 'notes');
|
|
29
|
+
if (!existsSync(notesBase)) return [];
|
|
30
|
+
|
|
31
|
+
const allFiles = [];
|
|
32
|
+
try {
|
|
33
|
+
const months = readdirSync(notesBase).filter(d => /^\d{4}-\d{2}$/.test(d));
|
|
34
|
+
for (const month of months) {
|
|
35
|
+
const monthDir = join(notesBase, month);
|
|
36
|
+
const files = readdirSync(monthDir).filter(f => f.endsWith('.json'));
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const filepath = join(monthDir, file);
|
|
39
|
+
allFiles.push({ filepath, mtime: statSync(filepath).mtimeMs });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (e) { debug(`getRecentNotes scan: ${e.message}`); return []; }
|
|
43
|
+
|
|
44
|
+
allFiles.sort((a, b) => b.mtime - a.mtime);
|
|
45
|
+
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const { filepath } of allFiles.slice(0, limit)) {
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
50
|
+
result.push({
|
|
51
|
+
id: data.id || '',
|
|
52
|
+
content: data.content || data.inputSummary || '',
|
|
53
|
+
timestamp: data.timestamp || '',
|
|
54
|
+
});
|
|
55
|
+
} catch (e) { debug(`getRecentNotes parse: ${e.message}`); }
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get recent CRs for an agent, sorted by mtime desc.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} root - project root
|
|
64
|
+
* @param {string} agentName
|
|
65
|
+
* @param {number} [limit=1] - max CRs to return
|
|
66
|
+
* @returns {Array<{ id: string, summary: string, timestamp: string }>}
|
|
67
|
+
*/
|
|
68
|
+
export function getRecentCRs(root, agentName, limit = 1) {
|
|
69
|
+
const crDir = join(root, '.nemesis', 'claude', agentName, 'cr');
|
|
70
|
+
if (!existsSync(crDir)) return [];
|
|
71
|
+
|
|
72
|
+
const allFiles = [];
|
|
73
|
+
try {
|
|
74
|
+
const files = readdirSync(crDir).filter(f => f.endsWith('.json'));
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const filepath = join(crDir, file);
|
|
77
|
+
allFiles.push({ filepath, mtime: statSync(filepath).mtimeMs });
|
|
78
|
+
}
|
|
79
|
+
} catch (e) { debug(`getRecentCRs scan: ${e.message}`); return []; }
|
|
80
|
+
|
|
81
|
+
allFiles.sort((a, b) => b.mtime - a.mtime);
|
|
82
|
+
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const { filepath } of allFiles.slice(0, limit)) {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
87
|
+
result.push({
|
|
88
|
+
id: data.id || '',
|
|
89
|
+
summary: data.content?.summary || data.summary || '',
|
|
90
|
+
timestamp: data.metadata?.date_execution || data.timestamp || data.period?.to || data.period?.from || '',
|
|
91
|
+
});
|
|
92
|
+
} catch (e) { debug(`getRecentCRs parse: ${e.message}`); }
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get recent memory summary (notes + CRs) formatted as markdown.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} root - project root
|
|
101
|
+
* @param {string} agentName
|
|
102
|
+
* @param {'leader'|'expert'} role
|
|
103
|
+
* @param {string} [leaderName] - leader name (for expert role)
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function getRecentMemory(root, agentName, role, leaderName) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
|
|
109
|
+
if (role === 'leader') {
|
|
110
|
+
// Own last CR
|
|
111
|
+
const crs = getRecentCRs(root, agentName, 1);
|
|
112
|
+
if (crs.length > 0) {
|
|
113
|
+
lines.push('## Dernier CR', '');
|
|
114
|
+
for (const cr of crs) {
|
|
115
|
+
lines.push(`**${cr.id}** (${cr.timestamp})`);
|
|
116
|
+
if (cr.summary) lines.push(cr.summary);
|
|
117
|
+
lines.push('');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Own 2 last notes
|
|
122
|
+
const notes = getRecentNotes(root, agentName, 2);
|
|
123
|
+
if (notes.length > 0) {
|
|
124
|
+
lines.push('## Notes recentes', '');
|
|
125
|
+
for (const note of notes) {
|
|
126
|
+
lines.push(`**${note.id}** (${note.timestamp})`);
|
|
127
|
+
if (note.content) lines.push(note.content);
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Expert: Leader's last CR
|
|
133
|
+
if (leaderName) {
|
|
134
|
+
const leaderCrs = getRecentCRs(root, leaderName, 1);
|
|
135
|
+
if (leaderCrs.length > 0) {
|
|
136
|
+
lines.push('## Dernier CR du Leader', '');
|
|
137
|
+
for (const cr of leaderCrs) {
|
|
138
|
+
lines.push(`**${cr.id}** (${cr.timestamp})`);
|
|
139
|
+
if (cr.summary) lines.push(cr.summary);
|
|
140
|
+
lines.push('');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Own 2 last CRs
|
|
146
|
+
const ownCrs = getRecentCRs(root, agentName, 2);
|
|
147
|
+
if (ownCrs.length > 0) {
|
|
148
|
+
lines.push('## Mes derniers CR', '');
|
|
149
|
+
for (const cr of ownCrs) {
|
|
150
|
+
lines.push(`**${cr.id}** (${cr.timestamp})`);
|
|
151
|
+
if (cr.summary) lines.push(cr.summary);
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Own 2 last notes
|
|
157
|
+
const notes = getRecentNotes(root, agentName, 2);
|
|
158
|
+
if (notes.length > 0) {
|
|
159
|
+
lines.push('## Notes recentes', '');
|
|
160
|
+
for (const note of notes) {
|
|
161
|
+
lines.push(`**${note.id}** (${note.timestamp})`);
|
|
162
|
+
if (note.content) lines.push(note.content);
|
|
163
|
+
lines.push('');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Format leader operational rules (dispatch + validation) from constants.
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function formatLeaderOperations() {
|
|
176
|
+
const lines = [
|
|
177
|
+
'', '## Dispatch OdM', '',
|
|
178
|
+
'OBLIGATOIRE : pour dispatcher un OdM, mets-le en READY_TO_DISPATCH. Le daemon nemesis orch le dispatche automatiquement.',
|
|
179
|
+
'Ne JAMAIS lancer claude --resume manuellement.', '',
|
|
180
|
+
`1. Verifie les agents disponibles :`,
|
|
181
|
+
` nemesis team list`, '',
|
|
182
|
+
`2. Mets l'OdM en READY_TO_DISPATCH :`,
|
|
183
|
+
` ${LEADER_DISPATCH.command}`, '',
|
|
184
|
+
...LEADER_DISPATCH.flow.map(r => `- ${r}`), '',
|
|
185
|
+
'## QA', '',
|
|
186
|
+
...LEADER_DISPATCH.qa.map(r => `- ${r}`), '',
|
|
187
|
+
'## Validation CR', '',
|
|
188
|
+
`- VALIDE : ${LEADER_VALIDATION.VALIDE}`,
|
|
189
|
+
'- REJET :',
|
|
190
|
+
...LEADER_VALIDATION.REJET.map(r => ` - ${r}`),
|
|
191
|
+
`- Sur rejet : ${LEADER_VALIDATION.sur_rejet}`, '',
|
|
192
|
+
'## Interdictions', '',
|
|
193
|
+
...LEADER_INTERDICTIONS.map(r => `- ${r}`), '',
|
|
194
|
+
];
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Format atoms as markdown lines.
|
|
200
|
+
* @param {Array} atoms
|
|
201
|
+
* @returns {string[]}
|
|
202
|
+
*/
|
|
203
|
+
function formatAtoms(atoms) {
|
|
204
|
+
const lines = [];
|
|
205
|
+
for (const atom of atoms) {
|
|
206
|
+
const content = typeof atom.content === 'object'
|
|
207
|
+
? (atom.content.description || JSON.stringify(atom.content))
|
|
208
|
+
: atom.content;
|
|
209
|
+
lines.push(`- [${atom.priority || 'INFO'}] **${atom.occurrence || atom.label}** : ${content || ''}`);
|
|
210
|
+
}
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build the unified context injection for an agent.
|
|
216
|
+
* Single canal: stdout. Driven by situation via client.navigate().
|
|
217
|
+
*
|
|
218
|
+
* @param {string} root - project root
|
|
219
|
+
* @param {string} agentName
|
|
220
|
+
* @param {'leader'|'expert'} role
|
|
221
|
+
* @param {string} situation - e.g. 'onboarding', 'debugging', 'general'
|
|
222
|
+
* @param {object} [opts]
|
|
223
|
+
* @param {string} [opts.projectId] - project ID for header
|
|
224
|
+
* @returns {Promise<string>} - markdown context to inject via stdout
|
|
225
|
+
*/
|
|
226
|
+
export async function buildUnifiedContext(root, agentName, role, situation, opts = {}) {
|
|
227
|
+
try {
|
|
228
|
+
const { createNemesisClient } = await import('../sync/nemesis-client.js');
|
|
229
|
+
const client = createNemesisClient({ projectRoot: root });
|
|
230
|
+
|
|
231
|
+
const header = [
|
|
232
|
+
'',
|
|
233
|
+
`# Contexte — ${agentName}`,
|
|
234
|
+
'',
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
if (opts.projectId) header.push(`**Projet** : ${opts.projectId}`);
|
|
238
|
+
header.push(`**Role** : ${role}`, `**Situation** : ${situation}`, '');
|
|
239
|
+
|
|
240
|
+
const sections = [];
|
|
241
|
+
|
|
242
|
+
// 1. B00 gouvernance (always)
|
|
243
|
+
try {
|
|
244
|
+
const govData = await client.bloc('B00');
|
|
245
|
+
if (govData?.atoms?.length) {
|
|
246
|
+
sections.push('## Gouvernance', '', ...formatAtoms(govData.atoms), '');
|
|
247
|
+
}
|
|
248
|
+
} catch (e) { debug(`buildUnifiedContext B00: ${e.message}`); }
|
|
249
|
+
|
|
250
|
+
// 2. Situational bloc (if not general)
|
|
251
|
+
if (situation !== 'general') {
|
|
252
|
+
try {
|
|
253
|
+
const navData = await client.navigate(situation);
|
|
254
|
+
if (navData?.atoms?.length) {
|
|
255
|
+
sections.push(`## ${situation} (${navData.bloc})`, '', ...formatAtoms(navData.atoms), '');
|
|
256
|
+
}
|
|
257
|
+
} catch (e) { debug(`buildUnifiedContext nav ${situation}: ${e.message}`); }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 3. Leader operations (dispatch + validation)
|
|
261
|
+
if (role === 'leader') {
|
|
262
|
+
sections.push(formatLeaderOperations());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 4. Recent memory (notes + CRs)
|
|
266
|
+
const leaderName = role === 'expert' ? await detectLeaderAsync(root) : null;
|
|
267
|
+
const memory = getRecentMemory(root, agentName, role, leaderName);
|
|
268
|
+
if (memory) {
|
|
269
|
+
sections.push(memory);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Return empty if no actual content sections
|
|
273
|
+
if (sections.length === 0) return '';
|
|
274
|
+
|
|
275
|
+
return [...header, ...sections].join('\n');
|
|
276
|
+
} catch (e) {
|
|
277
|
+
debug(`buildUnifiedContext: ${e.message}`);
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Build a lightweight situation update injection.
|
|
284
|
+
* Used when situation changes during UserPromptSubmit (not full re-injection).
|
|
285
|
+
*
|
|
286
|
+
* @param {string} root - project root
|
|
287
|
+
* @param {string} newSituation
|
|
288
|
+
* @returns {Promise<string>}
|
|
289
|
+
*/
|
|
290
|
+
export async function buildSituationUpdate(root, newSituation) {
|
|
291
|
+
try {
|
|
292
|
+
const { createNemesisClient } = await import('../sync/nemesis-client.js');
|
|
293
|
+
const client = createNemesisClient({ projectRoot: root });
|
|
294
|
+
|
|
295
|
+
const navData = await client.navigate(newSituation);
|
|
296
|
+
if (!navData?.atoms?.length) return '';
|
|
297
|
+
|
|
298
|
+
const parts = [
|
|
299
|
+
'',
|
|
300
|
+
`# Mise a jour contexte — ${newSituation}`,
|
|
301
|
+
'',
|
|
302
|
+
`**Bloc** : ${navData.bloc}`,
|
|
303
|
+
'',
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
parts.push(...formatAtoms(navData.atoms));
|
|
307
|
+
parts.push('');
|
|
308
|
+
|
|
309
|
+
return parts.join('\n');
|
|
310
|
+
} catch (e) {
|
|
311
|
+
debug(`buildSituationUpdate: ${e.message}`);
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Detect leader agent name asynchronously.
|
|
318
|
+
*/
|
|
319
|
+
async function detectLeaderAsync(root) {
|
|
320
|
+
try {
|
|
321
|
+
const { readRegistry, getLanes } = await import('../core/registry.js');
|
|
322
|
+
const { detectProject } = await import('../core/project.js');
|
|
323
|
+
const project = detectProject(root);
|
|
324
|
+
if (!project) return null;
|
|
325
|
+
const reg = readRegistry(project.hcm_dir);
|
|
326
|
+
if (!reg) return null;
|
|
327
|
+
const lanes = getLanes(reg);
|
|
328
|
+
const leader = lanes.find(l => l.leader);
|
|
329
|
+
return leader?.name || leader?.id || null;
|
|
330
|
+
} catch (e) { debug(`detectLeaderAsync: ${e.message}`); return null; }
|
|
331
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Loader — resolve full agent context from local files.
|
|
3
|
+
* Used by hooks to build Kairos context before injection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { readRegistry, getLanes } from '../core/registry.js';
|
|
9
|
+
import { detectProject } from '../core/project.js';
|
|
10
|
+
import { debug } from '../core/logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the complete context for an agent from local HCM files.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectRoot - project root directory
|
|
16
|
+
* @param {string} agentName - agent name (e.g. "Agent_Bot-CLI")
|
|
17
|
+
* @returns {{ agentId: string, projectId: string, kairosProfile: string|null, missionId: string|null, odmId: string|null, isLeader: boolean, role: string|null } | null}
|
|
18
|
+
*/
|
|
19
|
+
export function resolveAgentContext(projectRoot, agentName) {
|
|
20
|
+
try {
|
|
21
|
+
const project = detectProject(projectRoot);
|
|
22
|
+
if (!project) return null;
|
|
23
|
+
|
|
24
|
+
const reg = readRegistry(project.hcm_dir);
|
|
25
|
+
if (!reg) return null;
|
|
26
|
+
|
|
27
|
+
const lanes = getLanes(reg);
|
|
28
|
+
const lane = lanes.find(l => l.id === agentName || l.name === agentName);
|
|
29
|
+
if (!lane) return null;
|
|
30
|
+
|
|
31
|
+
const kairosProfile = resolveKairosProfile(project.hcm_dir, agentName);
|
|
32
|
+
const activeOdm = findActiveOdm(project.hcm_dir, agentName);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
agentId: lane.id || agentName,
|
|
36
|
+
projectId: project.id,
|
|
37
|
+
kairosProfile,
|
|
38
|
+
missionId: activeOdm?.missionId || null,
|
|
39
|
+
odmId: activeOdm?.odmId || null,
|
|
40
|
+
isLeader: !!lane.leader,
|
|
41
|
+
role: lane.role || null,
|
|
42
|
+
};
|
|
43
|
+
} catch (e) {
|
|
44
|
+
debug(`resolveAgentContext: ${e.message}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve Kairos profile ID from contributor card.
|
|
51
|
+
* Scans .nemesis/HCM/contributors/CONTRIB-*.json for matching agent.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} hcmDir
|
|
54
|
+
* @param {string} agentName
|
|
55
|
+
* @returns {string|null}
|
|
56
|
+
*/
|
|
57
|
+
export function resolveKairosProfile(hcmDir, agentName) {
|
|
58
|
+
const contribDir = join(hcmDir, 'contributors');
|
|
59
|
+
if (!existsSync(contribDir)) return null;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const files = readdirSync(contribDir).filter(f => f.startsWith('CONTRIB-') && f.endsWith('.json'));
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const data = JSON.parse(readFileSync(join(contribDir, file), 'utf-8'));
|
|
65
|
+
const id = data.contributor_meta?.contributor_id || data.id || '';
|
|
66
|
+
const name = data.contributor_meta?.name || data.name || '';
|
|
67
|
+
if (id === agentName || name === agentName) {
|
|
68
|
+
const profiles = data.contributor_payload?.profiles || data.profiles || [];
|
|
69
|
+
return profiles[0] || null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) { debug(`resolveKairosProfile: ${e.message}`); }
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find the active OdM assigned to an agent.
|
|
78
|
+
* Scans .nemesis/HCM/odm/ for OdMs with matching assigned_to and active status.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} hcmDir
|
|
81
|
+
* @param {string} agentName
|
|
82
|
+
* @returns {{ odmId: string, missionId: string|null, title: string } | null}
|
|
83
|
+
*/
|
|
84
|
+
export function findActiveOdm(hcmDir, agentName) {
|
|
85
|
+
const odmDir = join(hcmDir, 'odm');
|
|
86
|
+
if (!existsSync(odmDir)) return null;
|
|
87
|
+
|
|
88
|
+
const activeStatuses = new Set(['ASSIGNED', 'IN_PROGRESS', 'DRAFT']);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const files = readdirSync(odmDir).filter(f => f.endsWith('.json') && !f.startsWith('legacy_'));
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
const data = JSON.parse(readFileSync(join(odmDir, file), 'utf-8'));
|
|
94
|
+
const meta = data.odm_meta || {};
|
|
95
|
+
const assignee = meta.assigned_to?.actor_id || meta.assigned_to || '';
|
|
96
|
+
const status = meta.status || '';
|
|
97
|
+
|
|
98
|
+
if (assignee === agentName && activeStatuses.has(status)) {
|
|
99
|
+
return {
|
|
100
|
+
odmId: meta.odm_id || file.replace('.json', ''),
|
|
101
|
+
missionId: meta.mission_id || meta.contract_id || null,
|
|
102
|
+
title: data.odm_payload?.cadrage?.title || '',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (e) { debug(`findActiveOdm: ${e.message}`); }
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Writer — minimal situation tracking via .nemesis/.current-situation.
|
|
3
|
+
*
|
|
4
|
+
* All context injection now goes through stdout (context-injector.js).
|
|
5
|
+
* This module only persists/reads the current situation string.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { debug } from '../core/logger.js';
|
|
11
|
+
|
|
12
|
+
const SITUATION_FILENAME = '.current-situation';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the current situation from .nemesis/.current-situation.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} projectRoot
|
|
18
|
+
* @returns {string|null}
|
|
19
|
+
*/
|
|
20
|
+
export function readCurrentSituation(projectRoot) {
|
|
21
|
+
const filepath = join(projectRoot, '.nemesis', SITUATION_FILENAME);
|
|
22
|
+
if (!existsSync(filepath)) return null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(filepath, 'utf-8').trim();
|
|
26
|
+
return content || null;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
debug(`readCurrentSituation: ${e.message}`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Write the current situation to .nemesis/.current-situation.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} projectRoot
|
|
37
|
+
* @param {string} situation - e.g. 'onboarding', 'debugging', 'general'
|
|
38
|
+
*/
|
|
39
|
+
export function writeCurrentSituation(projectRoot, situation) {
|
|
40
|
+
const nemesisDir = join(projectRoot, '.nemesis');
|
|
41
|
+
if (!existsSync(nemesisDir)) {
|
|
42
|
+
mkdirSync(nemesisDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(join(nemesisDir, SITUATION_FILENAME), situation, 'utf-8');
|
|
45
|
+
}
|