@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,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox — manage human documents pipeline.
|
|
3
|
+
* Structure: .nemesis/inbox/{pending,processing,done}/
|
|
4
|
+
* Each document has a metadata JSON + the original file content.
|
|
5
|
+
*
|
|
6
|
+
* Lifecycle: pending → processing (dispatch launched) → done (MC created)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, copyFileSync, renameSync, unlinkSync, statSync } from 'node:fs';
|
|
10
|
+
import { join, basename, extname } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const INBOX_DIR = '.nemesis/inbox';
|
|
13
|
+
const SUBDIRS = ['pending', 'processing', 'done'];
|
|
14
|
+
|
|
15
|
+
let _idCounter = 0;
|
|
16
|
+
function nextDocId() {
|
|
17
|
+
const ts = Date.now();
|
|
18
|
+
return `DOC-${ts}-${String(++_idCounter).padStart(3, '0')}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ensure inbox directories exist.
|
|
23
|
+
* @param {string} root - project root
|
|
24
|
+
*/
|
|
25
|
+
export function ensureInboxDirs(root) {
|
|
26
|
+
for (const sub of SUBDIRS) {
|
|
27
|
+
const dir = join(root, INBOX_DIR, sub);
|
|
28
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Add a document to the inbox (copies to pending/).
|
|
34
|
+
*
|
|
35
|
+
* @param {string} root - project root
|
|
36
|
+
* @param {string} filePath - absolute path to the document file
|
|
37
|
+
* @param {object} [opts]
|
|
38
|
+
* @param {string} [opts.title] - human title
|
|
39
|
+
* @param {string} [opts.source] - source (e.g. "email", "slack", "manual")
|
|
40
|
+
* @returns {{ id: string, metadataPath: string, contentPath: string }}
|
|
41
|
+
*/
|
|
42
|
+
export function addDocument(root, filePath, opts = {}) {
|
|
43
|
+
ensureInboxDirs(root);
|
|
44
|
+
|
|
45
|
+
if (!existsSync(filePath)) {
|
|
46
|
+
throw new Error(`Fichier introuvable : ${filePath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ext = extname(filePath);
|
|
50
|
+
const name = basename(filePath, ext);
|
|
51
|
+
const id = nextDocId();
|
|
52
|
+
const contentFilename = `${id}${ext}`;
|
|
53
|
+
const metaFilename = `${id}.meta.json`;
|
|
54
|
+
|
|
55
|
+
const pendingDir = join(root, INBOX_DIR, 'pending');
|
|
56
|
+
const contentPath = join(pendingDir, contentFilename);
|
|
57
|
+
const metadataPath = join(pendingDir, metaFilename);
|
|
58
|
+
|
|
59
|
+
// Copy file content
|
|
60
|
+
copyFileSync(filePath, contentPath);
|
|
61
|
+
|
|
62
|
+
// Write metadata
|
|
63
|
+
const metadata = {
|
|
64
|
+
id,
|
|
65
|
+
title: opts.title || name,
|
|
66
|
+
source: opts.source || 'manual',
|
|
67
|
+
original_path: filePath,
|
|
68
|
+
original_filename: basename(filePath),
|
|
69
|
+
status: 'pending',
|
|
70
|
+
created_at: new Date().toISOString(),
|
|
71
|
+
content_file: contentFilename,
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
|
|
74
|
+
|
|
75
|
+
return { id, metadataPath, contentPath };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add a document from inline text (no file copy).
|
|
80
|
+
*
|
|
81
|
+
* @param {string} root - project root
|
|
82
|
+
* @param {string} text - document text content
|
|
83
|
+
* @param {object} [opts]
|
|
84
|
+
* @param {string} [opts.title] - human title
|
|
85
|
+
* @param {string} [opts.source] - source
|
|
86
|
+
* @returns {{ id: string, metadataPath: string, contentPath: string }}
|
|
87
|
+
*/
|
|
88
|
+
export function addDocumentFromText(root, text, opts = {}) {
|
|
89
|
+
ensureInboxDirs(root);
|
|
90
|
+
|
|
91
|
+
const id = nextDocId();
|
|
92
|
+
const contentFilename = `${id}.md`;
|
|
93
|
+
const metaFilename = `${id}.meta.json`;
|
|
94
|
+
|
|
95
|
+
const pendingDir = join(root, INBOX_DIR, 'pending');
|
|
96
|
+
const contentPath = join(pendingDir, contentFilename);
|
|
97
|
+
const metadataPath = join(pendingDir, metaFilename);
|
|
98
|
+
|
|
99
|
+
writeFileSync(contentPath, text, 'utf-8');
|
|
100
|
+
|
|
101
|
+
const metadata = {
|
|
102
|
+
id,
|
|
103
|
+
title: opts.title || `Document ${id}`,
|
|
104
|
+
source: opts.source || 'inline',
|
|
105
|
+
original_path: null,
|
|
106
|
+
original_filename: null,
|
|
107
|
+
status: 'pending',
|
|
108
|
+
created_at: new Date().toISOString(),
|
|
109
|
+
content_file: contentFilename,
|
|
110
|
+
};
|
|
111
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
|
|
112
|
+
|
|
113
|
+
return { id, metadataPath, contentPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* List documents in a given status directory.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} root - project root
|
|
120
|
+
* @param {'pending'|'processing'|'done'} status
|
|
121
|
+
* @returns {Array<{ id: string, title: string, source: string, status: string, created_at: string, content_file: string }>}
|
|
122
|
+
*/
|
|
123
|
+
export function listDocuments(root, status = 'pending') {
|
|
124
|
+
const dir = join(root, INBOX_DIR, status);
|
|
125
|
+
if (!existsSync(dir)) return [];
|
|
126
|
+
|
|
127
|
+
const metaFiles = readdirSync(dir).filter(f => f.endsWith('.meta.json'));
|
|
128
|
+
const docs = [];
|
|
129
|
+
|
|
130
|
+
for (const file of metaFiles) {
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
133
|
+
docs.push(data);
|
|
134
|
+
} catch (_e) { /* fallback: skip corrupted meta */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sort by created_at desc
|
|
138
|
+
docs.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
|
|
139
|
+
return docs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read a document's content.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} root - project root
|
|
146
|
+
* @param {string} docId - document ID (e.g. "DOC-1710496800000")
|
|
147
|
+
* @param {'pending'|'processing'|'done'} [status]
|
|
148
|
+
* @returns {{ metadata: object, content: string } | null}
|
|
149
|
+
*/
|
|
150
|
+
export function readDocument(root, docId, status) {
|
|
151
|
+
const statuses = status ? [status] : SUBDIRS;
|
|
152
|
+
|
|
153
|
+
for (const s of statuses) {
|
|
154
|
+
const dir = join(root, INBOX_DIR, s);
|
|
155
|
+
const metaPath = join(dir, `${docId}.meta.json`);
|
|
156
|
+
if (!existsSync(metaPath)) continue;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const metadata = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
160
|
+
const contentPath = join(dir, metadata.content_file);
|
|
161
|
+
const content = existsSync(contentPath) ? readFileSync(contentPath, 'utf-8') : '';
|
|
162
|
+
return { metadata, content };
|
|
163
|
+
} catch (_e) { /* fallback: unreadable document */ continue; }
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Move a document between status directories.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} root
|
|
172
|
+
* @param {string} docId
|
|
173
|
+
* @param {'pending'|'processing'|'done'} fromStatus
|
|
174
|
+
* @param {'pending'|'processing'|'done'} toStatus
|
|
175
|
+
* @param {object} [extraMeta] - extra metadata to merge
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
export function moveDocument(root, docId, fromStatus, toStatus, extraMeta = {}) {
|
|
179
|
+
ensureInboxDirs(root);
|
|
180
|
+
|
|
181
|
+
const fromDir = join(root, INBOX_DIR, fromStatus);
|
|
182
|
+
const toDir = join(root, INBOX_DIR, toStatus);
|
|
183
|
+
const metaPath = join(fromDir, `${docId}.meta.json`);
|
|
184
|
+
|
|
185
|
+
if (!existsSync(metaPath)) return false;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const metadata = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
189
|
+
const contentFile = metadata.content_file;
|
|
190
|
+
|
|
191
|
+
// Move content file
|
|
192
|
+
const fromContent = join(fromDir, contentFile);
|
|
193
|
+
const toContent = join(toDir, contentFile);
|
|
194
|
+
if (existsSync(fromContent)) {
|
|
195
|
+
renameSync(fromContent, toContent);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Update + move metadata
|
|
199
|
+
metadata.status = toStatus;
|
|
200
|
+
metadata[`${toStatus}_at`] = new Date().toISOString();
|
|
201
|
+
Object.assign(metadata, extraMeta);
|
|
202
|
+
|
|
203
|
+
const toMeta = join(toDir, `${docId}.meta.json`);
|
|
204
|
+
writeFileSync(toMeta, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
|
|
205
|
+
|
|
206
|
+
// Remove old meta
|
|
207
|
+
try { unlinkSync(metaPath); } catch (_e) { /* fallback: already moved */ }
|
|
208
|
+
|
|
209
|
+
return true;
|
|
210
|
+
} catch (_e) { /* fallback: move failed */ return false; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Mark a document as processing (dispatch launched).
|
|
215
|
+
*
|
|
216
|
+
* @param {string} root
|
|
217
|
+
* @param {string} docId
|
|
218
|
+
* @param {object} [dispatchInfo] - { txnId, agentName }
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
export function markProcessing(root, docId, dispatchInfo = {}) {
|
|
222
|
+
return moveDocument(root, docId, 'pending', 'processing', {
|
|
223
|
+
dispatch_txn_id: dispatchInfo.txnId || null,
|
|
224
|
+
dispatch_agent: dispatchInfo.agentName || null,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Mark a document as done (MC created).
|
|
230
|
+
*
|
|
231
|
+
* @param {string} root
|
|
232
|
+
* @param {string} docId
|
|
233
|
+
* @param {object} [mcInfo] - { missionId }
|
|
234
|
+
* @returns {boolean}
|
|
235
|
+
*/
|
|
236
|
+
export function markDone(root, docId, mcInfo = {}) {
|
|
237
|
+
return moveDocument(root, docId, 'processing', 'done', {
|
|
238
|
+
mission_id: mcInfo.missionId || null,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Cleanup done documents older than maxDays.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} root
|
|
246
|
+
* @param {number} [maxDays=30]
|
|
247
|
+
* @returns {number} count of cleaned documents
|
|
248
|
+
*/
|
|
249
|
+
export function cleanupDone(root, maxDays = 30) {
|
|
250
|
+
const doneDir = join(root, INBOX_DIR, 'done');
|
|
251
|
+
if (!existsSync(doneDir)) return 0;
|
|
252
|
+
|
|
253
|
+
const cleanAll = maxDays <= 0;
|
|
254
|
+
const cutoff = cleanAll ? Infinity : Date.now() - (maxDays * 24 * 60 * 60 * 1000);
|
|
255
|
+
let count = 0;
|
|
256
|
+
|
|
257
|
+
const metaFiles = readdirSync(doneDir).filter(f => f.endsWith('.meta.json'));
|
|
258
|
+
for (const file of metaFiles) {
|
|
259
|
+
const filepath = join(doneDir, file);
|
|
260
|
+
try {
|
|
261
|
+
const stat = statSync(filepath);
|
|
262
|
+
if (cleanAll || stat.mtimeMs < cutoff) {
|
|
263
|
+
const metadata = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
264
|
+
// Remove content file
|
|
265
|
+
const contentPath = join(doneDir, metadata.content_file);
|
|
266
|
+
try { unlinkSync(contentPath); } catch (_e) { /* fallback: already removed */ }
|
|
267
|
+
// Remove meta
|
|
268
|
+
unlinkSync(filepath);
|
|
269
|
+
count++;
|
|
270
|
+
}
|
|
271
|
+
} catch (_e) { /* fallback: skip corrupted entry */ }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return count;
|
|
275
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal logger — writes to stderr to avoid polluting stdout (reserved for hooks).
|
|
3
|
+
* Activate debug output via NEMESIS_VERBOSE=1 or --verbose flag.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function isVerbose() {
|
|
7
|
+
return process.env.NEMESIS_VERBOSE === '1' || process.argv.includes('--verbose');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function warn(msg) {
|
|
11
|
+
process.stderr.write(`[nemesis:warn] ${msg}\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function error(msg) {
|
|
15
|
+
process.stderr.write(`[nemesis:error] ${msg}\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function debug(msg) {
|
|
19
|
+
if (isVerbose()) process.stderr.write(`[nemesis:debug] ${msg}\n`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { writeFileSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadTemplate, mergeTemplate } from './templates.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Init a Mission Contract — creates a DRAFT MCT-*.json.
|
|
7
|
+
*/
|
|
8
|
+
export function initMission(id, opts = {}) {
|
|
9
|
+
const { title = '', summary = '', root = process.cwd(), projectId = '', owner = 'PM' } = opts;
|
|
10
|
+
|
|
11
|
+
const hcmDir = join(root, '.nemesis', 'HCM');
|
|
12
|
+
const contractsDir = join(hcmDir, 'contracts');
|
|
13
|
+
|
|
14
|
+
if (!existsSync(contractsDir)) {
|
|
15
|
+
throw new Error('.nemesis/HCM/contracts/ introuvable. Lancez "nemesis init" d\'abord.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const template = loadTemplate('mission-contract', root);
|
|
19
|
+
const contract = mergeTemplate(template, {
|
|
20
|
+
contract_meta: {
|
|
21
|
+
id,
|
|
22
|
+
mission_id: `MSN-${id.replace('MCT-', '')}`,
|
|
23
|
+
project_id: projectId,
|
|
24
|
+
status: 'DRAFT',
|
|
25
|
+
owner,
|
|
26
|
+
created_at: new Date().toISOString(),
|
|
27
|
+
updated_at: new Date().toISOString(),
|
|
28
|
+
},
|
|
29
|
+
contract_payload: {
|
|
30
|
+
cadrage: { title, summary },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const filepath = join(contractsDir, `${id}.json`);
|
|
35
|
+
if (existsSync(filepath)) {
|
|
36
|
+
throw new Error(`Mission Contract ${id} existe deja.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
writeFileSync(filepath, JSON.stringify(contract, null, 2) + '\n', 'utf-8');
|
|
40
|
+
return { filepath, id, status: 'DRAFT' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* List all Mission Contracts.
|
|
45
|
+
*/
|
|
46
|
+
export function listMissions(hcmDir) {
|
|
47
|
+
const contractsDir = join(hcmDir, 'contracts');
|
|
48
|
+
if (!existsSync(contractsDir)) return [];
|
|
49
|
+
|
|
50
|
+
return readdirSync(contractsDir)
|
|
51
|
+
.filter(f => f.startsWith('MCT-') && f.endsWith('.json'))
|
|
52
|
+
.map(f => {
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.parse(readFileSync(join(contractsDir, f), 'utf-8'));
|
|
55
|
+
return {
|
|
56
|
+
id: data.contract_meta?.id || f.replace('.json', ''),
|
|
57
|
+
title: data.contract_payload?.cadrage?.title || '',
|
|
58
|
+
status: data.contract_meta?.status || 'UNKNOWN',
|
|
59
|
+
};
|
|
60
|
+
} catch (_e) { /* fallback: malformed contract JSON */
|
|
61
|
+
return { id: f.replace('.json', ''), title: '', status: 'ERROR' };
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate a Mission Contract — transitions DRAFT → VALIDATED.
|
|
68
|
+
* @param {string} id - MCT ID
|
|
69
|
+
* @param {string} hcmDir
|
|
70
|
+
* @returns {{ id: string, status: string }}
|
|
71
|
+
*/
|
|
72
|
+
export function validateMission(id, hcmDir) {
|
|
73
|
+
const contractsDir = join(hcmDir, 'contracts');
|
|
74
|
+
const filepath = join(contractsDir, `${id}.json`);
|
|
75
|
+
|
|
76
|
+
if (!existsSync(filepath)) {
|
|
77
|
+
throw new Error(`Mission Contract ${id} introuvable.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
81
|
+
const status = data.contract_meta?.status;
|
|
82
|
+
|
|
83
|
+
if (status === 'VALIDATED') {
|
|
84
|
+
throw new Error(`Mission Contract ${id} est deja VALIDATED.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (data.contract_meta) {
|
|
88
|
+
data.contract_meta.status = 'VALIDATED';
|
|
89
|
+
data.contract_meta.validated_at = new Date().toISOString();
|
|
90
|
+
data.contract_meta.updated_at = new Date().toISOString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
94
|
+
return { id, status: 'VALIDATED' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inspect a specific Mission Contract — full content.
|
|
99
|
+
*/
|
|
100
|
+
export function inspectMission(id, hcmDir) {
|
|
101
|
+
const contractsDir = join(hcmDir, 'contracts');
|
|
102
|
+
const filepath = join(contractsDir, `${id}.json`);
|
|
103
|
+
|
|
104
|
+
if (!existsSync(filepath)) {
|
|
105
|
+
throw new Error(`Mission Contract ${id} introuvable.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
109
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const NOTEWRITER_DEFAULTS = {
|
|
5
|
+
note_enabled: true,
|
|
6
|
+
note_min_level: 'L1',
|
|
7
|
+
cr_enabled: true,
|
|
8
|
+
cr_timer_minutes: 30,
|
|
9
|
+
cr_min_level_for_inclusion: 'L3',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const VALID_LEVELS = ['L1', 'L2', 'L3', 'L4', 'L5'];
|
|
13
|
+
|
|
14
|
+
export function readNotewriterConfig(projectRoot) {
|
|
15
|
+
const servicesPath = join(projectRoot, '.nemesis', 'services.json');
|
|
16
|
+
if (!existsSync(servicesPath)) return { ...NOTEWRITER_DEFAULTS };
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(readFileSync(servicesPath, 'utf-8'));
|
|
19
|
+
const nw = data?.services?.notewriter || {};
|
|
20
|
+
return { ...NOTEWRITER_DEFAULTS, ...nw };
|
|
21
|
+
} catch (_e) { /* fallback: corrupted services config */
|
|
22
|
+
return { ...NOTEWRITER_DEFAULTS };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeNotewriterConfig(projectRoot, config) {
|
|
27
|
+
const servicesPath = join(projectRoot, '.nemesis', 'services.json');
|
|
28
|
+
let data = {};
|
|
29
|
+
if (existsSync(servicesPath)) {
|
|
30
|
+
try { data = JSON.parse(readFileSync(servicesPath, 'utf-8')); }
|
|
31
|
+
catch (_e) { /* fallback: corrupted services config */ data = {}; }
|
|
32
|
+
}
|
|
33
|
+
if (!data.services) data.services = {};
|
|
34
|
+
data.services.notewriter = config;
|
|
35
|
+
writeFileSync(servicesPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
36
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveCrDir, resolvePendingCrPath, ensureDir } from './paths.js';
|
|
4
|
+
import { readNwRegistry, nextCrId, registerCr, getLastCr, getNotesSinceCr } from './registry.js';
|
|
5
|
+
import { readNote } from './notes.js';
|
|
6
|
+
import { error as logError } from '../logger.js';
|
|
7
|
+
|
|
8
|
+
const CR_SYSTEM_PROMPT = `Tu es un systeme d'observation silencieux.
|
|
9
|
+
Tu produis un compte-rendu incremental a partir de notes d'observation.
|
|
10
|
+
|
|
11
|
+
## Contexte
|
|
12
|
+
Tu recois :
|
|
13
|
+
- Un resume du CR precedent (si existant)
|
|
14
|
+
- Les notes d'observation de la periode courante
|
|
15
|
+
|
|
16
|
+
## Format de sortie
|
|
17
|
+
|
|
18
|
+
Reponds UNIQUEMENT avec un JSON valide :
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
"title": "Titre synthetique de la periode",
|
|
22
|
+
"summary": "Synthese narrative de la periode (3-8 phrases)",
|
|
23
|
+
"decisions": ["Decision 1 actee", "Decision 2 actee"],
|
|
24
|
+
"actions": ["Action en attente 1", "Action en attente 2"],
|
|
25
|
+
"recontextualization": "Bloc narratif pret a injecter a l'agent. Contient : ou on en est, ce qui a ete decide, ce qui reste a faire, etat de la mission. 5-10 phrases."
|
|
26
|
+
}`;
|
|
27
|
+
|
|
28
|
+
export { CR_SYSTEM_PROMPT };
|
|
29
|
+
|
|
30
|
+
export function buildCrPrompt(previousCrSummary, notes) {
|
|
31
|
+
let prompt = '';
|
|
32
|
+
if (previousCrSummary) {
|
|
33
|
+
prompt += `## Resume du CR precedent\n${previousCrSummary}\n\n`;
|
|
34
|
+
}
|
|
35
|
+
prompt += `## Notes de la periode (${notes.length} notes)\n\n`;
|
|
36
|
+
for (const note of notes) {
|
|
37
|
+
prompt += `### ${note.id} [${note.level}]\n`;
|
|
38
|
+
prompt += `${note.content}\n`;
|
|
39
|
+
if (note.extractedActions?.length > 0) {
|
|
40
|
+
prompt += `Actions : ${note.extractedActions.join(', ')}\n`;
|
|
41
|
+
}
|
|
42
|
+
prompt += '\n';
|
|
43
|
+
}
|
|
44
|
+
return prompt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseCrResponse(text) {
|
|
48
|
+
try {
|
|
49
|
+
let json = text.trim();
|
|
50
|
+
if (json.startsWith('```')) {
|
|
51
|
+
json = json.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '');
|
|
52
|
+
}
|
|
53
|
+
const parsed = JSON.parse(json);
|
|
54
|
+
return {
|
|
55
|
+
title: parsed.title || 'CR sans titre',
|
|
56
|
+
summary: parsed.summary || '',
|
|
57
|
+
decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
|
|
58
|
+
actions: Array.isArray(parsed.actions) ? parsed.actions : [],
|
|
59
|
+
recontextualization: parsed.recontextualization || '',
|
|
60
|
+
};
|
|
61
|
+
} catch (_e) {
|
|
62
|
+
return {
|
|
63
|
+
title: 'CR non parsable',
|
|
64
|
+
summary: text.slice(0, 500),
|
|
65
|
+
decisions: [], actions: [],
|
|
66
|
+
recontextualization: text.slice(0, 300),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function generateCR(projectRoot, agentName, trigger, callLlm) {
|
|
72
|
+
// 1. Notes depuis le dernier CR
|
|
73
|
+
const noteEntries = getNotesSinceCr(projectRoot, agentName);
|
|
74
|
+
if (noteEntries.length === 0) return null;
|
|
75
|
+
|
|
76
|
+
// 2. Charger les notes completes
|
|
77
|
+
const notes = [];
|
|
78
|
+
for (const entry of noteEntries) {
|
|
79
|
+
const note = readNote(projectRoot, agentName, entry.id);
|
|
80
|
+
if (note) notes.push(note);
|
|
81
|
+
}
|
|
82
|
+
if (notes.length === 0) return null;
|
|
83
|
+
|
|
84
|
+
// 3. CR precedent
|
|
85
|
+
const lastCr = getLastCr(projectRoot, agentName);
|
|
86
|
+
let previousCrSummary = null;
|
|
87
|
+
let previousCrId = null;
|
|
88
|
+
if (lastCr) {
|
|
89
|
+
previousCrId = lastCr.id;
|
|
90
|
+
const crDir = resolveCrDir(projectRoot, agentName);
|
|
91
|
+
const crPath = join(crDir, `${lastCr.id}.json`);
|
|
92
|
+
if (existsSync(crPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const crData = JSON.parse(readFileSync(crPath, 'utf-8'));
|
|
95
|
+
previousCrSummary = crData.summary || null;
|
|
96
|
+
} catch (_e) { /* fallback: previous CR unreadable */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Appeler le LLM
|
|
101
|
+
const userPrompt = buildCrPrompt(previousCrSummary, notes);
|
|
102
|
+
let llmResponse;
|
|
103
|
+
try {
|
|
104
|
+
llmResponse = await callLlm(CR_SYSTEM_PROMPT, userPrompt);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
logError(`NoteWriter: erreur LLM CR — ${err.message}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 5. Parser
|
|
111
|
+
const parsed = parseCrResponse(llmResponse);
|
|
112
|
+
|
|
113
|
+
// 6. Construire le CR
|
|
114
|
+
const crId = nextCrId(projectRoot, agentName);
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
const cr = {
|
|
117
|
+
id: crId,
|
|
118
|
+
projectId: null,
|
|
119
|
+
agentId: agentName,
|
|
120
|
+
sessionId: notes[0]?.sessionId || null,
|
|
121
|
+
title: parsed.title,
|
|
122
|
+
trigger,
|
|
123
|
+
period: {
|
|
124
|
+
from: notes[0]?.timestamp || now,
|
|
125
|
+
to: notes[notes.length - 1]?.timestamp || now,
|
|
126
|
+
},
|
|
127
|
+
previousCrId,
|
|
128
|
+
previousCrSummary: previousCrSummary || null,
|
|
129
|
+
summary: parsed.summary,
|
|
130
|
+
decisions: parsed.decisions,
|
|
131
|
+
actions: parsed.actions,
|
|
132
|
+
noteIds: notes.map(n => n.id),
|
|
133
|
+
noteCount: notes.length,
|
|
134
|
+
recontextualization: parsed.recontextualization,
|
|
135
|
+
generatedAt: now,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// 7. Ecrire le fichier CR
|
|
139
|
+
const crDir = resolveCrDir(projectRoot, agentName);
|
|
140
|
+
ensureDir(crDir);
|
|
141
|
+
writeFileSync(join(crDir, `${crId}.json`), JSON.stringify(cr, null, 2) + '\n', 'utf-8');
|
|
142
|
+
|
|
143
|
+
// 8. Registry
|
|
144
|
+
registerCr(projectRoot, agentName, {
|
|
145
|
+
id: crId, timestamp: now, trigger,
|
|
146
|
+
noteCount: notes.length, previousCrId,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 9. .pending-cr
|
|
150
|
+
writePendingCr(projectRoot, agentName, cr);
|
|
151
|
+
|
|
152
|
+
return cr;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Timer lazy ---
|
|
156
|
+
|
|
157
|
+
export function shouldGenerateCR(projectRoot, agentName, configTimerMinutes = 30) {
|
|
158
|
+
const noteEntries = getNotesSinceCr(projectRoot, agentName);
|
|
159
|
+
if (noteEntries.length === 0) return false;
|
|
160
|
+
|
|
161
|
+
const oldestNote = noteEntries[0];
|
|
162
|
+
if (!oldestNote.timestamp) return false;
|
|
163
|
+
|
|
164
|
+
const elapsed = (Date.now() - new Date(oldestNote.timestamp).getTime()) / (1000 * 60);
|
|
165
|
+
return elapsed >= configTimerMinutes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Read / List ---
|
|
169
|
+
|
|
170
|
+
export function readCr(projectRoot, agentName, crId) {
|
|
171
|
+
const crDir = resolveCrDir(projectRoot, agentName);
|
|
172
|
+
const crPath = join(crDir, `${crId}.json`);
|
|
173
|
+
if (!existsSync(crPath)) return null;
|
|
174
|
+
try { return JSON.parse(readFileSync(crPath, 'utf-8')); }
|
|
175
|
+
catch (_e) { /* fallback: corrupted CR */ return null; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function listCrs(projectRoot, agentName) {
|
|
179
|
+
const reg = readNwRegistry(projectRoot, agentName);
|
|
180
|
+
return reg.crs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- .pending-cr ---
|
|
184
|
+
|
|
185
|
+
export function writePendingCr(projectRoot, agentName, cr) {
|
|
186
|
+
const pendingPath = resolvePendingCrPath(projectRoot, agentName);
|
|
187
|
+
ensureDir(join(pendingPath, '..'));
|
|
188
|
+
const payload = {
|
|
189
|
+
crId: cr.id,
|
|
190
|
+
recontextualization: cr.recontextualization,
|
|
191
|
+
summary: cr.summary,
|
|
192
|
+
decisions: cr.decisions,
|
|
193
|
+
actions: cr.actions,
|
|
194
|
+
writtenAt: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
writeFileSync(pendingPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function consumePendingCr(projectRoot, agentName) {
|
|
200
|
+
const pendingPath = resolvePendingCrPath(projectRoot, agentName);
|
|
201
|
+
if (!existsSync(pendingPath)) return null;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const data = JSON.parse(readFileSync(pendingPath, 'utf-8'));
|
|
205
|
+
|
|
206
|
+
// TTL 1h
|
|
207
|
+
if (data.writtenAt) {
|
|
208
|
+
const age = Date.now() - new Date(data.writtenAt).getTime();
|
|
209
|
+
if (age > 60 * 60 * 1000) {
|
|
210
|
+
unlinkSync(pendingPath);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Consommer
|
|
216
|
+
unlinkSync(pendingPath);
|
|
217
|
+
return formatCrForInjection(data);
|
|
218
|
+
} catch (_e) {
|
|
219
|
+
try { unlinkSync(pendingPath); } catch (_e) { /* fallback: already removed */ }
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function formatCrForInjection(data) {
|
|
225
|
+
let text = `Voici le compte-rendu de la derniere periode de travail :\n\n`;
|
|
226
|
+
text += `**${data.crId}**\n\n`;
|
|
227
|
+
text += data.recontextualization || data.summary || '';
|
|
228
|
+
if (data.decisions?.length > 0) {
|
|
229
|
+
text += `\n\n**Decisions actees :**\n`;
|
|
230
|
+
for (const d of data.decisions) text += `- ${d}\n`;
|
|
231
|
+
}
|
|
232
|
+
if (data.actions?.length > 0) {
|
|
233
|
+
text += `\n\n**Actions en attente :**\n`;
|
|
234
|
+
for (const a of data.actions) text += `- ${a}\n`;
|
|
235
|
+
}
|
|
236
|
+
return text;
|
|
237
|
+
}
|