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
@@ -11,6 +11,22 @@ import { dirname } from 'path';
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = dirname(__filename);
13
13
  const MAX_EXEC_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
14
+ /**
15
+ * Probe whether an optionalDependency actually resolved in this install
16
+ * (npm silently skips optionalDependencies it can't satisfy — see
17
+ * docs/AUDIT-BACKLOG.md P1-1/P1-23). Used to caveat generated docs instead
18
+ * of presenting these features as unconditionally working.
19
+ */
20
+ function _isOptionalPackageResolvable(pkg) {
21
+ try {
22
+ const req = createRequire(import.meta.url);
23
+ req.resolve(pkg);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
14
30
  /**
15
31
  * Atomic write helper — writes to a sibling .tmp file then renames into place.
16
32
  * SIGINT or crash during a partial write would otherwise corrupt user-critical
@@ -1180,13 +1196,15 @@ function findSourceHelpersDir(sourceBaseDir) {
1180
1196
  possiblePaths.push(helpersPath);
1181
1197
  currentDir = parentDir;
1182
1198
  }
1183
- // Strategy 4: Check cwd-relative paths (for local dev)
1184
- const cwdBased = [
1185
- path.join(process.cwd(), '.claude', 'helpers'),
1186
- path.join(process.cwd(), '..', '.claude', 'helpers'),
1187
- path.join(process.cwd(), '..', '..', '.claude', 'helpers'),
1188
- ];
1189
- possiblePaths.push(...cwdBased);
1199
+ // NOTE: deliberately no cwd-ancestor-search fallback here (removed see
1200
+ // docs/AUDIT-BACKLOG.md P3-25). Searching process.cwd() and its parents for
1201
+ // ".claude/helpers" could pick up an unrelated project's own helper scripts
1202
+ // (stale, customized, or untrusted) when `monomind init` is run from a
1203
+ // nested subdirectory of some other checkout. Helper source resolution is
1204
+ // restricted to the package's own bundled location(s) above; if none of
1205
+ // those are found, callers should treat it as a corrupt install rather than
1206
+ // silently falling back to scanning ancestor directories for someone else's
1207
+ // files.
1190
1208
  // Return first path that exists AND contains ALL sentinel files
1191
1209
  for (const p of possiblePaths) {
1192
1210
  if (fs.existsSync(p) && SENTINEL_FILES.every(f => fs.existsSync(path.join(p, f)))) {
@@ -1208,6 +1226,9 @@ async function writeHelpers(targetDir, options, result) {
1208
1226
  const copyRecursive = (srcDir, destDir, relBase) => {
1209
1227
  fs.mkdirSync(destDir, { recursive: true });
1210
1228
  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
1229
+ // Skip exFAT/macOS AppleDouble junk files (e.g. "._foo.cjs").
1230
+ if (entry.name.startsWith('._'))
1231
+ continue;
1211
1232
  const srcPath = path.join(srcDir, entry.name);
1212
1233
  const destPath = path.join(destDir, entry.name);
1213
1234
  const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
@@ -1549,6 +1570,7 @@ async function writeCapabilitiesDoc(targetDir, options, result) {
1549
1570
  result.skipped.push('.monomind/CAPABILITIES.md');
1550
1571
  return;
1551
1572
  }
1573
+ const hooksAvailable = _isOptionalPackageResolvable('@monomind/hooks');
1552
1574
  const capabilities = `# Monomind - Complete Capabilities Reference
1553
1575
  > Generated: ${new Date().toISOString()}
1554
1576
  > Full documentation: https://github.com/monoes/monomind
@@ -1558,8 +1580,8 @@ async function writeCapabilitiesDoc(targetDir, options, result) {
1558
1580
  1. [Overview](#overview)
1559
1581
  2. [Swarm Orchestration](#swarm-orchestration)
1560
1582
  3. [Available Agents (60+)](#available-agents)
1561
- 4. [CLI Commands (26 Commands, 140+ Subcommands)](#cli-commands)
1562
- 5. [Hooks System (27 Hooks + 12 Workers)](#hooks-system)
1583
+ 4. [CLI Commands](#cli-commands)
1584
+ 5. [Hooks System (29 Hook Subcommands + 15 Background Workers)](#hooks-system)
1563
1585
  6. [Memory & Intelligence](#memory--intelligence)
1564
1586
  7. [Hive-Mind Consensus](#hive-mind-consensus)
1565
1587
  8. [Performance Targets](#performance-targets)
@@ -1661,33 +1683,31 @@ npx monomind@latest swarm monitor
1661
1683
 
1662
1684
  ## CLI Commands
1663
1685
 
1664
- ### Core Commands (12)
1686
+ ### Core Commands
1665
1687
  | Command | Subcommands | Description |
1666
1688
  |---------|-------------|-------------|
1667
- | \`init\` | 4 | Project initialization |
1668
- | \`agent\` | 8 | Agent lifecycle management |
1689
+ | \`init\` | 5 | Project initialization |
1690
+ | \`agent\` | 7 | Agent lifecycle management |
1669
1691
  | \`swarm\` | 6 | Multi-agent coordination |
1670
- | \`memory\` | 11 | LanceDB with ANN vector search |
1692
+ | \`memory\` | 12 | LanceDB with ANN vector search |
1671
1693
  | \`mcp\` | 9 | MCP server management |
1672
- | \`task\` | 6 | Task assignment |
1673
- | \`session\` | 7 | Session persistence |
1694
+ | \`task\` | 5 | Task assignment |
1695
+ | \`session\` | 6 | Session persistence |
1674
1696
  | \`config\` | 7 | Configuration |
1675
1697
  | \`status\` | 3 | System monitoring |
1676
- | \`workflow\` | 6 | Workflow templates |
1677
- | \`hooks\` | 17 | Self-learning hooks |
1678
- | \`hive-mind\` | 6 | Consensus coordination |
1698
+ | \`hooks\` | 29 | Self-learning hooks + 15 background workers${hooksAvailable ? '' : ' (background workers unavailable in this install)'} |
1699
+
1700
+ > Note: there is no \`hive-mind\`, \`workflow\`, \`neural\`, \`embeddings\`, \`claims\`, \`migrate\`, or \`process\` CLI command.
1701
+ > Hive-Mind consensus (byzantine/raft/quorum) is available exclusively via MCP tools, not the CLI.
1702
+ > Neural pattern learning was merged into \`hooks intelligence\`.
1679
1703
 
1680
- ### Advanced Commands (14)
1704
+ ### Advanced Commands
1681
1705
  | Command | Subcommands | Description |
1682
1706
  |---------|-------------|-------------|
1683
- | \`neural\` | 5 | Pattern training |
1684
1707
  | \`security\` | 6 | Security scanning |
1685
- | \`performance\` | 5 | Profiling & benchmarks |
1686
- | \`providers\` | 5 | AI provider config |
1687
- | \`embeddings\` | 4 | Vector embeddings |
1688
- | \`claims\` | 4 | Authorization |
1689
- | \`migrate\` | 5 | V2→V1 migration |
1690
- | \`process\` | 4 | Process management |
1708
+ | \`performance\` | 4 | Profiling & benchmarks |
1709
+ | \`providers\` | 4 | AI provider config |
1710
+ | \`guidance\` | 1 | Governance gate setup |
1691
1711
  | \`doctor\` | 1 | Health diagnostics |
1692
1712
  | \`completions\` | 4 | Shell completions |
1693
1713
 
@@ -1711,7 +1731,7 @@ npx monomind@latest doctor --fix
1711
1731
 
1712
1732
  ## Hooks System
1713
1733
 
1714
- ### 27 Available Hooks
1734
+ ### 29 Available Hook Subcommands${hooksAvailable ? '' : ' — background workers unavailable in this install (@monomind/hooks did not resolve)'}
1715
1735
 
1716
1736
  #### Core Hooks (6)
1717
1737
  | Hook | Description |
@@ -1818,6 +1838,11 @@ npx monomind@latest memory init --force
1818
1838
 
1819
1839
  ## Hive-Mind Consensus
1820
1840
 
1841
+ > **Experimental, MCP-only.** There is no \`hive-mind\` CLI command — this is
1842
+ > single-process vote counting exposed exclusively via MCP tools
1843
+ > (\`hive-mind-tools.ts\`), not distributed networking. Reach it through the
1844
+ > MCP server (\`npx monomind@latest mcp start\`) once connected to an MCP client.
1845
+
1821
1846
  ### Queen Types
1822
1847
  | Type | Role |
1823
1848
  |------|------|
@@ -1829,28 +1854,13 @@ npx monomind@latest memory init --force
1829
1854
  \`researcher\`, \`coder\`, \`analyst\`, \`tester\`, \`architect\`, \`reviewer\`, \`optimizer\`, \`documenter\`
1830
1855
 
1831
1856
  ### Consensus Mechanisms
1832
- | Mechanism | Fault Tolerance | Use Case |
1833
- |-----------|-----------------|----------|
1834
- | \`byzantine\` | f < n/3 faulty | Adversarial |
1835
- | \`raft\` | f < n/2 failed | Leader-based |
1836
- | \`gossip\` | Eventually consistent | Large scale |
1837
- | \`crdt\` | Conflict-free | Distributed |
1838
- | \`quorum\` | Configurable | Flexible |
1839
-
1840
- ### Hive-Mind Commands
1841
- \`\`\`bash
1842
- # Initialize
1843
- npx monomind@latest hive-mind init --queen-type strategic
1844
-
1845
- # Status
1846
- npx monomind@latest hive-mind status
1847
-
1848
- # Spawn workers
1849
- npx monomind@latest hive-mind spawn --count 5 --type worker
1850
-
1851
- # Consensus
1852
- npx monomind@latest hive-mind consensus --propose "task"
1853
- \`\`\`
1857
+ | Mechanism | Fault Tolerance | Status |
1858
+ |-----------|-----------------|--------|
1859
+ | \`byzantine\` / \`bft\` | f < n/3 faulty | Implemented (vote counting) |
1860
+ | \`raft\` | f < n/2 failed | Implemented (vote counting) |
1861
+ | \`quorum\` | Configurable | Implemented |
1862
+ | \`gossip\` | Eventually consistent | Planned — not implemented, rejected by \`hive_mind_init\` |
1863
+ | \`crdt\` | Conflict-free | Planned — not implemented, rejected by \`hive_mind_init\` |
1854
1864
 
1855
1865
  ---
1856
1866
 
@@ -2074,6 +2084,10 @@ function copyDirRecursive(src, dest) {
2074
2084
  fs.mkdirSync(dest, { recursive: true });
2075
2085
  const entries = fs.readdirSync(src, { withFileTypes: true });
2076
2086
  for (const entry of entries) {
2087
+ // Skip exFAT/macOS AppleDouble junk files (e.g. "._foo.js") so they don't
2088
+ // get perpetuated into every newly-initialized project.
2089
+ if (entry.name.startsWith('._'))
2090
+ continue;
2077
2091
  const srcPath = path.join(src, entry.name);
2078
2092
  const destPath = path.join(dest, entry.name);
2079
2093
  if (entry.isDirectory()) {
@@ -8,6 +8,7 @@
8
8
  * containing hardcoded business logic. All business logic lives in MCP tool handlers.
9
9
  */
10
10
  import type { MCPTool } from './mcp-tools/types.js';
11
+ export declare function isToolDisabled(toolName: string, cwd?: string): boolean;
11
12
  /**
12
13
  * MCP Client Error
13
14
  */
@@ -54,10 +55,14 @@ export declare function getToolMetadata(toolName: string): Omit<MCPTool, 'handle
54
55
  * @param category - Optional category filter
55
56
  * @returns Array of tool metadata
56
57
  */
57
- export declare function listMCPTools(category?: string): Array<Omit<MCPTool, 'handler'>>;
58
+ export declare function listMCPTools(category?: string): Array<Omit<MCPTool, 'handler'> & {
59
+ enabled: boolean;
60
+ }>;
58
61
  /**
59
- * Return all registered tools including their handler functions.
60
- * Used by startHttpServer() to register tools with the HTTP/WS MCP server.
62
+ * Return all registered tools including their handler functions, excluding
63
+ * any disabled via `mcp toggle`. Used by startHttpServer() to register tools
64
+ * with the HTTP/WS MCP server, so a disabled tool is never exposed to
65
+ * external MCP clients after the next server start.
61
66
  */
62
67
  export declare function getAllMCPTools(): MCPTool[];
63
68
  /**
@@ -7,6 +7,8 @@
7
7
  * This provides a simple interface for CLI commands to call MCP tools without
8
8
  * containing hardcoded business logic. All business logic lives in MCP tool handlers.
9
9
  */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
10
12
  // Import MCP tool handlers from local package
11
13
  import { agentTools } from './mcp-tools/agent-tools.js';
12
14
  import { swarmTools } from './mcp-tools/swarm-tools.js';
@@ -95,6 +97,30 @@ registerTools([
95
97
  // Second Brain knowledge tools
96
98
  ...knowledgeTools,
97
99
  ]);
100
+ /**
101
+ * Disabled-tools registry (`mcp toggle`)
102
+ *
103
+ * Read fresh on every check (the file is tiny and toggles are infrequent) so a
104
+ * `mcp toggle` run in another process/session takes effect without restarting
105
+ * this one. Filters both direct invocation (callMCPTool) and MCP server
106
+ * registration (getAllMCPTools) so a disabled tool is actually excluded, not
107
+ * just cosmetically hidden.
108
+ */
109
+ function loadDisabledTools(cwd = process.cwd()) {
110
+ const stateFile = join(cwd, '.monomind', 'mcp-disabled-tools.json');
111
+ if (!existsSync(stateFile))
112
+ return new Set();
113
+ try {
114
+ const parsed = JSON.parse(readFileSync(stateFile, 'utf8'));
115
+ return Array.isArray(parsed) ? new Set(parsed) : new Set();
116
+ }
117
+ catch {
118
+ return new Set();
119
+ }
120
+ }
121
+ export function isToolDisabled(toolName, cwd) {
122
+ return loadDisabledTools(cwd).has(toolName);
123
+ }
98
124
  /**
99
125
  * MCP Client Error
100
126
  */
@@ -138,6 +164,9 @@ export async function callMCPTool(toolName, input = {}, context) {
138
164
  if (!tool) {
139
165
  throw new MCPClientError(`MCP tool not found: ${toolName}`, toolName);
140
166
  }
167
+ if (isToolDisabled(toolName)) {
168
+ throw new MCPClientError(`MCP tool '${toolName}' is disabled. Re-enable with: mcp toggle --enable ${toolName}`, toolName);
169
+ }
141
170
  try {
142
171
  // Call the tool handler
143
172
  const result = await tool.handler(input, context);
@@ -179,6 +208,7 @@ export function getToolMetadata(toolName) {
179
208
  */
180
209
  export function listMCPTools(category) {
181
210
  const tools = Array.from(TOOL_REGISTRY.values());
211
+ const disabled = loadDisabledTools();
182
212
  const filtered = category
183
213
  ? tools.filter(t => t.category === category)
184
214
  : tools;
@@ -191,14 +221,18 @@ export function listMCPTools(category) {
191
221
  version: tool.version,
192
222
  cacheable: tool.cacheable,
193
223
  cacheTTL: tool.cacheTTL,
224
+ enabled: !disabled.has(tool.name),
194
225
  }));
195
226
  }
196
227
  /**
197
- * Return all registered tools including their handler functions.
198
- * Used by startHttpServer() to register tools with the HTTP/WS MCP server.
228
+ * Return all registered tools including their handler functions, excluding
229
+ * any disabled via `mcp toggle`. Used by startHttpServer() to register tools
230
+ * with the HTTP/WS MCP server, so a disabled tool is never exposed to
231
+ * external MCP clients after the next server start.
199
232
  */
200
233
  export function getAllMCPTools() {
201
- return Array.from(TOOL_REGISTRY.values());
234
+ const disabled = loadDisabledTools();
235
+ return Array.from(TOOL_REGISTRY.values()).filter(t => !disabled.has(t.name));
202
236
  }
203
237
  /**
204
238
  * Check if an MCP tool exists
@@ -190,9 +190,15 @@ export class MCPServerManager extends EventEmitter {
190
190
  if (!pid) {
191
191
  // No PID file found. Detect if we are running in stdio mode
192
192
  // (e.g., launched by Claude Code via `claude mcp add`).
193
- const isStdio = !process.stdin.isTTY;
193
+ //
194
+ // SECURITY/CORRECTNESS: this must NOT fall back to a TTY heuristic
195
+ // (`!process.stdin.isTTY`) — that is true for ANY non-interactive
196
+ // invocation (piped, in CI, in a script), so it falsely reported
197
+ // "running" even when no server was actually started. Only trust
198
+ // real state: the explicit stdio-transport env var, or the
199
+ // `_stdioServerStarted` flag that tracks an actually-started server.
194
200
  const envTransport = process.env.MONOMIND_MCP_TRANSPORT;
195
- if (isStdio || envTransport === 'stdio' || this._stdioServerStarted) {
201
+ if (envTransport === 'stdio' || this._stdioServerStarted) {
196
202
  return {
197
203
  running: true,
198
204
  pid: process.pid,
@@ -5,5 +5,25 @@
5
5
  * Includes model routing integration for intelligent model selection.
6
6
  */
7
7
  import { type MCPTool } from './types.js';
8
+ type ClaudeModel = 'haiku' | 'sonnet' | 'opus' | 'inherit';
9
+ interface AgentRecord {
10
+ agentId: string;
11
+ agentType: string;
12
+ status: 'idle' | 'busy' | 'terminated';
13
+ health: number;
14
+ taskCount: number;
15
+ config: Record<string, unknown>;
16
+ createdAt: string;
17
+ domain?: string;
18
+ model?: ClaudeModel;
19
+ modelRoutedBy?: 'explicit' | 'router' | 'agent-booster' | 'default';
20
+ lastResult?: Record<string, unknown>;
21
+ }
22
+ interface AgentStore {
23
+ agents: Record<string, AgentRecord>;
24
+ version: string;
25
+ }
26
+ export declare function loadAgentStore(): AgentStore;
8
27
  export declare const agentTools: MCPTool[];
28
+ export {};
9
29
  //# sourceMappingURL=agent-tools.d.ts.map
@@ -7,13 +7,14 @@
7
7
  import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, statSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { randomBytes } from 'node:crypto';
10
- import { getProjectCwd } from './types.js';
11
- // Storage paths
12
- const STORAGE_DIR = '.monomind';
10
+ import { getMonomindDataRoot, migrateLegacyStoreFile } from './types.js';
11
+ // Storage paths — relative to the git-safe data root (see getMonomindDataRoot()).
12
+ // Canonical location matches task-tools.ts/session-tools.ts/hive-mind-tools.ts/
13
+ // swarm-tools.ts so the agent store is a single physical file across all tools.
13
14
  const AGENT_DIR = 'agents';
14
15
  const AGENT_FILE = 'store.json';
15
16
  function getAgentDir() {
16
- return join(getProjectCwd(), STORAGE_DIR, AGENT_DIR);
17
+ return join(getMonomindDataRoot(), AGENT_DIR);
17
18
  }
18
19
  function getAgentPath() {
19
20
  return join(getAgentDir(), AGENT_FILE);
@@ -25,9 +26,13 @@ function ensureAgentDir() {
25
26
  }
26
27
  }
27
28
  const MAX_AGENT_STORE_BYTES = 50 * 1024 * 1024;
28
- function loadAgentStore() {
29
+ // Exported so other tool modules reading the same physical store file
30
+ // (e.g. hive-mind-tools.ts) can reuse this hardened loader instead of
31
+ // maintaining their own weaker copy (missing the size cap / __proto__ guard).
32
+ export function loadAgentStore() {
29
33
  try {
30
34
  const path = getAgentPath();
35
+ migrateLegacyStoreFile(path, join(AGENT_DIR, AGENT_FILE));
31
36
  if (existsSync(path)) {
32
37
  if (statSync(path).size > MAX_AGENT_STORE_BYTES)
33
38
  return { agents: {}, version: '3.0.0' };
@@ -60,6 +60,18 @@ function runSafe(cmd, args, cwd) {
60
60
  return null;
61
61
  }
62
62
  }
63
+ /** Run a `gh` command and preserve the real failure reason instead of collapsing to null. */
64
+ function runSafeResult(cmd, args, cwd) {
65
+ try {
66
+ const stdout = execFileSync(cmd, args, { encoding: 'utf-8', timeout: 15000, cwd: cwd || getProjectCwd(), stdio: ['pipe', 'pipe', 'pipe'] }).trim();
67
+ return { ok: true, stdout };
68
+ }
69
+ catch (err) {
70
+ const e = err;
71
+ const stderr = e.stderr ? String(e.stderr).trim() : '';
72
+ return { ok: false, error: stderr || e.message || 'gh command failed' };
73
+ }
74
+ }
63
75
  /** Check if gh CLI is available */
64
76
  function hasGhCli() {
65
77
  return run('gh --version') !== null;
@@ -216,7 +228,11 @@ export const githubTools = [
216
228
  }
217
229
  // Fallback: local store
218
230
  const prId = `pr-${Date.now()}`;
219
- const pr = { id: prId, title, status: 'open', branch: headBranch, baseBranch, createdAt: new Date().toISOString() };
231
+ // Locally-assigned PR number, independent of the timestamp-based key
232
+ // monotonic within this store so lookups by number are unambiguous.
233
+ const existingNumbers = Object.values(store.prs).map(p => p.number || 0);
234
+ const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
235
+ const pr = { id: prId, number: nextNumber, title, status: 'open', branch: headBranch, baseBranch, createdAt: new Date().toISOString() };
220
236
  store.prs[prId] = pr;
221
237
  saveGitHubStore(store);
222
238
  return { success: true, source: 'local-store', action: 'created', pullRequest: pr };
@@ -241,13 +257,17 @@ export const githubTools = [
241
257
  if (!prNumber)
242
258
  return { success: false, error: 'prNumber is required and must be a positive integer for merge.' };
243
259
  if (gh) {
244
- const result = runSafe('gh', ['pr', 'merge', String(prNumber), '--merge']);
245
- if (result !== null) {
260
+ // gh CLI is actually installed trust its real exit status. A genuine
261
+ // failure (branch protection, conflicts, auth) must be reported as a
262
+ // failure, not silently swallowed into the local-store simulation.
263
+ const result = runSafeResult('gh', ['pr', 'merge', String(prNumber), '--merge']);
264
+ if (result.ok) {
246
265
  return { success: true, _real: true, action: 'merged', prNumber, mergedAt: new Date().toISOString() };
247
266
  }
267
+ return { success: false, error: result.error };
248
268
  }
249
- // Fallback: local store
250
- const prKey = Object.keys(store.prs).find(k => k.includes(String(prNumber)));
269
+ // Fallback: local store (only reached when gh CLI is not installed at all)
270
+ const prKey = Object.keys(store.prs).find(k => store.prs[k].number === prNumber);
251
271
  if (prKey && store.prs[prKey]) {
252
272
  store.prs[prKey].status = 'merged';
253
273
  saveGitHubStore(store);
@@ -259,12 +279,14 @@ export const githubTools = [
259
279
  if (!prNumber)
260
280
  return { success: false, error: 'prNumber is required and must be a positive integer for close.' };
261
281
  if (gh) {
262
- const result = runSafe('gh', ['pr', 'close', String(prNumber)]);
263
- if (result !== null) {
282
+ const result = runSafeResult('gh', ['pr', 'close', String(prNumber)]);
283
+ if (result.ok) {
264
284
  return { success: true, _real: true, action: 'closed', prNumber, closedAt: new Date().toISOString() };
265
285
  }
286
+ return { success: false, error: result.error };
266
287
  }
267
- const prKey = Object.keys(store.prs).find(k => k.includes(String(prNumber)));
288
+ // Fallback: local store (only reached when gh CLI is not installed at all)
289
+ const prKey = Object.keys(store.prs).find(k => store.prs[k].number === prNumber);
268
290
  if (prKey && store.prs[prKey]) {
269
291
  store.prs[prKey].status = 'closed';
270
292
  saveGitHubStore(store);
@@ -4,7 +4,14 @@
4
4
  * All graphify_* tools are deprecated. They proxy to monograph_* tools.
5
5
  * Will be removed in next major release.
6
6
  */
7
- import { allMonographTools } from './monograph-tools.js';
7
+ import { allMonographTools, monographTools } from './monograph-tools.js';
8
+ // Advanced monograph tools are gated behind MONOGRAPH_MCP_ADVANCED=1 (see
9
+ // monograph-tools.ts). Shims that target an advanced-tier tool must be gated
10
+ // the same way, otherwise they re-expose advanced tools under a different
11
+ // name regardless of the flag. `monographTools` is the already-gated export
12
+ // (core-only unless MONOGRAPH_MCP_ADVANCED=1); a target is "advanced" if it's
13
+ // present in allMonographTools but absent from monographTools.
14
+ const exposedMonographNames = new Set(monographTools.map(t => t.name));
8
15
  function findMonographTool(name) {
9
16
  const tool = allMonographTools.find(t => t.name === name);
10
17
  if (!tool)
@@ -12,6 +19,8 @@ function findMonographTool(name) {
12
19
  return tool;
13
20
  }
14
21
  function shimTool(graphifyName, monographName, paramMap) {
22
+ if (!exposedMonographNames.has(monographName))
23
+ return null;
15
24
  const target = findMonographTool(monographName);
16
25
  return {
17
26
  name: graphifyName,
@@ -44,6 +53,6 @@ export const graphifyTools = [
44
53
  shimTool('graphify_report', 'monograph_report'),
45
54
  // Bug fix: graphify_health previously referenced `files.length` (ReferenceError) — now delegates cleanly
46
55
  shimTool('graphify_health', 'monograph_health'),
47
- ];
56
+ ].filter((t) => t !== null);
48
57
  export default graphifyTools;
49
58
  //# sourceMappingURL=graphify-tools.js.map
@@ -13,8 +13,12 @@
13
13
  import { existsSync, readFileSync, statSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
14
14
  import { randomBytes } from 'node:crypto';
15
15
  import { join } from 'node:path';
16
- import { getProjectCwd } from './types.js';
16
+ import { getProjectCwd, getMonomindDataRoot, migrateLegacyStoreFile } from './types.js';
17
17
  import { weightedTally } from '../consensus/tally.js';
18
+ // Reuse agent-tools.ts's hardened store loader (50MB size cap + __proto__
19
+ // rejection) instead of maintaining a second, weaker copy that reads the
20
+ // same physical file — see loadAgentStore export note there.
21
+ import { loadAgentStore } from './agent-tools.js';
18
22
  // Storage paths
19
23
  const STORAGE_DIR = '.monomind';
20
24
  const HIVE_DIR = 'hive-mind';
@@ -92,7 +96,7 @@ function tryResolveProposal(proposal, totalNodes) {
92
96
  return null;
93
97
  }
94
98
  function getHiveDir() {
95
- return join(getProjectCwd(), STORAGE_DIR, HIVE_DIR);
99
+ return join(getMonomindDataRoot(), HIVE_DIR);
96
100
  }
97
101
  function getHivePath() {
98
102
  return join(getHiveDir(), HIVE_FILE);
@@ -107,6 +111,7 @@ const MAX_HIVE_STATE_BYTES = 10 * 1024 * 1024; // 10 MB
107
111
  function loadHiveState() {
108
112
  try {
109
113
  const path = getHivePath();
114
+ migrateLegacyStoreFile(path, join(HIVE_DIR, HIVE_FILE));
110
115
  if (existsSync(path) && statSync(path).size <= MAX_HIVE_STATE_BYTES) {
111
116
  const data = readFileSync(path, 'utf-8');
112
117
  return JSON.parse(data);
@@ -176,21 +181,10 @@ function getOrCreateSessionSecret() {
176
181
  }
177
182
  }
178
183
  // Import agent store helpers for spawn functionality
179
- import { existsSync as agentStoreExists, readFileSync as readAgentStore, writeFileSync as writeAgentStore, mkdirSync as mkdirAgentStore } from 'node:fs';
180
- // Canonical agent store path matches agent-tools.ts: .monomind/agents/store.json
181
- function loadAgentStore() {
182
- const storePath = join(getProjectCwd(), '.monomind', 'agents', 'store.json');
183
- try {
184
- if (agentStoreExists(storePath)) {
185
- return JSON.parse(readAgentStore(storePath, 'utf-8'));
186
- }
187
- }
188
- catch { /* ignore */ }
189
- return { agents: {} };
190
- }
184
+ import { existsSync as agentStoreExists, writeFileSync as writeAgentStore, mkdirSync as mkdirAgentStore } from 'node:fs';
191
185
  const HIVE_RESERVED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
192
186
  function saveAgentStore(store) {
193
- const storeDir = join(getProjectCwd(), '.monomind', 'agents');
187
+ const storeDir = join(getMonomindDataRoot(), 'agents');
194
188
  if (!agentStoreExists(storeDir)) {
195
189
  mkdirAgentStore(storeDir, { recursive: true });
196
190
  }
@@ -284,6 +278,10 @@ export const hiveMindTools = [
284
278
  properties: {
285
279
  topology: { type: 'string', enum: ['mesh', 'hierarchical', 'ring', 'star'], description: 'Network topology' },
286
280
  queenId: { type: 'string', description: 'Initial queen agent ID' },
281
+ consensus: { type: 'string', enum: ['byzantine', 'bft', 'raft', 'quorum'], description: 'Consensus strategy to govern hive-mind_consensus propose/vote (gossip/crdt are planned but not implemented and fall back to byzantine). Default: byzantine.' },
282
+ maxAgents: { type: 'number', description: 'Maximum number of agents in the hive. Default: 15.' },
283
+ persist: { type: 'boolean', description: 'Whether to persist hive state to disk. Default: true.' },
284
+ memoryBackend: { type: 'string', description: 'Shared memory backend for the hive. Default: hybrid.' },
287
285
  },
288
286
  },
289
287
  handler: async (input) => {
@@ -294,32 +292,34 @@ export const hiveMindTools = [
294
292
  const queenId = typeof rawQueenId === 'string' && rawQueenId.length > MAX_QUEEN_ID_LEN
295
293
  ? rawQueenId.slice(0, MAX_QUEEN_ID_LEN) : rawQueenId;
296
294
  const now = new Date().toISOString();
295
+ // Validate consensus strategy: gossip and crdt are planned but not implemented
296
+ const SUPPORTED_CONSENSUS = ['byzantine', 'bft', 'raft', 'quorum'];
297
+ const PLANNED_CONSENSUS = ['gossip', 'crdt'];
298
+ const requestedConsensus = input.consensus || 'byzantine';
299
+ const consensusWarning = PLANNED_CONSENSUS.includes(requestedConsensus)
300
+ ? `Strategy "${requestedConsensus}" is planned but not yet implemented; defaulting to "byzantine".`
301
+ : undefined;
302
+ const effectiveConsensus = PLANNED_CONSENSUS.includes(requestedConsensus)
303
+ ? 'byzantine'
304
+ : (SUPPORTED_CONSENSUS.includes(requestedConsensus) ? requestedConsensus : 'byzantine');
297
305
  const state = {
298
306
  initialized: true,
299
307
  hiveId,
300
308
  topology: input.topology || 'mesh',
301
309
  queen: { agentId: queenId, electedAt: now, term: 1 },
302
310
  workers: [],
311
+ // Persist the chosen strategy on the hive state so hive-mind_consensus
312
+ // propose/vote default to it instead of silently using 'raft'.
313
+ // 'byzantine' is the CLI-facing name; the internal ConsensusStrategy type uses 'bft'.
314
+ consensusStrategy: (effectiveConsensus === 'byzantine' ? 'bft' : effectiveConsensus),
303
315
  consensus: { pending: [], history: [] },
304
316
  sharedMemory: {},
305
317
  createdAt: now,
306
318
  updatedAt: now,
307
319
  };
308
- saveHiveState(state);
309
- // Validate consensus strategy: gossip and crdt are planned but not implemented
310
- const SUPPORTED_CONSENSUS = ['byzantine', 'bft', 'raft', 'quorum'];
311
- const PLANNED_CONSENSUS = ['gossip', 'crdt'];
312
- const requestedConsensus = input.consensus || 'byzantine';
313
- const consensusWarning = PLANNED_CONSENSUS.includes(requestedConsensus)
314
- ? `Strategy "${requestedConsensus}" is planned but not yet implemented; defaulting to "byzantine".`
315
- : undefined;
316
- const effectiveConsensus = PLANNED_CONSENSUS.includes(requestedConsensus)
317
- ? 'byzantine'
318
- : (SUPPORTED_CONSENSUS.includes(requestedConsensus) ? requestedConsensus : 'byzantine');
319
- // Persist the chosen strategy on the hive state so hive-mind_consensus
320
- // propose/vote default to it instead of silently using 'raft'.
321
- // 'byzantine' is the CLI-facing name; the internal ConsensusStrategy type uses 'bft'.
322
- state.consensusStrategy = (effectiveConsensus === 'byzantine' ? 'bft' : effectiveConsensus);
320
+ // Single write — a prior version of this handler saved state twice
321
+ // (once before setting consensusStrategy, once after), which was
322
+ // redundant.
323
323
  saveHiveState(state);
324
324
  return {
325
325
  success: true,
@@ -357,7 +357,8 @@ export const hiveMindTools = [
357
357
  // Load agent store once for all workers
358
358
  const agentStore = loadAgentStore();
359
359
  // Compute real task metrics from task store
360
- const taskStorePath = join(getProjectCwd(), '.monomind', 'tasks', 'store.json');
360
+ const taskStorePath = join(getMonomindDataRoot(), 'tasks', 'store.json');
361
+ migrateLegacyStoreFile(taskStorePath, join('tasks', 'store.json'));
361
362
  let pendingTaskCount = 0;
362
363
  let activeTaskCount = 0;
363
364
  let completedTaskCount = 0;
@@ -382,7 +383,14 @@ export const hiveMindTools = [
382
383
  hiveId: state.hiveId ?? `hive-${state.createdAt ? new Date(state.createdAt).getTime() : Date.now()}`,
383
384
  status: state.initialized ? 'active' : 'offline',
384
385
  topology: state.topology,
385
- consensus: 'byzantine', // Default consensus type
386
+ // Reflect the strategy actually chosen at hive-mind_init (persisted
387
+ // as state.consensusStrategy so it "actually governs voting" — see
388
+ // the field's doc comment) instead of hardcoding a default that
389
+ // ignored what was configured. 'bft' is the internal name for the
390
+ // CLI-facing 'byzantine' strategy.
391
+ consensus: state.consensusStrategy
392
+ ? (state.consensusStrategy === 'bft' ? 'byzantine' : state.consensusStrategy)
393
+ : 'byzantine',
386
394
  queen: state.queen ? {
387
395
  id: state.queen.agentId,
388
396
  agentId: state.queen.agentId,
@@ -729,9 +737,18 @@ export const hiveMindTools = [
729
737
  confidence: 1.0, // uniform until confidence-probe is wired
730
738
  }));
731
739
  const cpwbftTally = weightedTally(weightedVotes);
732
- // O-Information gate: defer resolution if we haven't seen enough divergent rounds
740
+ // O-Information gate: defer resolution if we haven't seen enough divergent rounds.
741
+ // BUT: if every expected voter has already cast a vote (electorate
742
+ // exhausted), no more votes can ever arrive to produce a divergent
743
+ // round — a unanimous first-round vote would otherwise deadlock the
744
+ // proposal permanently since divergenceRoundsSeen only increments on
745
+ // disagreement. Treat electorate exhaustion as a terminal condition
746
+ // that opens the gate regardless of divergence.
747
+ const totalVotesCast = Object.keys(proposal.votes).length;
748
+ const electorateExhausted = totalNodes > 0 && totalVotesCast >= totalNodes;
733
749
  const divergenceGateOpen = !proposal.minDivergenceRounds
734
- || (proposal.divergenceRoundsSeen ?? 0) >= proposal.minDivergenceRounds;
750
+ || (proposal.divergenceRoundsSeen ?? 0) >= proposal.minDivergenceRounds
751
+ || electorateExhausted;
735
752
  // Try to resolve
736
753
  const resolution = divergenceGateOpen ? tryResolveProposal(proposal, totalNodes) : null;
737
754
  let resolved = false;