monomind 2.3.0 → 2.3.2
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/package.json +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +41 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +59 -0
- package/packages/@monomind/cli/.claude/skills/mastermind-skills/taskdev.md +2 -0
- package/packages/@monomind/cli/dist/src/commands/cleanup.d.ts +14 -0
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +100 -0
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.d.ts +4 -0
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +25 -0
- package/packages/@monomind/cli/dist/src/commands/doctor.js +3 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +39 -5
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +76 -28
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +17 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +90 -1
- package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +2 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.js +5 -0
- package/packages/@monomind/cli/package.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Open-source CLI extension for Claude Code. Adds an MCP server with a codebase knowledge graph, persistent memory, multi-agent coordination, and reusable slash commands. MIT licensed, runs locally, no data leaves your machine.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -316,6 +316,41 @@ module.exports = {
|
|
|
316
316
|
|
|
317
317
|
if (output.length > 0) console.log(output.join('\n'));
|
|
318
318
|
|
|
319
|
+
// ── Second Brain: per-request knowledge injection ──────────────────
|
|
320
|
+
// When the project has an indexed knowledge base, surface the most
|
|
321
|
+
// relevant excerpts for THIS prompt so Claude has them without having
|
|
322
|
+
// to decide to call knowledge_search. Keyword-scored over chunks.jsonl
|
|
323
|
+
// (a hook subprocess can't afford a 1-3s embedding-model load per
|
|
324
|
+
// prompt; full semantic search stays behind the knowledge_search MCP
|
|
325
|
+
// tool / `monomind doc search`).
|
|
326
|
+
// monolean: [keyword-only injection] — upgrade path: warm control-server
|
|
327
|
+
// endpoint holding the embedding model for semantic per-prompt retrieval.
|
|
328
|
+
try {
|
|
329
|
+
var sbPrompt = String(prompt || '');
|
|
330
|
+
// Skip slash commands and trivial prompts — injection would be noise.
|
|
331
|
+
if (sbPrompt.length >= 12 && sbPrompt.charAt(0) !== '/') {
|
|
332
|
+
var sbKnowledgeDir = path.join(CWD, '.monomind', 'knowledge');
|
|
333
|
+
if (fs.existsSync(path.join(sbKnowledgeDir, 'chunks.jsonl'))) {
|
|
334
|
+
var sbSearch = hCtx._buildKnowledgeSearchFn(sbKnowledgeDir);
|
|
335
|
+
var sbHits = await hCtx.runWithTimeout(
|
|
336
|
+
function() { return sbSearch(sbPrompt, { namespace: 'knowledge:shared', limit: 3, minScore: 0.45 }); },
|
|
337
|
+
'second-brain-inject'
|
|
338
|
+
);
|
|
339
|
+
if (sbHits && sbHits.length > 0) {
|
|
340
|
+
var sbLines = ['[SECOND_BRAIN] ' + sbHits.length + ' relevant excerpt(s) from the project knowledge base:'];
|
|
341
|
+
for (var sbI = 0; sbI < sbHits.length; sbI++) {
|
|
342
|
+
var sbH = sbHits[sbI];
|
|
343
|
+
var sbSrc = (sbH.metadata && sbH.metadata.filePath) ? String(sbH.metadata.filePath).split('/').slice(-2).join('/') : sbH.key;
|
|
344
|
+
var sbText = String(sbH.value || '').replace(/\s+/g, ' ').slice(0, 240);
|
|
345
|
+
sbLines.push(' • [' + sbSrc + '] ' + sbText);
|
|
346
|
+
}
|
|
347
|
+
sbLines.push(' (deeper/semantic lookup: mcp__monomind__knowledge_search or `monomind doc search -q "..."`)');
|
|
348
|
+
console.log(sbLines.join('\n'));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (e) { /* non-fatal — knowledge injection must never block a prompt */ }
|
|
353
|
+
|
|
319
354
|
// Record any decision markers in this prompt (auto-ADR pipeline).
|
|
320
355
|
try { hCtx._recordDecisionMarkers(prompt); } catch (e) {}
|
|
321
356
|
|
|
@@ -358,7 +393,12 @@ module.exports = {
|
|
|
358
393
|
if (fs.existsSync(secAuditFile) && fs.statSync(secAuditFile).size < 32768) {
|
|
359
394
|
var secData = JSON.parse(fs.readFileSync(secAuditFile, 'utf-8'));
|
|
360
395
|
if (secData && secData.findings && secData.findings.length > 0) {
|
|
361
|
-
|
|
396
|
+
var serious = secData.findings.filter(function (f) { return f && (f.severity === 'high' || f.severity === 'medium'); });
|
|
397
|
+
if (serious.length > 0) {
|
|
398
|
+
console.log('[SECURITY] ' + serious.length + ' finding(s) from background scan. Review .monomind/metrics/security-audit.json');
|
|
399
|
+
} else {
|
|
400
|
+
console.log('[AUDIT] ' + secData.findings.length + ' low-severity note(s) (architecture heuristics, not vulnerabilities) — .monomind/metrics/security-audit.json');
|
|
401
|
+
}
|
|
362
402
|
}
|
|
363
403
|
}
|
|
364
404
|
// Codebase map top files (high-centrality god nodes from monograph)
|
|
@@ -344,6 +344,65 @@ module.exports = {
|
|
|
344
344
|
}
|
|
345
345
|
} catch (e) { /* non-fatal */ }
|
|
346
346
|
|
|
347
|
+
// Second Brain continuous ingestion — the generated CLAUDE.md promises
|
|
348
|
+
// "re-indexing happens automatically on session start"; make that true for
|
|
349
|
+
// user documents, not just the 3 fixed files above. Gated on (a) an active
|
|
350
|
+
// knowledge base, (b) at least one document newer than the last ingest,
|
|
351
|
+
// (c) a 30-min rate limit. Detached+unref spawn (same pattern as the
|
|
352
|
+
// helper-heal block below) — session start never waits on model loading.
|
|
353
|
+
try {
|
|
354
|
+
var _kbMetaPath = path.join(CWD, '.monomind', 'knowledge', 'doc-metadata.jsonl');
|
|
355
|
+
if (fs.existsSync(_kbMetaPath)) {
|
|
356
|
+
var _kbMetaMtime = fs.statSync(_kbMetaPath).mtimeMs;
|
|
357
|
+
var _kbMarkerPath = path.join(CWD, '.monomind', 'knowledge', 'reindex-check.json');
|
|
358
|
+
var _kbLastCheck = 0;
|
|
359
|
+
try { _kbLastCheck = JSON.parse(fs.readFileSync(_kbMarkerPath, 'utf-8')).ts || 0; } catch (_) {}
|
|
360
|
+
if (Date.now() - _kbLastCheck > 30 * 60 * 1000) {
|
|
361
|
+
// Cheap bounded scan: any document changed since the last ingest?
|
|
362
|
+
var _DOC_EXT = { '.md': 1, '.txt': 1, '.pdf': 1, '.docx': 1 };
|
|
363
|
+
var _kbDirty = false;
|
|
364
|
+
var _kbScanned = 0;
|
|
365
|
+
var _kbWalk = function(dir, depth) {
|
|
366
|
+
if (_kbDirty || depth > 3 || _kbScanned > 2000) return;
|
|
367
|
+
var names;
|
|
368
|
+
try { names = fs.readdirSync(dir); } catch (_) { return; }
|
|
369
|
+
for (var ni = 0; ni < names.length && !_kbDirty; ni++) {
|
|
370
|
+
var n = names[ni];
|
|
371
|
+
if (n.charAt(0) === '.' || n === 'node_modules' || n === 'dist') continue;
|
|
372
|
+
var p = path.join(dir, n);
|
|
373
|
+
var st;
|
|
374
|
+
try { st = fs.statSync(p); } catch (_) { continue; }
|
|
375
|
+
_kbScanned++;
|
|
376
|
+
if (st.isDirectory()) { _kbWalk(p, depth + 1); continue; }
|
|
377
|
+
var ext = path.extname(n).toLowerCase();
|
|
378
|
+
if (_DOC_EXT[ext] && st.mtimeMs > _kbMetaMtime) _kbDirty = true;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
_kbWalk(CWD, 0);
|
|
382
|
+
if (_kbDirty) {
|
|
383
|
+
var _kbMarkerOk = false;
|
|
384
|
+
try {
|
|
385
|
+
fs.writeFileSync(_kbMarkerPath, JSON.stringify({ ts: Date.now() }), 'utf-8');
|
|
386
|
+
_kbMarkerOk = true;
|
|
387
|
+
} catch (_) {}
|
|
388
|
+
if (_kbMarkerOk) {
|
|
389
|
+
var _kbSpawn = require('child_process').spawn;
|
|
390
|
+
var _kbChild = _kbSpawn('npx', ['-y', 'monomind@latest', 'doc', 'ingest', '.'], {
|
|
391
|
+
cwd: CWD,
|
|
392
|
+
detached: true,
|
|
393
|
+
stdio: 'ignore',
|
|
394
|
+
env: process.env,
|
|
395
|
+
shell: process.platform === 'win32',
|
|
396
|
+
windowsHide: true,
|
|
397
|
+
});
|
|
398
|
+
_kbChild.unref();
|
|
399
|
+
console.log('[KNOWLEDGE_REINDEX] changed documents detected — re-ingesting in background');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch (e) { /* non-fatal — reindex must never block session start */ }
|
|
405
|
+
|
|
347
406
|
// Monograph Context Injection — delegates to shared helper in utils/monograph.cjs.
|
|
348
407
|
injectGodNodesContext(CWD);
|
|
349
408
|
|
|
@@ -157,6 +157,8 @@ Implementer subagents report one of four statuses. Handle each appropriately:
|
|
|
157
157
|
|
|
158
158
|
**Never** ignore an escalation or force the same model to retry without changes. If the implementer said it's stuck, something needs to change.
|
|
159
159
|
|
|
160
|
+
**Async dispatches:** reviewer and implementer subagents may complete asynchronously even when dispatched synchronously. Wait for each result before acting on it — never proceed to the next gate, and never touch the files a dispatched subagent is working on, while its result is pending.
|
|
161
|
+
|
|
160
162
|
---
|
|
161
163
|
|
|
162
164
|
## Handling Reviewer ⚠️ Items
|
|
@@ -11,6 +11,20 @@ interface StaleScratchItem {
|
|
|
11
11
|
description: string;
|
|
12
12
|
size: number;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Find prunable entries under the per-project data base (default
|
|
16
|
+
* ~/.monomind/projects). Exported for tests — `baseDir`/`now` injectable.
|
|
17
|
+
*
|
|
18
|
+
* Classification per dir:
|
|
19
|
+
* - `origin.json` present and its recorded path still exists → keep the dir,
|
|
20
|
+
* but flag a leftover `lancedb/` subdir (dead since the SQLite engine swap).
|
|
21
|
+
* - `origin.json` present, recorded path gone → orphaned → prune.
|
|
22
|
+
* - no `origin.json` (pre-2.3.1 dirs can't prove their origin) → prune only
|
|
23
|
+
* when untouched for {@link UNKNOWN_DIR_MAX_AGE_MS} — or immediately with
|
|
24
|
+
* `--aggressive`, which treats unprovable dirs as junk (safe: every live
|
|
25
|
+
* project rewrites origin.json on its next memory access).
|
|
26
|
+
*/
|
|
27
|
+
export declare function findOrphanedProjectData(baseDir: string, now: number, aggressive: boolean): StaleScratchItem[];
|
|
14
28
|
/**
|
|
15
29
|
* Find stale mastermind scratch under `.monomind/taskdev/` and `.monomind/loops/`.
|
|
16
30
|
* Exported for tests. Never returns `progress.md` (the taskdev recovery ledger),
|
|
@@ -30,8 +30,66 @@ const KEEP_CONFIG_PATHS = [
|
|
|
30
30
|
join('.claude', 'settings.json'),
|
|
31
31
|
];
|
|
32
32
|
/** Scratch pruning (--scratch): taskdev handoff files and loop state. */
|
|
33
|
+
// monolean: manual flag only — upgrade path: invoke from the `cache` background worker so crashed-run scratch is pruned without anyone remembering the flag
|
|
33
34
|
const SCRATCH_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // taskdev scratch older than this is stale
|
|
34
35
|
const LOOP_STALE_GRACE_MS = 24 * 60 * 60 * 1000; // a live loop reschedules every <=1h; overdue by a day = abandoned
|
|
36
|
+
/** Orphaned per-project data (--data): ~/.monomind/projects/<slug> dirs whose
|
|
37
|
+
* source project is gone, plus dead lancedb/ dirs left by the pre-2.3.1 engine. */
|
|
38
|
+
const UNKNOWN_DIR_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
39
|
+
/**
|
|
40
|
+
* Find prunable entries under the per-project data base (default
|
|
41
|
+
* ~/.monomind/projects). Exported for tests — `baseDir`/`now` injectable.
|
|
42
|
+
*
|
|
43
|
+
* Classification per dir:
|
|
44
|
+
* - `origin.json` present and its recorded path still exists → keep the dir,
|
|
45
|
+
* but flag a leftover `lancedb/` subdir (dead since the SQLite engine swap).
|
|
46
|
+
* - `origin.json` present, recorded path gone → orphaned → prune.
|
|
47
|
+
* - no `origin.json` (pre-2.3.1 dirs can't prove their origin) → prune only
|
|
48
|
+
* when untouched for {@link UNKNOWN_DIR_MAX_AGE_MS} — or immediately with
|
|
49
|
+
* `--aggressive`, which treats unprovable dirs as junk (safe: every live
|
|
50
|
+
* project rewrites origin.json on its next memory access).
|
|
51
|
+
*/
|
|
52
|
+
export function findOrphanedProjectData(baseDir, now, aggressive) {
|
|
53
|
+
const out = [];
|
|
54
|
+
if (!existsSync(baseDir))
|
|
55
|
+
return out;
|
|
56
|
+
for (const name of readdirSync(baseDir)) {
|
|
57
|
+
if (name.startsWith('.'))
|
|
58
|
+
continue;
|
|
59
|
+
const dir = join(baseDir, name);
|
|
60
|
+
let mtime = 0;
|
|
61
|
+
try {
|
|
62
|
+
const st = lstatSync(dir);
|
|
63
|
+
if (!st.isDirectory())
|
|
64
|
+
continue;
|
|
65
|
+
mtime = st.mtimeMs;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const originFile = join(dir, 'origin.json');
|
|
71
|
+
let originPath = null;
|
|
72
|
+
let hasOrigin = false;
|
|
73
|
+
try {
|
|
74
|
+
originPath = String(JSON.parse(readFileSync(originFile, 'utf-8')).path ?? '');
|
|
75
|
+
hasOrigin = originPath.length > 0;
|
|
76
|
+
}
|
|
77
|
+
catch { /* no/corrupt marker */ }
|
|
78
|
+
if (hasOrigin && originPath && existsSync(originPath)) {
|
|
79
|
+
const lance = join(dir, 'lancedb');
|
|
80
|
+
if (existsSync(lance))
|
|
81
|
+
out.push({ path: lance, description: `dead lancedb store (project: ${originPath})`, size: 0 });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (hasOrigin) {
|
|
85
|
+
out.push({ path: dir, description: `orphaned project data (origin gone: ${originPath})`, size: 0 });
|
|
86
|
+
}
|
|
87
|
+
else if (aggressive || now - mtime > UNKNOWN_DIR_MAX_AGE_MS) {
|
|
88
|
+
out.push({ path: dir, description: aggressive ? 'unverifiable project data (no origin marker)' : 'unverifiable project data (untouched >30d)', size: 0 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
35
93
|
/**
|
|
36
94
|
* Find stale mastermind scratch under `.monomind/taskdev/` and `.monomind/loops/`.
|
|
37
95
|
* Exported for tests. Never returns `progress.md` (the taskdev recovery ledger),
|
|
@@ -207,6 +265,19 @@ export const cleanupCommand = {
|
|
|
207
265
|
type: 'boolean',
|
|
208
266
|
default: false,
|
|
209
267
|
},
|
|
268
|
+
{
|
|
269
|
+
name: 'data',
|
|
270
|
+
short: 'd',
|
|
271
|
+
description: 'Prune orphaned per-project data in ~/.monomind/projects (gone projects, dead lancedb stores)',
|
|
272
|
+
type: 'boolean',
|
|
273
|
+
default: false,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'aggressive',
|
|
277
|
+
description: 'With --data: also prune dirs that cannot prove their origin (pre-2.3.1, no origin marker)',
|
|
278
|
+
type: 'boolean',
|
|
279
|
+
default: false,
|
|
280
|
+
},
|
|
210
281
|
],
|
|
211
282
|
examples: [
|
|
212
283
|
{
|
|
@@ -231,6 +302,35 @@ export const cleanupCommand = {
|
|
|
231
302
|
const keepConfig = ctx.flags['keep-config'] === true;
|
|
232
303
|
const cwd = ctx.cwd;
|
|
233
304
|
const dryRun = !force;
|
|
305
|
+
if (ctx.flags.data === true) {
|
|
306
|
+
const { homedir } = await import('os');
|
|
307
|
+
const baseDir = join(homedir(), '.monomind', 'projects');
|
|
308
|
+
output.writeln();
|
|
309
|
+
output.writeln(output.bold(dryRun ? 'Monomind Project-Data Cleanup (dry run)' : 'Monomind Project-Data Cleanup'));
|
|
310
|
+
output.writeln();
|
|
311
|
+
const orphans = findOrphanedProjectData(baseDir, Date.now(), ctx.flags.aggressive === true);
|
|
312
|
+
if (orphans.length === 0) {
|
|
313
|
+
output.writeln(output.info('No orphaned project data found.'));
|
|
314
|
+
return { success: true, message: 'Nothing to clean' };
|
|
315
|
+
}
|
|
316
|
+
let removed = 0;
|
|
317
|
+
for (const o of orphans) {
|
|
318
|
+
output.writeln(` ${dryRun ? 'would remove' : 'removing'}: ${o.path} (${o.description})`);
|
|
319
|
+
if (!dryRun) {
|
|
320
|
+
try {
|
|
321
|
+
rmSync(o.path, { recursive: true, force: true });
|
|
322
|
+
removed++;
|
|
323
|
+
}
|
|
324
|
+
catch { /* skip unremovable */ }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
output.writeln();
|
|
328
|
+
if (dryRun) {
|
|
329
|
+
output.writeln(output.dim(` ${orphans.length} item(s). This was a dry run. Use --force to delete.`));
|
|
330
|
+
return { success: true, message: `Dry run: ${orphans.length} orphaned item(s) found`, data: { found: orphans, dryRun } };
|
|
331
|
+
}
|
|
332
|
+
return { success: true, message: `Removed ${removed} orphaned item(s)`, data: { found: orphans, removedCount: removed, dryRun } };
|
|
333
|
+
}
|
|
234
334
|
if (ctx.flags.scratch === true) {
|
|
235
335
|
const now = Date.now();
|
|
236
336
|
output.writeln();
|
|
@@ -6,6 +6,10 @@ import type { HealthCheck } from './doctor-env-checks.js';
|
|
|
6
6
|
export type { HealthCheck };
|
|
7
7
|
export declare function checkConfigFile(): Promise<HealthCheck>;
|
|
8
8
|
export declare function checkMemoryDatabase(): Promise<HealthCheck>;
|
|
9
|
+
/** Second Brain: when this project has an indexed knowledge base, semantic
|
|
10
|
+
* search needs the local embedding model. Its absence is silent (search
|
|
11
|
+
* degrades to keyword matching) — surface it here instead. */
|
|
12
|
+
export declare function checkSecondBrainModel(): Promise<HealthCheck>;
|
|
9
13
|
export declare function checkApiKeys(): Promise<HealthCheck>;
|
|
10
14
|
export declare function checkMcpServers(): Promise<HealthCheck>;
|
|
11
15
|
export declare function checkMonograph(): Promise<HealthCheck>;
|
|
@@ -44,6 +44,31 @@ export async function checkMemoryDatabase() {
|
|
|
44
44
|
}
|
|
45
45
|
return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'monomind memory configure --backend hybrid' };
|
|
46
46
|
}
|
|
47
|
+
/** Second Brain: when this project has an indexed knowledge base, semantic
|
|
48
|
+
* search needs the local embedding model. Its absence is silent (search
|
|
49
|
+
* degrades to keyword matching) — surface it here instead. */
|
|
50
|
+
export async function checkSecondBrainModel() {
|
|
51
|
+
const name = 'Second Brain Model';
|
|
52
|
+
if (!existsSync(join(process.cwd(), '.monomind', 'knowledge', 'chunks.jsonl'))) {
|
|
53
|
+
return { name, status: 'pass', message: 'No knowledge base in this project (nothing to check)' };
|
|
54
|
+
}
|
|
55
|
+
let pkgDir;
|
|
56
|
+
try {
|
|
57
|
+
// exports map blocks package.json — resolve the entry file and walk up
|
|
58
|
+
const entry = await import.meta.resolve?.('@huggingface/transformers');
|
|
59
|
+
if (!entry)
|
|
60
|
+
throw new Error('resolver unavailable');
|
|
61
|
+
pkgDir = fileURLToPath(entry).replace(/\/dist\/.*$/, '');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { name, status: 'warn', message: '@huggingface/transformers not installed — semantic search degraded to keyword matching', fix: 'reinstall monomind (the model dependency is optional and may have failed to build)' };
|
|
65
|
+
}
|
|
66
|
+
const modelCache = join(pkgDir, '.cache', 'Xenova');
|
|
67
|
+
if (existsSync(modelCache)) {
|
|
68
|
+
return { name, status: 'pass', message: 'Local embedding model cached — semantic search active' };
|
|
69
|
+
}
|
|
70
|
+
return { name, status: 'warn', message: 'Embedding model not downloaded yet — searches use keyword matching until it is', fix: 'run once while online: monomind doc search -q "warmup" (downloads ~90MB locally, one time)' };
|
|
71
|
+
}
|
|
47
72
|
export async function checkApiKeys() {
|
|
48
73
|
const keys = ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY', 'OPENAI_API_KEY'];
|
|
49
74
|
const found = keys.filter(k => process.env[k]);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { output } from '../output.js';
|
|
9
9
|
import { checkNodeVersion, checkNpmVersion, checkGit, checkGitRepo, checkDiskSpace, checkBuildTools, checkVersionFreshness, checkClaudeCode, installClaudeCode, } from './doctor-env-checks.js';
|
|
10
|
-
import { checkConfigFile, checkMemoryDatabase, checkApiKeys, checkMcpServers, checkMonograph, checkMonographFreshness, checkMonoesMemory, checkHelpersFresh, fixStaleHelpers, checkMonoesIntegration, checkGuidanceGates, checkGitignoreCoverage, checkAgentRegistry, checkMemoryProficiency, checkMetricsFreshness, checkSecurityAuditFindings, } from './doctor-project-checks.js';
|
|
10
|
+
import { checkConfigFile, checkMemoryDatabase, checkApiKeys, checkMcpServers, checkMonograph, checkMonographFreshness, checkMonoesMemory, checkHelpersFresh, fixStaleHelpers, checkMonoesIntegration, checkGuidanceGates, checkGitignoreCoverage, checkAgentRegistry, checkMemoryProficiency, checkMetricsFreshness, checkSecurityAuditFindings, checkSecondBrainModel, } from './doctor-project-checks.js';
|
|
11
11
|
import { checkMonoesTools, fixMonoesTools } from './doctor-monoes-checks.js';
|
|
12
12
|
function formatCheck(check) {
|
|
13
13
|
const icon = check.status === 'pass' ? output.success('✓') :
|
|
@@ -65,6 +65,7 @@ export const doctorCommand = {
|
|
|
65
65
|
checkMonograph, checkMonoesMemory, checkHelpersFresh, checkMonoesIntegration,
|
|
66
66
|
checkGuidanceGates, checkAgentRegistry, checkGit, checkApiKeys,
|
|
67
67
|
checkMemoryProficiency, checkMetricsFreshness, checkSecurityAuditFindings,
|
|
68
|
+
checkSecondBrainModel,
|
|
68
69
|
];
|
|
69
70
|
const codeOnlyChecks = [
|
|
70
71
|
checkGitRepo, checkMcpServers,
|
|
@@ -78,6 +79,7 @@ export const doctorCommand = {
|
|
|
78
79
|
node: checkNodeVersion, npm: checkNpmVersion, claude: checkClaudeCode,
|
|
79
80
|
config: checkConfigFile, memory: checkMemoryDatabase,
|
|
80
81
|
api: checkApiKeys, git: checkGit, mcp: checkMcpServers, disk: checkDiskSpace,
|
|
82
|
+
'second-brain': checkSecondBrainModel,
|
|
81
83
|
typescript: checkBuildTools, monograph: checkMonograph,
|
|
82
84
|
'graph-freshness': checkMonographFreshness, 'memory-pkg': checkMonoesMemory,
|
|
83
85
|
helpers: checkHelpersFresh, monoes: checkMonoesIntegration,
|
|
@@ -13,6 +13,21 @@ const DEFAULT_OVERLAP = 400;
|
|
|
13
13
|
// Inline fallback identical to @monoes/memory's knowledge/document-chunker.ts —
|
|
14
14
|
// used only if the dynamic import below fails (package not installed/built).
|
|
15
15
|
// Keep in sync if the shared chunker's boundary-snapping logic changes.
|
|
16
|
+
const HEADING_LINE_RE = /^#{1,6} /;
|
|
17
|
+
function lastHeadingBefore(text, pos) {
|
|
18
|
+
let i = text.lastIndexOf('\n#', pos - 1);
|
|
19
|
+
while (i !== -1) {
|
|
20
|
+
const eol = text.indexOf('\n', i + 1);
|
|
21
|
+
const line = text.slice(i + 1, eol === -1 ? undefined : eol);
|
|
22
|
+
if (HEADING_LINE_RE.test(line))
|
|
23
|
+
return line.replace(/^#+ /, '').trim();
|
|
24
|
+
i = text.lastIndexOf('\n#', i - 1);
|
|
25
|
+
}
|
|
26
|
+
const firstEol = text.indexOf('\n');
|
|
27
|
+
const firstLine = firstEol === -1 ? text : text.slice(0, firstEol);
|
|
28
|
+
return HEADING_LINE_RE.test(firstLine) && firstEol !== -1 && firstEol < pos
|
|
29
|
+
? firstLine.replace(/^#+ /, '').trim() : null;
|
|
30
|
+
}
|
|
16
31
|
function chunkDocumentInline(docId, text) {
|
|
17
32
|
if (text.length === 0)
|
|
18
33
|
return [];
|
|
@@ -21,18 +36,37 @@ function chunkDocumentInline(docId, text) {
|
|
|
21
36
|
let chunkIndex = 0;
|
|
22
37
|
while (startChar < text.length) {
|
|
23
38
|
let endChar = Math.min(startChar + DEFAULT_CHUNK_SIZE, text.length);
|
|
39
|
+
let brokeAtHeading = false;
|
|
24
40
|
if (endChar < text.length) {
|
|
25
41
|
const windowStart = Math.max(startChar, endChar - Math.floor(DEFAULT_CHUNK_SIZE * 0.2));
|
|
26
42
|
const window = text.slice(windowStart, endChar);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
let h = window.lastIndexOf('\n#');
|
|
44
|
+
while (h !== -1) {
|
|
45
|
+
const eol = window.indexOf('\n', h + 1);
|
|
46
|
+
const line = window.slice(h + 1, eol === -1 ? undefined : eol);
|
|
47
|
+
if (HEADING_LINE_RE.test(line) && windowStart + h > startChar)
|
|
48
|
+
break;
|
|
49
|
+
h = window.lastIndexOf('\n#', h - 1);
|
|
50
|
+
}
|
|
51
|
+
if (h !== -1 && windowStart + h > startChar) {
|
|
52
|
+
endChar = windowStart + h + 1;
|
|
53
|
+
brokeAtHeading = true;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const lastParagraph = window.lastIndexOf('\n\n');
|
|
57
|
+
if (lastParagraph !== -1)
|
|
58
|
+
endChar = windowStart + lastParagraph + 2;
|
|
59
|
+
}
|
|
30
60
|
}
|
|
31
|
-
|
|
61
|
+
let chunkText = text.slice(startChar, endChar);
|
|
62
|
+
const heading = lastHeadingBefore(text, startChar + 1);
|
|
63
|
+
if (heading && !HEADING_LINE_RE.test(chunkText.trimStart()))
|
|
64
|
+
chunkText = `§ ${heading}\n${chunkText}`;
|
|
65
|
+
chunks.push({ chunkId: `${docId}:${chunkIndex}`, docId, text: chunkText, startChar, endChar, chunkIndex });
|
|
32
66
|
chunkIndex++;
|
|
33
67
|
if (endChar >= text.length)
|
|
34
68
|
break;
|
|
35
|
-
startChar += Math.max(1, endChar - startChar - DEFAULT_OVERLAP);
|
|
69
|
+
startChar += brokeAtHeading ? Math.max(1, endChar - startChar) : Math.max(1, endChar - startChar - DEFAULT_OVERLAP);
|
|
36
70
|
}
|
|
37
71
|
return chunks;
|
|
38
72
|
}
|
|
@@ -121,6 +121,15 @@ let bridgeAvailable = null;
|
|
|
121
121
|
let _embedder = null;
|
|
122
122
|
const MAX_INIT_ATTEMPTS = 3;
|
|
123
123
|
let initAttempts = 0;
|
|
124
|
+
/** Flush after mutations: the sql.js fallback backend is in-memory WASM and
|
|
125
|
+
* only reaches disk via persist(); the CLI process is short-lived, so waiting
|
|
126
|
+
* for an auto-persist interval would lose writes. No-op on better-sqlite3. */
|
|
127
|
+
async function flushBackend(backend) {
|
|
128
|
+
try {
|
|
129
|
+
await backend?.persist?.();
|
|
130
|
+
}
|
|
131
|
+
catch { /* best effort */ }
|
|
132
|
+
}
|
|
124
133
|
async function getBackend(dbPath) {
|
|
125
134
|
if (bridgeAvailable === false)
|
|
126
135
|
return null;
|
|
@@ -134,14 +143,15 @@ async function getBackend(dbPath) {
|
|
|
134
143
|
backendPromise = (async () => {
|
|
135
144
|
try {
|
|
136
145
|
const mod = await import('@monoes/memory');
|
|
137
|
-
const { LanceDBBackend } = mod;
|
|
138
146
|
// Try to create embedding generator from HuggingFace transformers
|
|
139
147
|
let embeddingGenerator;
|
|
140
148
|
try {
|
|
141
149
|
const hf = await import('@huggingface/transformers');
|
|
142
150
|
// revision must be a git ref — 'main' is the HF default; 'default' 404s and
|
|
143
151
|
// silently killed embeddings (every search degraded to keyword matching)
|
|
144
|
-
|
|
152
|
+
// dtype pinned explicitly: transformers.js logs a "dtype not specified"
|
|
153
|
+
// warning to the console on every load otherwise (leaks into CLI output).
|
|
154
|
+
const extractor = await hf.pipeline('feature-extraction', BRIDGE_EMBEDDING_MODEL, { revision: 'main', dtype: 'fp32' });
|
|
145
155
|
embeddingGenerator = async (text) => {
|
|
146
156
|
const output = await extractor(text, { pooling: 'mean', normalize: true });
|
|
147
157
|
return new Float32Array(output.data);
|
|
@@ -152,26 +162,47 @@ async function getBackend(dbPath) {
|
|
|
152
162
|
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
153
163
|
console.error('[memory-bridge] embedding model failed to load — store and search without vectors:', e);
|
|
154
164
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
// Local SQLite engine (LanceDB replaced 2026-07): better-sqlite3 when its
|
|
166
|
+
// native binding loads, sql.js (pure WASM) otherwise — both persist text
|
|
167
|
+
// AND embeddings, so vectors are always recomputable/derivable data.
|
|
168
|
+
// getDbPath keeps its traversal guard and directory semantics; the SQLite
|
|
169
|
+
// file lives inside that directory.
|
|
170
|
+
const dir = getDbPath(dbPath);
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
// Origin marker: records which project this data dir belongs to, so
|
|
173
|
+
// `monomind cleanup --data` can verifiably prune dirs whose project
|
|
174
|
+
// no longer exists (the dir-name hash is one-way). Best-effort.
|
|
175
|
+
try {
|
|
176
|
+
const originFile = path.join(projectDataDir(), 'origin.json');
|
|
177
|
+
fs.writeFileSync(originFile, JSON.stringify({ path: path.resolve(process.cwd()), updatedAt: new Date().toISOString() }) + '\n', 'utf-8');
|
|
178
|
+
}
|
|
179
|
+
catch { /* non-fatal */ }
|
|
180
|
+
const cfg = {
|
|
181
|
+
databasePath: path.join(dir, 'memory.db'),
|
|
182
|
+
walMode: true,
|
|
183
|
+
optimize: true,
|
|
184
|
+
defaultNamespace: 'default',
|
|
162
185
|
embeddingGenerator,
|
|
163
|
-
|
|
164
|
-
nProbes: 20,
|
|
165
|
-
});
|
|
186
|
+
};
|
|
166
187
|
const origLog = console.log;
|
|
167
188
|
console.log = (...args) => {
|
|
168
189
|
const msg = String(args[0] ?? '');
|
|
169
|
-
if (msg.includes('Transformers.js') || msg.includes('
|
|
190
|
+
if (msg.includes('Transformers.js') || msg.includes('Loading model'))
|
|
170
191
|
return;
|
|
171
192
|
origLog.apply(console, args);
|
|
172
193
|
};
|
|
194
|
+
let backend;
|
|
173
195
|
try {
|
|
174
|
-
|
|
196
|
+
try {
|
|
197
|
+
backend = new mod.SQLiteBackend(cfg);
|
|
198
|
+
await backend.initialize();
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
202
|
+
console.error('[memory-bridge] better-sqlite3 unavailable — using sql.js backend:', e);
|
|
203
|
+
backend = new mod.SqlJsBackend(cfg);
|
|
204
|
+
await backend.initialize();
|
|
205
|
+
}
|
|
175
206
|
}
|
|
176
207
|
finally {
|
|
177
208
|
console.log = origLog;
|
|
@@ -256,6 +287,7 @@ export async function bridgeStoreEntry(options) {
|
|
|
256
287
|
catch { /* non-fatal — store anyway */ }
|
|
257
288
|
}
|
|
258
289
|
await backend.store(entry);
|
|
290
|
+
await flushBackend(backend);
|
|
259
291
|
return { success: true, id, embedding: embeddingInfo };
|
|
260
292
|
}
|
|
261
293
|
catch (err) {
|
|
@@ -302,20 +334,31 @@ export async function bridgeSearchEntries(options) {
|
|
|
302
334
|
namespace: namespace ?? 'default',
|
|
303
335
|
limit: 50000,
|
|
304
336
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
337
|
+
// Token-based matching, not whole-phrase substring: "semantic test" must
|
|
338
|
+
// match an entry keyed "semantic-test" (the old .includes(query) required
|
|
339
|
+
// the exact phrase — including its whitespace — to appear verbatim).
|
|
340
|
+
// Score = fraction of query tokens present in key+content.
|
|
341
|
+
const tokens = queryStr.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 1);
|
|
342
|
+
if (tokens.length) {
|
|
343
|
+
results = entries
|
|
344
|
+
.map((e) => {
|
|
345
|
+
const haystack = `${e.key || ''} ${e.content || ''}`.toLowerCase();
|
|
346
|
+
const hits = tokens.filter(t => haystack.includes(t)).length;
|
|
347
|
+
return { e, score: hits / tokens.length };
|
|
348
|
+
})
|
|
349
|
+
.filter((x) => x.score > 0)
|
|
350
|
+
.sort((a, b) => b.score - a.score)
|
|
351
|
+
.slice(0, limit)
|
|
352
|
+
.map(({ e, score }) => ({
|
|
353
|
+
id: e.id,
|
|
354
|
+
key: e.key,
|
|
355
|
+
content: e.content || '',
|
|
356
|
+
score: Math.min(0.9, 0.3 + score * 0.6),
|
|
357
|
+
namespace: e.namespace,
|
|
358
|
+
provenance: `keyword:${score.toFixed(2)}`,
|
|
359
|
+
_createdAt: e.createdAt || 0,
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
319
362
|
searchMethod = 'keyword';
|
|
320
363
|
}
|
|
321
364
|
// Filter stale entries based on automem config — skip for knowledge
|
|
@@ -413,6 +456,8 @@ export async function bridgeDeleteEntry(options) {
|
|
|
413
456
|
if (entry)
|
|
414
457
|
deleted = await backend.delete(entry.id);
|
|
415
458
|
}
|
|
459
|
+
if (deleted)
|
|
460
|
+
await flushBackend(backend);
|
|
416
461
|
return { success: true, deleted };
|
|
417
462
|
}
|
|
418
463
|
catch {
|
|
@@ -667,6 +712,7 @@ export async function bridgeSessionEnd(options) {
|
|
|
667
712
|
}),
|
|
668
713
|
tags: ['session', 'ended'],
|
|
669
714
|
});
|
|
715
|
+
await flushBackend(backend);
|
|
670
716
|
}
|
|
671
717
|
return { success: true };
|
|
672
718
|
}
|
|
@@ -758,6 +804,8 @@ export async function bridgeConsolidate(params) {
|
|
|
758
804
|
deleted++;
|
|
759
805
|
}
|
|
760
806
|
}
|
|
807
|
+
if (deleted)
|
|
808
|
+
await flushBackend(backend);
|
|
761
809
|
return { success: true, consolidated: deleted };
|
|
762
810
|
}
|
|
763
811
|
catch {
|
|
@@ -99,6 +99,23 @@ export declare class OrgDaemon {
|
|
|
99
99
|
private autoWake;
|
|
100
100
|
stopOrg(name: string): Promise<void>;
|
|
101
101
|
stopAll(): Promise<void>;
|
|
102
|
+
private orgMemoryNamespace;
|
|
103
|
+
/** Store dir for org cross-run memory — inside the org root so the bridge's
|
|
104
|
+
* path guard accepts it when the daemon runs from the project (the normal
|
|
105
|
+
* case) and test roots stay isolated. */
|
|
106
|
+
private orgMemoryDbPath;
|
|
107
|
+
/** The memory bridge's traversal guard silently redirects out-of-tree paths
|
|
108
|
+
* to the per-project default store. For an org rooted outside cwd (tests,
|
|
109
|
+
* unusual daemon setups) that redirect would write into the WRONG project's
|
|
110
|
+
* memory — verify the guard kept our path, and skip org memory otherwise. */
|
|
111
|
+
private orgMemoryUsable;
|
|
112
|
+
/** org_recall implementation: search the org's memory namespace via the
|
|
113
|
+
* memory bridge (semantic when the local model is available, tokenized
|
|
114
|
+
* keyword otherwise). Failures return a message, never throw into the tool. */
|
|
115
|
+
private recallOrgMemory;
|
|
116
|
+
/** Persist the run's outcome into cross-run org memory so org_recall (and
|
|
117
|
+
* future runs) can find it by meaning, not just recency. Best-effort. */
|
|
118
|
+
private storeRunMemory;
|
|
102
119
|
private persistState;
|
|
103
120
|
}
|
|
104
121
|
export {};
|
|
@@ -75,6 +75,11 @@ export class OrgDaemon {
|
|
|
75
75
|
onComplete: (r, outcome, summary) => {
|
|
76
76
|
bus.emit({ type: 'status', from: r, reason: 'org-complete', msg: `run outcome: ${outcome}`, data: { outcome, summary } });
|
|
77
77
|
},
|
|
78
|
+
recall: async (r, q) => {
|
|
79
|
+
const answer = await this.recallOrgMemory(name, def, q);
|
|
80
|
+
bus.emit({ type: 'status', from: r, reason: 'org-recall', msg: `recall: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
|
|
81
|
+
return answer.text;
|
|
82
|
+
},
|
|
78
83
|
queryFn: this.opts.queryFn,
|
|
79
84
|
};
|
|
80
85
|
// Supervised session: transient crashes (provider blips, network) restart
|
|
@@ -384,8 +389,11 @@ export class OrgDaemon {
|
|
|
384
389
|
try {
|
|
385
390
|
const events = readRunEvents(this.root, name, org.run);
|
|
386
391
|
if (events.length) {
|
|
392
|
+
const summary = summarizeRun(events);
|
|
387
393
|
const { appendFileSync } = await import('node:fs');
|
|
388
|
-
appendFileSync(historyFile(this.root, name), JSON.stringify(
|
|
394
|
+
appendFileSync(historyFile(this.root, name), JSON.stringify(summary) + '\n', 'utf8');
|
|
395
|
+
// Cross-run memory: make this run's outcome recallable by meaning
|
|
396
|
+
await this.storeRunMemory(name, org.def, org.run, summary);
|
|
389
397
|
}
|
|
390
398
|
}
|
|
391
399
|
catch (err) {
|
|
@@ -406,6 +414,87 @@ export class OrgDaemon {
|
|
|
406
414
|
async stopAll() {
|
|
407
415
|
await Promise.all([...this.orgs.keys()].map(n => this.stopOrg(n)));
|
|
408
416
|
}
|
|
417
|
+
orgMemoryNamespace(name, def) {
|
|
418
|
+
return def.run_config.memory_namespace ?? `org:${name}`;
|
|
419
|
+
}
|
|
420
|
+
/** Store dir for org cross-run memory — inside the org root so the bridge's
|
|
421
|
+
* path guard accepts it when the daemon runs from the project (the normal
|
|
422
|
+
* case) and test roots stay isolated. */
|
|
423
|
+
orgMemoryDbPath() {
|
|
424
|
+
return join(this.root, '.monomind', 'org-memory');
|
|
425
|
+
}
|
|
426
|
+
/** The memory bridge's traversal guard silently redirects out-of-tree paths
|
|
427
|
+
* to the per-project default store. For an org rooted outside cwd (tests,
|
|
428
|
+
* unusual daemon setups) that redirect would write into the WRONG project's
|
|
429
|
+
* memory — verify the guard kept our path, and skip org memory otherwise. */
|
|
430
|
+
async orgMemoryUsable() {
|
|
431
|
+
try {
|
|
432
|
+
const { bridgeGetDbPath } = await import('../memory/memory-bridge.js');
|
|
433
|
+
const want = this.orgMemoryDbPath();
|
|
434
|
+
const got = bridgeGetDbPath(want);
|
|
435
|
+
const { realpathSync } = await import('node:fs');
|
|
436
|
+
const real = (p) => { try {
|
|
437
|
+
return realpathSync(p);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return p;
|
|
441
|
+
} };
|
|
442
|
+
return real(got) === real(want);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** org_recall implementation: search the org's memory namespace via the
|
|
449
|
+
* memory bridge (semantic when the local model is available, tokenized
|
|
450
|
+
* keyword otherwise). Failures return a message, never throw into the tool. */
|
|
451
|
+
async recallOrgMemory(name, def, query) {
|
|
452
|
+
try {
|
|
453
|
+
if (!(await this.orgMemoryUsable()))
|
|
454
|
+
return { text: 'org memory is not available in this environment.', hits: 0 };
|
|
455
|
+
const { bridgeSearchEntries } = await import('../memory/memory-bridge.js');
|
|
456
|
+
const res = await bridgeSearchEntries({
|
|
457
|
+
query, namespace: this.orgMemoryNamespace(name, def), limit: 5, dbPath: this.orgMemoryDbPath(),
|
|
458
|
+
});
|
|
459
|
+
const results = res?.results ?? [];
|
|
460
|
+
if (!results.length)
|
|
461
|
+
return { text: 'No matching org memory found — this may be the first run covering this topic.', hits: 0 };
|
|
462
|
+
const text = results.map((r, i) => `${i + 1}. [${r.key}] ${r.content}`).join('\n\n');
|
|
463
|
+
return { text, hits: results.length };
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
return { text: `org memory unavailable (${err instanceof Error ? err.message : 'error'})`, hits: 0 };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/** Persist the run's outcome into cross-run org memory so org_recall (and
|
|
470
|
+
* future runs) can find it by meaning, not just recency. Best-effort. */
|
|
471
|
+
async storeRunMemory(name, def, run, summary) {
|
|
472
|
+
try {
|
|
473
|
+
if (!(await this.orgMemoryUsable()))
|
|
474
|
+
return;
|
|
475
|
+
const { bridgeStoreEntry } = await import('../memory/memory-bridge.js');
|
|
476
|
+
const when = summary.endedAt ? new Date(summary.endedAt).toISOString().slice(0, 10) : '';
|
|
477
|
+
const lines = [
|
|
478
|
+
`Org run ${run}${when ? ` (${when})` : ''} — goal: ${def.goal}`,
|
|
479
|
+
summary.outcome
|
|
480
|
+
? `Outcome: ${summary.outcome.status} — ${summary.outcome.summary}`
|
|
481
|
+
: `Outcome: not recorded (${summary.messages} messages exchanged)`,
|
|
482
|
+
summary.assets.length ? `Assets produced: ${summary.assets.slice(0, 10).join(', ')}` : '',
|
|
483
|
+
summary.crashes.length ? `Crashed agents: ${summary.crashes.join(', ')}` : '',
|
|
484
|
+
].filter(Boolean);
|
|
485
|
+
await bridgeStoreEntry({
|
|
486
|
+
key: `run-${run}`,
|
|
487
|
+
value: lines.join('\n'),
|
|
488
|
+
namespace: this.orgMemoryNamespace(name, def),
|
|
489
|
+
dbPath: this.orgMemoryDbPath(),
|
|
490
|
+
upsert: true,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
495
|
+
console.error(`org ${name}: run memory store failed:`, err instanceof Error ? err.message : err);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
409
498
|
persistState(name, status, run) {
|
|
410
499
|
const p = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
411
500
|
writeFileSync(p, JSON.stringify({ status, run, pid: process.pid, updated: new Date().toISOString() }, null, 2));
|
|
@@ -15,6 +15,8 @@ export interface SessionOpts {
|
|
|
15
15
|
askHuman?: (role: string, question: string) => Promise<string>;
|
|
16
16
|
/** Coordinator-only: records the run's outcome (daemon persists it to run history). */
|
|
17
17
|
onComplete?: (role: string, outcome: 'achieved' | 'partial' | 'failed', summary: string) => void;
|
|
18
|
+
/** Search the org's accumulated cross-run memory (memory_namespace). */
|
|
19
|
+
recall?: (role: string, query: string) => Promise<string>;
|
|
18
20
|
def?: OrgDef;
|
|
19
21
|
maxTurns?: number;
|
|
20
22
|
queryFn?: typeof query;
|
|
@@ -14,6 +14,7 @@ export function buildRolePrompt(role, def, roster) {
|
|
|
14
14
|
`The ONLY way to communicate with other agents is the org_send tool.`,
|
|
15
15
|
`Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
|
|
16
16
|
`If you need a human decision, call ask_human with your question, then end your turn — you'll receive the human's answer as a new message when it arrives. Do not call ask_human for anything you can resolve yourself.`,
|
|
17
|
+
`Before starting substantial work, call org_recall to check what previous runs already learned or delivered — do not redo finished work.`,
|
|
17
18
|
`When you receive a message, act on it, then org_send your result to the requester.`,
|
|
18
19
|
isCoordinator
|
|
19
20
|
? `When the org's goal for this run is achieved (or clearly can't be), call org_complete exactly once with the outcome and a concise summary of what was done — it is recorded in the org's run history and briefed to the next run. Then end your turn.`
|
|
@@ -52,6 +53,10 @@ async function runOneSession(opts) {
|
|
|
52
53
|
name: 'org',
|
|
53
54
|
version: '1.0.0',
|
|
54
55
|
tools: [
|
|
56
|
+
...(opts.recall ? [tool('org_recall', 'Search this org\'s accumulated memory from previous runs (outcomes, decisions, learnings). Use before starting work that may already have been done.', { query: z.string() }, async (args) => {
|
|
57
|
+
const text = await opts.recall(role.id, args.query);
|
|
58
|
+
return { content: [{ type: 'text', text }] };
|
|
59
|
+
})] : []),
|
|
55
60
|
...(isCoordinator && opts.onComplete ? [tool('org_complete', 'Record the outcome of this run. Call exactly once, when the goal is achieved or clearly cannot be. The outcome and summary are persisted to the org run history and briefed to the next run.', { outcome: z.enum(['achieved', 'partial', 'failed']), summary: z.string() }, async (args) => {
|
|
56
61
|
opts.onComplete(role.id, args.outcome, args.summary);
|
|
57
62
|
return { content: [{ type: 'text', text: `outcome "${args.outcome}" recorded` }] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI engine for Monomind — an open-source MCP server that extends Claude Code with a codebase knowledge graph (tree-sitter + SQLite), persistent memory, multi-agent task coordination, and session hooks. MIT licensed, fully local.",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
},
|
|
106
106
|
"optionalDependencies": {
|
|
107
107
|
"@huggingface/transformers": "^3.8.1",
|
|
108
|
-
"@monoes/memory": "^1.0.
|
|
108
|
+
"@monoes/memory": "^1.0.7",
|
|
109
109
|
"@monomind/hooks": "*",
|
|
110
110
|
"@monomind/mcp": "*",
|
|
111
111
|
"@monomind/routing": "*",
|