monomind 2.7.6 → 2.7.8
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 +5 -4
- package/packages/@monomind/cli/.claude/agents/core/coder.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/coordinator.md +62 -0
- package/packages/@monomind/cli/.claude/agents/core/planner.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/reviewer.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/tester.md +8 -0
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +180 -47
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +32 -3
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +55 -3
- package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +3 -1
- package/packages/@monomind/cli/.claude/helpers/statusline.cjs +27 -3
- package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +104 -18
- package/packages/@monomind/cli/.claude/settings.json +1 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-server.mjs +38 -1
- package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +6 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.d.ts +0 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +5 -1
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +32 -2
- package/packages/@monomind/cli/dist/src/commands/browse-workflow.js +30 -0
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/guidance.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/hooks-extended-commands.js +10 -2
- package/packages/@monomind/cli/dist/src/commands/init.js +18 -4
- package/packages/@monomind/cli/dist/src/commands/memory-crud.js +40 -3
- package/packages/@monomind/cli/dist/src/commands/org-observe.js +67 -13
- package/packages/@monomind/cli/dist/src/commands/org.d.ts +11 -0
- package/packages/@monomind/cli/dist/src/commands/org.js +162 -15
- package/packages/@monomind/cli/dist/src/commands/performance.js +31 -5
- package/packages/@monomind/cli/dist/src/commands/security-misc.js +18 -3
- package/packages/@monomind/cli/dist/src/commands/security-scan.d.ts +30 -1
- package/packages/@monomind/cli/dist/src/commands/security-scan.js +182 -69
- package/packages/@monomind/cli/dist/src/commands/swarm.js +41 -14
- package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +11 -10
- package/packages/@monomind/cli/dist/src/init/executor.js +138 -22
- package/packages/@monomind/cli/dist/src/init/settings-generator.js +4 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +17 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +62 -3
- package/packages/@monomind/cli/dist/src/mcp-client.js +78 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +16 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +27 -13
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +5 -1
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +26 -3
- package/packages/@monomind/cli/dist/src/memory/memory-read.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/memory/memory-read.js +13 -2
- package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +25 -6
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +21 -1
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +82 -11
- package/packages/@monomind/cli/dist/src/orgrt/inbox.js +53 -20
- package/packages/@monomind/cli/dist/src/parser.d.ts +4 -2
- package/packages/@monomind/cli/dist/src/parser.js +61 -25
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.d.ts +15 -0
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +44 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +10 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -2
- package/packages/@monomind/cli/dist/src/ui/collector.mjs +43 -3
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +27 -8
- package/packages/@monomind/cli/dist/src/ui/server.mjs +144 -14
- package/packages/@monomind/cli/package.json +7 -6
- package/packages/@monomind/cli/dist/src/capabilities/watcher.d.ts +0 -18
- package/packages/@monomind/cli/dist/src/capabilities/watcher.js +0 -107
- package/packages/@monomind/cli/dist/src/config-adapter.d.ts +0 -16
- package/packages/@monomind/cli/dist/src/config-adapter.js +0 -220
|
@@ -27,6 +27,7 @@ const RECENT_EDITS_FILE = path.join(DATA_DIR, 'recent-edits.jsonl');
|
|
|
27
27
|
|
|
28
28
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MiB guard
|
|
29
29
|
const RING_BUFFER_MAX = 50;
|
|
30
|
+
const MAX_ENTRIES = 200;
|
|
30
31
|
|
|
31
32
|
var _entries = []; // deduplicated memory entries loaded from store
|
|
32
33
|
var _recentEdits = []; // ring buffer of recently edited paths (in-memory, may be empty across subprocesses)
|
|
@@ -141,7 +142,7 @@ function init() {
|
|
|
141
142
|
if (seen.has(key)) return false;
|
|
142
143
|
seen.add(key);
|
|
143
144
|
return true;
|
|
144
|
-
});
|
|
145
|
+
}).slice(-MAX_ENTRIES);
|
|
145
146
|
|
|
146
147
|
// Bootstrap from monograph when store is sparse — called externally via bootstrapFromDb(db)
|
|
147
148
|
|
|
@@ -490,6 +491,7 @@ function bootstrapFromDb(db) {
|
|
|
490
491
|
files: h.file ? [h.file] : [],
|
|
491
492
|
ts: Date.now(),
|
|
492
493
|
};
|
|
494
|
+
if (_entries.length >= MAX_ENTRIES) _entries.shift();
|
|
493
495
|
_entries.push(hubEntry);
|
|
494
496
|
newHubEntries.push(hubEntry);
|
|
495
497
|
added++;
|
|
@@ -99,6 +99,22 @@ function readJSON(filePath) {
|
|
|
99
99
|
} catch { /* ignore */ }
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
|
+
/** Canonical data root — mirrors getMonomindDataRoot() in mcp-tools/types.ts. */
|
|
103
|
+
function monomindDataRoot(cwd) {
|
|
104
|
+
if (process.env.MONOMIND_DATA_DIR) return process.env.MONOMIND_DATA_DIR;
|
|
105
|
+
try {
|
|
106
|
+
const gitEntry = path.join(cwd, '.git');
|
|
107
|
+
const st = fs.statSync(gitEntry);
|
|
108
|
+
if (st.isDirectory()) return path.join(gitEntry, 'monomind');
|
|
109
|
+
const m = fs.readFileSync(gitEntry, 'utf8').match(/^gitdir:\s*(.+)/m);
|
|
110
|
+
if (m) {
|
|
111
|
+
const wt = path.resolve(cwd, m[1].trim());
|
|
112
|
+
return path.join(path.dirname(path.dirname(wt)), 'monomind');
|
|
113
|
+
}
|
|
114
|
+
} catch { /* not a git repo */ }
|
|
115
|
+
return path.join(cwd, '.monomind');
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
|
|
103
119
|
// Safe file stat (returns null on failure)
|
|
104
120
|
function safeStat(filePath) {
|
|
@@ -395,9 +411,17 @@ function getSwarmStatus() {
|
|
|
395
411
|
} catch { /* fall through */ }
|
|
396
412
|
}
|
|
397
413
|
|
|
398
|
-
// SECONDARY: swarm-state.json written by MCP swarm_init — trust if fresh
|
|
399
|
-
|
|
400
|
-
|
|
414
|
+
// SECONDARY: swarm-state.json written by MCP swarm_init — trust if fresh.
|
|
415
|
+
// The MCP tools resolve their data root via getMonomindDataRoot(), which
|
|
416
|
+
// inside a git repo is `<repo>/.git/monomind` — so reading only
|
|
417
|
+
// `<cwd>/.monomind` showed a stale or missing swarm in every real project.
|
|
418
|
+
// Canonical first, legacy second for projects written by an older CLI.
|
|
419
|
+
const swarmStateCandidates = [
|
|
420
|
+
path.join(monomindDataRoot(CWD), 'swarm', 'swarm-state.json'),
|
|
421
|
+
path.join(CWD, '.monomind', 'swarm', 'swarm-state.json'),
|
|
422
|
+
];
|
|
423
|
+
let swarmState = null;
|
|
424
|
+
for (const p of swarmStateCandidates) { swarmState = readJSON(p); if (swarmState) break; }
|
|
401
425
|
if (swarmState) {
|
|
402
426
|
const updatedAt = swarmState.updatedAt || swarmState.startedAt;
|
|
403
427
|
const age = updatedAt ? now - new Date(updatedAt).getTime() : Infinity;
|
|
@@ -330,25 +330,107 @@ function _graphGateReadSessions() {
|
|
|
330
330
|
return sessions;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// Atomic replace: write a per-process temp file, then rename() over the
|
|
334
|
+
// target. rename() within a directory is atomic on POSIX and on Windows via
|
|
335
|
+
// Node's fs, so a concurrent reader sees either the old file or the new one
|
|
336
|
+
// — never a half-written one. (The previous plain writeFileSync truncated
|
|
337
|
+
// the live file first, so a reader could observe a truncated/torn document.)
|
|
333
338
|
function _graphGateWriteSessions(sessions) {
|
|
334
339
|
var ids = Object.keys(sessions);
|
|
335
340
|
if (ids.length > 20) {
|
|
336
341
|
ids.sort(function (a, b) { return (sessions[a].ts || 0) - (sessions[b].ts || 0); });
|
|
337
342
|
for (var i = 0; i < ids.length - 20; i++) delete sessions[ids[i]];
|
|
338
343
|
}
|
|
339
|
-
|
|
340
|
-
fs.
|
|
344
|
+
var dir = path.join(CWD, '.monomind');
|
|
345
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
346
|
+
var target = _graphGateStateFile();
|
|
347
|
+
var tmp = target + '.' + process.pid + '.' + Math.random().toString(36).slice(2, 8) + '.tmp';
|
|
348
|
+
try {
|
|
349
|
+
fs.writeFileSync(tmp, JSON.stringify({ sessions: sessions }));
|
|
350
|
+
fs.renameSync(tmp, target);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
353
|
+
throw e;
|
|
354
|
+
}
|
|
341
355
|
}
|
|
342
356
|
|
|
343
|
-
|
|
344
|
-
|
|
357
|
+
// Cross-process advisory lock around the read-modify-write above.
|
|
358
|
+
//
|
|
359
|
+
// Three concurrent paths touch this state (pre-bash, pre-search, and
|
|
360
|
+
// post-graph-tool's markQueried), each in its own short-lived process. Without
|
|
361
|
+
// a lock, two of them read the same snapshot and the second write erases the
|
|
362
|
+
// first — measured: 12 concurrent writers landed as few as 6 of 12 session
|
|
363
|
+
// records. A lost `queried:true` re-blocks a session that already called
|
|
364
|
+
// monograph; a lost `blockedOnce` can block it a second time.
|
|
365
|
+
//
|
|
366
|
+
// mkdir is the atomic primitive (works on every platform and over network FS).
|
|
367
|
+
// The lock is best-effort: if it can't be taken within the budget, or a stale
|
|
368
|
+
// one is reclaimed, we still perform an atomic-rename write — no worse than the
|
|
369
|
+
// old behavior, and never a hang. Hooks run on every tool call, so the total
|
|
370
|
+
// wait is deliberately tiny.
|
|
371
|
+
var _GRAPH_GATE_LOCK_TIMEOUT_MS = 250;
|
|
372
|
+
var _GRAPH_GATE_LOCK_STALE_MS = 2000;
|
|
373
|
+
|
|
374
|
+
function _sleepSync(ms) {
|
|
375
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch (e) { /* no SAB */ }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function _graphGateAcquireLock() {
|
|
379
|
+
var lockDir = _graphGateStateFile() + '.lock';
|
|
380
|
+
var deadline = Date.now() + _GRAPH_GATE_LOCK_TIMEOUT_MS;
|
|
381
|
+
var held = false;
|
|
382
|
+
for (;;) {
|
|
383
|
+
try {
|
|
384
|
+
fs.mkdirSync(lockDir);
|
|
385
|
+
held = true;
|
|
386
|
+
break;
|
|
387
|
+
} catch (e) {
|
|
388
|
+
if (e && e.code !== 'EEXIST') break; // can't lock at all — proceed unlocked
|
|
389
|
+
try {
|
|
390
|
+
var st = fs.statSync(lockDir);
|
|
391
|
+
if (Date.now() - st.mtimeMs > _GRAPH_GATE_LOCK_STALE_MS) {
|
|
392
|
+
fs.rmSync(lockDir, { recursive: true, force: true }); // crashed holder
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
} catch (_) { continue; } // lock vanished — retry immediately
|
|
396
|
+
if (Date.now() >= deadline) break; // give up; write unlocked
|
|
397
|
+
_sleepSync(5);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return function release() {
|
|
401
|
+
if (!held) return;
|
|
402
|
+
try { fs.rmSync(lockDir, { recursive: true, force: true }); } catch (_) {}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Read-modify-write one session's entry under the lock. `mutate(s, ctx)`
|
|
408
|
+
* receives the session record (created if absent) and mutates it in place; its
|
|
409
|
+
* return value is passed through to the caller. A mutator that changed nothing
|
|
410
|
+
* can call `ctx.noWrite()` to skip the write entirely (the common read-only
|
|
411
|
+
* case: a session that already called monograph).
|
|
412
|
+
*/
|
|
413
|
+
function _graphGateUpdateSession(sessionId, mutate) {
|
|
414
|
+
var release = _graphGateAcquireLock();
|
|
345
415
|
try {
|
|
346
416
|
var sessions = _graphGateReadSessions();
|
|
347
|
-
var s = sessions[sessionId] || { blockedOnce: false };
|
|
348
|
-
|
|
417
|
+
var s = sessions[sessionId] || { queried: false, blockedOnce: false };
|
|
418
|
+
var write = true;
|
|
419
|
+
var out = mutate(s, { noWrite: function () { write = false; } });
|
|
420
|
+
if (!write) return out;
|
|
349
421
|
s.ts = Date.now();
|
|
350
422
|
sessions[sessionId] = s;
|
|
351
423
|
_graphGateWriteSessions(sessions);
|
|
424
|
+
return out;
|
|
425
|
+
} finally {
|
|
426
|
+
release();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function _graphGateMarkQueried(sessionId) {
|
|
431
|
+
if (!sessionId) return;
|
|
432
|
+
try {
|
|
433
|
+
_graphGateUpdateSession(sessionId, function (s) { s.queried = true; });
|
|
352
434
|
} catch (e) { /* non-fatal */ }
|
|
353
435
|
}
|
|
354
436
|
|
|
@@ -356,19 +438,23 @@ function _graphGateMarkQueried(sessionId) {
|
|
|
356
438
|
function _graphGateShouldBlock(sessionId) {
|
|
357
439
|
if (String(process.env.MONOMIND_GRAPH_GATE || '').toLowerCase() === 'off') return false;
|
|
358
440
|
if (!sessionId || !_isGraphFresh()) return false;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
441
|
+
// Test-and-set blockedOnce atomically: two concurrent greps in the same
|
|
442
|
+
// session must not both observe blockedOnce=false and both block.
|
|
443
|
+
try {
|
|
444
|
+
return _graphGateUpdateSession(sessionId, function (s, ctx) {
|
|
445
|
+
if (s.queried) { ctx.noWrite(); return false; }
|
|
446
|
+
if (!s.blockedOnce) {
|
|
447
|
+
s.blockedOnce = true;
|
|
448
|
+
return 'block';
|
|
449
|
+
}
|
|
450
|
+
// Already blocked once but monograph still not called — warn without
|
|
451
|
+
// blocking so subagents without MCP access don't deadlock.
|
|
452
|
+
ctx.noWrite();
|
|
453
|
+
return 'warn';
|
|
454
|
+
});
|
|
455
|
+
} catch (e) {
|
|
456
|
+
return false; // state unreadable/unwritable — never block on a state error
|
|
368
457
|
}
|
|
369
|
-
// Already blocked once but monograph still not called — warn without blocking
|
|
370
|
-
// so subagents without MCP access don't deadlock.
|
|
371
|
-
return 'warn';
|
|
372
458
|
}
|
|
373
459
|
|
|
374
460
|
function _getNodeCount() {
|
|
@@ -1112,7 +1112,8 @@ applyLegacyDeferredAcceptsOnStartup();
|
|
|
1112
1112
|
restorePendingEventsFromStore();
|
|
1113
1113
|
manualApply.pruneStaleEvidence();
|
|
1114
1114
|
const portArg = args.find(a => a.startsWith('--port='));
|
|
1115
|
-
|
|
1115
|
+
const explicitPort = portArg ? parseInt(portArg.split('=')[1], 10) : null;
|
|
1116
|
+
state.port = explicitPort ?? await findOpenPort();
|
|
1116
1117
|
// Annotation screenshots live in the project root so the agent's Read tool
|
|
1117
1118
|
// doesn't trip a per-file permission prompt. Sessioned by token so concurrent
|
|
1118
1119
|
// projects (or quick restarts) don't collide.
|
|
@@ -1123,7 +1124,43 @@ state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-'));
|
|
|
1123
1124
|
const { detectScript, liveScriptParts } = loadBrowserScripts();
|
|
1124
1125
|
httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts }));
|
|
1125
1126
|
|
|
1127
|
+
// findOpenPort() probes a port, closes the probe socket, and only then does the
|
|
1128
|
+
// real server bind it. Another process scanning the same range can take the
|
|
1129
|
+
// port inside that gap, which surfaced as an intermittent
|
|
1130
|
+
// "EADDRINUSE 127.0.0.1:8405" — about one run in five of the test suite, where
|
|
1131
|
+
// several server-starting test files run in parallel.
|
|
1132
|
+
//
|
|
1133
|
+
// Retry on the next port when we picked it ourselves. An explicit --port is
|
|
1134
|
+
// never silently moved: the caller asked for that port, so a conflict there
|
|
1135
|
+
// must be an error they can see.
|
|
1136
|
+
const MAX_PORT_RETRIES = 25;
|
|
1137
|
+
let portRetries = 0;
|
|
1138
|
+
|
|
1139
|
+
httpServer.on('error', (err) => {
|
|
1140
|
+
if (err?.code !== 'EADDRINUSE') throw err;
|
|
1141
|
+
|
|
1142
|
+
if (explicitPort !== null) {
|
|
1143
|
+
console.error(`\nPort ${state.port} is already in use.`);
|
|
1144
|
+
console.error('Another live server may be running — stop it, or pass a different --port.');
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (portRetries >= MAX_PORT_RETRIES) {
|
|
1149
|
+
console.error(`\nCould not find a free port after ${MAX_PORT_RETRIES} attempts (last tried ${state.port}).`);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
portRetries++;
|
|
1154
|
+
state.port++;
|
|
1155
|
+
httpServer.listen(state.port, '127.0.0.1');
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1126
1158
|
httpServer.listen(state.port, '127.0.0.1', () => {
|
|
1159
|
+
// Trust the address the OS actually bound over the one we asked for — after
|
|
1160
|
+
// a retry above, state.port and the bound port must not drift apart.
|
|
1161
|
+
const boundAddr = httpServer.address();
|
|
1162
|
+
if (boundAddr && typeof boundAddr === 'object') state.port = boundAddr.port;
|
|
1163
|
+
|
|
1127
1164
|
writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token });
|
|
1128
1165
|
const url = `http://localhost:${state.port}`;
|
|
1129
1166
|
console.log(`\nMonodesign live server running on ${url}`);
|
|
@@ -4,6 +4,7 @@ import { join, dirname } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { WebSocketServer } from 'ws';
|
|
7
|
+
import { getMonomindDataRoot } from '../../mcp-tools/types.js';
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
// 4243: the main monomind dashboard owns 4242 — keep this server off that port
|
|
9
10
|
const DEFAULT_PORT = 4243;
|
|
@@ -49,7 +50,11 @@ async function readMetricsDir(root) {
|
|
|
49
50
|
async function collectDashboardState(root) {
|
|
50
51
|
const [workerMetrics, swarmState, lastRoute, autoMemory] = await Promise.all([
|
|
51
52
|
readMetricsDir(root),
|
|
52
|
-
|
|
53
|
+
// Canonical root first (getMonomindDataRoot(): `<repo>/.git/monomind` in a
|
|
54
|
+
// git repo), legacy `<root>/.monomind` second. Reading only the legacy path
|
|
55
|
+
// showed stale/absent swarm state in every real project.
|
|
56
|
+
readJsonSafe(join(getMonomindDataRoot(root), 'swarm', 'swarm-state.json'))
|
|
57
|
+
.then((v) => v ?? readJsonSafe(join(root, '.monomind', 'swarm', 'swarm-state.json'))),
|
|
53
58
|
readJsonSafe(join(root, '.monomind', 'last-route.json')),
|
|
54
59
|
readJsonSafe(join(root, '.monomind', 'data', 'auto-memory-store.json')),
|
|
55
60
|
]);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { scanDirectory, saveFingerprint, loadFingerprint } from './scanner.js';
|
|
3
3
|
export { CapabilityManager } from './manager.js';
|
|
4
|
-
export { FileWatcher } from './watcher.js';
|
|
5
4
|
export { codeCapability } from './cap-code.js';
|
|
6
5
|
export { documentsCapability } from './cap-documents.js';
|
|
7
6
|
export { mediaCapability } from './cap-media.js';
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { scanDirectory, saveFingerprint, loadFingerprint } from './scanner.js';
|
|
3
3
|
export { CapabilityManager } from './manager.js';
|
|
4
|
-
|
|
4
|
+
// `FileWatcher` (./watcher.ts) was deleted 2026-07: it was built, exported and
|
|
5
|
+
// unit-tested but never instantiated anywhere in production. Nothing in the
|
|
6
|
+
// capabilities pipeline watches files — `search scan` re-fingerprints on demand
|
|
7
|
+
// (see commands/search-universal.ts) and monograph has its own separate watcher
|
|
8
|
+
// (`monograph_watch`). Nothing outside this package could reach it either:
|
|
9
|
+
// capabilities/ is not in package.json's `exports` map and src/index.ts never
|
|
10
|
+
// re-exported it. If incremental re-scanning is wanted later, wire it to the
|
|
11
|
+
// scanner at the same time rather than landing an unused class again.
|
|
5
12
|
export { codeCapability } from './cap-code.js';
|
|
6
13
|
export { documentsCapability } from './cap-documents.js';
|
|
7
14
|
export { mediaCapability } from './cap-media.js';
|
|
@@ -181,6 +181,10 @@ export const listCommand = {
|
|
|
181
181
|
],
|
|
182
182
|
action: async (ctx) => {
|
|
183
183
|
try {
|
|
184
|
+
// agent_list emits `agentId` (agent-tools.ts), not `id` — reading `id`
|
|
185
|
+
// here rendered a blank ID column for every agent. `id` is kept as a
|
|
186
|
+
// fallback because agent_pool/agent_health project the same records
|
|
187
|
+
// under that key.
|
|
184
188
|
const result = await callMCPTool('agent_list', {
|
|
185
189
|
status: ctx.flags.all ? 'all' : ctx.flags.status || undefined,
|
|
186
190
|
agentType: ctx.flags.type || undefined,
|
|
@@ -198,7 +202,7 @@ export const listCommand = {
|
|
|
198
202
|
return { success: true, data: result };
|
|
199
203
|
}
|
|
200
204
|
const displayAgents = result.agents.map(agent => ({
|
|
201
|
-
id: agent.id,
|
|
205
|
+
id: agent.agentId ?? agent.id ?? '',
|
|
202
206
|
type: agent.agentType,
|
|
203
207
|
status: agent.status,
|
|
204
208
|
created: new Date(agent.createdAt).toLocaleTimeString(),
|
|
@@ -180,6 +180,36 @@ export const healthCommand = {
|
|
|
180
180
|
{ command: 'monomind agent health -i agent-001 -d', description: 'Detailed health for specific agent' },
|
|
181
181
|
],
|
|
182
182
|
action: async (ctx) => {
|
|
183
|
+
// --watch was declared (and documented as "refresh every 5s") but never
|
|
184
|
+
// read, so the command rendered once and exited. Mirrors the watch loop in
|
|
185
|
+
// commands/status.ts.
|
|
186
|
+
if (ctx.flags.watch) {
|
|
187
|
+
return watchAgentHealth(ctx);
|
|
188
|
+
}
|
|
189
|
+
return renderAgentHealth(ctx);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const WATCH_INTERVAL_MS = 5000;
|
|
193
|
+
async function watchAgentHealth(ctx) {
|
|
194
|
+
const refresh = async () => {
|
|
195
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
196
|
+
output.writeln(output.dim(`Last updated: ${new Date().toLocaleTimeString()} — refreshing every 5s. Press Ctrl+C to exit.`));
|
|
197
|
+
await renderAgentHealth(ctx);
|
|
198
|
+
};
|
|
199
|
+
await refresh();
|
|
200
|
+
const intervalId = setInterval(() => { void refresh(); }, WATCH_INTERVAL_MS);
|
|
201
|
+
// `once` so repeated invocations don't accumulate SIGINT handlers.
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
process.once('SIGINT', () => {
|
|
204
|
+
clearInterval(intervalId);
|
|
205
|
+
output.writeln();
|
|
206
|
+
output.printInfo('Watch mode stopped');
|
|
207
|
+
resolve({ success: true });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function renderAgentHealth(ctx) {
|
|
212
|
+
{
|
|
183
213
|
const agentId = ctx.args[0] || ctx.flags.id;
|
|
184
214
|
const detailed = ctx.flags.detailed;
|
|
185
215
|
try {
|
|
@@ -242,6 +272,6 @@ export const healthCommand = {
|
|
|
242
272
|
output.printError(error instanceof MCPClientError ? `Health check error: ${error.message}` : `Unexpected error: ${String(error)}`);
|
|
243
273
|
return { success: false, exitCode: 1 };
|
|
244
274
|
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
247
277
|
//# sourceMappingURL=agent-ops.js.map
|
|
@@ -53,6 +53,35 @@ const runSubcommand = {
|
|
|
53
53
|
const wf = await readWorkflow(filePath).catch(e => { output.printError(e.message); return null; });
|
|
54
54
|
if (!wf)
|
|
55
55
|
return { success: false, exitCode: 1 };
|
|
56
|
+
// --items feeds the run's input set. Previously the flag was parsed and
|
|
57
|
+
// then dropped, so every run silently used the single empty default item.
|
|
58
|
+
let items;
|
|
59
|
+
const itemsFlag = ctx.flags.items;
|
|
60
|
+
if (itemsFlag) {
|
|
61
|
+
const itemsPath = isAbsolute(itemsFlag) ? itemsFlag : resolve(ctx.cwd, itemsFlag);
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
const { readFile } = await import('fs/promises');
|
|
65
|
+
parsed = JSON.parse(await readFile(itemsPath, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
output.printError(`Cannot read items file ${itemsPath}: ${e.message}`);
|
|
69
|
+
return { success: false, exitCode: 1 };
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(parsed)) {
|
|
72
|
+
output.printError(`Items file ${itemsPath} must contain a JSON array`);
|
|
73
|
+
return { success: false, exitCode: 1 };
|
|
74
|
+
}
|
|
75
|
+
// Accept both the engine's `{ data: {...} }` envelope and a bare array
|
|
76
|
+
// of objects, which is the shape people naturally write by hand.
|
|
77
|
+
items = parsed.map((entry) => {
|
|
78
|
+
const rec = entry;
|
|
79
|
+
return rec && typeof rec === 'object' && 'data' in rec && typeof rec.data === 'object' && rec.data !== null
|
|
80
|
+
? { data: rec.data }
|
|
81
|
+
: { data: (rec ?? {}) };
|
|
82
|
+
});
|
|
83
|
+
output.printInfo(`Loaded ${items.length} input item(s) from ${itemsFlag}`);
|
|
84
|
+
}
|
|
56
85
|
const port = ctx.flags.port ?? 4243;
|
|
57
86
|
const dashboard = getDashboardServer(port);
|
|
58
87
|
if (!ctx.flags['no-dashboard']) {
|
|
@@ -65,6 +94,7 @@ const runSubcommand = {
|
|
|
65
94
|
spinner.start();
|
|
66
95
|
const record = await runWorkflow(wf, {
|
|
67
96
|
onEvent: (ev) => dashboard.broadcast(ev),
|
|
97
|
+
...(items ? { items } : {}),
|
|
68
98
|
});
|
|
69
99
|
if (record.status === 'completed') {
|
|
70
100
|
spinner.succeed(`Done — ${record.itemsProcessed} items in ${((record.completedAt - record.startedAt) / 1000).toFixed(1)}s`);
|
|
@@ -627,7 +627,14 @@ export async function checkGuidanceGates() {
|
|
|
627
627
|
}
|
|
628
628
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
629
629
|
const preToolUse = settings?.hooks?.PreToolUse ?? [];
|
|
630
|
-
|
|
630
|
+
// Match on capability, not on an exact matcher string. The matcher is a
|
|
631
|
+
// regex alternation that legitimately changes as tools are added — it
|
|
632
|
+
// gained `|NotebookEdit` when that tool turned out to bypass the secret
|
|
633
|
+
// gate — and an `=== 'Write|Edit|MultiEdit'` test reported the gate as
|
|
634
|
+
// INACTIVE the moment it did, telling users their secrets gate was off
|
|
635
|
+
// while it was demonstrably blocking secrets, and sending them to
|
|
636
|
+
// `guidance setup`, which would then add a duplicate entry.
|
|
637
|
+
const hasPreWrite = preToolUse.some(e => /\bWrite\b/.test(e.matcher ?? '') && e.hooks.some(h => h.command?.includes('pre-write')));
|
|
631
638
|
const hasPreBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks.some(h => h.command?.includes('pre-bash')));
|
|
632
639
|
if (!hasPreWrite && !hasPreBash)
|
|
633
640
|
return { name: 'Guidance Gates', status: 'warn', message: 'gates-handler.cjs present but no gates registered', fix: 'monomind guidance setup' };
|
|
@@ -52,12 +52,18 @@ const setupCommand = {
|
|
|
52
52
|
const preToolUse = hooks.PreToolUse || [];
|
|
53
53
|
const PRE_BASH_MATCHER = 'Bash';
|
|
54
54
|
const PRE_BASH_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" pre-bash';
|
|
55
|
-
|
|
55
|
+
// Keep in step with .claude/settings.json and init/settings-generator.ts.
|
|
56
|
+
// NotebookEdit is listed explicitly: its content field is `new_source`, so
|
|
57
|
+
// it slipped past the secret gate until the handler learned to read it.
|
|
58
|
+
const PRE_WRITE_MATCHER = 'Write|Edit|MultiEdit|NotebookEdit';
|
|
56
59
|
const PRE_WRITE_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" pre-write';
|
|
57
60
|
// Find any existing Bash/Write entry (may have different hooks from other tools)
|
|
58
61
|
const existingBashEntry = preToolUse.find(e => e.matcher === PRE_BASH_MATCHER);
|
|
59
62
|
const alreadyHasPreBash = existingBashEntry?.hooks.some(h => h.command.includes('pre-bash')) ?? false;
|
|
60
|
-
|
|
63
|
+
// Find by capability, not exact string: a settings.json written before
|
|
64
|
+
// NotebookEdit was added still says 'Write|Edit|MultiEdit', and an exact
|
|
65
|
+
// match would miss it and append a second, overlapping entry.
|
|
66
|
+
const existingWriteEntry = preToolUse.find(e => /\bWrite\b/.test(e.matcher ?? ''));
|
|
61
67
|
const alreadyHasPreWrite = existingWriteEntry?.hooks.some(h => h.command.includes('pre-write')) ?? false;
|
|
62
68
|
const changes = [];
|
|
63
69
|
// Register pre-bash (destructive-ops gate)
|
|
@@ -200,7 +200,7 @@ export const notifyCommand = {
|
|
|
200
200
|
options: [
|
|
201
201
|
{ name: 'message', short: 'm', type: 'string', description: 'Notification message', required: true },
|
|
202
202
|
{ name: 'level', short: 'l', type: 'string', description: 'Level: info, warn, error', default: 'info' },
|
|
203
|
-
{ name: 'channel', short: 'c', type: 'string', description: 'Notification channel', default: 'console' },
|
|
203
|
+
{ name: 'channel', short: 'c', type: 'string', description: 'Notification channel (only "console" is implemented)', default: 'console' },
|
|
204
204
|
],
|
|
205
205
|
examples: [
|
|
206
206
|
{ command: 'monomind hooks notify -m "Build complete"', description: 'Send info notification' },
|
|
@@ -213,6 +213,14 @@ export const notifyCommand = {
|
|
|
213
213
|
output.printError('Message is required: --message "your message"');
|
|
214
214
|
return { success: false, exitCode: 1 };
|
|
215
215
|
}
|
|
216
|
+
// Console is the only delivery mechanism that exists. Accepting
|
|
217
|
+
// `--channel slack` and then printing to the console anyway would tell the
|
|
218
|
+
// user their message went somewhere it did not.
|
|
219
|
+
const channel = ctx.flags.channel || 'console';
|
|
220
|
+
if (channel !== 'console') {
|
|
221
|
+
output.writeln(output.warning(`Channel "${channel}" is not implemented — delivering to console. ` +
|
|
222
|
+
`Only "console" is supported today.`));
|
|
223
|
+
}
|
|
216
224
|
const timestamp = new Date().toISOString();
|
|
217
225
|
if (level === 'error') {
|
|
218
226
|
output.printError(`[${timestamp}] ${message}`);
|
|
@@ -229,7 +237,7 @@ export const notifyCommand = {
|
|
|
229
237
|
await storeEntry({ key: `notify-${Date.now()}`, value: `[${level}] ${message}`, namespace: 'notifications' });
|
|
230
238
|
}
|
|
231
239
|
catch { /* memory not available */ }
|
|
232
|
-
return { success: true, data: { timestamp, level, message } };
|
|
240
|
+
return { success: true, data: { timestamp, level, message, channel: 'console' } };
|
|
233
241
|
}
|
|
234
242
|
};
|
|
235
243
|
//# sourceMappingURL=hooks-extended-commands.js.map
|
|
@@ -90,7 +90,18 @@ const initAction = async (ctx) => {
|
|
|
90
90
|
// Start monograph watch for ongoing file-change rebuilds, unless --no-watch was passed.
|
|
91
91
|
// Guard: skip if a watcher PID file already exists and the process is still alive,
|
|
92
92
|
// preventing duplicate watchers from accumulating on repeated `init --force` runs.
|
|
93
|
-
|
|
93
|
+
// `--no-watch` is the parser's negation of the declared boolean `watch`
|
|
94
|
+
// option. It used to be declared as its own boolean named 'no-watch', which
|
|
95
|
+
// the parser never populated: parseFlag strips the `--no-` prefix and looks
|
|
96
|
+
// up `watch`, which IS a declared boolean (status/agent-ops declare it and
|
|
97
|
+
// getBooleanFlags() is global across all registered commands). So
|
|
98
|
+
// `--no-watch` set the unrelated `watch` flag to false and left `no-watch`
|
|
99
|
+
// at its `false` default — the flag was a silent no-op and the watcher
|
|
100
|
+
// started anyway. The legacy `no-watch`/`noWatch` keys are still honoured
|
|
101
|
+
// for programmatic callers that set ctx.flags directly.
|
|
102
|
+
const noWatch = ctx.flags.watch === false ||
|
|
103
|
+
ctx.flags['no-watch'] === true ||
|
|
104
|
+
ctx.flags.noWatch === true;
|
|
94
105
|
if (!noWatch) {
|
|
95
106
|
try {
|
|
96
107
|
const { spawn } = await import('child_process');
|
|
@@ -354,10 +365,13 @@ export const initCommand = {
|
|
|
354
365
|
default: true,
|
|
355
366
|
},
|
|
356
367
|
{
|
|
357
|
-
|
|
358
|
-
|
|
368
|
+
// Declared as the positive `watch` so the parser's `--no-X` negation
|
|
369
|
+
// actually reaches it. Declaring it as `no-watch` made `--no-watch` a
|
|
370
|
+
// no-op — see the noWatch resolution in initAction.
|
|
371
|
+
name: 'watch',
|
|
372
|
+
description: 'Start the monograph knowledge graph watcher after init (default: true; --no-watch skips it)',
|
|
359
373
|
type: 'boolean',
|
|
360
|
-
default:
|
|
374
|
+
default: true,
|
|
361
375
|
},
|
|
362
376
|
{
|
|
363
377
|
name: 'with-embeddings',
|
|
@@ -297,7 +297,10 @@ export const searchCommand = {
|
|
|
297
297
|
output.writeln();
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
// Requested type only — the method that ACTUALLY ran is printed after the
|
|
301
|
+
// search, from searchResult.searchMethod. Labelling this line "(semantic)"
|
|
302
|
+
// used to claim a vector search that may never have happened.
|
|
303
|
+
output.printInfo(`Searching: "${query}" (requested: ${searchType})`);
|
|
301
304
|
output.writeln();
|
|
302
305
|
// Use direct sql.js search with vector similarity
|
|
303
306
|
try {
|
|
@@ -318,11 +321,45 @@ export const searchCommand = {
|
|
|
318
321
|
namespace: r.namespace,
|
|
319
322
|
preview: r.content
|
|
320
323
|
}));
|
|
324
|
+
const actualMethod = searchResult.searchMethod ?? 'unknown';
|
|
325
|
+
const fallbackReason = searchResult.fallbackReason;
|
|
321
326
|
if (ctx.flags.format === 'json') {
|
|
322
|
-
output.printJson({
|
|
327
|
+
output.printJson({
|
|
328
|
+
query,
|
|
329
|
+
searchType,
|
|
330
|
+
searchMethod: actualMethod,
|
|
331
|
+
...(fallbackReason ? { fallbackReason } : {}),
|
|
332
|
+
results,
|
|
333
|
+
searchTime: `${searchResult.searchTime}ms`,
|
|
334
|
+
});
|
|
323
335
|
return { success: true, data: results };
|
|
324
336
|
}
|
|
325
|
-
// Performance stats
|
|
337
|
+
// Performance stats — method first, so a keyword fallback is never hidden
|
|
338
|
+
// behind a "(semantic)" header.
|
|
339
|
+
const REASON_TEXT = {
|
|
340
|
+
'no-embedding-model': 'embedding model unavailable',
|
|
341
|
+
'empty-query': 'query was empty, so no vector could be built',
|
|
342
|
+
'embedding-failed': 'embedding generation failed',
|
|
343
|
+
'no-semantic-matches': 'vector search returned no matches',
|
|
344
|
+
};
|
|
345
|
+
const why = fallbackReason ? REASON_TEXT[fallbackReason] ?? fallbackReason : undefined;
|
|
346
|
+
if (actualMethod === 'semantic') {
|
|
347
|
+
output.writeln(output.dim(' Method: semantic (vector similarity)'));
|
|
348
|
+
}
|
|
349
|
+
else if (actualMethod === 'hybrid') {
|
|
350
|
+
output.writeln(output.dim(' Method: hybrid (per-entry cosine, keyword overlap where no vector exists)'));
|
|
351
|
+
}
|
|
352
|
+
else if (actualMethod === 'hash-vector' || actualMethod === 'hash-hybrid') {
|
|
353
|
+
// A vector search did run, but over hash-fallback embeddings — the
|
|
354
|
+
// scores are cosines of a lexical hash, not of a semantic model.
|
|
355
|
+
output.printWarning(`Method: ${actualMethod}${why ? ` — ${why}` : ''}. Scores are cosines over deterministic hash embeddings, not semantic similarity.`);
|
|
356
|
+
}
|
|
357
|
+
else if (actualMethod === 'unknown') {
|
|
358
|
+
output.writeln(output.dim(' Method: unknown'));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
output.printWarning(`Method: ${actualMethod}${why ? ` — ${why}` : ''}. Scores are token-overlap fractions, not vector similarity.`);
|
|
362
|
+
}
|
|
326
363
|
output.writeln(output.dim(` Search time: ${searchResult.searchTime}ms`));
|
|
327
364
|
output.writeln();
|
|
328
365
|
if (results.length === 0) {
|