@hivehub/rulebook 4.4.1 → 5.1.1

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 (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +68 -8
  3. package/dist/cli/commands.d.ts.map +1 -1
  4. package/dist/cli/commands.js +86 -0
  5. package/dist/cli/commands.js.map +1 -1
  6. package/dist/core/agent-template-engine.d.ts +51 -0
  7. package/dist/core/agent-template-engine.d.ts.map +1 -0
  8. package/dist/core/agent-template-engine.js +285 -0
  9. package/dist/core/agent-template-engine.js.map +1 -0
  10. package/dist/core/complexity-detector.d.ts +36 -0
  11. package/dist/core/complexity-detector.d.ts.map +1 -0
  12. package/dist/core/complexity-detector.js +254 -0
  13. package/dist/core/complexity-detector.js.map +1 -0
  14. package/dist/core/config-manager.d.ts.map +1 -1
  15. package/dist/core/config-manager.js +7 -8
  16. package/dist/core/config-manager.js.map +1 -1
  17. package/dist/core/generator.d.ts +1 -0
  18. package/dist/core/generator.d.ts.map +1 -1
  19. package/dist/core/generator.js +21 -3
  20. package/dist/core/generator.js.map +1 -1
  21. package/dist/core/indexer/background-indexer.d.ts +2 -2
  22. package/dist/core/indexer/background-indexer.d.ts.map +1 -1
  23. package/dist/core/indexer/background-indexer.js +55 -61
  24. package/dist/core/indexer/background-indexer.js.map +1 -1
  25. package/dist/core/rule-engine.d.ts +64 -0
  26. package/dist/core/rule-engine.d.ts.map +1 -0
  27. package/dist/core/rule-engine.js +333 -0
  28. package/dist/core/rule-engine.js.map +1 -0
  29. package/dist/core/task-manager.d.ts +4 -0
  30. package/dist/core/task-manager.d.ts.map +1 -1
  31. package/dist/core/task-manager.js +39 -24
  32. package/dist/core/task-manager.js.map +1 -1
  33. package/dist/index.js +182 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/rulebook-server.d.ts +6 -0
  36. package/dist/mcp/rulebook-server.d.ts.map +1 -1
  37. package/dist/mcp/rulebook-server.js +394 -49
  38. package/dist/mcp/rulebook-server.js.map +1 -1
  39. package/dist/memory/hnsw-index.d.ts +6 -1
  40. package/dist/memory/hnsw-index.d.ts.map +1 -1
  41. package/dist/memory/hnsw-index.js +133 -18
  42. package/dist/memory/hnsw-index.js.map +1 -1
  43. package/dist/memory/memory-manager.d.ts +2 -0
  44. package/dist/memory/memory-manager.d.ts.map +1 -1
  45. package/dist/memory/memory-manager.js +16 -7
  46. package/dist/memory/memory-manager.js.map +1 -1
  47. package/dist/memory/memory-store.d.ts +15 -3
  48. package/dist/memory/memory-store.d.ts.map +1 -1
  49. package/dist/memory/memory-store.js +327 -274
  50. package/dist/memory/memory-store.js.map +1 -1
  51. package/dist/types.d.ts +17 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +8 -3
  54. package/templates/agents/compiler/codegen-debugger.md +34 -0
  55. package/templates/agents/compiler/stdlib-engineer.md +28 -0
  56. package/templates/agents/compiler/test-coverage-guardian.md +31 -0
  57. package/templates/agents/game-engine/cpp-core-expert.md +35 -0
  58. package/templates/agents/game-engine/render-engineer.md +22 -0
  59. package/templates/agents/game-engine/shader-engineer.md +38 -0
  60. package/templates/agents/game-engine/systems-integration.md +43 -0
  61. package/templates/agents/generic/code-reviewer.md +41 -0
  62. package/templates/agents/generic/docs-writer.md +25 -0
  63. package/templates/agents/generic/project-manager.md +36 -0
  64. package/templates/agents/generic/researcher.md +34 -0
  65. package/templates/agents/generic/test-engineer.md +41 -0
  66. package/templates/agents/implementer.md +8 -4
  67. package/templates/agents/mobile/platform-specialist.md +22 -0
  68. package/templates/agents/mobile/ui-engineer.md +22 -0
  69. package/templates/agents/tester.md +7 -4
  70. package/templates/agents/web-app/api-designer.md +22 -0
  71. package/templates/agents/web-app/backend-engineer.md +30 -0
  72. package/templates/agents/web-app/database-engineer.md +22 -0
  73. package/templates/agents/web-app/frontend-engineer.md +29 -0
  74. package/templates/agents/web-app/security-reviewer.md +32 -0
  75. package/templates/core/AGENT_AUTOMATION.md +8 -0
  76. package/templates/core/RULEBOOK.md +12 -0
  77. package/templates/core/TIER1_PROHIBITIONS.md +154 -0
  78. package/templates/core/TOKEN_OPTIMIZATION.md +49 -0
  79. package/templates/git/GIT_WORKFLOW.md +35 -0
  80. package/templates/rules/follow-task-sequence.md +36 -0
  81. package/templates/rules/git-safety.md +29 -0
  82. package/templates/rules/incremental-implementation.md +56 -0
  83. package/templates/rules/incremental-tests.md +29 -0
  84. package/templates/rules/no-deferred.md +31 -0
  85. package/templates/rules/no-shortcuts.md +30 -0
  86. package/templates/rules/research-first.md +30 -0
  87. package/templates/rules/sequential-editing.md +21 -0
  88. package/templates/rules/session-workflow.md +24 -0
  89. package/templates/rules/task-decomposition.md +32 -0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { existsSync, readFileSync, statSync } from 'fs';
4
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
5
5
  import { basename, dirname, join, resolve } from 'path';
6
6
  import { z } from 'zod';
7
7
  import { ConfigManager } from '../core/config-manager.js';
@@ -17,7 +17,13 @@ function withTimeout(promise, ms = MCP_TOOL_TIMEOUT_MS, label = 'tool') {
17
17
  const timer = setTimeout(() => {
18
18
  reject(new Error(`[rulebook-mcp] ${label} timed out after ${ms}ms`));
19
19
  }, ms);
20
- promise.then((val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); });
20
+ promise.then((val) => {
21
+ clearTimeout(timer);
22
+ resolve(val);
23
+ }, (err) => {
24
+ clearTimeout(timer);
25
+ reject(err);
26
+ });
21
27
  });
22
28
  }
23
29
  // Find .rulebook file/directory by walking up directories
@@ -66,6 +72,58 @@ function loadConfig() {
66
72
  const archiveDir = resolve(projectRoot, mcp.archiveDir || '.rulebook/archive');
67
73
  return { projectRoot, tasksDir, archiveDir };
68
74
  }
75
+ // --- PID file guard: prevent multiple MCP server instances for the same project ---
76
+ const PID_FILE_NAME = 'mcp-server.pid';
77
+ /**
78
+ * Check if a PID file exists and if the recorded process is still alive.
79
+ * If alive, exit this instance. If stale (process died), take over.
80
+ */
81
+ export function acquirePidLock(projectRoot) {
82
+ const pidDir = join(projectRoot, '.rulebook');
83
+ const pidPath = join(pidDir, PID_FILE_NAME);
84
+ if (existsSync(pidPath)) {
85
+ try {
86
+ const raw = readFileSync(pidPath, 'utf8').trim();
87
+ const existingPid = parseInt(raw, 10);
88
+ if (!isNaN(existingPid)) {
89
+ // Check if process is still alive (signal 0 = existence check)
90
+ try {
91
+ process.kill(existingPid, 0);
92
+ // Process is alive — another server is running
93
+ console.error(`[rulebook-mcp] Another instance is already running (PID ${existingPid}). Exiting.`);
94
+ process.exit(0);
95
+ }
96
+ catch {
97
+ // Process is dead — stale PID file, take over
98
+ console.error(`[rulebook-mcp] Stale PID file found (PID ${existingPid} no longer exists). Taking over.`);
99
+ }
100
+ }
101
+ }
102
+ catch {
103
+ // Corrupt PID file — overwrite
104
+ }
105
+ }
106
+ // Write our PID
107
+ if (!existsSync(pidDir)) {
108
+ mkdirSync(pidDir, { recursive: true });
109
+ }
110
+ writeFileSync(pidPath, String(process.pid), 'utf8');
111
+ return pidPath;
112
+ }
113
+ export function releasePidLock(pidPath) {
114
+ try {
115
+ if (existsSync(pidPath)) {
116
+ const raw = readFileSync(pidPath, 'utf8').trim();
117
+ // Only remove if it's OUR PID (another instance may have taken over)
118
+ if (parseInt(raw, 10) === process.pid) {
119
+ unlinkSync(pidPath);
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Best-effort cleanup
125
+ }
126
+ }
69
127
  export async function startRulebookMcpServer() {
70
128
  // --- Workspace vs Single-Project Mode ---
71
129
  let isWorkspaceMode = process.argv.includes('--workspace');
@@ -123,6 +181,8 @@ export async function startRulebookMcpServer() {
123
181
  skillsManager = new SkillsManager(getDefaultTemplatesPath(), projectRoot);
124
182
  configManager = new ConfigManager(projectRoot);
125
183
  }
184
+ // --- PID file lock: prevent multiple instances for the same project ---
185
+ const pidFilePath = acquirePidLock(projectRoot);
126
186
  // --- Manager Resolution Helpers (workspace-aware) ---
127
187
  async function getTaskMgr(projectId) {
128
188
  if (!projectId || !workspaceManager)
@@ -152,7 +212,7 @@ export async function startRulebookMcpServer() {
152
212
  }
153
213
  const server = new McpServer({
154
214
  name: 'rulebook-task-management',
155
- version: '4.4.1',
215
+ version: '5.1.1',
156
216
  });
157
217
  // --- Wrap all tool handlers with timeout guard ---
158
218
  // Intercept registerTool to automatically add timeout protection to every handler.
@@ -166,7 +226,9 @@ export async function startRulebookMcpServer() {
166
226
  const msg = error instanceof Error ? error.message : String(error);
167
227
  console.error(`[rulebook-mcp] ${name} error: ${msg}`);
168
228
  return {
169
- content: [{ type: 'text', text: JSON.stringify({ success: false, error: msg }) }],
229
+ content: [
230
+ { type: 'text', text: JSON.stringify({ success: false, error: msg }) },
231
+ ],
170
232
  };
171
233
  }
172
234
  };
@@ -701,16 +763,19 @@ export async function startRulebookMcpServer() {
701
763
  console.error(`[rulebook-mcp] Memory DB: ${memoryDbPath}`);
702
764
  memoryManager = createMemoryManager(projectRoot, rulebookConfig.memory);
703
765
  autoCaptureEnabled = rulebookConfig.memory.autoCapture !== false;
704
- // Boot Background Indexer deferred to avoid blocking server startup
705
- bgIndexer = new BackgroundIndexer(memoryManager, projectRoot, { enabled: true });
706
- setTimeout(() => {
707
- try {
708
- bgIndexer?.start();
709
- }
710
- catch (e) {
711
- console.error('[rulebook-mcp] BackgroundIndexer start failed:', e);
712
- }
713
- }, 5000);
766
+ // Boot Background Indexer only if memory is enabled (opt-in to save resources)
767
+ const indexerEnabled = rulebookConfig.memory?.enabled === true;
768
+ if (indexerEnabled) {
769
+ bgIndexer = new BackgroundIndexer(memoryManager, projectRoot, { enabled: true });
770
+ setTimeout(() => {
771
+ try {
772
+ bgIndexer?.start();
773
+ }
774
+ catch (e) {
775
+ console.error('[rulebook-mcp] BackgroundIndexer start failed:', e);
776
+ }
777
+ }, 5000);
778
+ }
714
779
  global.__indexerStatus = () => bgIndexer?.getStatus();
715
780
  }
716
781
  catch (e) {
@@ -718,31 +783,67 @@ export async function startRulebookMcpServer() {
718
783
  }
719
784
  }
720
785
  }
721
- // Graceful shutdown for both modes
722
- process.on('SIGINT', async () => {
723
- console.error('[rulebook-mcp] Shutting down...');
724
- if (workspaceManager) {
725
- await workspaceManager.shutdownAll();
786
+ // --- Graceful shutdown for both modes ---
787
+ // Handles SIGINT (Ctrl+C), SIGTERM (parent exit), SIGHUP (terminal close),
788
+ // and stdin close (parent process died without signaling).
789
+ let isShuttingDown = false;
790
+ async function gracefulShutdown(reason) {
791
+ if (isShuttingDown)
792
+ return; // Prevent double-shutdown races
793
+ isShuttingDown = true;
794
+ console.error(`[rulebook-mcp] Shutting down (${reason})...`);
795
+ try {
796
+ if (bgIndexer)
797
+ bgIndexer.stop();
798
+ if (workspaceManager) {
799
+ await workspaceManager.shutdownAll();
800
+ }
801
+ if (memoryManager)
802
+ await memoryManager.close();
803
+ releasePidLock(pidFilePath);
804
+ }
805
+ catch (e) {
806
+ console.error('[rulebook-mcp] Error during shutdown:', e);
726
807
  }
727
- if (bgIndexer)
728
- bgIndexer.stop();
729
- if (memoryManager)
730
- await memoryManager.close();
731
808
  process.exit(0);
732
- });
809
+ }
810
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
811
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
812
+ process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
813
+ // Detect parent process exit: when the MCP client (e.g. Claude Code) dies,
814
+ // stdin closes. Without this, the server becomes an orphan that leaks memory.
815
+ process.stdin.on('end', () => gracefulShutdown('stdin closed'));
816
+ process.stdin.on('close', () => gracefulShutdown('stdin closed'));
817
+ // Safety net: if stdin becomes unreadable but doesn't emit 'end'/'close'
818
+ // (can happen on Windows), poll stdin.readable every 30s.
819
+ const STDIN_POLL_INTERVAL_MS = 30_000;
820
+ const stdinPollTimer = setInterval(() => {
821
+ if (process.stdin.destroyed || !process.stdin.readable) {
822
+ clearInterval(stdinPollTimer);
823
+ gracefulShutdown('stdin unreadable');
824
+ }
825
+ }, STDIN_POLL_INTERVAL_MS);
826
+ stdinPollTimer.unref(); // Don't keep the process alive just for this timer
733
827
  /**
734
828
  * Auto-capture: save tool interactions to memory in the background.
735
829
  * Fire-and-forget — never blocks or fails the original tool call.
736
830
  * Has its own 2s timeout to prevent hanging the event loop.
831
+ *
832
+ * The dynamic import is cached after the first call to avoid repeated
833
+ * module loading overhead on every tool invocation.
737
834
  */
738
835
  const AUTO_CAPTURE_TIMEOUT_MS = 2000;
836
+ let _captureFromToolCall = null;
739
837
  async function autoCapture(toolName, args, resultText) {
740
838
  if (!memoryManager || !autoCaptureEnabled)
741
839
  return;
742
840
  try {
743
841
  await withTimeout((async () => {
744
- const { captureFromToolCall } = await import('../memory/memory-hooks.js');
745
- const captured = captureFromToolCall(toolName, args, resultText);
842
+ if (!_captureFromToolCall) {
843
+ const mod = await import('../memory/memory-hooks.js');
844
+ _captureFromToolCall = mod.captureFromToolCall;
845
+ }
846
+ const captured = _captureFromToolCall(toolName, args, resultText);
746
847
  if (!captured)
747
848
  return;
748
849
  await memoryManager.saveMemory({
@@ -1046,12 +1147,9 @@ export async function startRulebookMcpServer() {
1046
1147
  ],
1047
1148
  };
1048
1149
  }
1049
- // Ensure lock is released on exit, even on crashes
1050
- const cleanupLock = async () => {
1051
- await ralphManager.releaseLock();
1052
- };
1053
- process.on('SIGTERM', cleanupLock);
1054
- process.on('SIGINT', cleanupLock);
1150
+ // Lock cleanup is handled by the top-level SIGINT handler (line ~858)
1151
+ // which shuts down all managers. Adding per-call SIGINT/SIGTERM listeners
1152
+ // causes accumulation and race conditions with the server shutdown handler.
1055
1153
  try {
1056
1154
  // Validate tool is available before starting
1057
1155
  const toolCmdNames = {
@@ -1312,8 +1410,6 @@ export async function startRulebookMcpServer() {
1312
1410
  finally {
1313
1411
  // Always release lock, even on error
1314
1412
  await ralphManager.releaseLock();
1315
- process.removeListener('SIGTERM', cleanupLock);
1316
- process.removeListener('SIGINT', cleanupLock);
1317
1413
  }
1318
1414
  }
1319
1415
  catch (error) {
@@ -1724,7 +1820,10 @@ export async function startRulebookMcpServer() {
1724
1820
  content: [
1725
1821
  {
1726
1822
  type: 'text',
1727
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
1823
+ text: JSON.stringify({
1824
+ success: false,
1825
+ error: error instanceof Error ? error.message : String(error),
1826
+ }),
1728
1827
  },
1729
1828
  ],
1730
1829
  };
@@ -1735,7 +1834,10 @@ export async function startRulebookMcpServer() {
1735
1834
  title: 'List Decision Records',
1736
1835
  description: 'List all architectural decision records',
1737
1836
  inputSchema: {
1738
- status: z.string().optional().describe('Filter by status (proposed, accepted, rejected, superseded, deprecated)'),
1837
+ status: z
1838
+ .string()
1839
+ .optional()
1840
+ .describe('Filter by status (proposed, accepted, rejected, superseded, deprecated)'),
1739
1841
  projectId: projectIdSchema,
1740
1842
  },
1741
1843
  }, async (args) => {
@@ -1760,7 +1862,10 @@ export async function startRulebookMcpServer() {
1760
1862
  content: [
1761
1863
  {
1762
1864
  type: 'text',
1763
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
1865
+ text: JSON.stringify({
1866
+ success: false,
1867
+ error: error instanceof Error ? error.message : String(error),
1868
+ }),
1764
1869
  },
1765
1870
  ],
1766
1871
  };
@@ -1796,7 +1901,11 @@ export async function startRulebookMcpServer() {
1796
1901
  content: [
1797
1902
  {
1798
1903
  type: 'text',
1799
- text: JSON.stringify({ success: true, decision: result.decision, content: result.content }),
1904
+ text: JSON.stringify({
1905
+ success: true,
1906
+ decision: result.decision,
1907
+ content: result.content,
1908
+ }),
1800
1909
  },
1801
1910
  ],
1802
1911
  };
@@ -1806,7 +1915,10 @@ export async function startRulebookMcpServer() {
1806
1915
  content: [
1807
1916
  {
1808
1917
  type: 'text',
1809
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
1918
+ text: JSON.stringify({
1919
+ success: false,
1920
+ error: error instanceof Error ? error.message : String(error),
1921
+ }),
1810
1922
  },
1811
1923
  ],
1812
1924
  };
@@ -1818,7 +1930,10 @@ export async function startRulebookMcpServer() {
1818
1930
  description: 'Update an existing architectural decision record',
1819
1931
  inputSchema: {
1820
1932
  id: z.number().describe('Decision ID to update'),
1821
- status: z.string().optional().describe('New status (proposed, accepted, rejected, superseded, deprecated)'),
1933
+ status: z
1934
+ .string()
1935
+ .optional()
1936
+ .describe('New status (proposed, accepted, rejected, superseded, deprecated)'),
1822
1937
  context: z.string().optional().describe('Updated context'),
1823
1938
  decision: z.string().optional().describe('Updated decision text'),
1824
1939
  projectId: projectIdSchema,
@@ -1859,7 +1974,10 @@ export async function startRulebookMcpServer() {
1859
1974
  content: [
1860
1975
  {
1861
1976
  type: 'text',
1862
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
1977
+ text: JSON.stringify({
1978
+ success: false,
1979
+ error: error instanceof Error ? error.message : String(error),
1980
+ }),
1863
1981
  },
1864
1982
  ],
1865
1983
  };
@@ -1909,7 +2027,10 @@ export async function startRulebookMcpServer() {
1909
2027
  content: [
1910
2028
  {
1911
2029
  type: 'text',
1912
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
2030
+ text: JSON.stringify({
2031
+ success: false,
2032
+ error: error instanceof Error ? error.message : String(error),
2033
+ }),
1913
2034
  },
1914
2035
  ],
1915
2036
  };
@@ -1946,7 +2067,10 @@ export async function startRulebookMcpServer() {
1946
2067
  content: [
1947
2068
  {
1948
2069
  type: 'text',
1949
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
2070
+ text: JSON.stringify({
2071
+ success: false,
2072
+ error: error instanceof Error ? error.message : String(error),
2073
+ }),
1950
2074
  },
1951
2075
  ],
1952
2076
  };
@@ -1973,7 +2097,10 @@ export async function startRulebookMcpServer() {
1973
2097
  content: [
1974
2098
  {
1975
2099
  type: 'text',
1976
- text: JSON.stringify({ success: false, error: `Knowledge entry "${args.id}" not found` }),
2100
+ text: JSON.stringify({
2101
+ success: false,
2102
+ error: `Knowledge entry "${args.id}" not found`,
2103
+ }),
1977
2104
  },
1978
2105
  ],
1979
2106
  };
@@ -1992,7 +2119,10 @@ export async function startRulebookMcpServer() {
1992
2119
  content: [
1993
2120
  {
1994
2121
  type: 'text',
1995
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
2122
+ text: JSON.stringify({
2123
+ success: false,
2124
+ error: error instanceof Error ? error.message : String(error),
2125
+ }),
1996
2126
  },
1997
2127
  ],
1998
2128
  };
@@ -2034,7 +2164,10 @@ export async function startRulebookMcpServer() {
2034
2164
  content: [
2035
2165
  {
2036
2166
  type: 'text',
2037
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
2167
+ text: JSON.stringify({
2168
+ success: false,
2169
+ error: error instanceof Error ? error.message : String(error),
2170
+ }),
2038
2171
  },
2039
2172
  ],
2040
2173
  };
@@ -2070,7 +2203,10 @@ export async function startRulebookMcpServer() {
2070
2203
  content: [
2071
2204
  {
2072
2205
  type: 'text',
2073
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
2206
+ text: JSON.stringify({
2207
+ success: false,
2208
+ error: error instanceof Error ? error.message : String(error),
2209
+ }),
2074
2210
  },
2075
2211
  ],
2076
2212
  };
@@ -2082,7 +2218,9 @@ export async function startRulebookMcpServer() {
2082
2218
  description: 'Promote a learning to a knowledge entry or decision record',
2083
2219
  inputSchema: {
2084
2220
  id: z.string().describe('Learning ID to promote'),
2085
- target: z.enum(['knowledge', 'decision']).describe('Promote to knowledge base or decision record'),
2221
+ target: z
2222
+ .enum(['knowledge', 'decision'])
2223
+ .describe('Promote to knowledge base or decision record'),
2086
2224
  title: z.string().optional().describe('Override title for the promoted entry'),
2087
2225
  projectId: projectIdSchema,
2088
2226
  },
@@ -2118,7 +2256,214 @@ export async function startRulebookMcpServer() {
2118
2256
  content: [
2119
2257
  {
2120
2258
  type: 'text',
2121
- text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }),
2259
+ text: JSON.stringify({
2260
+ success: false,
2261
+ error: error instanceof Error ? error.message : String(error),
2262
+ }),
2263
+ },
2264
+ ],
2265
+ };
2266
+ }
2267
+ });
2268
+ // ── v5.0 Tools: Session Management, Rules, Blockers ──────────────────
2269
+ // Register tool: rulebook_session_start
2270
+ server.registerTool('rulebook_session_start', {
2271
+ title: 'Start Session',
2272
+ description: 'Load session context: reads PLANS.md and searches relevant memories. Call at the start of every session.',
2273
+ inputSchema: {
2274
+ query: z
2275
+ .string()
2276
+ .optional()
2277
+ .describe('Optional search query to find relevant past memories'),
2278
+ projectId: projectIdSchema,
2279
+ },
2280
+ }, async (args) => {
2281
+ try {
2282
+ const root = args.projectId && workspaceManager
2283
+ ? (await workspaceManager.getWorker(args.projectId)).projectRoot
2284
+ : projectRoot;
2285
+ const { join } = await import('path');
2286
+ const { existsSync } = await import('fs');
2287
+ const { readFile } = await import('fs/promises');
2288
+ const result = { plans: null, memories: [] };
2289
+ // Read PLANS.md
2290
+ const plansPath = join(root, '.rulebook', 'PLANS.md');
2291
+ if (existsSync(plansPath)) {
2292
+ result.plans = await readFile(plansPath, 'utf-8');
2293
+ }
2294
+ // Search relevant memories
2295
+ if (args.query && memoryManager) {
2296
+ const searchResults = await memoryManager.searchMemories({
2297
+ query: args.query,
2298
+ mode: 'hybrid',
2299
+ limit: 5,
2300
+ });
2301
+ result.memories = searchResults;
2302
+ }
2303
+ return {
2304
+ content: [{ type: 'text', text: JSON.stringify({ success: true, ...result }) }],
2305
+ };
2306
+ }
2307
+ catch (error) {
2308
+ return {
2309
+ content: [
2310
+ {
2311
+ type: 'text',
2312
+ text: JSON.stringify({
2313
+ success: false,
2314
+ error: error instanceof Error ? error.message : String(error),
2315
+ }),
2316
+ },
2317
+ ],
2318
+ };
2319
+ }
2320
+ });
2321
+ // Register tool: rulebook_session_end
2322
+ server.registerTool('rulebook_session_end', {
2323
+ title: 'End Session',
2324
+ description: 'Save session summary to PLANS.md history section. Call at the end of every session.',
2325
+ inputSchema: {
2326
+ summary: z.string().describe('Session summary: what was accomplished, key decisions, next steps'),
2327
+ projectId: projectIdSchema,
2328
+ },
2329
+ }, async (args) => {
2330
+ try {
2331
+ const root = args.projectId && workspaceManager
2332
+ ? (await workspaceManager.getWorker(args.projectId)).projectRoot
2333
+ : projectRoot;
2334
+ const { join } = await import('path');
2335
+ const { existsSync } = await import('fs');
2336
+ const { readFile, writeFile } = await import('fs/promises');
2337
+ const plansPath = join(root, '.rulebook', 'PLANS.md');
2338
+ const date = new Date().toISOString().split('T')[0];
2339
+ const entry = `### ${date}\n${args.summary}\n`;
2340
+ if (existsSync(plansPath)) {
2341
+ let content = await readFile(plansPath, 'utf-8');
2342
+ // Insert after <!-- PLANS:HISTORY:START -->
2343
+ if (content.includes('<!-- PLANS:HISTORY:START -->')) {
2344
+ content = content.replace('<!-- PLANS:HISTORY:START -->', `<!-- PLANS:HISTORY:START -->\n${entry}`);
2345
+ }
2346
+ else {
2347
+ content += `\n## Session History\n\n<!-- PLANS:HISTORY:START -->\n${entry}<!-- PLANS:HISTORY:END -->\n`;
2348
+ }
2349
+ await writeFile(plansPath, content, 'utf-8');
2350
+ }
2351
+ else {
2352
+ // Create PLANS.md from scratch
2353
+ const newContent = `# Project Plans & Session Context\n\n<!-- PLANS:CONTEXT:START -->\n_No active context._\n<!-- PLANS:CONTEXT:END -->\n\n<!-- PLANS:TASK:START -->\n_No task in progress._\n<!-- PLANS:TASK:END -->\n\n## Session History\n\n<!-- PLANS:HISTORY:START -->\n${entry}<!-- PLANS:HISTORY:END -->\n`;
2354
+ const { mkdirSync } = await import('fs');
2355
+ const dir = join(root, '.rulebook');
2356
+ if (!existsSync(dir))
2357
+ mkdirSync(dir, { recursive: true });
2358
+ await writeFile(plansPath, newContent, 'utf-8');
2359
+ }
2360
+ // Also save to memory if available
2361
+ if (memoryManager) {
2362
+ await memoryManager.saveMemory({
2363
+ type: 'observation',
2364
+ title: `Session summary ${date}`,
2365
+ content: args.summary,
2366
+ tags: ['session', 'summary'],
2367
+ });
2368
+ }
2369
+ return {
2370
+ content: [
2371
+ { type: 'text', text: JSON.stringify({ success: true, message: 'Session summary saved to PLANS.md' }) },
2372
+ ],
2373
+ };
2374
+ }
2375
+ catch (error) {
2376
+ return {
2377
+ content: [
2378
+ {
2379
+ type: 'text',
2380
+ text: JSON.stringify({
2381
+ success: false,
2382
+ error: error instanceof Error ? error.message : String(error),
2383
+ }),
2384
+ },
2385
+ ],
2386
+ };
2387
+ }
2388
+ });
2389
+ // Register tool: rulebook_rules_list
2390
+ server.registerTool('rulebook_rules_list', {
2391
+ title: 'List Rules',
2392
+ description: 'List all canonical rules from .rulebook/rules/ with tier and tool targeting info',
2393
+ inputSchema: {
2394
+ projectId: projectIdSchema,
2395
+ },
2396
+ }, async (args) => {
2397
+ try {
2398
+ const root = args.projectId && workspaceManager
2399
+ ? (await workspaceManager.getWorker(args.projectId)).projectRoot
2400
+ : projectRoot;
2401
+ const { listRules } = await import('../core/rule-engine.js');
2402
+ const rules = await listRules(root);
2403
+ return {
2404
+ content: [{ type: 'text', text: JSON.stringify({ success: true, rules, count: rules.length }) }],
2405
+ };
2406
+ }
2407
+ catch (error) {
2408
+ return {
2409
+ content: [
2410
+ {
2411
+ type: 'text',
2412
+ text: JSON.stringify({
2413
+ success: false,
2414
+ error: error instanceof Error ? error.message : String(error),
2415
+ }),
2416
+ },
2417
+ ],
2418
+ };
2419
+ }
2420
+ });
2421
+ // Register tool: rulebook_blockers
2422
+ server.registerTool('rulebook_blockers', {
2423
+ title: 'Show Blockers',
2424
+ description: 'Show task blocker chain with cascade impact analysis',
2425
+ inputSchema: {
2426
+ projectId: projectIdSchema,
2427
+ },
2428
+ }, async (args) => {
2429
+ try {
2430
+ const tm = await getTaskMgr(args.projectId);
2431
+ const tasks = await tm.listTasks();
2432
+ // Build blocker chain from task metadata
2433
+ const blockers = [];
2434
+ for (const task of tasks) {
2435
+ const metadata = await tm.getTaskMetadata(task.id);
2436
+ const blocks = Array.isArray(metadata?.blocks) ? metadata.blocks : [];
2437
+ const blockedBy = Array.isArray(metadata?.blockedBy) ? metadata.blockedBy : [];
2438
+ if (blocks.length > 0 || blockedBy.length > 0) {
2439
+ blockers.push({
2440
+ taskId: task.id,
2441
+ blocks,
2442
+ blockedBy,
2443
+ cascadeImpact: metadata?.cascadeImpact || blocks.length,
2444
+ });
2445
+ }
2446
+ }
2447
+ // Sort by cascade impact (highest first)
2448
+ blockers.sort((a, b) => b.cascadeImpact - a.cascadeImpact);
2449
+ return {
2450
+ content: [
2451
+ {
2452
+ type: 'text',
2453
+ text: JSON.stringify({ success: true, blockers, count: blockers.length }),
2454
+ },
2455
+ ],
2456
+ };
2457
+ }
2458
+ catch (error) {
2459
+ return {
2460
+ content: [
2461
+ {
2462
+ type: 'text',
2463
+ text: JSON.stringify({
2464
+ success: false,
2465
+ error: error instanceof Error ? error.message : String(error),
2466
+ }),
2122
2467
  },
2123
2468
  ],
2124
2469
  };