monomind 2.0.2 → 2.1.0

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 (90) hide show
  1. package/package.json +9 -5
  2. package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +4 -0
  3. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +50 -5
  4. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  5. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  6. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  7. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +75 -18
  8. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  9. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +16 -7
  10. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  11. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +4 -4
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +40 -22
  13. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  14. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  15. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +54 -4
  16. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  17. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  18. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  19. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  20. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  22. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  23. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  24. package/packages/@monomind/cli/bin/cli.js +10 -1
  25. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  26. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  27. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  28. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  29. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  30. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  31. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  32. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  33. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  34. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  35. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  36. package/packages/@monomind/cli/dist/src/commands/org.js +306 -129
  37. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  38. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  39. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  40. package/packages/@monomind/cli/dist/src/index.js +64 -28
  41. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  42. package/packages/@monomind/cli/dist/src/init/executor.js +63 -49
  43. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  44. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  45. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  46. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  47. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  48. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  49. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  50. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +51 -34
  51. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  52. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  53. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +65 -43
  54. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  55. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  56. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  57. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  58. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  59. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  60. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  61. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  62. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  63. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +24 -0
  64. package/packages/@monomind/cli/dist/src/orgrt/broker.js +68 -0
  65. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +30 -0
  66. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +96 -7
  67. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  68. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +105 -8
  69. package/packages/@monomind/cli/dist/src/orgrt/live.html +56 -0
  70. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  71. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  72. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  73. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +8 -0
  74. package/packages/@monomind/cli/dist/src/orgrt/server.js +72 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  76. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  77. package/packages/@monomind/cli/dist/src/output.js +12 -5
  78. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  79. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  80. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  81. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  82. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  83. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  84. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  85. package/packages/@monomind/cli/dist/src/ui/dashboard.html +40 -40
  86. package/packages/@monomind/cli/dist/src/ui/orgs.html +110 -11
  87. package/packages/@monomind/cli/dist/src/ui/server.mjs +304 -133
  88. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  89. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  90. package/packages/@monomind/cli/package.json +6 -2
@@ -7,6 +7,19 @@ import { confirm } from '../prompt.js';
7
7
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
+ import { spawn } from 'child_process';
11
+ /** Check liveness of a pid via a zero-signal, matching the pattern used elsewhere (e.g. .claude/helpers/control-start.cjs). */
12
+ function isPidAlive(pid) {
13
+ if (!Number.isInteger(pid) || pid <= 0)
14
+ return false;
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
10
23
  // Default configuration
11
24
  const DEFAULT_TOPOLOGY = 'hierarchical-mesh';
12
25
  const DEFAULT_MAX_AGENTS = 15;
@@ -92,6 +105,60 @@ const startAction = async (ctx) => {
92
105
  output.printInfo('Run "monomind init" first to initialize');
93
106
  return { success: false, exitCode: 1 };
94
107
  }
108
+ // Daemon mode, parent side: a Node process cannot truly self-daemonize —
109
+ // every handle (streams, intervals) would need to be unref'd, which leaves
110
+ // nothing keeping the event loop alive and the process exits almost
111
+ // immediately. Instead, spawn a genuine DETACHED CHILD process that
112
+ // re-invokes this same command with an internal marker flag; the parent's
113
+ // only job is to confirm the child came up and then exit. The child (see
114
+ // the `--foreground-worker-internal` branch below) is the one that actually
115
+ // stays running, with a real ref'd keep-alive.
116
+ const isDaemonChild = Boolean(ctx.flags['foreground-worker-internal']);
117
+ if (daemon && !isDaemonChild) {
118
+ output.writeln();
119
+ output.writeln(output.bold('Starting Monomind (daemon)'));
120
+ output.writeln();
121
+ const daemonPidPath = path.join(cwd, '.monomind', 'daemon.pid');
122
+ if (fs.existsSync(daemonPidPath)) {
123
+ const stalePid = Number(fs.readFileSync(daemonPidPath, 'utf-8').trim());
124
+ if (isPidAlive(stalePid)) {
125
+ output.printError(`Daemon already running (pid ${stalePid}). Run "monomind stop" first.`);
126
+ return { success: false, exitCode: 1 };
127
+ }
128
+ // Stale pid file — the prior daemon is dead, safe to remove.
129
+ fs.unlinkSync(daemonPidPath);
130
+ }
131
+ // Reconstruct the child's argv explicitly rather than reusing process.argv —
132
+ // startAction can be reached via `start`, `start quick`, or `restart`
133
+ // (which calls startAction directly in-process), so process.argv may not
134
+ // even contain the `start` subcommand. Building the args from ctx.flags
135
+ // guarantees the respawned child always dispatches to a valid daemon start.
136
+ const entry = process.argv[1];
137
+ const childArgs = ['start', '--daemon', '--foreground-worker-internal'];
138
+ if (topology)
139
+ childArgs.push('--topology', topology);
140
+ const child = spawn(process.execPath, [entry, ...childArgs], {
141
+ detached: true,
142
+ stdio: 'ignore',
143
+ cwd,
144
+ });
145
+ child.unref();
146
+ // Give the child a moment to actually start before trusting its pid.
147
+ await new Promise((resolve) => setTimeout(resolve, 500));
148
+ if (!child.pid || !isPidAlive(child.pid)) {
149
+ output.printError('Daemon child process failed to start');
150
+ return { success: false, exitCode: 1 };
151
+ }
152
+ fs.mkdirSync(path.dirname(daemonPidPath), { recursive: true });
153
+ fs.writeFileSync(daemonPidPath, String(child.pid), { mode: 0o600 });
154
+ output.printSuccess(`Monomind daemon started (pid ${child.pid})`);
155
+ output.printInfo('Use "monomind stop" to stop it.');
156
+ const daemonStartResult = { daemon: true, pid: child.pid, startedAt: new Date().toISOString() };
157
+ if (ctx.flags.format === 'json') {
158
+ output.printJson(daemonStartResult);
159
+ }
160
+ return { success: true, data: daemonStartResult };
161
+ }
95
162
  // Load configuration
96
163
  const config = loadConfig(cwd);
97
164
  const swarmConfig = config?.swarm || {};
@@ -147,48 +214,28 @@ const startAction = async (ctx) => {
147
214
  `${output.highlight('monomind swarm status')} - View swarm details`,
148
215
  `${output.highlight('monomind stop')} - Stop the system`
149
216
  ]);
150
- // Daemon mode
151
- if (daemon) {
217
+ // Daemon mode: this branch only runs inside the detached child spawned by
218
+ // the parent-side block above (isDaemonChild === true — the parent never
219
+ // reaches here, it already returned after confirming the child is alive).
220
+ // The pid file was already written by the parent with the CHILD's pid, so
221
+ // just confirm it's in place and start the real, ref'd keep-alive that
222
+ // actually holds the event loop open.
223
+ if (daemon && isDaemonChild) {
152
224
  output.writeln();
153
225
  output.printInfo('Running in daemon mode. Use "monomind stop" to stop.');
154
- // Store PID for daemon management.
155
- // wx flag refuses to follow a pre-staged symlink at the path; mode 0o600
156
- // keeps the file from being world-readable on multi-user systems.
157
226
  const daemonPidPath = path.join(cwd, '.monomind', 'daemon.pid');
158
- try {
159
- fs.writeFileSync(daemonPidPath, String(process.pid), { flag: 'wx', mode: 0o600 });
160
- }
161
- catch (e) {
162
- if (e.code === 'EEXIST') {
163
- // Stale PID file — caller should have verified the prior daemon is dead
164
- fs.unlinkSync(daemonPidPath);
165
- fs.writeFileSync(daemonPidPath, String(process.pid), { flag: 'wx', mode: 0o600 });
166
- }
167
- else {
168
- throw e;
169
- }
170
- }
171
- // Detach from parent process for true daemon behavior
172
- if (process.platform !== 'win32') {
173
- // Unix-like systems: create new session
174
- try {
175
- process.stdin.unref?.();
176
- process.stdout.unref?.();
177
- process.stderr.unref?.();
178
- }
179
- catch {
180
- // Ignore errors if streams can't be unref'd
181
- }
182
- }
183
- // Keep process alive in daemon mode
227
+ // Keep the process alive with a REAL (ref'd) interval — this is what
228
+ // actually keeps Node's event loop from exiting, since stdio was
229
+ // already set to 'ignore' by the parent's spawn() call.
184
230
  const keepAlive = setInterval(() => {
185
- // Heartbeat - check if we should still be running
231
+ // Heartbeat - exit if our pid file has been removed (e.g. by `stop`)
186
232
  if (!fs.existsSync(daemonPidPath)) {
187
233
  clearInterval(keepAlive);
188
234
  process.exit(0);
189
235
  }
190
236
  }, 5000);
191
- keepAlive.unref(); // Don't prevent process from exiting if no other work
237
+ // Deliberately NOT unref'd this is the one handle that must keep the
238
+ // daemon child's event loop alive indefinitely.
192
239
  }
193
240
  const result = {
194
241
  swarmId: swarmResult.swarmId,
@@ -267,16 +314,74 @@ const stopCommand = {
267
314
  catch {
268
315
  spinner.fail('Swarm was not running');
269
316
  }
270
- // Clean up daemon PID
317
+ // Stop the daemon process itself: read its real pid, verify liveness,
318
+ // send a real termination signal, then wait and confirm it's actually
319
+ // dead before reporting success — rather than unconditionally deleting
320
+ // the pid file and claiming success regardless of reality.
271
321
  const daemonPidPath = path.join(ctx.cwd, '.monomind', 'daemon.pid');
322
+ let daemonWasRunning = false;
323
+ let daemonStopped = true;
324
+ let daemonPid = null;
272
325
  if (fs.existsSync(daemonPidPath)) {
273
- fs.unlinkSync(daemonPidPath);
326
+ const pidStr = fs.readFileSync(daemonPidPath, 'utf-8').trim();
327
+ const pid = Number(pidStr);
328
+ daemonPid = Number.isInteger(pid) && pid > 0 ? pid : null;
329
+ if (daemonPid && isPidAlive(daemonPid)) {
330
+ daemonWasRunning = true;
331
+ spinner.setText(`Stopping daemon (pid ${daemonPid})...`);
332
+ spinner.start();
333
+ try {
334
+ process.kill(daemonPid, force ? 'SIGKILL' : 'SIGTERM');
335
+ }
336
+ catch {
337
+ // Process may have exited between the liveness check and the signal
338
+ }
339
+ // Wait and confirm the process actually exited before claiming success.
340
+ const waitMs = Math.min(timeout, 10) * 1000;
341
+ const deadline = Date.now() + waitMs;
342
+ while (Date.now() < deadline && isPidAlive(daemonPid)) {
343
+ await new Promise((resolve) => setTimeout(resolve, 200));
344
+ }
345
+ if (isPidAlive(daemonPid) && force) {
346
+ try {
347
+ process.kill(daemonPid, 'SIGKILL');
348
+ }
349
+ catch { /* already gone */ }
350
+ await new Promise((resolve) => setTimeout(resolve, 300));
351
+ }
352
+ daemonStopped = !isPidAlive(daemonPid);
353
+ if (daemonStopped) {
354
+ spinner.succeed(`Daemon stopped (pid ${daemonPid})`);
355
+ }
356
+ else {
357
+ spinner.fail(`Daemon (pid ${daemonPid}) did not stop within ${waitMs}ms`);
358
+ }
359
+ }
360
+ // Only clean up the pid file once we've confirmed the process is gone
361
+ // (or it was already stale) — never delete it while a live daemon
362
+ // still owns it.
363
+ if (daemonStopped) {
364
+ fs.unlinkSync(daemonPidPath);
365
+ }
274
366
  }
275
367
  output.writeln();
276
- output.printSuccess('MonoMind stopped successfully');
368
+ if (!daemonWasRunning) {
369
+ output.printInfo('No daemon was running.');
370
+ }
371
+ else if (daemonStopped) {
372
+ output.printSuccess(`MonoMind daemon stopped successfully (pid ${daemonPid})`);
373
+ }
374
+ else {
375
+ output.printError(`Failed to stop daemon (pid ${daemonPid}) — it did not exit in time`);
376
+ return {
377
+ success: false,
378
+ exitCode: 1,
379
+ data: { stopped: false, force, pid: daemonPid }
380
+ };
381
+ }
277
382
  return {
278
383
  success: true,
279
- data: { stopped: true, force, stoppedAt: new Date().toISOString() }
384
+ data: { stopped: daemonWasRunning ? daemonStopped : null, force, pid: daemonPid, stoppedAt: new Date().toISOString() }
280
385
  };
281
386
  }
282
387
  catch (error) {
@@ -6,14 +6,32 @@ import { output } from '../output.js';
6
6
  import { createRequire } from 'module';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname, join } from 'path';
9
+ import { existsSync } from 'fs';
9
10
  function getTrackerPath() {
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
- // From dist/src/commands/ -> back to project root -> .claude/helpers/
12
- return join(__dirname, '..', '..', '..', '..', '..', '..', '.claude', 'helpers', 'token-tracker.cjs');
12
+ // The CLI package ships its own copy of token-tracker.cjs at the package
13
+ // root: from dist/src/commands/tokens.js, that's 3 levels up
14
+ // (dist/src/commands -> dist/src -> dist -> package root), then
15
+ // .claude/helpers/token-tracker.cjs. Verified empirically — do not "fix"
16
+ // this by guessing a different depth without re-checking the build output.
17
+ const bundled = join(__dirname, '..', '..', '..', '.claude', 'helpers', 'token-tracker.cjs');
18
+ if (existsSync(bundled))
19
+ return bundled;
20
+ // Fall back to a copy in the user's own project (e.g. a stripped install
21
+ // that dropped .claude/helpers, or a monorepo checkout like this one where
22
+ // the user's cwd has its own tracker copy).
23
+ const cwdCopy = join(process.cwd(), '.claude', 'helpers', 'token-tracker.cjs');
24
+ if (existsSync(cwdCopy))
25
+ return cwdCopy;
26
+ return null;
13
27
  }
14
28
  function loadTracker() {
29
+ const trackerPath = getTrackerPath();
30
+ if (!trackerPath) {
31
+ throw new Error('token-tracker.cjs not found (checked bundled package copy and cwd/.claude/helpers)');
32
+ }
15
33
  const require = createRequire(import.meta.url);
16
- return require(getTrackerPath());
34
+ return require(trackerPath);
17
35
  }
18
36
  const VALID_PERIODS = new Set(['today', 'week', '30days', 'month']);
19
37
  function validatePeriod(raw) {
@@ -2,6 +2,23 @@
2
2
  * Configuration Adapter
3
3
  * Converts between SystemConfig and MonomindConfig types
4
4
  */
5
+ // Share fallback values with config-file-manager.ts's own DEFAULT_CONFIG so
6
+ // a config that round-trips through this adapter doesn't silently diverge
7
+ // from what `monomind config create`/`reset` would produce (e.g. maxAgents
8
+ // 8 vs a separately-hardcoded 15, hooks.enabled true vs false).
9
+ import { DEFAULT_CONFIG } from './services/config-file-manager.js';
10
+ // structuredClone so nested arrays/objects (e.g. hooks.hooks) returned by
11
+ // systemConfigToMonomindConfig() below are never the live DEFAULT_CONFIG
12
+ // reference — matches config-file-manager.ts's own cloneDefaultConfig() fix,
13
+ // which this file's direct DEFAULT_CONFIG import would otherwise bypass.
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ const AGENTS_DEFAULTS = structuredClone(DEFAULT_CONFIG.agents);
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ const SWARM_DEFAULTS = structuredClone(DEFAULT_CONFIG.swarm);
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const CLI_DEFAULTS = structuredClone(DEFAULT_CONFIG.cli);
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ const HOOKS_DEFAULTS = structuredClone(DEFAULT_CONFIG.hooks);
5
22
  /**
6
23
  * Convert SystemConfig to MonomindConfig (CLI-specific format)
7
24
  */
@@ -11,19 +28,19 @@ export function systemConfigToMonomindConfig(systemConfig) {
11
28
  projectRoot: systemConfig.orchestrator?.session?.dataDir || process.cwd(),
12
29
  // Agent configuration
13
30
  agents: {
14
- defaultType: 'coder',
15
- autoSpawn: false, // Not in SystemConfig
16
- maxConcurrent: systemConfig.orchestrator?.lifecycle?.maxConcurrentAgents ?? 15,
17
- timeout: systemConfig.orchestrator?.lifecycle?.spawnTimeout ?? 300000,
31
+ defaultType: systemConfig.agents?.defaultType ?? AGENTS_DEFAULTS.defaultType,
32
+ autoSpawn: systemConfig.agents?.autoSpawn ?? AGENTS_DEFAULTS.autoSpawn,
33
+ maxConcurrent: systemConfig.orchestrator?.lifecycle?.maxConcurrentAgents ?? AGENTS_DEFAULTS.maxConcurrent,
34
+ timeout: systemConfig.orchestrator?.lifecycle?.spawnTimeout ?? AGENTS_DEFAULTS.timeout,
18
35
  providers: [],
19
36
  },
20
37
  // Swarm configuration
21
38
  swarm: {
22
39
  topology: normalizeTopology(systemConfig.swarm?.topology),
23
- maxAgents: systemConfig.swarm?.maxAgents ?? 15,
24
- autoScale: systemConfig.swarm?.autoScale?.enabled ?? false,
40
+ maxAgents: systemConfig.swarm?.maxAgents ?? SWARM_DEFAULTS.maxAgents,
41
+ autoScale: systemConfig.swarm?.autoScale?.enabled ?? SWARM_DEFAULTS.autoScale,
25
42
  coordinationStrategy: systemConfig.swarm?.coordination?.consensusRequired ? 'consensus' : 'leader',
26
- healthCheckInterval: systemConfig.swarm?.coordination?.timeoutMs ?? 10000,
43
+ healthCheckInterval: systemConfig.swarm?.coordination?.timeoutMs ?? SWARM_DEFAULTS.healthCheckInterval,
27
44
  },
28
45
  // Memory configuration
29
46
  memory: {
@@ -41,19 +58,22 @@ export function systemConfigToMonomindConfig(systemConfig) {
41
58
  transportType: systemConfig.mcp?.transport?.type || 'stdio',
42
59
  tools: [], // Not in SystemConfig
43
60
  },
44
- // CLI preferences
61
+ // CLI preferences — read from SystemConfig.cli when present, falling
62
+ // back to the shared DEFAULT_CONFIG.cli values (not separately
63
+ // hardcoded numbers that could silently drop user-configured values).
45
64
  cli: {
46
- colorOutput: true,
47
- interactive: true,
48
- verbosity: 'normal',
49
- outputFormat: 'text',
50
- progressStyle: 'spinner',
65
+ colorOutput: systemConfig.cli?.colorOutput ?? CLI_DEFAULTS.colorOutput,
66
+ interactive: systemConfig.cli?.interactive ?? CLI_DEFAULTS.interactive,
67
+ verbosity: systemConfig.cli?.verbosity ?? CLI_DEFAULTS.verbosity,
68
+ outputFormat: systemConfig.cli?.outputFormat ?? CLI_DEFAULTS.outputFormat,
69
+ progressStyle: systemConfig.cli?.progressStyle ?? CLI_DEFAULTS.progressStyle,
51
70
  },
52
- // Hooks configuration
71
+ // Hooks configuration — likewise read from SystemConfig.hooks when
72
+ // present, falling back to the shared DEFAULT_CONFIG.hooks values.
53
73
  hooks: {
54
- enabled: false,
55
- autoExecute: false,
56
- hooks: [],
74
+ enabled: systemConfig.hooks?.enabled ?? HOOKS_DEFAULTS.enabled,
75
+ autoExecute: systemConfig.hooks?.autoExecute ?? HOOKS_DEFAULTS.autoExecute,
76
+ hooks: systemConfig.hooks?.hooks ?? HOOKS_DEFAULTS.hooks,
57
77
  },
58
78
  // Neural pattern-learning configuration
59
79
  neural: {
@@ -82,12 +82,16 @@ export class CLI {
82
82
  this.output.printDebug(`Flags: ${JSON.stringify(Object.fromEntries(Object.entries(flags).filter(([k]) => k !== '_')))}`);
83
83
  this.output.printDebug(`CWD: ${process.cwd()}`);
84
84
  }
85
- // Run startup update check (non-blocking, silent on skip)
86
- if (!flags.noUpdate && commandPath[0] !== 'update') {
85
+ // Run startup update check (non-blocking, silent on skip).
86
+ // `update` is now a properly-declared global boolean option (default
87
+ // true) — `--no-update` negates it via the parser's standard boolean
88
+ // negation path rather than relying on the generic --no-X fallback
89
+ // (which checked the flag name against the GLOBAL pool of every
90
+ // command's boolean options, so an unrelated command declaring its
91
+ // own `update` boolean flag could hijack `--no-update`'s meaning).
92
+ if (flags.update !== false && commandPath[0] !== 'update') {
87
93
  this.checkForUpdatesOnStartup().catch(() => { });
88
94
  }
89
- // Initialize optional subsystems (non-blocking — never delay CLI startup)
90
- this.initSubsystems().catch(() => { });
91
95
  // Handle lazy-loaded commands that weren't recognized by the parser
92
96
  // If commandPath is empty but positional has a command name, check if it's lazy-loadable
93
97
  if (commandPath.length === 0 && positional.length > 0 && !positional[0].startsWith('-')) {
@@ -101,8 +105,11 @@ export class CLI {
101
105
  // No command - show help or suggest correction
102
106
  if (commandPath.length === 0 || flags.help || flags.h) {
103
107
  if (commandPath.length > 0) {
104
- // Show command-specific help
105
- await this.showCommandHelp(commandPath[0]);
108
+ // Show help for the fully-resolved (sub)command — walking the same
109
+ // path the dispatcher below resolves — not just the top-level
110
+ // parent's subcommand list. `monomind memory store --help` should
111
+ // show `store`'s own options, not memory's list of subcommands.
112
+ await this.showCommandHelp(commandPath);
106
113
  }
107
114
  else if (positional.length > 0 && !positional[0].startsWith('-')) {
108
115
  // First positional looks like an attempted command - suggest correction
@@ -135,6 +142,13 @@ export class CLI {
135
142
  this.output.writeln(this.output.dim(` ${message}`));
136
143
  process.exit(1);
137
144
  }
145
+ // Initialize optional subsystems (non-blocking — never delay CLI
146
+ // startup). Deliberately placed AFTER the --help/--version/unknown-
147
+ // command short-circuits above: those paths don't touch project state,
148
+ // so running this here means `monomind --help` (or any invocation in a
149
+ // directory that's never been a monomind project) no longer creates
150
+ // .monomind/registry.json as a side effect of just asking for help.
151
+ this.initSubsystems().catch(() => { });
138
152
  // Handle subcommand (supports nested subcommands)
139
153
  let targetCommand = command;
140
154
  let subcommandArgs = positional;
@@ -208,8 +222,8 @@ export class CLI {
208
222
  }
209
223
  }
210
224
  else {
211
- // No action - show command help
212
- await this.showCommandHelp(commandName);
225
+ // No action - show help for the resolved (sub)command path
226
+ await this.showCommandHelp(commandPath.length > 0 ? commandPath : [commandName]);
213
227
  }
214
228
  }
215
229
  catch (error) {
@@ -308,24 +322,40 @@ export class CLI {
308
322
  /**
309
323
  * Show command-specific help
310
324
  */
311
- async showCommandHelp(commandName) {
325
+ async showCommandHelp(commandPath) {
326
+ const path = Array.isArray(commandPath) ? commandPath : [commandPath];
327
+ const topName = path[0];
312
328
  // Try sync first, then lazy load
313
- let command = getCommand(commandName);
314
- if (!command && hasCommand(commandName)) {
315
- command = await getCommandAsync(commandName);
329
+ let command = getCommand(topName);
330
+ if (!command && hasCommand(topName)) {
331
+ command = await getCommandAsync(topName);
316
332
  }
317
333
  if (!command) {
318
- this.output.printError(`Unknown command: ${commandName}`);
334
+ this.output.printError(`Unknown command: ${topName}`);
319
335
  return;
320
336
  }
337
+ // Walk the remaining path segments through subcommands/nested
338
+ // subcommands — mirrors the resolution the dispatcher uses in run() —
339
+ // so `monomind <command> <subcommand> --help` shows the actual target
340
+ // (sub)command's own options and examples, not just the parent's list
341
+ // of subcommands.
342
+ let target = command;
343
+ const resolvedNames = [command.name];
344
+ for (const segment of path.slice(1)) {
345
+ const next = target.subcommands?.find(sc => sc.name === segment || sc.aliases?.includes(segment));
346
+ if (!next)
347
+ break;
348
+ target = next;
349
+ resolvedNames.push(next.name);
350
+ }
321
351
  this.output.writeln();
322
- this.output.writeln(this.output.bold(`${this.name} ${command.name}`));
323
- this.output.writeln(command.description);
352
+ this.output.writeln(this.output.bold(`${this.name} ${resolvedNames.join(' ')}`));
353
+ this.output.writeln(target.description);
324
354
  this.output.writeln();
325
355
  // Subcommands
326
- if (command.subcommands && command.subcommands.length > 0) {
356
+ if (target.subcommands && target.subcommands.length > 0) {
327
357
  this.output.writeln(this.output.bold('SUBCOMMANDS:'));
328
- for (const sub of command.subcommands) {
358
+ for (const sub of target.subcommands) {
329
359
  if (sub.hidden)
330
360
  continue;
331
361
  const name = sub.name.padEnd(15);
@@ -335,9 +365,9 @@ export class CLI {
335
365
  this.output.writeln();
336
366
  }
337
367
  // Options
338
- if (command.options && command.options.length > 0) {
368
+ if (target.options && target.options.length > 0) {
339
369
  this.output.writeln(this.output.bold('OPTIONS:'));
340
- for (const opt of command.options) {
370
+ for (const opt of target.options) {
341
371
  const flags = opt.short ? `-${opt.short}, --${opt.name}` : ` --${opt.name}`;
342
372
  const required = opt.required ? this.output.error(' (required)') : '';
343
373
  const defaultVal = opt.default !== undefined ? this.output.dim(` [default: ${opt.default}]`) : '';
@@ -346,9 +376,9 @@ export class CLI {
346
376
  this.output.writeln();
347
377
  }
348
378
  // Examples
349
- if (command.examples && command.examples.length > 0) {
379
+ if (target.examples && target.examples.length > 0) {
350
380
  this.output.writeln(this.output.bold('EXAMPLES:'));
351
- for (const example of command.examples) {
381
+ for (const example of target.examples) {
352
382
  this.output.writeln(` ${this.output.dim('$')} ${example.command}`);
353
383
  this.output.writeln(` ${this.output.dim(example.description)}`);
354
384
  }
@@ -392,16 +422,22 @@ export class CLI {
392
422
  * Load configuration file
393
423
  */
394
424
  async loadConfig(configPath) {
425
+ const { configManager } = await import('./services/config-file-manager.js');
426
+ // An explicit --config/-c path names an EXACT file — load it directly
427
+ // instead of directory-searching from its dirname (which previously
428
+ // discarded the filename the user gave and either loaded an unrelated
429
+ // monomind.config.json from that directory or found nothing). Failure
430
+ // to find/parse an explicitly-named config file is a loud error, not a
431
+ // silent fallback to defaults.
432
+ if (configPath) {
433
+ const raw = configManager.loadExact(configPath);
434
+ return raw;
435
+ }
395
436
  try {
396
- const { configManager } = await import('./services/config-file-manager.js');
397
- const cwd = configPath
398
- ? (await import('path')).dirname(configPath)
399
- : process.cwd();
400
- const raw = configManager.load(cwd);
437
+ const raw = configManager.load(process.cwd());
401
438
  if (!raw)
402
439
  return undefined;
403
- const config = raw;
404
- return config;
440
+ return raw;
405
441
  }
406
442
  catch (error) {
407
443
  // Config loading is optional - don't fail if it doesn't exist
@@ -6,6 +6,32 @@
6
6
  * Templates: minimal | standard | full | security | performance | solo
7
7
  * All templates use bullet-format rules with imperative keywords for enforceability.
8
8
  */
9
+ import { createRequire } from 'module';
10
+ let _availabilityCache = null;
11
+ function detectOptionalPackages() {
12
+ if (_availabilityCache)
13
+ return _availabilityCache;
14
+ const req = createRequire(import.meta.url);
15
+ const resolvable = (pkg) => {
16
+ try {
17
+ req.resolve(pkg);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ };
24
+ _availabilityCache = {
25
+ hooks: resolvable('@monomind/hooks'),
26
+ mcp: resolvable('@monomind/mcp'),
27
+ routing: resolvable('@monomind/routing'),
28
+ monofence: resolvable('monofence-ai'),
29
+ };
30
+ return _availabilityCache;
31
+ }
32
+ function unavailNote(available) {
33
+ return available ? '' : ' _(unavailable in this install)_';
34
+ }
9
35
  // --- Section Generators (each returns enforceable markdown) ---
10
36
  function behavioralRules() {
11
37
  return `## Behavioral Rules (Always Enforced)
@@ -156,20 +182,25 @@ function executionRules() {
156
182
  - When agent results arrive, review ALL results before proceeding`;
157
183
  }
158
184
  function cliCommandsTable() {
159
- return `## V1 CLI Commands
185
+ const avail = detectOptionalPackages();
186
+ return `## CLI Commands
160
187
 
161
188
  ### Core Commands
162
189
 
163
190
  | Command | Subcommands | Description |
164
191
  |---------|-------------|-------------|
165
- | \`init\` | 4 | Project initialization |
166
- | \`agent\` | 8 | Agent lifecycle management |
192
+ | \`init\` | 5 | Project initialization |
193
+ | \`agent\` | 7 | Agent lifecycle management |
167
194
  | \`swarm\` | 6 | Multi-agent swarm coordination |
168
- | \`memory\` | 11 | LanceDB memory with ANN search |
169
- | \`task\` | 6 | Task creation and lifecycle |
170
- | \`session\` | 7 | Session state management |
171
- | \`hooks\` | 17 | Self-learning hooks + 12 workers |
172
- | \`hive-mind\` | 6 | Byzantine fault-tolerant consensus |
195
+ | \`memory\` | 12 | LanceDB memory with ANN search |
196
+ | \`task\` | 5 | Task creation and lifecycle |
197
+ | \`session\` | 6 | Session state management |
198
+ | \`hooks\` | 29 | Self-learning hooks + 15 background workers${unavailNote(avail.hooks)} |
199
+
200
+ > Note: there is no \`hive-mind\` or \`neural\` CLI command. Hive-mind
201
+ > consensus (byzantine/raft/quorum) is available exclusively via MCP tools
202
+ > (\`hive-mind_*\`), not the CLI. Neural pattern learning was merged into
203
+ > \`hooks intelligence\`.
173
204
 
174
205
  ### Quick CLI Examples
175
206
 
@@ -197,7 +228,8 @@ function agentTypes() {
197
228
  \`pr-manager\`, \`code-review-swarm\`, \`issue-tracker\`, \`release-manager\``;
198
229
  }
199
230
  function hooksSystem() {
200
- return `## Hooks System (27 Hooks + 12 Workers)
231
+ const avail = detectOptionalPackages();
232
+ return `## Hooks System (29 Hook Subcommands + 15 Background Workers)
201
233
 
202
234
  ### Essential Hooks
203
235
 
@@ -210,7 +242,7 @@ function hooksSystem() {
210
242
  | \`intelligence\` | Pattern-learning intelligence system |
211
243
  | \`worker\` | Background worker management |
212
244
 
213
- ### Background Workers (@monomind/hooks, run in-process)
245
+ ### Background Workers (@monomind/hooks, run in-process)${unavailNote(avail.hooks)}
214
246
 
215
247
  | Worker | Priority | Description |
216
248
  |--------|----------|-------------|
@@ -220,9 +252,10 @@ function hooksSystem() {
220
252
  | \`consolidate\` | low | Memory consolidation |
221
253
  | \`ddd\` | low | DDD progress tracking |
222
254
  | \`security\` | high | Secret/vulnerability scan |
255
+ | \`performance\`, \`health\`, \`swarm\`, \`git\`, \`learning\`, \`adr\`, \`patterns\`, \`cache\`, \`progress\` | various | See \`hooks worker list\` for the full 15 |
223
256
 
224
257
  Metrics-producing workers refresh at session start when output is >6h old.
225
-
258
+ ${avail.hooks ? '' : '\n> \\@monomind/hooks is not resolvable in this install — background workers will fail to load (see `hooks worker list`). This is an install/publish gap, not a project misconfiguration.\n'}
226
259
  \`\`\`bash
227
260
  npx monomind@latest hooks pre-task --description "[task]"
228
261
  npx monomind@latest hooks post-task --task-id "[id]" --success true