@yemi33/minions 0.1.1
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/CHANGELOG.md +819 -0
- package/LICENSE +21 -0
- package/README.md +598 -0
- package/agents/dallas/charter.md +56 -0
- package/agents/lambert/charter.md +67 -0
- package/agents/ralph/charter.md +45 -0
- package/agents/rebecca/charter.md +57 -0
- package/agents/ripley/charter.md +47 -0
- package/bin/minions.js +467 -0
- package/config.template.json +28 -0
- package/dashboard.html +4822 -0
- package/dashboard.js +2623 -0
- package/docs/auto-discovery.md +416 -0
- package/docs/blog-first-successful-dispatch.md +128 -0
- package/docs/command-center.md +156 -0
- package/docs/demo/01-dashboard-overview.gif +0 -0
- package/docs/demo/02-command-center.gif +0 -0
- package/docs/demo/03-work-items.gif +0 -0
- package/docs/demo/04-plan-docchat.gif +0 -0
- package/docs/demo/05-prd-progress.gif +0 -0
- package/docs/demo/06-inbox-metrics.gif +0 -0
- package/docs/deprecated.json +83 -0
- package/docs/distribution.md +96 -0
- package/docs/engine-restart.md +92 -0
- package/docs/human-vs-automated.md +108 -0
- package/docs/index.html +221 -0
- package/docs/plan-lifecycle.md +140 -0
- package/docs/self-improvement.md +344 -0
- package/engine/ado-mcp-wrapper.js +42 -0
- package/engine/ado.js +383 -0
- package/engine/check-status.js +23 -0
- package/engine/cli.js +754 -0
- package/engine/consolidation.js +417 -0
- package/engine/github.js +331 -0
- package/engine/lifecycle.js +1113 -0
- package/engine/llm.js +116 -0
- package/engine/queries.js +677 -0
- package/engine/shared.js +397 -0
- package/engine/spawn-agent.js +151 -0
- package/engine.js +3227 -0
- package/minions.js +556 -0
- package/package.json +48 -0
- package/playbooks/ask.md +49 -0
- package/playbooks/build-and-test.md +155 -0
- package/playbooks/explore.md +64 -0
- package/playbooks/fix.md +57 -0
- package/playbooks/implement-shared.md +68 -0
- package/playbooks/implement.md +95 -0
- package/playbooks/plan-to-prd.md +104 -0
- package/playbooks/plan.md +99 -0
- package/playbooks/review.md +68 -0
- package/playbooks/test.md +75 -0
- package/playbooks/verify.md +190 -0
- package/playbooks/work-item.md +74 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/consolidation.js — Inbox note consolidation for Minions engine.
|
|
3
|
+
* Extracted from engine.js: LLM-powered and regex fallback consolidation,
|
|
4
|
+
* knowledge base classification, inbox archival.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const shared = require('./shared');
|
|
10
|
+
const { safeRead, safeWrite, safeUnlink, runFile, cleanChildEnv,
|
|
11
|
+
parseStreamJsonOutput, classifyInboxItem, KB_CATEGORIES } = shared;
|
|
12
|
+
const { trackEngineUsage } = require('./llm');
|
|
13
|
+
const queries = require('./queries');
|
|
14
|
+
const { getInboxFiles, getNotes, INBOX_DIR, ENGINE_DIR, MINIONS_DIR,
|
|
15
|
+
NOTES_PATH, KNOWLEDGE_DIR, ARCHIVE_DIR } = queries;
|
|
16
|
+
|
|
17
|
+
// Lazy require — only for log() and dateStamp() which live on engine.js
|
|
18
|
+
let _engine = null;
|
|
19
|
+
function engine() {
|
|
20
|
+
if (!_engine) _engine = require('../engine');
|
|
21
|
+
return _engine;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Track in-flight LLM consolidation to prevent concurrent runs
|
|
25
|
+
let _consolidationInFlight = false;
|
|
26
|
+
let _consolidationStartedAt = 0;
|
|
27
|
+
const _processingFiles = new Set(); // files currently being consolidated (race guard)
|
|
28
|
+
|
|
29
|
+
function consolidateInbox(config) {
|
|
30
|
+
const e = engine();
|
|
31
|
+
const { ENGINE_DEFAULTS } = shared;
|
|
32
|
+
const threshold = config.engine?.inboxConsolidateThreshold || ENGINE_DEFAULTS.inboxConsolidateThreshold;
|
|
33
|
+
const files = getInboxFiles().filter(f => !_processingFiles.has(f));
|
|
34
|
+
if (files.length < threshold) return;
|
|
35
|
+
// Auto-reset stale flag if consolidation has been running for >5 minutes (process died without cleanup)
|
|
36
|
+
if (_consolidationInFlight && (Date.now() - _consolidationStartedAt) > 300000) {
|
|
37
|
+
e.log('warn', 'Consolidation flag was stale (>5m) — resetting');
|
|
38
|
+
_consolidationInFlight = false;
|
|
39
|
+
_processingFiles.clear();
|
|
40
|
+
}
|
|
41
|
+
if (_consolidationInFlight) return;
|
|
42
|
+
|
|
43
|
+
e.log('info', `Consolidating ${files.length} inbox items into notes.md`);
|
|
44
|
+
|
|
45
|
+
const items = files.map(f => ({
|
|
46
|
+
name: f,
|
|
47
|
+
content: safeRead(path.join(INBOX_DIR, f))
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const existingNotes = getNotes() || '';
|
|
51
|
+
consolidateWithLLM(items, existingNotes, files, config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── LLM-Powered Consolidation ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function buildConsolidationPrompt(items, existingNotes, kbPaths) {
|
|
57
|
+
const e = engine();
|
|
58
|
+
const kbRefBlock = kbPaths.map(p => `- \`${p.file}\` \u2192 \`${p.kbPath}\``).join('\n');
|
|
59
|
+
const notesBlock = items.map(item =>
|
|
60
|
+
`<note file="${item.name}">\n${(item.content || '').slice(0, 8000)}\n</note>`
|
|
61
|
+
).join('\n\n');
|
|
62
|
+
const existingTail = existingNotes.length > 2000
|
|
63
|
+
? '...\n' + existingNotes.slice(-2000)
|
|
64
|
+
: existingNotes;
|
|
65
|
+
|
|
66
|
+
return `You are a knowledge manager for a software engineering minions. Your job is to consolidate agent notes into team memory.
|
|
67
|
+
|
|
68
|
+
## Inbox Notes to Process
|
|
69
|
+
|
|
70
|
+
${notesBlock}
|
|
71
|
+
|
|
72
|
+
## Existing Team Notes (for deduplication — do NOT repeat what's already here)
|
|
73
|
+
|
|
74
|
+
<existing_notes>
|
|
75
|
+
${existingTail}
|
|
76
|
+
</existing_notes>
|
|
77
|
+
|
|
78
|
+
## Instructions
|
|
79
|
+
|
|
80
|
+
Read every inbox note carefully. Produce a consolidated digest following these rules:
|
|
81
|
+
|
|
82
|
+
1. **Extract actionable knowledge only**: patterns, conventions, gotchas, warnings, build results, architectural decisions, review findings. Skip boilerplate (dates, filenames, task IDs).
|
|
83
|
+
|
|
84
|
+
2. **Deduplicate aggressively**: If an insight already exists in the existing team notes, skip it entirely. If multiple agents report the same finding, merge into one entry and credit all agents.
|
|
85
|
+
|
|
86
|
+
3. **Write concisely**: Each insight should be 1-2 sentences max. Use **bold key** at the start of each bullet.
|
|
87
|
+
|
|
88
|
+
4. **Group by category**: Use these exact headers (only include categories that have content):
|
|
89
|
+
- \`#### Patterns & Conventions\`
|
|
90
|
+
- \`#### Build & Test Results\`
|
|
91
|
+
- \`#### PR Review Findings\`
|
|
92
|
+
- \`#### Bugs & Gotchas\`
|
|
93
|
+
- \`#### Architecture Notes\`
|
|
94
|
+
- \`#### Action Items\`
|
|
95
|
+
|
|
96
|
+
5. **Attribute sources**: End each bullet with _(agentName)_ or _(agent1, agent2)_ if multiple.
|
|
97
|
+
|
|
98
|
+
6. **Write a descriptive title**: First line must be a single-line title summarizing what was learned. Do NOT use generic text like "Consolidated from N items".
|
|
99
|
+
|
|
100
|
+
7. **Reference the knowledge base**: Each note is being filed into the knowledge base at these paths. After each insight bullet, add a reference link so readers know where to find the full detail:
|
|
101
|
+
${kbRefBlock}
|
|
102
|
+
Format: \`\u2192 see knowledge/category/filename.md\` on a new line after the insight, indented.
|
|
103
|
+
|
|
104
|
+
## Output Format
|
|
105
|
+
|
|
106
|
+
Respond with ONLY the markdown below — no preamble, no explanation, no code fences:
|
|
107
|
+
|
|
108
|
+
### YYYY-MM-DD: <descriptive title>
|
|
109
|
+
**By:** Engine (LLM-consolidated)
|
|
110
|
+
|
|
111
|
+
#### Category Name
|
|
112
|
+
- **Bold key**: insight text _(agent)_
|
|
113
|
+
\u2192 see \`knowledge/category/filename.md\`
|
|
114
|
+
|
|
115
|
+
_Processed N notes, M insights extracted, K duplicates removed._
|
|
116
|
+
|
|
117
|
+
Use today's date: ${e.dateStamp()}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function consolidateWithLLM(items, existingNotes, files, config) {
|
|
121
|
+
const e = engine();
|
|
122
|
+
_consolidationInFlight = true;
|
|
123
|
+
_consolidationStartedAt = Date.now();
|
|
124
|
+
for (const f of files) _processingFiles.add(f);
|
|
125
|
+
|
|
126
|
+
const kbPaths = items.map(item => {
|
|
127
|
+
const cat = classifyInboxItem(item.name, item.content);
|
|
128
|
+
const agentMatch = item.name.match(/^(\w+)-/);
|
|
129
|
+
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
|
130
|
+
const titleMatch = (item.content || '').match(/^#\s+(.+)/m);
|
|
131
|
+
const titleSlug = titleMatch ? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50) : item.name.replace(/\.md$/, '');
|
|
132
|
+
return { file: item.name, category: cat, kbPath: path.join('knowledge', cat, `${e.dateStamp()}-${agent}-${titleSlug}.md`) };
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const prompt = buildConsolidationPrompt(items, existingNotes, kbPaths);
|
|
136
|
+
|
|
137
|
+
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
138
|
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
139
|
+
const promptPath = path.join(tmpDir, 'consolidate-prompt.md');
|
|
140
|
+
safeWrite(promptPath, prompt);
|
|
141
|
+
|
|
142
|
+
const sysPrompt = 'You are a concise knowledge manager. Output only markdown. No preamble. No code fences around your output.';
|
|
143
|
+
const sysPromptPath = path.join(tmpDir, 'consolidate-sysprompt.md');
|
|
144
|
+
safeWrite(sysPromptPath, sysPrompt);
|
|
145
|
+
|
|
146
|
+
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
147
|
+
const args = [
|
|
148
|
+
'--output-format', 'stream-json',
|
|
149
|
+
'--max-turns', '1',
|
|
150
|
+
'--model', 'haiku',
|
|
151
|
+
'--permission-mode', 'bypassPermissions',
|
|
152
|
+
'--verbose',
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
e.log('info', 'Spawning Haiku for LLM consolidation...');
|
|
156
|
+
|
|
157
|
+
const proc = runFile(process.execPath, [spawnScript, promptPath, sysPromptPath, ...args], {
|
|
158
|
+
cwd: MINIONS_DIR,
|
|
159
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
160
|
+
env: cleanChildEnv()
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
let stdout = '';
|
|
164
|
+
let stderr = '';
|
|
165
|
+
proc.stdout.on('data', d => { stdout += d.toString(); if (stdout.length > 100000) stdout = stdout.slice(-50000); });
|
|
166
|
+
proc.stderr.on('data', d => { stderr += d.toString(); if (stderr.length > 50000) stderr = stderr.slice(-25000); });
|
|
167
|
+
|
|
168
|
+
const timeout = setTimeout(() => {
|
|
169
|
+
e.log('warn', 'LLM consolidation timed out after 3m — killing and falling back to regex');
|
|
170
|
+
try { proc.kill('SIGTERM'); } catch {}
|
|
171
|
+
// Escalate to SIGKILL after 10s if process doesn't exit
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
174
|
+
if (_consolidationInFlight) {
|
|
175
|
+
_consolidationInFlight = false;
|
|
176
|
+
_processingFiles.clear();
|
|
177
|
+
e.log('warn', 'Consolidation flag force-reset after SIGKILL');
|
|
178
|
+
}
|
|
179
|
+
}, 10000);
|
|
180
|
+
}, 180000);
|
|
181
|
+
|
|
182
|
+
function _clearProcessingState() {
|
|
183
|
+
for (const f of files) _processingFiles.delete(f);
|
|
184
|
+
_consolidationInFlight = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
proc.on('close', (code) => {
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
safeUnlink(promptPath);
|
|
190
|
+
safeUnlink(sysPromptPath);
|
|
191
|
+
|
|
192
|
+
const parsed = parseStreamJsonOutput(stdout);
|
|
193
|
+
const extractedText = parsed.text;
|
|
194
|
+
trackEngineUsage('consolidation', parsed.usage);
|
|
195
|
+
|
|
196
|
+
if (code === 0 && (extractedText || stdout).trim().length > 50) {
|
|
197
|
+
let digest = (extractedText || stdout).trim();
|
|
198
|
+
digest = digest.replace(/^\`\`\`\w*\n?/gm, '').replace(/\n?\`\`\`$/gm, '').trim();
|
|
199
|
+
|
|
200
|
+
if (!digest.startsWith('### ')) {
|
|
201
|
+
const sectionIdx = digest.indexOf('### ');
|
|
202
|
+
if (sectionIdx >= 0) {
|
|
203
|
+
digest = digest.slice(sectionIdx);
|
|
204
|
+
} else {
|
|
205
|
+
e.log('warn', 'LLM consolidation output missing expected format — falling back to regex');
|
|
206
|
+
consolidateWithRegex(items, files);
|
|
207
|
+
_clearProcessingState();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const entry = '\n\n---\n\n' + digest;
|
|
213
|
+
const current = getNotes();
|
|
214
|
+
let newContent = current + entry;
|
|
215
|
+
|
|
216
|
+
if (newContent.length > 50000) {
|
|
217
|
+
const sections = newContent.split('\n---\n\n### ');
|
|
218
|
+
if (sections.length > 10) {
|
|
219
|
+
const header = sections[0];
|
|
220
|
+
const recent = sections.slice(-8);
|
|
221
|
+
newContent = header + '\n---\n\n### ' + recent.join('\n---\n\n### ');
|
|
222
|
+
e.log('info', `Pruned notes.md: removed ${sections.length - 9} old sections`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
safeWrite(NOTES_PATH, newContent);
|
|
227
|
+
classifyToKnowledgeBase(items);
|
|
228
|
+
archiveInboxFiles(files);
|
|
229
|
+
e.log('info', `LLM consolidation complete: ${files.length} notes processed by Haiku`);
|
|
230
|
+
} else {
|
|
231
|
+
e.log('warn', `LLM consolidation failed (code=${code}) — falling back to regex`);
|
|
232
|
+
if (stderr) e.log('debug', `LLM stderr: ${stderr.slice(0, 500)}`);
|
|
233
|
+
consolidateWithRegex(items, files);
|
|
234
|
+
}
|
|
235
|
+
_clearProcessingState();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
proc.on('error', (err) => {
|
|
239
|
+
clearTimeout(timeout);
|
|
240
|
+
e.log('warn', `LLM consolidation spawn error: ${err.message} — falling back to regex`);
|
|
241
|
+
safeUnlink(promptPath);
|
|
242
|
+
safeUnlink(sysPromptPath);
|
|
243
|
+
consolidateWithRegex(items, files);
|
|
244
|
+
_clearProcessingState();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Regex Fallback Consolidation ────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
function consolidateWithRegex(items, files) {
|
|
251
|
+
const e = engine();
|
|
252
|
+
const allInsights = [];
|
|
253
|
+
for (const item of items) {
|
|
254
|
+
const content = item.content || '';
|
|
255
|
+
const agentMatch = item.name.match(/^(\w+)-/);
|
|
256
|
+
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
|
257
|
+
const lines = content.split('\n');
|
|
258
|
+
const titleLine = lines.find(l => /^#\s/.test(l));
|
|
259
|
+
const noteTitle = titleLine ? titleLine.replace(/^#+\s*/, '').trim() : item.name;
|
|
260
|
+
|
|
261
|
+
const nameLower = item.name.toLowerCase();
|
|
262
|
+
const contentLower = content.toLowerCase();
|
|
263
|
+
let category = 'learnings';
|
|
264
|
+
if (nameLower.includes('review') || nameLower.includes('pr-') || nameLower.includes('pr4')) category = 'reviews';
|
|
265
|
+
else if (nameLower.includes('feedback')) category = 'feedback';
|
|
266
|
+
else if (nameLower.includes('build') || nameLower.includes('bt-')) category = 'build-results';
|
|
267
|
+
else if (nameLower.includes('explore')) category = 'exploration';
|
|
268
|
+
else if (contentLower.includes('bug') || contentLower.includes('fix')) category = 'bugs-fixes';
|
|
269
|
+
|
|
270
|
+
const numberedPattern = /^\d+\.\s+\*\*(.+?)\*\*\s*[\u2014\u2013:-]\s*(.+)/;
|
|
271
|
+
const bulletPattern = /^[-*]\s+\*\*(.+?)\*\*[:\s]+(.+)/;
|
|
272
|
+
const sectionPattern = /^###+\s+(.+)/;
|
|
273
|
+
const importantKeywords = /\b(must|never|always|convention|pattern|gotcha|warning|important|rule|tip|note that)\b/i;
|
|
274
|
+
|
|
275
|
+
for (const line of lines) {
|
|
276
|
+
const trimmed = line.trim();
|
|
277
|
+
if (!trimmed || sectionPattern.test(trimmed)) continue;
|
|
278
|
+
let insight = null;
|
|
279
|
+
const numMatch = trimmed.match(numberedPattern);
|
|
280
|
+
if (numMatch) insight = `**${numMatch[1].trim()}**: ${numMatch[2].trim()}`;
|
|
281
|
+
if (!insight) {
|
|
282
|
+
const bulMatch = trimmed.match(bulletPattern);
|
|
283
|
+
if (bulMatch) insight = `**${bulMatch[1].trim()}**: ${bulMatch[2].trim()}`;
|
|
284
|
+
}
|
|
285
|
+
if (!insight && importantKeywords.test(trimmed) && !trimmed.startsWith('#') && trimmed.length > 30 && trimmed.length < 500) {
|
|
286
|
+
insight = trimmed;
|
|
287
|
+
}
|
|
288
|
+
if (insight) {
|
|
289
|
+
if (insight.length > 300) insight = insight.slice(0, 297) + '...';
|
|
290
|
+
const fp = insight.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
291
|
+
allInsights.push({ text: insight, source: item.name, noteTitle, category, agent, fingerprint: fp });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!allInsights.some(i => i.source === item.name)) {
|
|
295
|
+
allInsights.push({ text: `See full note: ${noteTitle}`, source: item.name, noteTitle, category, agent,
|
|
296
|
+
fingerprint: noteTitle.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim().slice(0, 80) });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Dedup
|
|
301
|
+
const existingNotes = (getNotes() || '').toLowerCase();
|
|
302
|
+
const seen = new Map();
|
|
303
|
+
const deduped = [];
|
|
304
|
+
for (const insight of allInsights) {
|
|
305
|
+
const fpWords = insight.fingerprint.split(' ').filter(w => w.length > 4).slice(0, 5);
|
|
306
|
+
if (fpWords.length >= 3 && fpWords.every(w => existingNotes.includes(w))) continue;
|
|
307
|
+
const existing = seen.get(insight.fingerprint);
|
|
308
|
+
if (existing) { if (!existing.sources.includes(insight.agent)) existing.sources.push(insight.agent); continue; }
|
|
309
|
+
let isDup = false;
|
|
310
|
+
for (const [fp, entry] of seen) {
|
|
311
|
+
const a = new Set(fp.split(' ')), b = new Set(insight.fingerprint.split(' '));
|
|
312
|
+
// Require at least 3 words in both fingerprints for meaningful similarity check
|
|
313
|
+
if (a.size >= 3 && b.size >= 3 && [...a].filter(w => b.has(w)).length / Math.max(a.size, b.size) > 0.7) {
|
|
314
|
+
if (!entry.sources.includes(insight.agent)) entry.sources.push(insight.agent); isDup = true; break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (isDup) continue;
|
|
318
|
+
seen.set(insight.fingerprint, { insight, sources: [insight.agent] });
|
|
319
|
+
deduped.push({ ...insight, sources: seen.get(insight.fingerprint).sources });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const agents = [...new Set(items.map(i => { const m = i.name.match(/^(\w+)-/); return m ? m[1].charAt(0).toUpperCase() + m[1].slice(1) : 'Unknown'; }))];
|
|
323
|
+
const catLabels = { reviews: 'PR Review Findings', feedback: 'Review Feedback', 'build-results': 'Build & Test Results', exploration: 'Codebase Exploration', 'bugs-fixes': 'Bugs & Gotchas', learnings: 'Patterns & Conventions' };
|
|
324
|
+
const topicHints = [...new Set(deduped.map(i => i.category))].map(c => ({ reviews: 'PR reviews', feedback: 'review feedback', 'build-results': 'build/test results', exploration: 'codebase exploration', 'bugs-fixes': 'bug findings' }[c] || 'learnings'));
|
|
325
|
+
const title = `${agents.join(', ')}: ${topicHints.join(', ')} (${deduped.length} insights from ${items.length} notes)`;
|
|
326
|
+
|
|
327
|
+
const grouped = {};
|
|
328
|
+
for (const item of deduped) { if (!grouped[item.category]) grouped[item.category] = []; grouped[item.category].push(item); }
|
|
329
|
+
|
|
330
|
+
let entry = `\n\n---\n\n### ${e.dateStamp()}: ${title}\n`;
|
|
331
|
+
entry += '**By:** Engine (regex fallback)\n\n';
|
|
332
|
+
for (const [cat, catItems] of Object.entries(grouped)) {
|
|
333
|
+
entry += `#### ${catLabels[cat] || cat} (${catItems.length})\n`;
|
|
334
|
+
for (const item of catItems) {
|
|
335
|
+
const src = item.sources.length > 1 ? ` _(${item.sources.join(', ')})_` : ` _(${item.agent})_`;
|
|
336
|
+
entry += `- ${item.text}${src}\n`;
|
|
337
|
+
}
|
|
338
|
+
entry += '\n';
|
|
339
|
+
}
|
|
340
|
+
const dupCount = allInsights.length - deduped.length;
|
|
341
|
+
if (dupCount > 0) entry += `_Deduplication: ${dupCount} duplicate(s) removed._\n`;
|
|
342
|
+
|
|
343
|
+
const current = getNotes();
|
|
344
|
+
let newContent = current + entry;
|
|
345
|
+
if (newContent.length > 50000) {
|
|
346
|
+
const sections = newContent.split('\n---\n\n### ');
|
|
347
|
+
if (sections.length > 10) { newContent = sections[0] + '\n---\n\n### ' + sections.slice(-8).join('\n---\n\n### '); }
|
|
348
|
+
}
|
|
349
|
+
safeWrite(NOTES_PATH, newContent);
|
|
350
|
+
classifyToKnowledgeBase(items);
|
|
351
|
+
archiveInboxFiles(files);
|
|
352
|
+
e.log('info', `Regex fallback: consolidated ${files.length} notes \u2192 ${deduped.length} insights into notes.md`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Knowledge Base Classification ───────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
function classifyToKnowledgeBase(items) {
|
|
358
|
+
const e = engine();
|
|
359
|
+
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
360
|
+
|
|
361
|
+
const categoryDirs = {};
|
|
362
|
+
for (const cat of KB_CATEGORIES) {
|
|
363
|
+
categoryDirs[cat] = path.join(KNOWLEDGE_DIR, cat);
|
|
364
|
+
if (!fs.existsSync(categoryDirs[cat])) fs.mkdirSync(categoryDirs[cat], { recursive: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let classified = 0;
|
|
368
|
+
for (const item of items) {
|
|
369
|
+
const content = item.content || '';
|
|
370
|
+
const category = classifyInboxItem(item.name, content);
|
|
371
|
+
|
|
372
|
+
const agentMatch = item.name.match(/^(\w+)-/);
|
|
373
|
+
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
|
374
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
375
|
+
const titleSlug = titleMatch
|
|
376
|
+
? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)
|
|
377
|
+
: item.name.replace(/\.md$/, '');
|
|
378
|
+
const kbFilename = `${e.dateStamp()}-${agent}-${titleSlug}.md`;
|
|
379
|
+
const kbPath = shared.uniquePath(path.join(categoryDirs[category], kbFilename));
|
|
380
|
+
|
|
381
|
+
const frontmatter = `---\nsource: ${item.name}\nagent: ${agent}\ncategory: ${category}\ndate: ${e.dateStamp()}\n---\n\n`;
|
|
382
|
+
try {
|
|
383
|
+
safeWrite(kbPath, frontmatter + content);
|
|
384
|
+
classified++;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
e.log('warn', `Failed to classify ${item.name} to knowledge base: ${err.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (classified > 0) {
|
|
391
|
+
e.log('info', `Knowledge base: classified ${classified} note(s) into knowledge/`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Save KB file count checkpoint so the watchdog can detect unexpected deletions
|
|
395
|
+
try {
|
|
396
|
+
let count = 0;
|
|
397
|
+
for (const cat of KB_CATEGORIES) {
|
|
398
|
+
const dir = path.join(KNOWLEDGE_DIR, cat);
|
|
399
|
+
if (fs.existsSync(dir)) count += fs.readdirSync(dir).length;
|
|
400
|
+
}
|
|
401
|
+
safeWrite(path.join(ENGINE_DIR, 'kb-checkpoint.json'), JSON.stringify({ count, updatedAt: new Date().toISOString() }));
|
|
402
|
+
} catch {}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function archiveInboxFiles(files) {
|
|
406
|
+
const e = engine();
|
|
407
|
+
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
408
|
+
for (const f of files) {
|
|
409
|
+
try { fs.renameSync(path.join(INBOX_DIR, f), shared.uniquePath(path.join(ARCHIVE_DIR, `${e.dateStamp()}-${f}`))); } catch {}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = {
|
|
414
|
+
consolidateInbox,
|
|
415
|
+
classifyToKnowledgeBase,
|
|
416
|
+
};
|
|
417
|
+
|