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.
Files changed (63) hide show
  1. package/package.json +5 -4
  2. package/packages/@monomind/cli/.claude/agents/core/coder.md +9 -0
  3. package/packages/@monomind/cli/.claude/agents/core/coordinator.md +62 -0
  4. package/packages/@monomind/cli/.claude/agents/core/planner.md +9 -0
  5. package/packages/@monomind/cli/.claude/agents/core/reviewer.md +9 -0
  6. package/packages/@monomind/cli/.claude/agents/core/tester.md +8 -0
  7. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +180 -47
  8. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +32 -3
  9. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +55 -3
  10. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +3 -1
  11. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +27 -3
  12. package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +104 -18
  13. package/packages/@monomind/cli/.claude/settings.json +1 -1
  14. package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-server.mjs +38 -1
  15. package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +6 -1
  16. package/packages/@monomind/cli/dist/src/capabilities/index.d.ts +0 -1
  17. package/packages/@monomind/cli/dist/src/capabilities/index.js +8 -1
  18. package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +5 -1
  19. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +32 -2
  20. package/packages/@monomind/cli/dist/src/commands/browse-workflow.js +30 -0
  21. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +8 -1
  22. package/packages/@monomind/cli/dist/src/commands/guidance.js +8 -2
  23. package/packages/@monomind/cli/dist/src/commands/hooks-extended-commands.js +10 -2
  24. package/packages/@monomind/cli/dist/src/commands/init.js +18 -4
  25. package/packages/@monomind/cli/dist/src/commands/memory-crud.js +40 -3
  26. package/packages/@monomind/cli/dist/src/commands/org-observe.js +67 -13
  27. package/packages/@monomind/cli/dist/src/commands/org.d.ts +11 -0
  28. package/packages/@monomind/cli/dist/src/commands/org.js +162 -15
  29. package/packages/@monomind/cli/dist/src/commands/performance.js +31 -5
  30. package/packages/@monomind/cli/dist/src/commands/security-misc.js +18 -3
  31. package/packages/@monomind/cli/dist/src/commands/security-scan.d.ts +30 -1
  32. package/packages/@monomind/cli/dist/src/commands/security-scan.js +182 -69
  33. package/packages/@monomind/cli/dist/src/commands/swarm.js +41 -14
  34. package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +11 -10
  35. package/packages/@monomind/cli/dist/src/init/executor.js +138 -22
  36. package/packages/@monomind/cli/dist/src/init/settings-generator.js +4 -1
  37. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +17 -0
  38. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +62 -3
  39. package/packages/@monomind/cli/dist/src/mcp-client.js +78 -0
  40. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +16 -8
  41. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +27 -13
  42. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +5 -1
  43. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +26 -3
  44. package/packages/@monomind/cli/dist/src/memory/memory-read.d.ts +9 -0
  45. package/packages/@monomind/cli/dist/src/memory/memory-read.js +13 -2
  46. package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +25 -6
  47. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +21 -1
  48. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +82 -11
  49. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +53 -20
  50. package/packages/@monomind/cli/dist/src/parser.d.ts +4 -2
  51. package/packages/@monomind/cli/dist/src/parser.js +61 -25
  52. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.d.ts +15 -0
  53. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +44 -2
  54. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +10 -2
  55. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -2
  56. package/packages/@monomind/cli/dist/src/ui/collector.mjs +43 -3
  57. package/packages/@monomind/cli/dist/src/ui/dashboard.html +27 -8
  58. package/packages/@monomind/cli/dist/src/ui/server.mjs +144 -14
  59. package/packages/@monomind/cli/package.json +7 -6
  60. package/packages/@monomind/cli/dist/src/capabilities/watcher.d.ts +0 -18
  61. package/packages/@monomind/cli/dist/src/capabilities/watcher.js +0 -107
  62. package/packages/@monomind/cli/dist/src/config-adapter.d.ts +0 -16
  63. 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
- const swarmStatePath = path.join(CWD, '.monomind', 'swarm', 'swarm-state.json');
400
- const swarmState = readJSON(swarmStatePath);
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
- fs.mkdirSync(path.join(CWD, '.monomind'), { recursive: true });
340
- fs.writeFileSync(_graphGateStateFile(), JSON.stringify({ sessions: sessions }));
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
- function _graphGateMarkQueried(sessionId) {
344
- if (!sessionId) return;
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
- s.queried = true;
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
- var sessions = _graphGateReadSessions();
360
- var s = sessions[sessionId] || { queried: false, blockedOnce: false };
361
- if (s.queried) return false;
362
- if (!s.blockedOnce) {
363
- s.blockedOnce = true;
364
- s.ts = Date.now();
365
- sessions[sessionId] = s;
366
- try { _graphGateWriteSessions(sessions); } catch (e) { return false; }
367
- return 'block';
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() {
@@ -57,7 +57,7 @@
57
57
  ]
58
58
  },
59
59
  {
60
- "matcher": "Write|Edit|MultiEdit",
60
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit",
61
61
  "hooks": [
62
62
  {
63
63
  "type": "command",
@@ -1112,7 +1112,8 @@ applyLegacyDeferredAcceptsOnStartup();
1112
1112
  restorePendingEventsFromStore();
1113
1113
  manualApply.pruneStaleEvidence();
1114
1114
  const portArg = args.find(a => a.startsWith('--port='));
1115
- state.port = portArg ? parseInt(portArg.split('=')[1], 10) : await findOpenPort();
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
- readJsonSafe(join(root, '.monomind', 'swarm', 'swarm-state.json')),
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
- export { FileWatcher } from './watcher.js';
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
- const hasPreWrite = preToolUse.some(e => e.matcher === 'Write|Edit|MultiEdit' && e.hooks.some(h => h.command?.includes('pre-write')));
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
- const PRE_WRITE_MATCHER = 'Write|Edit|MultiEdit';
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
- const existingWriteEntry = preToolUse.find(e => e.matcher === PRE_WRITE_MATCHER);
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
- const noWatch = ctx.flags['no-watch'];
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
- name: 'no-watch',
358
- description: 'Skip starting the monograph knowledge graph watcher after init',
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: false,
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
- output.printInfo(`Searching: "${query}" (${searchType})`);
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({ query, searchType, results, searchTime: `${searchResult.searchTime}ms` });
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) {