monomind 2.7.7 → 2.7.9

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 (54) hide show
  1. package/package.json +3 -2
  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/dist/src/browser/dashboard/server.js +6 -1
  15. package/packages/@monomind/cli/dist/src/capabilities/index.d.ts +0 -1
  16. package/packages/@monomind/cli/dist/src/capabilities/index.js +8 -1
  17. package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +5 -1
  18. package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +8 -1
  19. package/packages/@monomind/cli/dist/src/commands/guidance.js +8 -2
  20. package/packages/@monomind/cli/dist/src/commands/init.js +18 -4
  21. package/packages/@monomind/cli/dist/src/commands/memory-crud.js +7 -1
  22. package/packages/@monomind/cli/dist/src/commands/org-observe.js +17 -7
  23. package/packages/@monomind/cli/dist/src/commands/org.d.ts +11 -0
  24. package/packages/@monomind/cli/dist/src/commands/org.js +162 -15
  25. package/packages/@monomind/cli/dist/src/commands/security-scan.d.ts +30 -1
  26. package/packages/@monomind/cli/dist/src/commands/security-scan.js +182 -69
  27. package/packages/@monomind/cli/dist/src/commands/swarm.js +41 -14
  28. package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +11 -10
  29. package/packages/@monomind/cli/dist/src/init/executor.js +138 -22
  30. package/packages/@monomind/cli/dist/src/init/settings-generator.js +4 -1
  31. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +17 -0
  32. package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +62 -3
  33. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +16 -8
  34. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +27 -13
  35. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +1 -1
  36. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +5 -1
  37. package/packages/@monomind/cli/dist/src/memory/memory-read.d.ts +7 -2
  38. package/packages/@monomind/cli/dist/src/memory/memory-read.js +10 -2
  39. package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +25 -6
  40. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +21 -1
  41. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +82 -11
  42. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +53 -20
  43. package/packages/@monomind/cli/dist/src/parser.d.ts +4 -2
  44. package/packages/@monomind/cli/dist/src/parser.js +61 -25
  45. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +10 -2
  46. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -2
  47. package/packages/@monomind/cli/dist/src/ui/collector.mjs +43 -3
  48. package/packages/@monomind/cli/dist/src/ui/dashboard.html +27 -8
  49. package/packages/@monomind/cli/dist/src/ui/server.mjs +144 -14
  50. package/packages/@monomind/cli/package.json +4 -4
  51. package/packages/@monomind/cli/dist/src/capabilities/watcher.d.ts +0 -18
  52. package/packages/@monomind/cli/dist/src/capabilities/watcher.js +0 -107
  53. package/packages/@monomind/cli/dist/src/config-adapter.d.ts +0 -16
  54. 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",
@@ -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(),
@@ -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)
@@ -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',
@@ -338,20 +338,26 @@ export const searchCommand = {
338
338
  // behind a "(semantic)" header.
339
339
  const REASON_TEXT = {
340
340
  'no-embedding-model': 'embedding model unavailable',
341
+ 'empty-query': 'query was empty, so no vector could be built',
341
342
  'embedding-failed': 'embedding generation failed',
342
343
  'no-semantic-matches': 'vector search returned no matches',
343
344
  };
345
+ const why = fallbackReason ? REASON_TEXT[fallbackReason] ?? fallbackReason : undefined;
344
346
  if (actualMethod === 'semantic') {
345
347
  output.writeln(output.dim(' Method: semantic (vector similarity)'));
346
348
  }
347
349
  else if (actualMethod === 'hybrid') {
348
350
  output.writeln(output.dim(' Method: hybrid (per-entry cosine, keyword overlap where no vector exists)'));
349
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
+ }
350
357
  else if (actualMethod === 'unknown') {
351
358
  output.writeln(output.dim(' Method: unknown'));
352
359
  }
353
360
  else {
354
- const why = fallbackReason ? REASON_TEXT[fallbackReason] ?? fallbackReason : undefined;
355
361
  output.printWarning(`Method: ${actualMethod}${why ? ` — ${why}` : ''}. Scores are token-overlap fractions, not vector similarity.`);
356
362
  }
357
363
  output.writeln(output.dim(` Search time: ${searchResult.searchTime}ms`));
@@ -295,6 +295,23 @@ export const answerAction = async (ctx, name) => {
295
295
  if (freshQ && freshQ.answer !== null) {
296
296
  return { success: false, message: `question "${questionId}" was answered while this command was running` };
297
297
  }
298
+ // Queue BEFORE marking answered (same rule as daemon.answerQuestion): if the
299
+ // append fails, the question must stay pending and answerable. Marking first meant
300
+ // a failed queueMessage recorded the answer as delivered while nothing was queued,
301
+ // and the `already answered` guard then rejected every retry.
302
+ const { queueMessage } = await import('../orgrt/inbox.js');
303
+ try {
304
+ queueMessage(ctx.cwd, name, {
305
+ fromQualified: 'human', toRole: q.role,
306
+ subject: `answer:${questionId}`,
307
+ body: `question: ${q.question}\n\nanswer: ${answer}`,
308
+ ts: Date.now(),
309
+ });
310
+ }
311
+ catch (err) {
312
+ log(output.error(`Could not queue the answer for ${name}:${q.role} (${err instanceof Error ? err.message : String(err)}) — answer NOT recorded, retry it.`));
313
+ return { success: false, message: 'queueing failed — answer not recorded' };
314
+ }
298
315
  const merged = fresh.some(x => x.questionId === questionId)
299
316
  ? fresh.map(x => x.questionId === questionId ? { ...x, answer, answeredAt: Date.now() } : x)
300
317
  : [...fresh, { ...q, answer, answeredAt: Date.now() }];
@@ -303,13 +320,6 @@ export const answerAction = async (ctx, name) => {
303
320
  writeFileSync(tmp, JSON.stringify({ questions: merged }, null, 2));
304
321
  const { renameSync } = await import('node:fs');
305
322
  renameSync(tmp, dest);
306
- const { queueMessage } = await import('../orgrt/inbox.js');
307
- queueMessage(ctx.cwd, name, {
308
- fromQualified: 'human', toRole: q.role,
309
- subject: `answer:${questionId}`,
310
- body: `question: ${q.question}\n\nanswer: ${answer}`,
311
- ts: Date.now(),
312
- });
313
323
  log(output.success(`Answer recorded — ${name}:${q.role} receives it when the org next runs.`));
314
324
  return { success: true };
315
325
  };
@@ -1,4 +1,5 @@
1
1
  import type { Command, CommandResult } from '../types.js';
2
+ import { OrgDaemon } from '../orgrt/daemon.js';
2
3
  export declare function validateOrgName(name: string | undefined): {
3
4
  ok: true;
4
5
  name: string;
@@ -9,6 +10,16 @@ export declare function validateOrgName(name: string | undefined): {
9
10
  export declare function listOrgConfigFiles(orgsDir: string): string[];
10
11
  /** Remove a lingering stopfile so a fresh `org run` doesn't self-terminate. */
11
12
  export declare const clearStopfile: (cwd: string, name: string) => void;
13
+ /** One pass of the `org serve` stopfile poll.
14
+ *
15
+ * `monomind org stop <name>` writes `.monomind/orgs/<name>/stop`. `org run` has always
16
+ * polled that file; `org serve` did not — so against a serve daemon `org stop` was a
17
+ * silent no-op that still printed "daemon exits within 2s" and exited 0 while the org
18
+ * kept running. Stops every running org whose stopfile is present, then clears the
19
+ * stopfile so the next scheduled iteration isn't killed on sight.
20
+ *
21
+ * Returns the names it stopped (awaited), so callers/tests don't have to guess. */
22
+ export declare const pollStopfiles: (cwd: string, daemon: OrgDaemon) => Promise<string[]>;
12
23
  export declare const orgCommand: Command;
13
24
  export default orgCommand;
14
25
  //# sourceMappingURL=org.d.ts.map