@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,112 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, unlinkSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveLogPath, resolveLogDir, resolveAgentDir, ensureDir } from './paths.js';
|
|
4
|
+
|
|
5
|
+
// In-memory pending turns — atomic write strategy
|
|
6
|
+
const pendingTurns = new Map();
|
|
7
|
+
|
|
8
|
+
export function openTurn(projectRoot, agentName, sessionId, turnNumber, promptText) {
|
|
9
|
+
const entry = {
|
|
10
|
+
turn: turnNumber,
|
|
11
|
+
session_id: sessionId,
|
|
12
|
+
timestamp_in: new Date().toISOString(),
|
|
13
|
+
in: promptText,
|
|
14
|
+
};
|
|
15
|
+
pendingTurns.set(sessionId, entry);
|
|
16
|
+
|
|
17
|
+
// Persist for inter-process hooks (UserPromptSubmit → Stop are separate processes)
|
|
18
|
+
const pendingDir = resolveAgentDir(projectRoot, agentName);
|
|
19
|
+
ensureDir(pendingDir);
|
|
20
|
+
const pendingFile = join(pendingDir, `.pending-turn-${sessionId}.json`);
|
|
21
|
+
writeFileSync(pendingFile, JSON.stringify(entry) + '\n', 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function closeTurn(projectRoot, agentName, sessionId, responseText) {
|
|
25
|
+
let pending = pendingTurns.get(sessionId);
|
|
26
|
+
|
|
27
|
+
// Inter-process fallback: read .pending-turn file if Map is empty (hooks are separate processes)
|
|
28
|
+
if (!pending) {
|
|
29
|
+
const pendingDir = resolveAgentDir(projectRoot, agentName);
|
|
30
|
+
const pendingFile = join(pendingDir, `.pending-turn-${sessionId}.json`);
|
|
31
|
+
if (existsSync(pendingFile)) {
|
|
32
|
+
try {
|
|
33
|
+
pending = JSON.parse(readFileSync(pendingFile, 'utf-8'));
|
|
34
|
+
unlinkSync(pendingFile);
|
|
35
|
+
} catch (_e) { /* fallback: pending turn corrupted */ }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!pending) {
|
|
40
|
+
const entry = {
|
|
41
|
+
turn: 0,
|
|
42
|
+
session_id: sessionId,
|
|
43
|
+
timestamp_in: null,
|
|
44
|
+
timestamp_out: new Date().toISOString(),
|
|
45
|
+
in: null,
|
|
46
|
+
out: responseText,
|
|
47
|
+
};
|
|
48
|
+
writeEntry(projectRoot, agentName, sessionId, entry);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = {
|
|
53
|
+
...pending,
|
|
54
|
+
timestamp_out: new Date().toISOString(),
|
|
55
|
+
out: responseText,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
pendingTurns.delete(sessionId);
|
|
59
|
+
writeEntry(projectRoot, agentName, sessionId, entry);
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeEntry(projectRoot, agentName, sessionId, entry) {
|
|
64
|
+
const logDir = resolveLogDir(projectRoot, agentName);
|
|
65
|
+
ensureDir(logDir);
|
|
66
|
+
const logPath = resolveLogPath(projectRoot, agentName, sessionId);
|
|
67
|
+
appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readLog(projectRoot, agentName, sessionId) {
|
|
71
|
+
const logPath = resolveLogPath(projectRoot, agentName, sessionId);
|
|
72
|
+
if (!existsSync(logPath)) return [];
|
|
73
|
+
const lines = readFileSync(logPath, 'utf-8').trim().split('\n');
|
|
74
|
+
const entries = [];
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (!line.trim()) continue;
|
|
77
|
+
try { entries.push(JSON.parse(line)); }
|
|
78
|
+
catch (_e) { /* fallback: skip malformed entry */ }
|
|
79
|
+
}
|
|
80
|
+
return entries;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readLastEntry(projectRoot, agentName, sessionId) {
|
|
84
|
+
const entries = readLog(projectRoot, agentName, sessionId);
|
|
85
|
+
return entries.length > 0 ? entries[entries.length - 1] : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function replaySession(projectRoot, agentName, sessionId) {
|
|
89
|
+
const entries = readLog(projectRoot, agentName, sessionId);
|
|
90
|
+
const messages = [];
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
if (e.in) messages.push({ role: 'user', content: e.in, timestamp: e.timestamp_in });
|
|
93
|
+
if (e.out) messages.push({ role: 'assistant', content: e.out, timestamp: e.timestamp_out });
|
|
94
|
+
}
|
|
95
|
+
return messages;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function listLogFiles(projectRoot, agentName) {
|
|
99
|
+
const agentDir = resolveAgentDir(projectRoot, agentName);
|
|
100
|
+
const logsRoot = join(agentDir, 'logs');
|
|
101
|
+
if (!existsSync(logsRoot)) return [];
|
|
102
|
+
const months = readdirSync(logsRoot).filter(d => /^\d{4}-\d{2}$/.test(d)).sort().reverse();
|
|
103
|
+
const files = [];
|
|
104
|
+
for (const month of months) {
|
|
105
|
+
const monthDir = join(logsRoot, month);
|
|
106
|
+
const jsonls = readdirSync(monthDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
107
|
+
for (const f of jsonls) {
|
|
108
|
+
files.push({ filename: f, path: join(monthDir, f), month });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return files;
|
|
112
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveNotesDir, resolveCrDir, resolveAgentDir, ensureDir } from './paths.js';
|
|
4
|
+
import { readNwRegistry, nextNoteId, registerNote, getLastCr } from './registry.js';
|
|
5
|
+
|
|
6
|
+
const SYSTEM_PROMPT = `Tu es un systeme d'observation silencieux.
|
|
7
|
+
Tu analyses un echange entre un PM et un agent IA.
|
|
8
|
+
Tu ne fais PAS partie de la conversation. Tu observes.
|
|
9
|
+
|
|
10
|
+
Ton role : extraire ce qui est notable de cet echange et produire une NOTE structuree.
|
|
11
|
+
|
|
12
|
+
## Niveaux de profondeur
|
|
13
|
+
|
|
14
|
+
Attribue UN niveau a cet echange :
|
|
15
|
+
|
|
16
|
+
- L1 : Information passante, bruit, commande technique sans interet
|
|
17
|
+
- L2 : Echange simple, confirmation, acquittement
|
|
18
|
+
- L3 : Clarification, question importante, explication significative
|
|
19
|
+
- L4 : Decision, choix structurant, arbitrage
|
|
20
|
+
- L5 : Decision critique, pivot, changement de direction majeur
|
|
21
|
+
|
|
22
|
+
## Format de sortie
|
|
23
|
+
|
|
24
|
+
Reponds UNIQUEMENT avec un JSON valide (pas de markdown, pas de texte avant/apres) :
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"level": "L3",
|
|
28
|
+
"inputSummary": "Resume court du prompt user (1 phrase)",
|
|
29
|
+
"content": "Ce qui est notable dans cet echange (2-5 phrases)",
|
|
30
|
+
"tags": ["technique", "decision"],
|
|
31
|
+
"extractedActions": ["Action identifiee 1"]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Tags possibles : technique, decision, scope, bug, architecture, ux, performance,
|
|
35
|
+
securite, documentation, test, refactoring, clarification, validation, blocage.
|
|
36
|
+
|
|
37
|
+
Si rien n'est notable (L1), mets content a une phrase et extractedActions a [].`;
|
|
38
|
+
|
|
39
|
+
export { SYSTEM_PROMPT };
|
|
40
|
+
|
|
41
|
+
export function buildNotePrompt(turnEntry, lastCrSummary) {
|
|
42
|
+
let prompt = '## Echange a analyser\n\n';
|
|
43
|
+
prompt += `**Prompt user :**\n${turnEntry.in || '(vide)'}\n\n`;
|
|
44
|
+
prompt += `**Reponse agent :**\n${turnEntry.out || '(vide)'}\n\n`;
|
|
45
|
+
if (lastCrSummary) {
|
|
46
|
+
prompt += `## Contexte (dernier CR)\n${lastCrSummary}\n`;
|
|
47
|
+
}
|
|
48
|
+
return prompt;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseNoteResponse(text) {
|
|
52
|
+
try {
|
|
53
|
+
let json = text.trim();
|
|
54
|
+
if (json.startsWith('```')) {
|
|
55
|
+
json = json.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '');
|
|
56
|
+
}
|
|
57
|
+
const parsed = JSON.parse(json);
|
|
58
|
+
return {
|
|
59
|
+
level: parsed.level || 'L1',
|
|
60
|
+
inputSummary: parsed.inputSummary || '',
|
|
61
|
+
content: parsed.content || '',
|
|
62
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags : [],
|
|
63
|
+
extractedActions: Array.isArray(parsed.extractedActions) ? parsed.extractedActions : [],
|
|
64
|
+
};
|
|
65
|
+
} catch (_e) {
|
|
66
|
+
return {
|
|
67
|
+
level: 'L2',
|
|
68
|
+
inputSummary: 'Reponse LLM non parsable',
|
|
69
|
+
content: text.slice(0, 500),
|
|
70
|
+
tags: [],
|
|
71
|
+
extractedActions: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function generateNote(projectRoot, agentName, turnEntry, callLlm) {
|
|
77
|
+
// 1. Contexte CR precedent
|
|
78
|
+
const lastCr = getLastCr(projectRoot, agentName);
|
|
79
|
+
let lastCrSummary = null;
|
|
80
|
+
if (lastCr) {
|
|
81
|
+
const crDir = resolveCrDir(projectRoot, agentName);
|
|
82
|
+
const crPath = join(crDir, `${lastCr.id}.json`);
|
|
83
|
+
if (existsSync(crPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const crData = JSON.parse(readFileSync(crPath, 'utf-8'));
|
|
86
|
+
lastCrSummary = crData.summary || crData.previousCrSummary || null;
|
|
87
|
+
} catch (_e) { /* fallback: previous CR unreadable */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Construire le prompt
|
|
92
|
+
const userPrompt = buildNotePrompt(turnEntry, lastCrSummary);
|
|
93
|
+
|
|
94
|
+
// 3. Appeler le LLM
|
|
95
|
+
let llmResponse;
|
|
96
|
+
try {
|
|
97
|
+
llmResponse = await callLlm(SYSTEM_PROMPT, userPrompt);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
llmResponse = JSON.stringify({
|
|
100
|
+
level: 'L1',
|
|
101
|
+
inputSummary: turnEntry.in?.slice(0, 100) || '',
|
|
102
|
+
content: `Erreur LLM: ${err.message}`,
|
|
103
|
+
tags: ['erreur-llm'],
|
|
104
|
+
extractedActions: [],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 4. Parser la reponse
|
|
109
|
+
const parsed = parseNoteResponse(llmResponse);
|
|
110
|
+
|
|
111
|
+
// 5. Construire la note complete
|
|
112
|
+
const noteId = nextNoteId(projectRoot, agentName);
|
|
113
|
+
const note = {
|
|
114
|
+
id: noteId,
|
|
115
|
+
projectId: null,
|
|
116
|
+
agentId: agentName,
|
|
117
|
+
sessionId: turnEntry.session_id,
|
|
118
|
+
turn: turnEntry.turn,
|
|
119
|
+
level: parsed.level,
|
|
120
|
+
timestamp: turnEntry.timestamp_out || new Date().toISOString(),
|
|
121
|
+
previousCrId: lastCr?.id || null,
|
|
122
|
+
inputSummary: parsed.inputSummary,
|
|
123
|
+
content: parsed.content,
|
|
124
|
+
tags: parsed.tags,
|
|
125
|
+
extractedActions: parsed.extractedActions,
|
|
126
|
+
generatedAt: new Date().toISOString(),
|
|
127
|
+
llmProvider: callLlm.lastProvider || null,
|
|
128
|
+
llmFallback: callLlm.wasFallback || false,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// 6. Ecrire le fichier
|
|
132
|
+
const notesDir = resolveNotesDir(projectRoot, agentName);
|
|
133
|
+
ensureDir(notesDir);
|
|
134
|
+
const notePath = join(notesDir, `${noteId}.json`);
|
|
135
|
+
writeFileSync(notePath, JSON.stringify(note, null, 2) + '\n', 'utf-8');
|
|
136
|
+
|
|
137
|
+
// 7. Mettre a jour le registry
|
|
138
|
+
registerNote(projectRoot, agentName, {
|
|
139
|
+
id: noteId,
|
|
140
|
+
level: parsed.level,
|
|
141
|
+
timestamp: note.timestamp,
|
|
142
|
+
sessionId: turnEntry.session_id,
|
|
143
|
+
crId: lastCr?.id || null,
|
|
144
|
+
tags: parsed.tags,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return note;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function readNote(projectRoot, agentName, noteId) {
|
|
151
|
+
const agentDir = resolveAgentDir(projectRoot, agentName);
|
|
152
|
+
const notesRoot = join(agentDir, 'notes');
|
|
153
|
+
if (!existsSync(notesRoot)) return null;
|
|
154
|
+
const months = readdirSync(notesRoot).filter(d => /^\d{4}-\d{2}$/.test(d));
|
|
155
|
+
for (const month of months) {
|
|
156
|
+
const filePath = join(notesRoot, month, `${noteId}.json`);
|
|
157
|
+
if (existsSync(filePath)) {
|
|
158
|
+
try { return JSON.parse(readFileSync(filePath, 'utf-8')); }
|
|
159
|
+
catch (_e) { /* fallback: corrupted note */ return null; }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function listNotes(projectRoot, agentName) {
|
|
166
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
167
|
+
return reg.notes;
|
|
168
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
export function agentShort(agentName) {
|
|
5
|
+
return agentName.replace(/^Agent_/i, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveAgentDir(projectRoot, agentName) {
|
|
9
|
+
return join(projectRoot, '.nemesis', 'claude', agentName);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveLogDir(projectRoot, agentName) {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
15
|
+
return join(resolveAgentDir(projectRoot, agentName), 'logs', month);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveLogPath(projectRoot, agentName, sessionId) {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const date = now.toISOString().slice(0, 10);
|
|
21
|
+
const dir = resolveLogDir(projectRoot, agentName);
|
|
22
|
+
return join(dir, `${date}-${sessionId}.jsonl`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveNotesDir(projectRoot, agentName) {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
28
|
+
return join(resolveAgentDir(projectRoot, agentName), 'notes', month);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveCrDir(projectRoot, agentName) {
|
|
32
|
+
return join(resolveAgentDir(projectRoot, agentName), 'cr');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveRegistryPath(projectRoot, agentName) {
|
|
36
|
+
return join(resolveAgentDir(projectRoot, agentName), 'registry.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolvePendingCrPath(projectRoot, agentName) {
|
|
40
|
+
return join(resolveAgentDir(projectRoot, agentName), '.pending-cr');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ensureDir(dirPath) {
|
|
44
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
45
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readNote, listNotes } from './notes.js';
|
|
2
|
+
import { readCr, listCrs } from './cr.js';
|
|
3
|
+
import { detectProject } from '../project.js';
|
|
4
|
+
import { readRegistry, getLanes } from '../registry.js';
|
|
5
|
+
|
|
6
|
+
export function loadAgentEntries(projectRoot, agentName) {
|
|
7
|
+
const entries = [];
|
|
8
|
+
const agentShort = agentName.replace(/^Agent_/i, '');
|
|
9
|
+
|
|
10
|
+
// Notes
|
|
11
|
+
const noteRefs = listNotes(projectRoot, agentName);
|
|
12
|
+
for (const ref of noteRefs) {
|
|
13
|
+
const note = readNote(projectRoot, agentName, ref.id);
|
|
14
|
+
if (!note) continue;
|
|
15
|
+
entries.push({
|
|
16
|
+
type: 'NOTE', id: note.id, agentId: agentName, agentShort,
|
|
17
|
+
level: note.level, timestamp: note.timestamp,
|
|
18
|
+
sessionId: note.sessionId, tags: note.tags || [],
|
|
19
|
+
trigger: null, data: note,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// CRs
|
|
24
|
+
const crRefs = listCrs(projectRoot, agentName);
|
|
25
|
+
for (const ref of crRefs) {
|
|
26
|
+
const cr = readCr(projectRoot, agentName, ref.id);
|
|
27
|
+
if (!cr) continue;
|
|
28
|
+
entries.push({
|
|
29
|
+
type: 'CR', id: cr.id, agentId: agentName, agentShort,
|
|
30
|
+
level: null, timestamp: cr.generatedAt || cr.period?.to,
|
|
31
|
+
sessionId: cr.sessionId, tags: [],
|
|
32
|
+
trigger: cr.trigger, data: cr,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tri par timestamp desc
|
|
37
|
+
entries.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
38
|
+
return entries;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function loadProjectTimeline(projectRoot) {
|
|
42
|
+
const project = detectProject(projectRoot);
|
|
43
|
+
if (!project) return [];
|
|
44
|
+
const reg = readRegistry(project.hcm_dir);
|
|
45
|
+
if (!reg) return [];
|
|
46
|
+
const lanes = getLanes(reg);
|
|
47
|
+
if (!lanes || lanes.length === 0) return [];
|
|
48
|
+
|
|
49
|
+
const allEntries = [];
|
|
50
|
+
for (const lane of lanes) {
|
|
51
|
+
const agentEntries = loadAgentEntries(projectRoot, lane.name);
|
|
52
|
+
allEntries.push(...agentEntries);
|
|
53
|
+
}
|
|
54
|
+
allEntries.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
55
|
+
return allEntries;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function applyFilters(entries, filters = {}) {
|
|
59
|
+
let result = entries;
|
|
60
|
+
|
|
61
|
+
if (filters.type) {
|
|
62
|
+
result = result.filter(e => e.type === filters.type);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (filters.levels?.length > 0) {
|
|
66
|
+
result = result.filter(e => {
|
|
67
|
+
if (e.type === 'CR') return true; // CR passe tous les filtres de niveau
|
|
68
|
+
return filters.levels.includes(e.level);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (filters.tags?.length > 0) {
|
|
73
|
+
result = result.filter(e =>
|
|
74
|
+
e.tags.some(t => filters.tags.includes(t))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (filters.dateFrom) {
|
|
79
|
+
result = result.filter(e => (e.timestamp || '') >= filters.dateFrom);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (filters.dateTo) {
|
|
83
|
+
result = result.filter(e => (e.timestamp || '') <= filters.dateTo);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (filters.sessionId) {
|
|
87
|
+
result = result.filter(e => e.sessionId === filters.sessionId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (filters.content === 'decisions') {
|
|
91
|
+
result = result.filter(e => {
|
|
92
|
+
if (e.type === 'CR') return (e.data.decisions?.length > 0);
|
|
93
|
+
return e.level === 'L4' || e.level === 'L5';
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (filters.content === 'actions') {
|
|
98
|
+
result = result.filter(e => {
|
|
99
|
+
if (e.type === 'CR') return (e.data.actions?.length > 0);
|
|
100
|
+
return (e.data.extractedActions?.length > 0);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getAllTags(entries) {
|
|
108
|
+
const tags = new Set();
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
for (const t of e.tags) tags.add(t);
|
|
111
|
+
}
|
|
112
|
+
return [...tags].sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getAllSessions(entries) {
|
|
116
|
+
const sessions = new Set();
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
if (e.sessionId) sessions.add(e.sessionId);
|
|
119
|
+
}
|
|
120
|
+
return [...sessions];
|
|
121
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { resolveRegistryPath, agentShort, ensureDir } from './paths.js';
|
|
4
|
+
|
|
5
|
+
export function readNwRegistry(projectRoot, agentName) {
|
|
6
|
+
const path = resolveRegistryPath(projectRoot, agentName);
|
|
7
|
+
if (!existsSync(path)) {
|
|
8
|
+
return {
|
|
9
|
+
agentId: agentName,
|
|
10
|
+
notes: [],
|
|
11
|
+
crs: [],
|
|
12
|
+
lastCrId: null,
|
|
13
|
+
lastNoteId: null,
|
|
14
|
+
updatedAt: null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')); }
|
|
18
|
+
catch (_e) { /* fallback: corrupted registry */ return { agentId: agentName, notes: [], crs: [], lastCrId: null, lastNoteId: null, updatedAt: null }; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeNwRegistry(projectRoot, agentName, registry) {
|
|
22
|
+
const path = resolveRegistryPath(projectRoot, agentName);
|
|
23
|
+
ensureDir(dirname(path));
|
|
24
|
+
registry.updatedAt = new Date().toISOString();
|
|
25
|
+
writeFileSync(path, JSON.stringify(registry, null, 2) + '\n', 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function nextNoteId(projectRoot, agentName) {
|
|
29
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
30
|
+
const short = agentShort(agentName);
|
|
31
|
+
const prefix = `NOTE-${short}-`;
|
|
32
|
+
const existing = reg.notes.map(n => n.id);
|
|
33
|
+
let max = 0;
|
|
34
|
+
for (const id of existing) {
|
|
35
|
+
const match = id.match(new RegExp(`^${prefix.replace(/[-]/g, '\\-')}(\\d+)$`));
|
|
36
|
+
if (match) max = Math.max(max, parseInt(match[1]));
|
|
37
|
+
}
|
|
38
|
+
return `${prefix}${String(max + 1).padStart(3, '0')}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function nextCrId(projectRoot, agentName) {
|
|
42
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
43
|
+
const short = agentShort(agentName);
|
|
44
|
+
const prefix = `CR-${short}-`;
|
|
45
|
+
const existing = reg.crs.map(c => c.id);
|
|
46
|
+
let max = 0;
|
|
47
|
+
for (const id of existing) {
|
|
48
|
+
const match = id.match(new RegExp(`^${prefix.replace(/[-]/g, '\\-')}(\\d+)$`));
|
|
49
|
+
if (match) max = Math.max(max, parseInt(match[1]));
|
|
50
|
+
}
|
|
51
|
+
return `${prefix}${String(max + 1).padStart(3, '0')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerNote(projectRoot, agentName, noteEntry) {
|
|
55
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
56
|
+
reg.notes.push(noteEntry);
|
|
57
|
+
reg.lastNoteId = noteEntry.id;
|
|
58
|
+
writeNwRegistry(projectRoot, agentName, reg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function registerCr(projectRoot, agentName, crEntry) {
|
|
62
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
63
|
+
reg.crs.push(crEntry);
|
|
64
|
+
reg.lastCrId = crEntry.id;
|
|
65
|
+
writeNwRegistry(projectRoot, agentName, reg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getNotesSinceCr(projectRoot, agentName) {
|
|
69
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
70
|
+
if (!reg.lastCrId) return reg.notes;
|
|
71
|
+
const lastCr = reg.crs.find(c => c.id === reg.lastCrId);
|
|
72
|
+
if (!lastCr) return reg.notes;
|
|
73
|
+
return reg.notes.filter(n => n.timestamp > lastCr.timestamp);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getLastCr(projectRoot, agentName) {
|
|
77
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
78
|
+
if (!reg.lastCrId) return null;
|
|
79
|
+
return reg.crs.find(c => c.id === reg.lastCrId) || null;
|
|
80
|
+
}
|