@monoes/monomindcli 2.9.3 → 2.9.5

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 (99) hide show
  1. package/.claude/skills/mastermind-createorg/SKILL.md +2 -1
  2. package/README.md +9 -5
  3. package/dist/src/autopilot-state.d.ts +16 -4
  4. package/dist/src/autopilot-state.d.ts.map +1 -1
  5. package/dist/src/autopilot-state.js +42 -19
  6. package/dist/src/autopilot-state.js.map +1 -1
  7. package/dist/src/commands/agent-lifecycle.js +4 -4
  8. package/dist/src/commands/agent-lifecycle.js.map +1 -1
  9. package/dist/src/commands/autopilot.d.ts.map +1 -1
  10. package/dist/src/commands/autopilot.js +106 -25
  11. package/dist/src/commands/autopilot.js.map +1 -1
  12. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  13. package/dist/src/commands/doctor-project-checks.js +20 -3
  14. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  15. package/dist/src/commands/hooks-coverage-commands.d.ts.map +1 -1
  16. package/dist/src/commands/hooks-coverage-commands.js +67 -75
  17. package/dist/src/commands/hooks-coverage-commands.js.map +1 -1
  18. package/dist/src/commands/hooks-routing-commands.d.ts.map +1 -1
  19. package/dist/src/commands/hooks-routing-commands.js +12 -26
  20. package/dist/src/commands/hooks-routing-commands.js.map +1 -1
  21. package/dist/src/commands/hooks-workers.d.ts.map +1 -1
  22. package/dist/src/commands/hooks-workers.js +16 -47
  23. package/dist/src/commands/hooks-workers.js.map +1 -1
  24. package/dist/src/commands/hooks.d.ts.map +1 -1
  25. package/dist/src/commands/hooks.js +96 -54
  26. package/dist/src/commands/hooks.js.map +1 -1
  27. package/dist/src/commands/memory-list.d.ts.map +1 -1
  28. package/dist/src/commands/memory-list.js +79 -36
  29. package/dist/src/commands/memory-list.js.map +1 -1
  30. package/dist/src/commands/org.js +2 -2
  31. package/dist/src/commands/org.js.map +1 -1
  32. package/dist/src/commands/security-cve.d.ts.map +1 -1
  33. package/dist/src/commands/security-cve.js +11 -1
  34. package/dist/src/commands/security-cve.js.map +1 -1
  35. package/dist/src/commands/security-misc.d.ts +9 -0
  36. package/dist/src/commands/security-misc.d.ts.map +1 -1
  37. package/dist/src/commands/security-misc.js +54 -33
  38. package/dist/src/commands/security-misc.js.map +1 -1
  39. package/dist/src/commands/security-scan.d.ts.map +1 -1
  40. package/dist/src/commands/security-scan.js +11 -3
  41. package/dist/src/commands/security-scan.js.map +1 -1
  42. package/dist/src/commands/swarm.d.ts.map +1 -1
  43. package/dist/src/commands/swarm.js +137 -96
  44. package/dist/src/commands/swarm.js.map +1 -1
  45. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  46. package/dist/src/init/statusline-generator.js +47 -45
  47. package/dist/src/init/statusline-generator.js.map +1 -1
  48. package/dist/src/init/types.d.ts +1 -1
  49. package/dist/src/init/types.d.ts.map +1 -1
  50. package/dist/src/init/types.js +5 -1
  51. package/dist/src/init/types.js.map +1 -1
  52. package/dist/src/mcp-server.d.ts +6 -0
  53. package/dist/src/mcp-server.d.ts.map +1 -1
  54. package/dist/src/mcp-server.js +47 -22
  55. package/dist/src/mcp-server.js.map +1 -1
  56. package/dist/src/mcp-tools/embeddings-tools.js +2 -2
  57. package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
  58. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  59. package/dist/src/mcp-tools/hive-mind-tools.js +13 -11
  60. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  61. package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -1
  62. package/dist/src/mcp-tools/hooks-intelligence.js +99 -68
  63. package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -1
  64. package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
  65. package/dist/src/mcp-tools/hooks-routing.js +403 -215
  66. package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
  67. package/dist/src/mcp-tools/monograph/impact-tools.d.ts.map +1 -1
  68. package/dist/src/mcp-tools/monograph/impact-tools.js +109 -44
  69. package/dist/src/mcp-tools/monograph/impact-tools.js.map +1 -1
  70. package/dist/src/mcp-tools/performance-tools.d.ts.map +1 -1
  71. package/dist/src/mcp-tools/performance-tools.js +10 -15
  72. package/dist/src/mcp-tools/performance-tools.js.map +1 -1
  73. package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
  74. package/dist/src/mcp-tools/system-tools.js +100 -38
  75. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  76. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  77. package/dist/src/memory/memory-bridge.js +172 -69
  78. package/dist/src/memory/memory-bridge.js.map +1 -1
  79. package/dist/src/monovector/diff-classifier.js +3 -3
  80. package/dist/src/monovector/diff-classifier.js.map +1 -1
  81. package/dist/src/orgrt/daemon.d.ts +5 -3
  82. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  83. package/dist/src/orgrt/daemon.js +291 -93
  84. package/dist/src/orgrt/daemon.js.map +1 -1
  85. package/dist/src/orgrt/kimicode-runner.js +1 -1
  86. package/dist/src/orgrt/kimicode-runner.js.map +1 -1
  87. package/dist/src/orgrt/opencode-runner.js +1 -1
  88. package/dist/src/orgrt/opencode-runner.js.map +1 -1
  89. package/dist/src/orgrt/remote.d.ts.map +1 -1
  90. package/dist/src/orgrt/remote.js +12 -6
  91. package/dist/src/orgrt/remote.js.map +1 -1
  92. package/dist/src/orgrt/server.d.ts.map +1 -1
  93. package/dist/src/orgrt/server.js +71 -14
  94. package/dist/src/orgrt/server.js.map +1 -1
  95. package/dist/src/types.d.ts +1 -1
  96. package/dist/src/types.d.ts.map +1 -1
  97. package/dist/src/ui/server.mjs +2647 -838
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +7 -7
@@ -4,9 +4,10 @@
4
4
  * build-agents, transfer, session, list, metrics, pre-task, post-task, intelligence.
5
5
  * Extracted from hooks-tools.ts.
6
6
  */
7
- import { mkdirSync, writeFileSync, renameSync, existsSync, readFileSync } from 'fs';
7
+ import { mkdirSync, writeFileSync, renameSync, existsSync, readFileSync, } from 'fs';
8
8
  import { join, resolve, sep } from 'path';
9
9
  import { getProjectCwd } from './types.js';
10
+ import { validateMcpString } from '../utils/input-guards.js';
10
11
  import { randomUUID } from 'node:crypto';
11
12
  import { recordRoute, joinOutcome, joinLatestUnresolved } from '../monovector/route-outcomes.js';
12
13
  import { recordCommand, deriveRecentSuccess } from '../monovector/command-outcomes.js';
@@ -19,7 +20,10 @@ export const hooksPreEdit = {
19
20
  type: 'object',
20
21
  properties: {
21
22
  filePath: { type: 'string', description: 'Path to the file being edited' },
22
- operation: { type: 'string', description: 'Type of operation (create, update, delete, refactor)' },
23
+ operation: {
24
+ type: 'string',
25
+ description: 'Type of operation (create, update, delete, refactor)',
26
+ },
23
27
  context: { type: 'string', description: 'Additional context' },
24
28
  },
25
29
  required: ['filePath'],
@@ -29,14 +33,11 @@ export const hooksPreEdit = {
29
33
  // response. Cap operation to prevent oversized strings in recommendations.
30
34
  const MAX_PRE_EDIT_PATH_LEN = 4 * 1024;
31
35
  const MAX_PRE_EDIT_OP_LEN = 64;
32
- const rawFilePath = params.filePath;
33
- const filePath = typeof rawFilePath === 'string' && rawFilePath.length > MAX_PRE_EDIT_PATH_LEN
34
- ? rawFilePath.slice(0, MAX_PRE_EDIT_PATH_LEN)
35
- : rawFilePath;
36
- const rawOperation = params.operation || 'update';
37
- const operation = typeof rawOperation === 'string' && rawOperation.length > MAX_PRE_EDIT_OP_LEN
38
- ? rawOperation.slice(0, MAX_PRE_EDIT_OP_LEN)
39
- : rawOperation;
36
+ const filePath = validateMcpString(params.filePath, 'filePath', MAX_PRE_EDIT_PATH_LEN);
37
+ if (!filePath) {
38
+ return { error: 'filePath is required (non-empty string, no control chars, max 4KB)' };
39
+ }
40
+ const operation = validateMcpString(params.operation, 'operation', MAX_PRE_EDIT_OP_LEN) ?? 'update';
40
41
  const suggestedAgents = suggestAgentsForFile(filePath);
41
42
  const ext = getFileExtension(filePath);
42
43
  return {
@@ -47,9 +48,7 @@ export const hooksPreEdit = {
47
48
  fileType: ext || 'unknown',
48
49
  relatedFiles: [],
49
50
  suggestedAgents,
50
- patterns: [
51
- { pattern: `${ext} file editing`, confidence: 0.85 },
52
- ],
51
+ patterns: [{ pattern: `${ext} file editing`, confidence: 0.85 }],
53
52
  risks: operation === 'delete' ? ['File deletion is irreversible'] : [],
54
53
  },
55
54
  recommendations: [
@@ -77,15 +76,12 @@ export const hooksPostEdit = {
77
76
  // Cap agent: stored in feedback record and forwarded to bridge.
78
77
  const MAX_POST_EDIT_PATH_LEN = 4 * 1024;
79
78
  const MAX_POST_EDIT_AGENT_LEN = 256;
80
- const rawFilePath = params.filePath;
81
- const filePath = typeof rawFilePath === 'string' && rawFilePath.length > MAX_POST_EDIT_PATH_LEN
82
- ? rawFilePath.slice(0, MAX_POST_EDIT_PATH_LEN)
83
- : rawFilePath;
79
+ const filePath = validateMcpString(params.filePath, 'filePath', MAX_POST_EDIT_PATH_LEN);
80
+ if (!filePath) {
81
+ return { error: 'filePath is required (non-empty string, no control chars, max 4KB)' };
82
+ }
84
83
  const success = params.success !== false;
85
- const rawAgent = params.agent;
86
- const agent = typeof rawAgent === 'string' && rawAgent.length > MAX_POST_EDIT_AGENT_LEN
87
- ? rawAgent.slice(0, MAX_POST_EDIT_AGENT_LEN)
88
- : rawAgent;
84
+ const agent = validateMcpString(params.agent, 'agent', MAX_POST_EDIT_AGENT_LEN) ?? undefined;
89
85
  // Wire recordFeedback through bridge (issue #1209)
90
86
  let feedbackResult = null;
91
87
  try {
@@ -108,11 +104,13 @@ export const hooksPostEdit = {
108
104
  success,
109
105
  timestamp: new Date().toISOString(),
110
106
  learningUpdate: success ? 'pattern_reinforced' : 'pattern_adjusted',
111
- feedback: feedbackResult ? {
112
- recorded: feedbackResult.success,
113
- controller: feedbackResult.success ? 'lancedb' : 'unavailable',
114
- updates: feedbackResult.success ? 1 : 0,
115
- } : { recorded: false, controller: 'unavailable', updates: 0 },
107
+ feedback: feedbackResult
108
+ ? {
109
+ recorded: feedbackResult.success,
110
+ controller: feedbackResult.success ? 'sqlite' : 'unavailable',
111
+ updates: feedbackResult.success ? 1 : 0,
112
+ }
113
+ : { recorded: false, controller: 'unavailable', updates: 0 },
116
114
  };
117
115
  },
118
116
  };
@@ -131,14 +129,17 @@ export const hooksPreCommand = {
131
129
  // raw command is reflected verbatim in the response. Limit to 4 KB which
132
130
  // is far beyond any realistic shell command.
133
131
  const MAX_CMD_LEN = 4 * 1024;
134
- const rawCommand = params.command;
135
- const command = typeof rawCommand === 'string' && rawCommand.length > MAX_CMD_LEN
136
- ? rawCommand.slice(0, MAX_CMD_LEN)
137
- : rawCommand;
132
+ const command = validateMcpString(params.command, 'command', MAX_CMD_LEN);
133
+ if (!command) {
134
+ return { error: 'command is required (non-empty string, no control chars, max 4KB)' };
135
+ }
138
136
  const assessment = assessCommandRisk(command);
139
- const riskLevel = assessment.level >= 0.8 ? 'critical'
140
- : assessment.level >= 0.6 ? 'high'
141
- : assessment.level >= 0.3 ? 'medium'
137
+ const riskLevel = assessment.level >= 0.8
138
+ ? 'critical'
139
+ : assessment.level >= 0.6
140
+ ? 'high'
141
+ : assessment.level >= 0.3
142
+ ? 'medium'
142
143
  : 'low';
143
144
  return {
144
145
  command,
@@ -174,11 +175,13 @@ export const hooksPostCommand = {
174
175
  // already caps to 200 chars; apply a consistent 4 KB cap here that still
175
176
  // covers any realistic shell command.
176
177
  const MAX_POST_CMD_LEN = 4 * 1024;
177
- const rawPostCommand = params.command;
178
- const command = typeof rawPostCommand === 'string' && rawPostCommand.length > MAX_POST_CMD_LEN
179
- ? rawPostCommand.slice(0, MAX_POST_CMD_LEN)
180
- : rawPostCommand;
181
- const exitCode = params.exitCode || 0;
178
+ const command = validateMcpString(params.command, 'command', MAX_POST_CMD_LEN);
179
+ if (!command) {
180
+ return { error: 'command is required (non-empty string, no control chars, max 4KB)' };
181
+ }
182
+ const exitCode = typeof params.exitCode === 'number' && Number.isFinite(params.exitCode)
183
+ ? Math.floor(params.exitCode)
184
+ : 0;
182
185
  const success = exitCode === 0;
183
186
  // Record the real exit code in the time-windowed command-outcome store so
184
187
  // post-task can derive a MEASURED success signal (grounded in actual exit
@@ -198,14 +201,19 @@ export const hooksPostCommand = {
198
201
  namespace: 'commands',
199
202
  tags: [success ? 'success' : 'error'],
200
203
  });
201
- _storedIn = 'lancedb';
204
+ _storedIn = 'sqlite';
202
205
  }
203
206
  catch {
204
207
  // memory backend unavailable — store in JSON
205
208
  try {
206
209
  const store = loadMemoryStore();
207
210
  const key = `cmd-${Date.now()}`;
208
- store.entries[key] = { key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands', createdAt: new Date().toISOString() };
211
+ store.entries[key] = {
212
+ key,
213
+ value: JSON.stringify({ command, exitCode, success }),
214
+ namespace: 'commands',
215
+ createdAt: new Date().toISOString(),
216
+ };
209
217
  const memDir = join(getProjectCwd(), MEMORY_DIR);
210
218
  if (!existsSync(memDir))
211
219
  mkdirSync(memDir, { recursive: true });
@@ -215,7 +223,9 @@ export const hooksPostCommand = {
215
223
  renameSync(_mptmp, _mp);
216
224
  _storedIn = 'json-store';
217
225
  }
218
- catch { /* non-critical */ }
226
+ catch {
227
+ /* non-critical */
228
+ }
219
229
  }
220
230
  return {
221
231
  recorded: _storedIn !== 'none',
@@ -229,13 +239,16 @@ export const hooksPostCommand = {
229
239
  };
230
240
  export const hooksRoute = {
231
241
  name: 'hooks_route',
232
- description: 'Route task to optimal agent using semantic similarity (native HNSW or pure JS)',
242
+ description: 'Route task to optimal agent using keyword matching and optional memory-backend semantic search',
233
243
  inputSchema: {
234
244
  type: 'object',
235
245
  properties: {
236
246
  task: { type: 'string', description: 'Task description' },
237
247
  context: { type: 'string', description: 'Additional context' },
238
- useSemanticRouter: { type: 'boolean', description: 'Use semantic similarity routing (default: true)' },
248
+ useSemanticRouter: {
249
+ type: 'boolean',
250
+ description: 'Use semantic similarity routing (default: true)',
251
+ },
239
252
  },
240
253
  required: ['task'],
241
254
  },
@@ -245,15 +258,13 @@ export const hooksRoute = {
245
258
  // route-outcomes.jsonl. 16 KB matches the cap in hooksPatternSearch.
246
259
  const MAX_ROUTE_TASK_LEN = 16 * 1024;
247
260
  const MAX_ROUTE_CTX_LEN = 4 * 1024;
248
- const rawTask = params.task;
249
- const task = typeof rawTask === 'string' && rawTask.length > MAX_ROUTE_TASK_LEN
250
- ? rawTask.slice(0, MAX_ROUTE_TASK_LEN)
251
- : rawTask;
252
- const rawContext = params.context;
253
- const context = typeof rawContext === 'string' && rawContext.length > MAX_ROUTE_CTX_LEN
254
- ? rawContext.slice(0, MAX_ROUTE_CTX_LEN)
255
- : rawContext;
261
+ const task = validateMcpString(params.task, 'task', MAX_ROUTE_TASK_LEN);
262
+ if (!task) {
263
+ return { error: 'task is required (non-empty string, no control chars, max 16KB)' };
264
+ }
265
+ const context = validateMcpString(params.context, 'context', MAX_ROUTE_CTX_LEN) ?? undefined;
256
266
  const useSemanticRouter = params.useSemanticRouter !== false;
267
+ const routeStart = Date.now();
257
268
  // Phase 5: Try memory backend SemanticRouter / LearningSystem first
258
269
  if (useSemanticRouter) {
259
270
  try {
@@ -265,7 +276,7 @@ export const hooksRoute = {
265
276
  if (routeConfidence > 0.5) {
266
277
  const agents = memoryRoute.routes.map((r) => r.agentType);
267
278
  const complexity = task.length > 200 ? 'high' : task.length < 50 ? 'low' : 'medium';
268
- const memoryMethod = 'memory-lancedb';
279
+ const memoryMethod = 'memory-sqlite';
269
280
  const memoryConfidence = Math.round(routeConfidence * 100) / 100;
270
281
  const matchedPattern = topRoute.pattern ?? task.slice(0, 60);
271
282
  // Record the route recommendation so post-task can join the actual outcome
@@ -284,8 +295,8 @@ export const hooksRoute = {
284
295
  task,
285
296
  routing: {
286
297
  method: memoryMethod,
287
- backend: 'lancedb',
288
- latencyMs: 0,
298
+ backend: 'sqlite',
299
+ latencyMs: Date.now() - routeStart,
289
300
  throughput: 'N/A',
290
301
  },
291
302
  matchedPattern,
@@ -293,19 +304,25 @@ export const hooksRoute = {
293
304
  primaryAgent: {
294
305
  type: agents[0],
295
306
  confidence: memoryConfidence,
296
- reason: `memory:lancedb: "${matchedPattern}" (${Math.round(routeConfidence * 100)}%)`,
307
+ reason: `memory:sqlite: "${matchedPattern}" (${Math.round(routeConfidence * 100)}%)`,
297
308
  },
298
309
  alternativeAgents: agents.slice(1).map((agent, i) => ({
299
310
  type: agent,
300
- confidence: Math.round((routeConfidence - (0.1 * (i + 1))) * 100) / 100,
301
- reason: 'Alternative from lancedb',
311
+ confidence: Math.round((routeConfidence - 0.1 * (i + 1)) * 100) / 100,
312
+ reason: 'Alternative from sqlite',
302
313
  })),
303
- estimatedMetrics: {
314
+ heuristics: {
304
315
  successProbability: memoryConfidence,
305
- estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
316
+ estimatedDuration: complexity === 'high'
317
+ ? '2-4 hours'
318
+ : complexity === 'medium'
319
+ ? '30-60 min'
320
+ : '10-30 min',
306
321
  complexity,
307
322
  },
308
- swarmRecommendation: agents.length > 2 ? { topology: 'hierarchical', agents, coordination: 'queen-led' } : null,
323
+ swarmRecommendation: agents.length > 2
324
+ ? { topology: 'hierarchical', agents, coordination: 'queen-led' }
325
+ : null,
309
326
  };
310
327
  }
311
328
  }
@@ -317,7 +334,7 @@ export const hooksRoute = {
317
334
  // Deterministic keyword routing is the baseline (and only) local path.
318
335
  const semanticResult = [];
319
336
  let routingMethod = 'keyword';
320
- const routingLatencyMs = 0;
337
+ const routingLatencyMs = Date.now() - routeStart;
321
338
  let backendInfo = '';
322
339
  // Get agents from keyword routing
323
340
  let agents;
@@ -338,8 +355,11 @@ export const hooksRoute = {
338
355
  if (intelSuggestion && intelSuggestion.confidence > 0.5) {
339
356
  // Prepend neural agents (deduped) and boost confidence
340
357
  const existingSet = new Set(agents);
341
- const neuralOnly = intelSuggestion.agents.filter(a => !existingSet.has(a));
342
- agents = [...intelSuggestion.agents, ...agents.filter(a => !new Set(intelSuggestion.agents).has(a))];
358
+ const neuralOnly = intelSuggestion.agents.filter((a) => !existingSet.has(a));
359
+ agents = [
360
+ ...intelSuggestion.agents,
361
+ ...agents.filter((a) => !new Set(intelSuggestion.agents).has(a)),
362
+ ];
343
363
  const neuralWeight = intelSuggestion.confidence > 0.7 ? 0.65 : 0.5;
344
364
  const keywordWeight = 1 - neuralWeight;
345
365
  confidence = Math.min(0.95, intelSuggestion.confidence * neuralWeight +
@@ -379,7 +399,7 @@ export const hooksRoute = {
379
399
  throughput: routingLatencyMs > 0 ? `${Math.round(1000 / routingLatencyMs)} routes/s` : 'N/A',
380
400
  },
381
401
  matchedPattern,
382
- semanticMatches: semanticResult.slice(0, 3).map(r => ({
402
+ semanticMatches: semanticResult.slice(0, 3).map((r) => ({
383
403
  pattern: r.intent,
384
404
  score: Math.round(r.score * 100) / 100,
385
405
  })),
@@ -392,19 +412,21 @@ export const hooksRoute = {
392
412
  },
393
413
  alternativeAgents: agents.slice(1).map((agent, i) => ({
394
414
  type: agent,
395
- confidence: Math.round((confidence - (0.1 * (i + 1))) * 100) / 100,
415
+ confidence: Math.round((confidence - 0.1 * (i + 1)) * 100) / 100,
396
416
  reason: `Alternative agent for ${agent} capabilities`,
397
417
  })),
398
- estimatedMetrics: {
418
+ heuristics: {
399
419
  successProbability: Math.round(confidence * 100) / 100,
400
420
  estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
401
421
  complexity,
402
422
  },
403
- swarmRecommendation: agents.length > 2 ? {
404
- topology: 'hierarchical',
405
- agents,
406
- coordination: 'queen-led',
407
- } : null,
423
+ swarmRecommendation: agents.length > 2
424
+ ? {
425
+ topology: 'hierarchical',
426
+ agents,
427
+ coordination: 'queen-led',
428
+ }
429
+ : null,
408
430
  };
409
431
  },
410
432
  };
@@ -419,19 +441,19 @@ export const hooksRouteSemantic = {
419
441
  type: 'object',
420
442
  properties: {
421
443
  task: { type: 'string', description: 'Task description' },
422
- debug: { type: 'boolean', description: 'Include all route scores in the response (default: false)' },
444
+ debug: {
445
+ type: 'boolean',
446
+ description: 'Include all route scores in the response (default: false)',
447
+ },
423
448
  },
424
449
  required: ['task'],
425
450
  },
426
451
  handler: async (params) => {
427
452
  const MAX_ROUTE_TASK_LEN = 2000; // matches route-layer-factory's MAX_TASK_LENGTH
428
- const rawTask = params.task;
429
- const task = typeof rawTask === 'string' && rawTask.length > MAX_ROUTE_TASK_LEN
430
- ? rawTask.slice(0, MAX_ROUTE_TASK_LEN)
431
- : rawTask;
453
+ const task = validateMcpString(params.task, 'task', MAX_ROUTE_TASK_LEN);
432
454
  const debug = params.debug === true;
433
- if (!task || typeof task !== 'string') {
434
- throw new Error('task is required');
455
+ if (!task) {
456
+ throw new Error('task is required (non-empty string, no control chars, max 2000 chars)');
435
457
  }
436
458
  const { createConfiguredRouteLayer } = await import('../routing/route-layer-factory.js');
437
459
  const layer = await createConfiguredRouteLayer({ debug });
@@ -445,7 +467,9 @@ export const hooksRouteSemantic = {
445
467
  routingMethod: `routing-pkg:${result.method}`,
446
468
  confidence: result.confidence,
447
469
  learningMode: 'js',
448
- }).catch(() => { });
470
+ }).catch(() => {
471
+ /* non-fatal — outcome joining is best-effort */
472
+ });
449
473
  return { routeId, task, ...result };
450
474
  },
451
475
  };
@@ -460,14 +484,14 @@ export const hooksMetrics = {
460
484
  },
461
485
  },
462
486
  handler: async (params) => {
463
- const period = params.period || '24h';
487
+ const period = validateMcpString(params.period, 'period', 32) ?? '24h';
464
488
  // Try to read real counts from memory store
465
489
  const store = loadMemoryStore();
466
490
  const entries = Object.values(store.entries);
467
491
  // Count patterns by looking at stored pattern entries
468
- const patternEntries = entries.filter(e => e.key.includes('pattern'));
469
- const routingEntries = entries.filter(e => e.key.includes('route') || e.key.includes('routing'));
470
- const taskEntries = entries.filter(e => e.key.includes('task'));
492
+ const patternEntries = entries.filter((e) => e.key.includes('pattern'));
493
+ const routingEntries = entries.filter((e) => e.key.includes('route') || e.key.includes('routing'));
494
+ const taskEntries = entries.filter((e) => e.key.includes('task'));
471
495
  if (entries.length === 0) {
472
496
  return {
473
497
  _real: true,
@@ -568,23 +592,25 @@ export const hooksPreTask = {
568
592
  // in causal-graph edges persisted to SQLite. An uncapped ID can inflate the DB key
569
593
  // column and every JSON payload that includes the ID.
570
594
  const MAX_TASK_ID_LEN = 256;
571
- const rawTaskId = params.taskId;
572
- const taskId = typeof rawTaskId === 'string' && rawTaskId.length > MAX_TASK_ID_LEN
573
- ? rawTaskId.slice(0, MAX_TASK_ID_LEN)
574
- : rawTaskId;
595
+ const taskId = validateMcpString(params.taskId, 'taskId', MAX_TASK_ID_LEN);
596
+ if (!taskId) {
597
+ return { error: 'taskId is required (non-empty string, no control chars, max 256 chars)' };
598
+ }
575
599
  // Cap description: it is forwarded to generateEmbedding twice (ERL heuristics
576
600
  // + TextGrad gradient queries) and used in O(n) keyword extraction.
577
601
  // 16 KB matches the cap applied in hooks_route and hooksPatternSearch.
578
602
  const MAX_PRE_TASK_DESC_LEN = 16 * 1024;
579
- const rawDescription = params.description;
580
- const description = typeof rawDescription === 'string' && rawDescription.length > MAX_PRE_TASK_DESC_LEN
581
- ? rawDescription.slice(0, MAX_PRE_TASK_DESC_LEN)
582
- : rawDescription;
583
- const filePath = params.filePath;
603
+ const description = validateMcpString(params.description, 'description', MAX_PRE_TASK_DESC_LEN);
604
+ if (!description) {
605
+ return { error: 'description is required (non-empty string, no control chars, max 16KB)' };
606
+ }
607
+ const filePath = validateMcpString(params.filePath, 'filePath', 4 * 1024) ?? undefined;
584
608
  const suggestion = suggestAgentsForTask(description);
585
609
  // Determine complexity
586
610
  const descLower = description.toLowerCase();
587
- const complexity = descLower.includes('complex') || descLower.includes('architecture') || description.length > 200
611
+ const complexity = descLower.includes('complex') ||
612
+ descLower.includes('architecture') ||
613
+ description.length > 200
588
614
  ? 'high'
589
615
  : descLower.includes('simple') || descLower.includes('fix') || description.length < 50
590
616
  ? 'low'
@@ -603,14 +629,16 @@ export const hooksPreTask = {
603
629
  limit: 3,
604
630
  threshold: 0.6,
605
631
  });
606
- for (const r of (heuristicResults?.results ?? [])) {
632
+ for (const r of heuristicResults?.results ?? []) {
607
633
  try {
608
634
  const h = JSON.parse(r.content ?? '{}');
609
635
  if (h.action && h.confidence !== undefined && h.confidence >= 0.6) {
610
636
  erlHints.push(`ERL hint (conf=${h.confidence.toFixed(2)}): use "${h.action}" for tasks involving "${h.condition ?? 'similar context'}"`);
611
637
  }
612
638
  }
613
- catch { /* skip malformed */ }
639
+ catch {
640
+ /* skip malformed */
641
+ }
614
642
  }
615
643
  // TextGrad: also inject relevant past failure gradients to guide away from known pitfalls
616
644
  // Source: https://arxiv.org/abs/2406.07496
@@ -620,7 +648,7 @@ export const hooksPreTask = {
620
648
  limit: 2,
621
649
  threshold: 0.55,
622
650
  });
623
- for (const r of (gradientResults?.results ?? [])) {
651
+ for (const r of gradientResults?.results ?? []) {
624
652
  const critique = r.content ?? '';
625
653
  if (critique && critique.length > 10) {
626
654
  erlHints.push(`TextGrad warning: ${critique.slice(0, 120)}`);
@@ -628,7 +656,9 @@ export const hooksPreTask = {
628
656
  }
629
657
  }
630
658
  }
631
- catch { /* non-critical */ }
659
+ catch {
660
+ /* non-critical */
661
+ }
632
662
  // NOTE: a LATS planning pass used to be attempted here via
633
663
  // `import('@monoes/hooks').buildLATSPlan` — that function never existed
634
664
  // in the package (the planning module was removed), so the import failed
@@ -639,7 +669,7 @@ export const hooksPreTask = {
639
669
  description,
640
670
  suggestedAgents: suggestion.agents.map((agent, i) => ({
641
671
  type: agent,
642
- confidence: suggestion.confidence - (0.05 * i),
672
+ confidence: suggestion.confidence - 0.05 * i,
643
673
  reason: i === 0
644
674
  ? `Primary agent for ${agent} tasks based on learned patterns`
645
675
  : `Alternative agent with ${agent} capabilities`,
@@ -649,7 +679,9 @@ export const hooksPreTask = {
649
679
  risks: complexity === 'high' ? ['Complex task may require multiple iterations'] : [],
650
680
  recommendations: [
651
681
  `Use ${suggestion.agents[0]} as primary agent`,
652
- suggestion.agents.length > 2 ? 'Consider using swarm coordination' : 'Single agent recommended',
682
+ suggestion.agents.length > 2
683
+ ? 'Consider using swarm coordination'
684
+ : 'Single agent recommended',
653
685
  ...erlHints,
654
686
  ],
655
687
  modelRouting,
@@ -668,9 +700,15 @@ export const hooksPostTask = {
668
700
  success: { type: 'boolean', description: 'Whether task was successful' },
669
701
  agent: { type: 'string', description: 'Agent that completed the task' },
670
702
  quality: { type: 'number', description: 'Quality score (0-1)' },
671
- task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
703
+ task: {
704
+ type: 'string',
705
+ description: 'Task description text (used for learning keyword extraction)',
706
+ },
672
707
  storeDecisions: { type: 'boolean', description: 'Also store routing decision in memory DB' },
673
- routeId: { type: 'string', description: 'Route ID from a prior hooks_route call — joins the recommendation to this outcome' },
708
+ routeId: {
709
+ type: 'string',
710
+ description: 'Route ID from a prior hooks_route call — joins the recommendation to this outcome',
711
+ },
674
712
  },
675
713
  required: ['taskId'],
676
714
  },
@@ -680,10 +718,10 @@ export const hooksPostTask = {
680
718
  // into causal-graph edge IDs persisted to the DB. Without a cap an attacker can
681
719
  // inflate every row that stores the raw ID.
682
720
  const MAX_POST_TASK_ID_LEN = 256;
683
- const rawPostTaskId = params.taskId;
684
- const taskId = typeof rawPostTaskId === 'string' && rawPostTaskId.length > MAX_POST_TASK_ID_LEN
685
- ? rawPostTaskId.slice(0, MAX_POST_TASK_ID_LEN)
686
- : rawPostTaskId;
721
+ const taskId = validateMcpString(params.taskId, 'taskId', MAX_POST_TASK_ID_LEN);
722
+ if (!taskId) {
723
+ return { error: 'taskId is required (non-empty string, no control chars, max 256 chars)' };
724
+ }
687
725
  // The success flag, when the caller asserts it (--success true), is taken as
688
726
  // ground truth. But callers usually do NOT pass it. Rather than treating every
689
727
  // unverified task as "unknown" (and thus excluding it from learning), we now
@@ -702,7 +740,9 @@ export const hooksPostTask = {
702
740
  const explicitSuccess = typeof params.success === 'boolean';
703
741
  let outcomeKnown = explicitSuccess;
704
742
  let success = params.success !== false;
705
- let successSource = explicitSuccess ? 'explicit' : 'unknown';
743
+ let successSource = explicitSuccess
744
+ ? 'explicit'
745
+ : 'unknown';
706
746
  if (!explicitSuccess) {
707
747
  const derived = await deriveRecentSuccess(getRouteOutcomesBaseDir());
708
748
  if (derived !== null) {
@@ -715,19 +755,17 @@ export const hooksPostTask = {
715
755
  // feedback record and used as a tag string in the JSON store. An uncapped
716
756
  // agent value inflates the on-disk store entry.
717
757
  const MAX_POST_TASK_AGENT_LEN = 256;
718
- const rawPostTaskAgent = params.agent;
719
- const agent = typeof rawPostTaskAgent === 'string' && rawPostTaskAgent.length > MAX_POST_TASK_AGENT_LEN
720
- ? rawPostTaskAgent.slice(0, MAX_POST_TASK_AGENT_LEN)
721
- : rawPostTaskAgent;
722
- const quality = params.quality || (success ? 0.85 : 0.3);
758
+ const agent = validateMcpString(params.agent, 'agent', MAX_POST_TASK_AGENT_LEN) ?? undefined;
759
+ const quality = typeof params.quality === 'number' && Number.isFinite(params.quality)
760
+ ? Math.max(0, Math.min(1, params.quality))
761
+ : success
762
+ ? 0.85
763
+ : 0.3;
723
764
  const startTime = Date.now();
724
765
  // Cap task description: passed to generateEmbedding via bridgeRecordFeedback
725
766
  // and persisted to route-outcomes.jsonl. 16 KB matches hooks_route cap.
726
767
  const MAX_POST_TASK_LEN = 16 * 1024;
727
- const rawPostTask = params.task;
728
- const cappedPostTask = typeof rawPostTask === 'string' && rawPostTask.length > MAX_POST_TASK_LEN
729
- ? rawPostTask.slice(0, MAX_POST_TASK_LEN)
730
- : rawPostTask;
768
+ const cappedPostTask = validateMcpString(params.task, 'task', MAX_POST_TASK_LEN) ?? undefined;
731
769
  // Phase 3: Wire recordFeedback through bridge → LearningSystem + ReasoningBank
732
770
  let feedbackResult = null;
733
771
  try {
@@ -735,9 +773,19 @@ export const hooksPostTask = {
735
773
  feedbackResult = await bridge.bridgeRecordFeedback({
736
774
  taskType: agent ?? 'task',
737
775
  action: cappedPostTask?.slice(0, 80) ?? taskId,
738
- outcome: success ? 'success' : (outcomeKnown ? 'failure' : 'partial'),
776
+ outcome: success ? 'success' : outcomeKnown ? 'failure' : 'partial',
739
777
  confidence: quality,
740
- metadata: { taskId, duration: params.duration || undefined, patterns: params.patterns || undefined },
778
+ metadata: {
779
+ taskId,
780
+ duration: typeof params.duration === 'number' && Number.isFinite(params.duration)
781
+ ? params.duration
782
+ : undefined,
783
+ patterns: Array.isArray(params.patterns)
784
+ ? params.patterns
785
+ .filter((p) => typeof p === 'string' && p.length > 0 && p.length <= 200)
786
+ .slice(0, 50)
787
+ : undefined,
788
+ },
741
789
  });
742
790
  }
743
791
  catch (e) {
@@ -767,7 +815,11 @@ export const hooksPostTask = {
767
815
  const taskText = cappedPostTask || '';
768
816
  const outcomeKeywords = extractKeywords(taskText);
769
817
  let outcomePersisted = false;
770
- if (outcomeKnown && taskText && agent && agent.length <= 100 && /^[a-zA-Z0-9_-]+$/.test(agent)) {
818
+ if (outcomeKnown &&
819
+ taskText &&
820
+ agent &&
821
+ agent.length <= 100 &&
822
+ /^[a-zA-Z0-9_-]+$/.test(agent)) {
771
823
  try {
772
824
  const outcomes = loadRoutingOutcomes();
773
825
  outcomes.push({
@@ -781,7 +833,9 @@ export const hooksPostTask = {
781
833
  saveRoutingOutcomes(outcomes);
782
834
  outcomePersisted = true;
783
835
  }
784
- catch { /* non-critical */ }
836
+ catch {
837
+ /* non-critical */
838
+ }
785
839
  }
786
840
  // Join this outcome back onto the original route recommendation. This is the
787
841
  // recommendation→actual→success link that routing-accuracy metrics and SONA
@@ -797,7 +851,10 @@ export const hooksPostTask = {
797
851
  quality: typeof params.quality === 'number' ? params.quality : undefined,
798
852
  };
799
853
  if (params.routeId) {
800
- await joinOutcome(getRouteOutcomesBaseDir(), params.routeId, outcome);
854
+ const routeId = validateMcpString(params.routeId, 'routeId', 256);
855
+ if (routeId) {
856
+ await joinOutcome(getRouteOutcomesBaseDir(), routeId, outcome);
857
+ }
801
858
  }
802
859
  else {
803
860
  await joinLatestUnresolved(getRouteOutcomesBaseDir(), outcome);
@@ -822,7 +879,9 @@ export const hooksPostTask = {
822
879
  });
823
880
  }
824
881
  }
825
- catch { /* non-critical */ }
882
+ catch {
883
+ /* non-critical */
884
+ }
826
885
  }
827
886
  // Optionally store in memory DB for cross-session vector retrieval
828
887
  if (params.storeDecisions && taskText && agent) {
@@ -832,12 +891,20 @@ export const hooksPostTask = {
832
891
  await storeFn({
833
892
  key: `routing-decision:${taskId}`,
834
893
  namespace: 'patterns',
835
- value: JSON.stringify({ task: taskText, agent, success, quality, keywords: outcomeKeywords }),
894
+ value: JSON.stringify({
895
+ task: taskText,
896
+ agent,
897
+ success,
898
+ quality,
899
+ keywords: outcomeKeywords,
900
+ }),
836
901
  tags: ['routing-decision'],
837
902
  });
838
903
  }
839
904
  }
840
- catch { /* non-critical */ }
905
+ catch {
906
+ /* non-critical */
907
+ }
841
908
  }
842
909
  const duration = Date.now() - startTime;
843
910
  // TextGrad: Store textual gradient critique for failed tasks
@@ -857,21 +924,28 @@ export const hooksPostTask = {
857
924
  });
858
925
  }
859
926
  }
860
- catch { /* non-critical */ }
927
+ catch {
928
+ /* non-critical */
929
+ }
861
930
  }
862
931
  // MAR: Structured multi-agent reflection on failure
863
932
  // Source: https://arxiv.org/html/2512.20845 (MAR — December 2025)
864
- const marReflection = !success ? {
865
- needed: true,
866
- suggestedAgents: [
867
- { role: 'diagnoser', description: 'Analyze root cause of task failure' },
868
- { role: 'critic-1', description: 'Critique from correctness angle (temperature 0.3)' },
869
- { role: 'critic-2', description: 'Critique from efficiency angle (temperature 0.8)' },
870
- { role: 'aggregator', description: 'Synthesize critiques into actionable reflection heuristic' },
871
- ],
872
- storeAs: 'heuristics',
873
- note: 'Spawn agents sequentially: Diagnoser → Critics in parallel → Aggregator',
874
- } : { needed: false };
933
+ const marReflection = !success
934
+ ? {
935
+ needed: true,
936
+ suggestedAgents: [
937
+ { role: 'diagnoser', description: 'Analyze root cause of task failure' },
938
+ { role: 'critic-1', description: 'Critique from correctness angle (temperature 0.3)' },
939
+ { role: 'critic-2', description: 'Critique from efficiency angle (temperature 0.8)' },
940
+ {
941
+ role: 'aggregator',
942
+ description: 'Synthesize critiques into actionable reflection heuristic',
943
+ },
944
+ ],
945
+ storeAs: 'heuristics',
946
+ note: 'Spawn agents sequentially: Diagnoser → Critics in parallel → Aggregator',
947
+ }
948
+ : { needed: false };
875
949
  return {
876
950
  taskId,
877
951
  success,
@@ -882,15 +956,17 @@ export const hooksPostTask = {
882
956
  patternsUpdated: feedbackResult?.success ? (success ? 2 : 1) : 0,
883
957
  newPatterns: success ? 1 : 0,
884
958
  trajectoryId: `traj-${Date.now()}`,
885
- controller: feedbackResult?.success ? 'lancedb' : 'none',
959
+ controller: feedbackResult?.success ? 'sqlite' : 'none',
886
960
  outcomePersisted,
887
961
  },
888
962
  quality,
889
- feedback: feedbackResult ? {
890
- recorded: feedbackResult.success,
891
- controller: feedbackResult.success ? 'lancedb' : 'unavailable',
892
- updates: feedbackResult.success ? 1 : 0,
893
- } : { recorded: false, controller: 'unavailable', updates: 0 },
963
+ feedback: feedbackResult
964
+ ? {
965
+ recorded: feedbackResult.success,
966
+ controller: feedbackResult.success ? 'sqlite' : 'unavailable',
967
+ updates: feedbackResult.success ? 1 : 0,
968
+ }
969
+ : { recorded: false, controller: 'unavailable', updates: 0 },
894
970
  marReflection,
895
971
  timestamp: new Date().toISOString(),
896
972
  };
@@ -913,10 +989,10 @@ export const hooksExplain = {
913
989
  // Cap task: forwarded to suggestAgentsForTask (O(n) keyword loop + extractKeywords),
914
990
  // .toLowerCase() (O(n)), and reflected verbatim in the response.
915
991
  const MAX_EXPLAIN_TASK_LEN = 16 * 1024;
916
- const rawExplainTask = params.task;
917
- const task = typeof rawExplainTask === 'string' && rawExplainTask.length > MAX_EXPLAIN_TASK_LEN
918
- ? rawExplainTask.slice(0, MAX_EXPLAIN_TASK_LEN)
919
- : rawExplainTask;
992
+ const task = validateMcpString(params.task, 'task', MAX_EXPLAIN_TASK_LEN);
993
+ if (!task) {
994
+ return { error: 'task is required (non-empty string, no control chars, max 16KB)' };
995
+ }
920
996
  const suggestion = suggestAgentsForTask(task);
921
997
  const taskLower = task.toLowerCase();
922
998
  // Determine matched patterns
@@ -939,7 +1015,7 @@ export const hooksExplain = {
939
1015
  const data = JSON.parse(readFileSync(outcomesPath, 'utf-8'));
940
1016
  const outcomes = data.outcomes || [];
941
1017
  if (outcomes.length > 0) {
942
- historicalSuccess = outcomes.filter(o => o.success).length / outcomes.length;
1018
+ historicalSuccess = outcomes.filter((o) => o.success).length / outcomes.length;
943
1019
  historicalNote = `Calculated from ${outcomes.length} recorded outcomes`;
944
1020
  }
945
1021
  }
@@ -954,14 +1030,44 @@ export const hooksExplain = {
954
1030
  explanation: `The routing decision was made based on keyword analysis of the task description. ` +
955
1031
  `The task contains keywords that match the "${suggestion.agents[0]}" specialization with ${(suggestion.confidence * 100).toFixed(0)}% confidence.`,
956
1032
  factors: [
957
- { factor: 'Keyword Match', weight: 0.4, value: suggestion.confidence, impact: 'Primary routing signal' },
958
- { factor: 'Historical Success', weight: 0.3, value: historicalSuccess, impact: historicalNote },
959
- { factor: 'Agent Availability', weight: 0.2, value: null, impact: 'Agent availability tracking not implemented' },
960
- { factor: 'Task Complexity', weight: 0.1, value: task.length > 100 ? 0.8 : 0.3, impact: 'Complexity assessment' },
961
- ],
962
- patterns: matchedPatterns.length > 0 ? matchedPatterns : [
963
- { pattern: 'general-task', matchScore: 0.7, examples: ['Default pattern for unclassified tasks'] }
1033
+ {
1034
+ factor: 'Keyword Match',
1035
+ weight: 0.4,
1036
+ heuristic: true,
1037
+ value: suggestion.confidence,
1038
+ impact: 'Primary routing signal',
1039
+ },
1040
+ {
1041
+ factor: 'Historical Success',
1042
+ weight: 0.3,
1043
+ heuristic: true,
1044
+ value: historicalSuccess,
1045
+ impact: historicalNote,
1046
+ },
1047
+ {
1048
+ factor: 'Agent Availability',
1049
+ weight: 0.2,
1050
+ heuristic: true,
1051
+ value: null,
1052
+ impact: 'Agent availability tracking not implemented',
1053
+ },
1054
+ {
1055
+ factor: 'Task Complexity',
1056
+ weight: 0.1,
1057
+ heuristic: true,
1058
+ value: task.length > 100 ? 0.8 : 0.3,
1059
+ impact: 'Complexity assessment',
1060
+ },
964
1061
  ],
1062
+ patterns: matchedPatterns.length > 0
1063
+ ? matchedPatterns
1064
+ : [
1065
+ {
1066
+ pattern: 'general-task',
1067
+ matchScore: 0.7,
1068
+ examples: ['Default pattern for unclassified tasks'],
1069
+ },
1070
+ ],
965
1071
  decision: {
966
1072
  agent: suggestion.agents[0],
967
1073
  confidence: suggestion.confidence,
@@ -990,12 +1096,16 @@ export const hooksPretrain = {
990
1096
  },
991
1097
  },
992
1098
  handler: async (params) => {
993
- const repoPath = resolve(params.path || '.');
1099
+ const repoPath = resolve(validateMcpString(params.path, 'path', 4 * 1024) ?? '.');
994
1100
  const projectRoot = getProjectCwd();
995
1101
  if (repoPath !== projectRoot && !repoPath.startsWith(projectRoot + sep)) {
996
1102
  return { error: 'Invalid path: must be within the project directory.' };
997
1103
  }
998
- const depth = params.depth || 'medium';
1104
+ const depth = validateMcpString(params.depth, 'depth', 16) ?? 'medium';
1105
+ const allowedDepths = new Set(['shallow', 'medium', 'deep']);
1106
+ if (!allowedDepths.has(depth)) {
1107
+ return { error: 'Invalid depth: must be shallow, medium, or deep' };
1108
+ }
999
1109
  const startTime = performance.now();
1000
1110
  // Real file scanning — count files by extension, extract patterns
1001
1111
  const { readdirSync, statSync } = await import('node:fs');
@@ -1017,7 +1127,9 @@ export const hooksPretrain = {
1017
1127
  scan(full, currentDepth + 1);
1018
1128
  }
1019
1129
  else if (entry.isFile()) {
1020
- const ext = entry.name.includes('.') ? entry.name.slice(entry.name.lastIndexOf('.')) : '';
1130
+ const ext = entry.name.includes('.')
1131
+ ? entry.name.slice(entry.name.lastIndexOf('.'))
1132
+ : '';
1021
1133
  if (ext)
1022
1134
  extCounts[ext] = (extCounts[ext] || 0) + 1;
1023
1135
  filesAnalyzed++;
@@ -1036,7 +1148,9 @@ export const hooksPretrain = {
1036
1148
  // Extract import patterns (first 50 files max for performance)
1037
1149
  if (filesAnalyzed <= 50) {
1038
1150
  for (const line of lines.slice(0, 30)) {
1039
- if (line.startsWith('import ') || line.startsWith('from ') || line.startsWith('const ') && line.includes('require(')) {
1151
+ if (line.startsWith('import ') ||
1152
+ line.startsWith('from ') ||
1153
+ (line.startsWith('const ') && line.includes('require('))) {
1040
1154
  const trimmed = line.trim();
1041
1155
  if (trimmed.length < 120 && !patterns.includes(trimmed))
1042
1156
  patterns.push(trimmed);
@@ -1046,12 +1160,16 @@ export const hooksPretrain = {
1046
1160
  }
1047
1161
  }
1048
1162
  }
1049
- catch { /* skip unreadable */ }
1163
+ catch {
1164
+ /* skip unreadable */
1165
+ }
1050
1166
  }
1051
1167
  }
1052
1168
  }
1053
1169
  }
1054
- catch { /* skip inaccessible dirs */ }
1170
+ catch {
1171
+ /* skip inaccessible dirs */
1172
+ }
1055
1173
  };
1056
1174
  scan(repoPath, 0);
1057
1175
  const elapsed = Math.round(performance.now() - startTime);
@@ -1061,7 +1179,14 @@ export const hooksPretrain = {
1061
1179
  const bridge = await import('../memory/memory-bridge.js');
1062
1180
  await bridge.bridgeStoreEntry({
1063
1181
  key: `pretrain-${Date.now()}`,
1064
- value: JSON.stringify({ filesAnalyzed, totalLines, topExtensions: Object.entries(extCounts).sort((a, b) => b[1] - a[1]).slice(0, 10), importPatterns: patterns.slice(0, 20) }),
1182
+ value: JSON.stringify({
1183
+ filesAnalyzed,
1184
+ totalLines,
1185
+ topExtensions: Object.entries(extCounts)
1186
+ .sort((a, b) => b[1] - a[1])
1187
+ .slice(0, 10),
1188
+ importPatterns: patterns.slice(0, 20),
1189
+ }),
1065
1190
  namespace: 'pretrain',
1066
1191
  tags: ['pretrain', depth],
1067
1192
  });
@@ -1078,13 +1203,20 @@ export const hooksPretrain = {
1078
1203
  if (patterns.length > 0) {
1079
1204
  try {
1080
1205
  const intel = await import('../memory/intelligence.js');
1081
- await intel.initializeIntelligence({ confidenceLearningRate: 0.002, maxTrajectorySize: patterns.length });
1206
+ await intel.initializeIntelligence({
1207
+ confidenceLearningRate: 0.002,
1208
+ maxTrajectorySize: patterns.length,
1209
+ });
1082
1210
  // Record each extracted pattern as an action step
1083
1211
  for (const pat of patterns.slice(0, 50)) {
1084
- await intel.recordStep({ type: 'action', content: pat, metadata: { source: 'pretrain', depth } });
1212
+ await intel.recordStep({
1213
+ type: 'action',
1214
+ content: pat,
1215
+ metadata: { source: 'pretrain', depth },
1216
+ });
1085
1217
  }
1086
1218
  // Record the entire scan as a completed trajectory
1087
- const steps = patterns.slice(0, 50).map(p => ({ type: 'action', content: p }));
1219
+ const steps = patterns.slice(0, 50).map((p) => ({ type: 'action', content: p }));
1088
1220
  await intel.recordTrajectory(steps, 'success');
1089
1221
  intel.flushPatterns();
1090
1222
  neuralPatternsLearned = steps.length;
@@ -1107,7 +1239,10 @@ export const hooksPretrain = {
1107
1239
  patternsExtracted: patterns.length,
1108
1240
  patternsStored,
1109
1241
  neuralPatternsLearned,
1110
- fileTypes: Object.entries(extCounts).sort((a, b) => b[1] - a[1]).slice(0, 15).map(([ext, count]) => ({ ext, count })),
1242
+ fileTypes: Object.entries(extCounts)
1243
+ .sort((a, b) => b[1] - a[1])
1244
+ .slice(0, 15)
1245
+ .map(([ext, count]) => ({ ext, count })),
1111
1246
  },
1112
1247
  };
1113
1248
  },
@@ -1120,38 +1255,68 @@ export const hooksBuildAgents = {
1120
1255
  type: 'object',
1121
1256
  properties: {
1122
1257
  outputDir: { type: 'string', description: 'Output directory for configs' },
1123
- focus: { type: 'string', description: 'Focus area (v1-implementation, security, performance, all)' },
1258
+ focus: {
1259
+ type: 'string',
1260
+ description: 'Focus area (v1-implementation, security, performance, all)',
1261
+ },
1124
1262
  format: { type: 'string', description: 'Config format (yaml, json)' },
1125
1263
  persist: { type: 'boolean', description: 'Write configs to disk' },
1126
1264
  },
1127
1265
  },
1128
1266
  handler: async (params) => {
1129
- const rawOutputDir = resolve(params.outputDir || './agents');
1267
+ const rawOutputDir = resolve(validateMcpString(params.outputDir, 'outputDir', 4 * 1024) ?? './agents');
1130
1268
  const outputDir = rawOutputDir;
1131
1269
  if (!outputDir.startsWith(getProjectCwd() + sep) && outputDir !== getProjectCwd()) {
1132
1270
  return { error: 'Invalid outputDir: must be within the project directory.' };
1133
1271
  }
1134
- const focus = params.focus || 'all';
1272
+ const focus = validateMcpString(params.focus, 'focus', 64) ?? 'all';
1135
1273
  // Strict allowlist on `format` — without this, `format = "yaml/../../../etc/cron.d/x"`
1136
1274
  // collapses through `join` and lets writes escape the validated outputDir.
1137
1275
  const ALLOWED_FORMATS = new Set(['yaml', 'json']);
1138
- const formatRaw = params.format || 'yaml';
1139
- if (!ALLOWED_FORMATS.has(formatRaw)) {
1276
+ const format = validateMcpString(params.format, 'format', 16) ?? 'yaml';
1277
+ if (!ALLOWED_FORMATS.has(format)) {
1140
1278
  return { error: 'Invalid format: must be yaml or json' };
1141
1279
  }
1142
- const format = formatRaw;
1143
1280
  const persist = params.persist !== false; // Default to true
1144
1281
  const agents = [
1145
- { type: 'coder', configFile: join(outputDir, `coder.${format}`), capabilities: ['code-generation', 'refactoring', 'debugging'], optimizations: ['token-reduction', 'context-caching'] },
1146
- { type: 'architect', configFile: join(outputDir, `architect.${format}`), capabilities: ['system-design', 'api-design', 'documentation'], optimizations: ['context-caching', 'memory-persistence'] },
1147
- { type: 'tester', configFile: join(outputDir, `tester.${format}`), capabilities: ['unit-testing', 'integration-testing', 'coverage'], optimizations: ['parallel-execution'] },
1148
- { type: 'security-architect', configFile: join(outputDir, `security-architect.${format}`), capabilities: ['threat-modeling', 'vulnerability-analysis', 'security-review'], optimizations: ['pattern-matching'] },
1149
- { type: 'reviewer', configFile: join(outputDir, `reviewer.${format}`), capabilities: ['code-review', 'quality-analysis', 'best-practices'], optimizations: ['incremental-analysis'] },
1282
+ {
1283
+ type: 'coder',
1284
+ configFile: join(outputDir, `coder.${format}`),
1285
+ capabilities: ['code-generation', 'refactoring', 'debugging'],
1286
+ optimizations: ['token-reduction', 'context-caching'],
1287
+ },
1288
+ {
1289
+ type: 'architect',
1290
+ configFile: join(outputDir, `architect.${format}`),
1291
+ capabilities: ['system-design', 'api-design', 'documentation'],
1292
+ optimizations: ['context-caching', 'memory-persistence'],
1293
+ },
1294
+ {
1295
+ type: 'tester',
1296
+ configFile: join(outputDir, `tester.${format}`),
1297
+ capabilities: ['unit-testing', 'integration-testing', 'coverage'],
1298
+ optimizations: ['parallel-execution'],
1299
+ },
1300
+ {
1301
+ type: 'security-architect',
1302
+ configFile: join(outputDir, `security-architect.${format}`),
1303
+ capabilities: ['threat-modeling', 'vulnerability-analysis', 'security-review'],
1304
+ optimizations: ['pattern-matching'],
1305
+ },
1306
+ {
1307
+ type: 'reviewer',
1308
+ configFile: join(outputDir, `reviewer.${format}`),
1309
+ capabilities: ['code-review', 'quality-analysis', 'best-practices'],
1310
+ optimizations: ['incremental-analysis'],
1311
+ },
1150
1312
  ];
1151
- const filteredAgents = focus === 'all' ? agents :
1152
- focus === 'security' ? agents.filter(a => a.type.includes('security') || a.type === 'reviewer') :
1153
- focus === 'performance' ? agents.filter(a => ['coder', 'tester'].includes(a.type)) :
1154
- agents;
1313
+ const filteredAgents = focus === 'all'
1314
+ ? agents
1315
+ : focus === 'security'
1316
+ ? agents.filter((a) => a.type.includes('security') || a.type === 'reviewer')
1317
+ : focus === 'performance'
1318
+ ? agents.filter((a) => ['coder', 'tester'].includes(a.type))
1319
+ : agents;
1155
1320
  // Persist configs to disk if requested
1156
1321
  if (persist) {
1157
1322
  // Ensure output directory exists
@@ -1169,7 +1334,7 @@ export const hooksBuildAgents = {
1169
1334
  };
1170
1335
  const content = format === 'json'
1171
1336
  ? JSON.stringify(config, null, 2)
1172
- : `# ${agent.type} agent configuration\ntype: ${agent.type}\nversion: "3.0.0"\ncapabilities:\n${agent.capabilities.map(c => ` - ${c}`).join('\n')}\noptimizations:\n${agent.optimizations.map(o => ` - ${o}`).join('\n')}\ncreatedAt: "${config.createdAt}"\n`;
1337
+ : `# ${agent.type} agent configuration\ntype: ${agent.type}\nversion: "3.0.0"\ncapabilities:\n${agent.capabilities.map((c) => ` - ${c}`).join('\n')}\noptimizations:\n${agent.optimizations.map((o) => ` - ${o}`).join('\n')}\ncreatedAt: "${config.createdAt}"\n`;
1173
1338
  const _cftmp = agent.configFile + '.tmp';
1174
1339
  writeFileSync(_cftmp, content, 'utf-8');
1175
1340
  renameSync(_cftmp, agent.configFile);
@@ -1202,9 +1367,14 @@ export const hooksTransfer = {
1202
1367
  required: ['sourcePath'],
1203
1368
  },
1204
1369
  handler: async (params) => {
1205
- const sourcePath = params.sourcePath;
1206
- const minConfidence = params.minConfidence || 0.7;
1207
- const filter = params.filter;
1370
+ const sourcePath = validateMcpString(params.sourcePath, 'sourcePath', 4 * 1024);
1371
+ if (!sourcePath) {
1372
+ return { error: 'sourcePath is required (non-empty string, no control chars, max 4KB)' };
1373
+ }
1374
+ const minConfidence = typeof params.minConfidence === 'number' && Number.isFinite(params.minConfidence)
1375
+ ? Math.max(0, Math.min(1, params.minConfidence))
1376
+ : 0.7;
1377
+ const filter = validateMcpString(params.filter, 'filter', 64) ?? undefined;
1208
1378
  // Validate sourcePath is an existing directory before reading from it
1209
1379
  const resolvedSource = resolve(sourcePath);
1210
1380
  const { statSync } = await import('fs');
@@ -1227,7 +1397,8 @@ export const hooksTransfer = {
1227
1397
  let sourceStore = { entries: {}, version: '3.0.0' };
1228
1398
  const MAX_SOURCE_STORE_BYTES = 50 * 1024 * 1024; // 50 MB — matches other store readers
1229
1399
  try {
1230
- if (existsSync(sourceMemoryPath) && statSync(sourceMemoryPath).size <= MAX_SOURCE_STORE_BYTES) {
1400
+ if (existsSync(sourceMemoryPath) &&
1401
+ statSync(sourceMemoryPath).size <= MAX_SOURCE_STORE_BYTES) {
1231
1402
  sourceStore = JSON.parse(readFileSync(sourceMemoryPath, 'utf-8'));
1232
1403
  }
1233
1404
  }
@@ -1239,13 +1410,13 @@ export const hooksTransfer = {
1239
1410
  const sourceEntries = Object.values(sourceStore.entries);
1240
1411
  // Count patterns by type from source
1241
1412
  const byType = {
1242
- 'file-patterns': sourceEntries.filter(e => e.key.includes('file') || e.metadata?.type === 'file-pattern').length,
1243
- 'task-routing': sourceEntries.filter(e => e.key.includes('routing') || e.metadata?.type === 'routing').length,
1244
- 'command-risk': sourceEntries.filter(e => e.key.includes('command') || e.metadata?.type === 'command-risk').length,
1245
- 'agent-success': sourceEntries.filter(e => e.key.includes('agent') || e.metadata?.type === 'agent-success').length,
1413
+ 'file-patterns': sourceEntries.filter((e) => e.key.includes('file') || e.metadata?.type === 'file-pattern').length,
1414
+ 'task-routing': sourceEntries.filter((e) => e.key.includes('routing') || e.metadata?.type === 'routing').length,
1415
+ 'command-risk': sourceEntries.filter((e) => e.key.includes('command') || e.metadata?.type === 'command-risk').length,
1416
+ 'agent-success': sourceEntries.filter((e) => e.key.includes('agent') || e.metadata?.type === 'agent-success').length,
1246
1417
  };
1247
1418
  // If source has no patterns, report honestly instead of substituting demo data
1248
- if (Object.values(byType).every(v => v === 0)) {
1419
+ if (Object.values(byType).every((v) => v === 0)) {
1249
1420
  return {
1250
1421
  success: false,
1251
1422
  message: 'No patterns found in source project',
@@ -1254,7 +1425,7 @@ export const hooksTransfer = {
1254
1425
  };
1255
1426
  }
1256
1427
  if (filter) {
1257
- Object.keys(byType).forEach(key => {
1428
+ Object.keys(byType).forEach((key) => {
1258
1429
  if (!key.includes(filter))
1259
1430
  delete byType[key];
1260
1431
  });
@@ -1283,8 +1454,8 @@ export const hooksSessionStart = {
1283
1454
  },
1284
1455
  },
1285
1456
  handler: async (params) => {
1286
- const sessionId = params.sessionId || `session-${Date.now()}`;
1287
- const restoreLatest = params.restoreLatest;
1457
+ const sessionId = validateMcpString(params.sessionId, 'sessionId', 256) ?? `session-${Date.now()}`;
1458
+ const restoreLatest = params.restoreLatest === true;
1288
1459
  // Phase 5: Wire ReflexionMemory session start via bridge
1289
1460
  let sessionMemory = null;
1290
1461
  try {
@@ -1295,7 +1466,7 @@ export const hooksSessionStart = {
1295
1466
  });
1296
1467
  if (result) {
1297
1468
  sessionMemory = {
1298
- controller: result.success ? 'lancedb' : 'none',
1469
+ controller: result.success ? 'sqlite' : 'none',
1299
1470
  restoredPatterns: 0,
1300
1471
  };
1301
1472
  }
@@ -1315,11 +1486,13 @@ export const hooksSessionStart = {
1315
1486
  memoryPersistence: true,
1316
1487
  },
1317
1488
  sessionMemory: sessionMemory || { controller: 'none', restoredPatterns: 0 },
1318
- previousSession: restoreLatest ? {
1319
- id: `session-${Date.now() - 86400000}`,
1320
- tasksRestored: sessionMemory?.restoredPatterns || 0,
1321
- memoryRestored: sessionMemory?.restoredPatterns || 0,
1322
- } : null,
1489
+ previousSession: restoreLatest && sessionMemory && sessionMemory.restoredPatterns > 0
1490
+ ? {
1491
+ id: sessionId,
1492
+ tasksRestored: sessionMemory.restoredPatterns,
1493
+ memoryRestored: sessionMemory.restoredPatterns,
1494
+ }
1495
+ : null,
1323
1496
  };
1324
1497
  },
1325
1498
  };
@@ -1338,12 +1511,18 @@ export const hooksSessionRestore = {
1338
1511
  type: 'object',
1339
1512
  properties: {
1340
1513
  sessionId: { type: 'string', description: 'Session ID to restore, or "latest"' },
1341
- restoreAgents: { type: 'boolean', description: 'Include a count of currently-live agents (default true)' },
1342
- restoreTasks: { type: 'boolean', description: 'Include a count of currently-live tasks (default true)' },
1514
+ restoreAgents: {
1515
+ type: 'boolean',
1516
+ description: 'Include a count of currently-live agents (default true)',
1517
+ },
1518
+ restoreTasks: {
1519
+ type: 'boolean',
1520
+ description: 'Include a count of currently-live tasks (default true)',
1521
+ },
1343
1522
  },
1344
1523
  },
1345
1524
  handler: async (params) => {
1346
- const originalSessionId = params.sessionId || 'latest';
1525
+ const originalSessionId = validateMcpString(params.sessionId, 'sessionId', 256) ?? 'latest';
1347
1526
  const newSessionId = `session-${Date.now()}`;
1348
1527
  const warnings = [];
1349
1528
  let agentsRestored = 0;
@@ -1424,9 +1603,9 @@ export const hooksSessionEnd = {
1424
1603
  // Read actual counts from stores
1425
1604
  const store = loadMemoryStore();
1426
1605
  const allEntries = Object.values(store.entries);
1427
- const taskCount = allEntries.filter(e => e.key.includes('task')).length;
1428
- const agentCount = allEntries.filter(e => e.key.includes('agent')).length;
1429
- const patternCount = allEntries.filter(e => e.key.includes('pattern')).length;
1606
+ const taskCount = allEntries.filter((e) => e.key.includes('task')).length;
1607
+ const agentCount = allEntries.filter((e) => e.key.includes('agent')).length;
1608
+ const patternCount = allEntries.filter((e) => e.key.includes('pattern')).length;
1430
1609
  const trajectoryCount = activeTrajectories.size;
1431
1610
  // Check for pending-insights.jsonl
1432
1611
  let insightCount = 0;
@@ -1451,7 +1630,7 @@ export const hooksSessionEnd = {
1451
1630
  });
1452
1631
  if (result) {
1453
1632
  sessionPersistence = {
1454
- controller: result.success ? 'lancedb' : 'none',
1633
+ controller: result.success ? 'sqlite' : 'none',
1455
1634
  persisted: result.success,
1456
1635
  };
1457
1636
  }
@@ -1521,7 +1700,7 @@ export const hooksIntelligence = {
1521
1700
  },
1522
1701
  },
1523
1702
  handler: async (params) => {
1524
- const mode = params.mode || 'balanced';
1703
+ const mode = validateMcpString(params.mode, 'mode', 32) ?? 'balanced';
1525
1704
  const enableSona = params.enableSona !== false;
1526
1705
  const enableMoe = params.enableMoe !== false;
1527
1706
  const enableHnsw = params.enableHnsw !== false;
@@ -1551,11 +1730,11 @@ export const hooksIntelligence = {
1551
1730
  },
1552
1731
  hnsw: {
1553
1732
  enabled: enableHnsw,
1554
- status: enableHnsw ? 'active' : 'disabled',
1555
- implemented: true,
1733
+ status: enableHnsw ? 'fallback-only' : 'disabled',
1734
+ implemented: false,
1556
1735
  indexSize: realStats.memory.indexSize,
1557
1736
  memorySizeBytes: realStats.memory.memorySizeBytes,
1558
- note: 'Pure-JS HNSW vector indexing (O(log n) vs O(n))',
1737
+ note: 'Pure-JS HNSW is a fallback path only, not on the default search path',
1559
1738
  },
1560
1739
  flashAttention: {
1561
1740
  enabled: false,
@@ -1580,7 +1759,7 @@ export const hooksIntelligence = {
1580
1759
  model: 'Xenova/all-MiniLM-L6-v2',
1581
1760
  dimension: 384,
1582
1761
  implemented: true,
1583
- note: 'Real ONNX embeddings via Xenova/all-MiniLM-L6-v2',
1762
+ note: 'ONNX embeddings via Xenova/all-MiniLM-L6-v2 when available; deterministic hash fallback otherwise',
1584
1763
  },
1585
1764
  },
1586
1765
  realMetrics: {
@@ -1591,14 +1770,23 @@ export const hooksIntelligence = {
1591
1770
  },
1592
1771
  implementationStatus: {
1593
1772
  working: [
1594
- 'memory-store', 'embeddings', 'trajectory-recording', 'claims', 'swarm-coordination',
1595
- 'hnsw-index', 'pattern-storage', 'keyword-routing'
1773
+ 'memory-store',
1774
+ 'embeddings',
1775
+ 'trajectory-recording',
1776
+ 'claims',
1777
+ 'swarm-coordination',
1778
+ 'pattern-storage',
1779
+ 'keyword-routing',
1596
1780
  ],
1597
- partial: [],
1781
+ partial: ['hnsw-index'],
1598
1782
  notImplemented: [],
1599
1783
  removed: [
1600
- 'moe-routing', 'flash-attention', 'lora-adapter',
1601
- 'native-sona-engine', 'native-router', 'native-attention',
1784
+ 'moe-routing',
1785
+ 'flash-attention',
1786
+ 'lora-adapter',
1787
+ 'native-sona-engine',
1788
+ 'native-router',
1789
+ 'native-attention',
1602
1790
  ],
1603
1791
  },
1604
1792
  version: '3.0.0-alpha.102',