specmem-hardwicksoftware 3.7.35 → 3.7.36

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 (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +11 -15
  3. package/bin/specmem-console.cjs +839 -51
  4. package/claude-hooks/agent-chooser-hook.js +6 -6
  5. package/claude-hooks/agent-loading-hook.cjs +16 -16
  6. package/claude-hooks/agent-loading-hook.js +18 -18
  7. package/claude-hooks/agent-type-matcher.js +1 -1
  8. package/claude-hooks/background-completion-silencer.js +1 -1
  9. package/claude-hooks/file-claim-enforcer.cjs +37 -36
  10. package/claude-hooks/output-cleaner.cjs +1 -1
  11. package/claude-hooks/settings.json +27 -3
  12. package/claude-hooks/specmem-search-enforcer.cjs +2 -11
  13. package/claude-hooks/specmem-team-member-inject.js +1 -1
  14. package/claude-hooks/specmem-unified-hook.py +1 -1
  15. package/claude-hooks/subagent-loading-hook.cjs +1 -1
  16. package/claude-hooks/task-progress-hook.cjs +7 -7
  17. package/claude-hooks/task-progress-hook.js +3 -3
  18. package/claude-hooks/team-comms-enforcer.cjs +49 -47
  19. package/dist/claude-sessions/sessionParser.js +5 -0
  20. package/dist/codebase/codebaseIndexer.js +48 -17
  21. package/dist/codebase/exclusions.js +3 -4
  22. package/dist/codebase/index.js +4 -0
  23. package/dist/codebase/pdfExtractor.js +298 -0
  24. package/dist/dashboard/api/taskTeamMembers.js +2 -2
  25. package/dist/db/bigBrainMigrations.js +29 -0
  26. package/dist/hooks/hookManager.js +4 -4
  27. package/dist/hooks/teamFramingCli.js +1 -1
  28. package/dist/hooks/teamMemberPrepromptHook.js +5 -5
  29. package/dist/init/claudeConfigInjector.js +2 -2
  30. package/dist/mcp/compactionProxy.js +834 -186
  31. package/dist/mcp/compactionProxyDaemon.js +112 -37
  32. package/dist/mcp/contextVault.js +439 -0
  33. package/dist/mcp/embeddingServerManager.js +61 -1
  34. package/dist/mcp/mcpProtocolHandler.js +6 -1
  35. package/dist/mcp/miniCOTServerManager.js +82 -8
  36. package/dist/mcp/specMemServer.js +45 -10
  37. package/dist/mcp/toolRegistry.js +6 -0
  38. package/dist/startup/startupIndexing.js +14 -0
  39. package/dist/team-members/taskOrchestrator.js +3 -3
  40. package/dist/team-members/taskTeamMemberLogger.js +2 -2
  41. package/dist/tools/goofy/deployTeamMember.js +3 -3
  42. package/dist/tools/goofy/digInTheVault.js +81 -0
  43. package/dist/tools/goofy/stashTheGoods.js +56 -0
  44. package/dist/tools/teamMemberDeployer.js +2 -2
  45. package/dist/watcher/changeHandler.js +65 -8
  46. package/dist/watcher/changeQueue.js +20 -1
  47. package/embedding-sandbox/mini-cot-service.py +11 -13
  48. package/embedding-sandbox/pdf-text-extract.py +208 -0
  49. package/package.json +1 -1
  50. package/scripts/deploy-hooks.cjs +2 -2
  51. package/scripts/global-postinstall.cjs +2 -2
  52. package/scripts/specmem-init.cjs +130 -36
  53. package/specmem/model-config.json +6 -6
  54. package/specmem/supervisord.conf +1 -1
  55. package/svg-sections/readme-token-compaction.svg +246 -0
@@ -187,6 +187,8 @@ export class EmbeddingServerManager extends EventEmitter {
187
187
  this.isRunning = true;
188
188
  return true;
189
189
  }
190
+ // Refresh socket path before start to pick up any externally-created sockets
191
+ this.refreshSocketPath();
190
192
  if (this.isRunning) {
191
193
  logger.debug('[EmbeddingServerManager] Server already running');
192
194
  return true;
@@ -1119,6 +1121,8 @@ export class EmbeddingServerManager extends EventEmitter {
1119
1121
  this.isShuttingDown = true;
1120
1122
  this.isStarting = false; // Reset starting flag on stop
1121
1123
  this.startupGraceUntil = 0; // Clear any active grace period
1124
+ // Refresh socket path to find actual socket before stopping
1125
+ this.refreshSocketPath();
1122
1126
  logger.info('[EmbeddingServerManager] Stopping embedding server...');
1123
1127
  // Stop health monitoring
1124
1128
  this.stopHealthMonitoring();
@@ -1186,6 +1190,8 @@ export class EmbeddingServerManager extends EventEmitter {
1186
1190
  */
1187
1191
  async healthCheck() {
1188
1192
  const startTime = Date.now();
1193
+ // Refresh socket path to find socket that may have appeared after construction
1194
+ this.refreshSocketPath();
1189
1195
  // Quick check: socket must exist
1190
1196
  if (!existsSync(this.socketPath)) {
1191
1197
  return {
@@ -1300,11 +1306,54 @@ export class EmbeddingServerManager extends EventEmitter {
1300
1306
  /**
1301
1307
  * Get current server status
1302
1308
  */
1309
+ /**
1310
+ * Refresh socketPath by re-scanning known locations.
1311
+ * Ensures we pick up a socket that appeared after construction.
1312
+ */
1313
+ refreshSocketPath() {
1314
+ try {
1315
+ const freshPath = getEmbeddingSocketPath();
1316
+ if (freshPath && freshPath !== this.socketPath) {
1317
+ logger.debug({ old: this.socketPath, new: freshPath }, '[EmbeddingServerManager] Socket path updated');
1318
+ this.socketPath = freshPath;
1319
+ this.pidFilePath = join(dirname(freshPath), 'embedding.pid');
1320
+ this.stoppedFlagPath = join(dirname(freshPath), 'embedding.stopped');
1321
+ }
1322
+ }
1323
+ catch (err) {
1324
+ logger.debug({ error: err.message }, '[EmbeddingServerManager] refreshSocketPath failed');
1325
+ }
1326
+ }
1327
+ /**
1328
+ * Check if the PID from the PID file is alive (signal 0 check).
1329
+ */
1330
+ isPidAlive(pid) {
1331
+ if (!pid || pid <= 0)
1332
+ return false;
1333
+ try {
1334
+ process.kill(pid, 0);
1335
+ return true;
1336
+ }
1337
+ catch {
1338
+ return false;
1339
+ }
1340
+ }
1303
1341
  getStatus() {
1342
+ // Refresh socket path to pick up sockets that appeared after construction
1343
+ this.refreshSocketPath();
1304
1344
  const pid = this.readPidFile();
1305
1345
  const socketExists = existsSync(this.socketPath);
1346
+ // Determine running state: use in-memory flag OR check PID file + process alive
1347
+ // This handles the case where server is running but manager was freshly created
1348
+ const running = this.isRunning || (pid != null && this.isPidAlive(pid) && socketExists);
1349
+ // Sync in-memory flag if we detected a running server via PID
1350
+ if (running && !this.isRunning && pid != null) {
1351
+ this.isRunning = true;
1352
+ this.startTime = this.startTime || Date.now();
1353
+ logger.info({ pid }, '[EmbeddingServerManager] Detected running server from PID file, syncing state');
1354
+ }
1306
1355
  return {
1307
- running: this.isRunning,
1356
+ running,
1308
1357
  pid,
1309
1358
  socketPath: this.socketPath,
1310
1359
  socketExists,
@@ -2009,6 +2058,9 @@ export class EmbeddingServerManager extends EventEmitter {
2009
2058
  // Phase 1: Wait for socket file to appear (use 50% of timeout for file appearance)
2010
2059
  const fileWaitDeadline = startTime + (this.config.startupTimeoutMs * 0.5);
2011
2060
  while (Date.now() < fileWaitDeadline) {
2061
+ // Refresh socket path each iteration — the socket may appear at a path
2062
+ // different from what was computed at construction time
2063
+ this.refreshSocketPath();
2012
2064
  if (existsSync(this.socketPath)) {
2013
2065
  logger.debug({ elapsed: Date.now() - startTime }, '[EmbeddingServerManager] Socket file appeared, starting health check polling');
2014
2066
  break;
@@ -2672,6 +2724,14 @@ export function getEmbeddingServerManager(config) {
2672
2724
  }
2673
2725
  // Create new manager for this project
2674
2726
  const manager = new EmbeddingServerManager(config);
2727
+ // Recover state from PID file if server is already running externally
2728
+ // This handles the case where manager is recreated but server is still alive
2729
+ const existingPid = manager.readPidFile();
2730
+ if (existingPid && manager.isPidAlive(existingPid) && existsSync(manager.socketPath)) {
2731
+ manager.isRunning = true;
2732
+ manager.startTime = Date.now();
2733
+ logger.info({ pid: existingPid, socketPath: manager.socketPath }, '[EmbeddingServerManager] Recovered running state from existing PID file');
2734
+ }
2675
2735
  embeddingManagersByProject.set(projectPath, manager);
2676
2736
  logger.info({ projectPath, totalManagers: embeddingManagersByProject.size }, '[EmbeddingServerManager] Created new per-project manager');
2677
2737
  return manager;
@@ -388,8 +388,13 @@ export class MCPProtocolHandler {
388
388
  // validate inputs if we have a schema
389
389
  const schema = TOOL_SCHEMAS[toolName];
390
390
  let validatedArgs = args;
391
+ // Strip proxy artifacts before validation — compaction proxy sometimes injects _stripped
392
+ if (validatedArgs && typeof validatedArgs === 'object' && '_stripped' in validatedArgs) {
393
+ const { _stripped, ...clean } = validatedArgs;
394
+ validatedArgs = clean;
395
+ }
391
396
  if (schema) {
392
- const result = schema.safeParse(args);
397
+ const result = schema.safeParse(validatedArgs);
393
398
  if (!result.success) {
394
399
  throw new Error(`validation failed fr: ${result.error.message}`);
395
400
  }
@@ -28,7 +28,7 @@
28
28
  * @author hardwicksoftwareservices
29
29
  */
30
30
  import { spawn, execSync } from 'child_process';
31
- import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
31
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs';
32
32
  import { join, dirname } from 'path';
33
33
  import { createConnection } from 'net';
34
34
  import { EventEmitter } from 'events';
@@ -220,6 +220,18 @@ export class MiniCOTServerManager extends EventEmitter {
220
220
  this.startTime = Date.now();
221
221
  this.consecutiveFailures = 0;
222
222
  this.isStarting = false;
223
+ // Clean stale PID file — we don't own this process
224
+ const stalePid = this.readPidFile();
225
+ if (stalePid) {
226
+ try {
227
+ process.kill(stalePid, 0);
228
+ }
229
+ catch (_e) {
230
+ // Stale PID — remove it
231
+ this.removePidFile();
232
+ logger.debug({ stalePid }, '[MiniCOTServerManager] Removed stale PID file for dead process while adopting external server');
233
+ }
234
+ }
223
235
  this.emit('started', { pid: null, external: true });
224
236
  return true;
225
237
  }
@@ -231,6 +243,17 @@ export class MiniCOTServerManager extends EventEmitter {
231
243
  catch (err) {
232
244
  logger.warn({ error: err }, '[MiniCOTServerManager] Failed to remove old socket');
233
245
  }
246
+ // Also clean stale PID file if process is dead
247
+ const stalePid = this.readPidFile();
248
+ if (stalePid) {
249
+ try {
250
+ process.kill(stalePid, 0);
251
+ }
252
+ catch (_e) {
253
+ this.removePidFile();
254
+ logger.debug({ stalePid }, '[MiniCOTServerManager] Removed stale PID file for dead process during startup cleanup');
255
+ }
256
+ }
234
257
  }
235
258
  // Find the Mini COT script
236
259
  const miniCOTScript = this.findMiniCOTScript();
@@ -376,7 +399,21 @@ export class MiniCOTServerManager extends EventEmitter {
376
399
  return true;
377
400
  }
378
401
  catch (err) {
379
- logger.error({ error: err }, '[MiniCOTServerManager] Failed to start server');
402
+ const errMsg = err?.message || String(err);
403
+ const isEADDRINUSE = errMsg.includes('EADDRINUSE') || errMsg.includes('address already in use');
404
+ const isENOENT = errMsg.includes('ENOENT') || errMsg.includes('no such file');
405
+ const isEACCES = errMsg.includes('EACCES') || errMsg.includes('permission denied');
406
+ let hint = '';
407
+ if (isEADDRINUSE) {
408
+ hint = ` (socket ${this.socketPath} already in use — another Mini COT instance may be running)`;
409
+ }
410
+ else if (isENOENT) {
411
+ hint = ` (Mini COT script not found — check installation)`;
412
+ }
413
+ else if (isEACCES) {
414
+ hint = ` (permission denied — check file permissions on script or socket directory)`;
415
+ }
416
+ logger.error({ error: err, socketPath: this.socketPath }, `[MiniCOTServerManager] Failed to start server${hint}`);
380
417
  this.isStarting = false;
381
418
  return false;
382
419
  }
@@ -424,11 +461,12 @@ export class MiniCOTServerManager extends EventEmitter {
424
461
  logger.debug({ error: err }, '[MiniCOTServerManager] Failed to remove socket');
425
462
  }
426
463
  }
464
+ const stoppedPid = this.process?.pid ?? null;
427
465
  this.process = null;
428
466
  this.isRunning = false;
429
467
  this.startTime = null;
430
468
  logger.info('[MiniCOTServerManager] Server stopped');
431
- this.emit('stopped', { pid: this.process?.pid });
469
+ this.emit('stopped', { pid: stoppedPid });
432
470
  }
433
471
  /**
434
472
  * Perform a health check on the Mini COT server
@@ -529,12 +567,31 @@ export class MiniCOTServerManager extends EventEmitter {
529
567
  getStatus() {
530
568
  const pid = this.readPidFile();
531
569
  const socketExists = existsSync(this.socketPath);
570
+ // Check actual process liveness, not just in-memory flag
571
+ let processAlive = false;
572
+ if (pid) {
573
+ try {
574
+ process.kill(pid, 0); // signal 0 = existence check
575
+ processAlive = true;
576
+ }
577
+ catch (_e) {
578
+ // Process does not exist
579
+ processAlive = false;
580
+ }
581
+ }
582
+ // Reconcile: if isRunning but process is dead, correct the state
583
+ const actuallyRunning = this.isRunning && processAlive;
584
+ if (this.isRunning && !processAlive && pid) {
585
+ logger.warn({ pid }, '[MiniCOTServerManager] getStatus: process dead but isRunning was true, correcting state');
586
+ this.isRunning = false;
587
+ }
532
588
  return {
533
- running: this.isRunning,
589
+ running: actuallyRunning,
534
590
  pid,
591
+ processAlive,
535
592
  socketPath: this.socketPath,
536
593
  socketExists,
537
- healthy: this.isRunning && socketExists && this.consecutiveFailures === 0,
594
+ healthy: actuallyRunning && socketExists && this.consecutiveFailures === 0,
538
595
  lastHealthCheck: this.healthCheckTimer ? Date.now() : null,
539
596
  consecutiveFailures: this.consecutiveFailures,
540
597
  restartCount: this.restartCount,
@@ -1184,6 +1241,14 @@ export class MiniCOTServerManager extends EventEmitter {
1184
1241
  logger.debug({ pid: healthInfo.pid }, '[MiniCOTServerManager] Process from PID file no longer exists');
1185
1242
  }
1186
1243
  this.removePidFile();
1244
+ // Also clean up stale socket
1245
+ try {
1246
+ if (existsSync(this.socketPath)) {
1247
+ unlinkSync(this.socketPath);
1248
+ logger.debug('[MiniCOTServerManager] Cleaned up stale socket in killByPidFile');
1249
+ }
1250
+ }
1251
+ catch (_e) { /* ignore */ }
1187
1252
  }
1188
1253
  /**
1189
1254
  * Find the Mini COT script path
@@ -1237,6 +1302,8 @@ export class MiniCOTServerManager extends EventEmitter {
1237
1302
  if (this.process) {
1238
1303
  this.process = null;
1239
1304
  }
1305
+ // Always clean up PID file on exit to prevent stale PIDs
1306
+ this.removePidFile();
1240
1307
  if (this.isShuttingDown) {
1241
1308
  // Expected exit during shutdown
1242
1309
  return;
@@ -1392,9 +1459,11 @@ export class MiniCOTServerManager extends EventEmitter {
1392
1459
  if (!existsSync(pidDir)) {
1393
1460
  mkdirSync(pidDir, { recursive: true });
1394
1461
  }
1395
- // Format: PID:TIMESTAMP
1396
- writeFileSync(this.pidFilePath, `${pid}:${Date.now()}`, 'utf8');
1397
- logger.debug({ pid, path: this.pidFilePath }, '[MiniCOTServerManager] Wrote PID file');
1462
+ // Format: PID:TIMESTAMP — atomic write via rename to prevent partial reads
1463
+ const tmpPath = this.pidFilePath + '.tmp';
1464
+ writeFileSync(tmpPath, `${pid}:${Date.now()}`, 'utf8');
1465
+ renameSync(tmpPath, this.pidFilePath);
1466
+ logger.debug({ pid, path: this.pidFilePath }, '[MiniCOTServerManager] Wrote PID file (atomic)');
1398
1467
  }
1399
1468
  catch (err) {
1400
1469
  logger.error({ error: err }, '[MiniCOTServerManager] Failed to write PID file');
@@ -1425,6 +1494,11 @@ export class MiniCOTServerManager extends EventEmitter {
1425
1494
  unlinkSync(this.pidFilePath);
1426
1495
  logger.debug('[MiniCOTServerManager] Removed PID file');
1427
1496
  }
1497
+ // Also clean up any leftover .tmp from atomic write
1498
+ const tmpPath = this.pidFilePath + '.tmp';
1499
+ if (existsSync(tmpPath)) {
1500
+ unlinkSync(tmpPath);
1501
+ }
1428
1502
  }
1429
1503
  catch (err) {
1430
1504
  logger.debug({ error: err }, '[MiniCOTServerManager] Failed to remove PID file');
@@ -85,7 +85,9 @@ import { getSkillScanner } from '../skills/skillScanner.js';
85
85
  import { getSkillResourceProvider } from '../skills/skillsResource.js';
86
86
  import { getSkillReminder } from '../reminders/skillReminder.js';
87
87
  // Compaction proxy — MITM token stripper for faster compaction
88
- import { startCompactionProxy, stopCompactionProxy, killCompactionProxy } from './compactionProxy.js';
88
+ import { startCompactionProxy, stopCompactionProxy, killCompactionProxy, registerWithDaemon, checkDaemonHealth } from './compactionProxy.js';
89
+ // Context vault — token-saving stash for thicc tool outputs (auto-stash + search)
90
+ import { initContextVault, stashTheGoods as vaultStash, formatVaultReceipt, VAULT_THRESHOLD, VAULT_SKIP_TOOLS } from './contextVault.js';
89
91
  // Command loader - load command .md files as MCP prompts
90
92
  import { getCommandLoader } from '../commands/commandLoader.js';
91
93
  // CLI Notifications - centralized notification system
@@ -586,6 +588,25 @@ export class SpecMemServer {
586
588
  __debugLog('[MCP DEBUG]', Date.now(), 'TOOL_SLOW_WARNING', { callId, toolName: name, durationMs: duration, threshold: 100 });
587
589
  logger.warn({ duration, tool: name }, 'tool execution kinda slow ngl');
588
590
  }
591
+ // === Auto-vault large tool results to save tokens ===
592
+ // If a tool returns >5KB, stash it and hand back a compact receipt.
593
+ // Claude can dig_in_the_vault later for specific data.
594
+ if (!VAULT_SKIP_TOOLS.has(name)) {
595
+ try {
596
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
597
+ if (resultStr.length > VAULT_THRESHOLD) {
598
+ const vaultResult = await vaultStash(resultStr, { tool: name, callId });
599
+ if (!vaultResult.deduplicated) {
600
+ startupLog(`[VAULT] Auto-stashed ${name}: ${resultStr.length} chars → receipt (vault:${vaultResult.vaultId})`);
601
+ }
602
+ result = formatVaultReceipt(vaultResult);
603
+ }
604
+ }
605
+ catch (vaultErr) {
606
+ // Non-fatal — fall through with original result
607
+ __debugLog('[MCP DEBUG]', Date.now(), 'AUTO_VAULT_FAILED', { callId, toolName: name, error: vaultErr.message });
608
+ }
609
+ }
589
610
  __debugLog('[MCP DEBUG]', Date.now(), 'FORMATTING_RESPONSE', { callId, toolName: name });
590
611
  const formattedResponse = this.formatResponse(result);
591
612
  __debugLog('[MCP DEBUG]', Date.now(), 'RESPONSE_FORMATTED', {
@@ -1250,20 +1271,23 @@ export class SpecMemServer {
1250
1271
  // Each step waits for the previous to complete + yields to event loop
1251
1272
  // This prevents CPU spikes that cause MCP connection timeouts
1252
1273
  logger.info('SpecMem MCP server connected — lazy init starting');
1253
- // Step 2.5: Compaction Proxy lightweight HTTP server, starts in <100ms
1254
- // Sits between Claude Code and Anthropic API, strips tool_result bodies
1255
- // from compaction requests (50-80% token reduction = faster compaction)
1274
+ // Step 2.5: Register with compaction proxy daemon (DO NOT spawn here)
1275
+ // The proxy daemon is started by specmem-init BEFORE Claude launches.
1276
+ // By the time oninitialized fires, Claude is already running and routing
1277
+ // traffic through ANTHROPIC_BASE_URL — spawning here is too late.
1278
+ // We just register this project so the daemon knows we're alive.
1256
1279
  try {
1257
- const proxyResult = await startCompactionProxy();
1258
- if (proxyResult.started) {
1259
- startupLog(`Compaction proxy started on port ${proxyResult.port}`);
1260
- logger.info({ port: proxyResult.port }, 'Compaction proxy started');
1280
+ const healthy = await checkDaemonHealth();
1281
+ if (healthy) {
1282
+ registerWithDaemon(process.env.SPECMEM_PROJECT_PATH, process.pid);
1283
+ startupLog('Registered with existing compaction proxy daemon');
1284
+ logger.info('Registered with compaction proxy daemon');
1261
1285
  } else {
1262
- startupLog(`Compaction proxy skipped: ${proxyResult.reason}`);
1286
+ startupLog('Compaction proxy daemon not running — skipping registration (must be started by specmem-init)');
1263
1287
  }
1264
1288
  } catch (proxyErr) {
1265
1289
  // Non-fatal — proxy is optional, compaction still works without it
1266
- startupLog('Compaction proxy failed to start (non-fatal)', proxyErr);
1290
+ startupLog('Compaction proxy registration check failed (non-fatal)', proxyErr);
1267
1291
  }
1268
1292
  // Step 3: Database init (deferred, tools wait via waitForReady())
1269
1293
  startupLog('Starting deferred database initialization...');
@@ -1928,6 +1952,17 @@ export class SpecMemServer {
1928
1952
  // Non-fatal - tools can still work with in-memory fallback
1929
1953
  logger.warn({ error: teamCommsError }, 'Team comms DB init failed - using in-memory fallback');
1930
1954
  }
1955
+ // Initialize context vault for token-saving auto-stash
1956
+ try {
1957
+ if (pool) {
1958
+ initContextVault(pool, process.env['SPECMEM_PROJECT_PATH'] || '/');
1959
+ logger.info('Context vault initialized — auto-stash ready');
1960
+ }
1961
+ }
1962
+ catch (vaultError) {
1963
+ // Non-fatal — auto-stash disabled, manual tools still work
1964
+ logger.warn({ error: vaultError }, 'Context vault init failed — auto-stash disabled');
1965
+ }
1931
1966
  // FIX 1.02: Create CommandHandler AFTER DB is initialized
1932
1967
  // Previously created in constructor with uninitialized db
1933
1968
  // ORDERING INVARIANT (CRIT-1): commandHandler MUST be created here in deferredInit(),
@@ -57,6 +57,9 @@ import { GetMemoryFull } from '../tools/goofy/getMemoryFull.js';
57
57
  // Import project memory import/export tools - carry context across projects
58
58
  import { ImportProjectMemories } from '../tools/goofy/importProjectMemories.js';
59
59
  import { ExportProjectMemories } from '../tools/goofy/exportProjectMemories.js';
60
+ // Import context vault tools - token-saving stash for thicc tool outputs
61
+ import { StashTheGoods } from '../tools/goofy/stashTheGoods.js';
62
+ import { DigInTheVault } from '../tools/goofy/digInTheVault.js';
60
63
  // Import MCP-based team communication tools (NEW - replaces HTTP team member comms)
61
64
  import { createTeamCommTools } from './tools/teamComms.js';
62
65
  // Import embedding server control tools (Phase 4 - user start/stop/status)
@@ -543,6 +546,9 @@ export function createToolRegistry(db, embeddingProvider) {
543
546
  // Project memory import/export tools - carry context across projects
544
547
  registry.register(new ImportProjectMemories(db, cachingProvider));
545
548
  registry.register(new ExportProjectMemories(db));
549
+ // Context vault tools - token-saving stash + search for thicc tool outputs
550
+ registry.register(new StashTheGoods());
551
+ registry.register(new DigInTheVault());
546
552
  // Team communication tools - multi-team member coordination
547
553
  const teamCommTools = createTeamCommTools();
548
554
  for (const tool of teamCommTools) {
@@ -183,6 +183,16 @@ export async function triggerBackgroundIndexing(embeddingProvider, options = {})
183
183
  }
184
184
  state.indexingInProgress = true;
185
185
  state.lastIndexingCheck = new Date();
186
+ // Pause file watcher queue during indexing to avoid resource contention
187
+ let watcherQueue = null;
188
+ try {
189
+ const { getWatcherManager } = await import('../mcp/watcherIntegration.js');
190
+ const mgr = getWatcherManager();
191
+ if (mgr?.queue) {
192
+ watcherQueue = mgr.queue;
193
+ watcherQueue.pause('background indexing');
194
+ }
195
+ } catch { /* watcher not initialized yet — that's fine */ }
186
196
  try {
187
197
  const db = getDatabase();
188
198
  if (!db) {
@@ -241,6 +251,10 @@ export async function triggerBackgroundIndexing(embeddingProvider, options = {})
241
251
  }
242
252
  finally {
243
253
  state.indexingInProgress = false;
254
+ // Resume watcher queue after indexing completes (or fails)
255
+ if (watcherQueue) {
256
+ watcherQueue.resume();
257
+ }
244
258
  }
245
259
  }
246
260
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Task Orchestrator - Smart Work Distribution System
2
+ * Agent Orchestrator - Smart Work Distribution System
3
3
  *
4
- * Provides intelligent task distribution to teamMembers:
5
- * - Task queue with priority management
4
+ * Provides intelligent agent distribution to teamMembers:
5
+ * - Agent queue with priority management
6
6
  * - Capability-based task matching
7
7
  * - Load balancing across team members
8
8
  * - Automatic failover and task reassignment
@@ -1,7 +1,7 @@
1
1
  /**
2
- * taskTeamMemberLogger.ts - Logs Code Task team member activity to SpecMem database
2
+ * taskTeamMemberLogger.ts - Logs Code Agent team member activity to SpecMem database
3
3
  *
4
- * yo fr fr this bridges the gap between Code's Task tool and SpecMem tracking
4
+ * yo fr fr this bridges the gap between Code's Agent tool and SpecMem tracking
5
5
  *
6
6
  * Problem: Task-deployed team members are invisible to SpecMem dashboard
7
7
  * Solution: Log team member activity before/after Task deployment
@@ -1,8 +1,8 @@
1
1
  /**
2
- * deployTeamMember - The Task tool but actually works with MCP
2
+ * deployTeamMember - The Agent tool but actually works with MCP
3
3
  *
4
4
  * Spawns team members in screen sessions with full SpecMem MCP access.
5
- * This is basically skidding Code's Task tool but making it not suck.
5
+ * This is basically skidding Code's Agent tool but making it not suck.
6
6
  */
7
7
  import { deployTeamMember as deployTeamMemberImpl } from '../teamMemberDeployer.js';
8
8
  import { logger } from '../../utils/logger.js';
@@ -10,7 +10,7 @@ export class DeployTeamMember {
10
10
  name = 'deployTeamMember';
11
11
  description = `Deploy a team member with full SpecMem MCP access.
12
12
 
13
- This is like the Task tool but actually works - spawned team members get full access
13
+ This is like the Agent tool but actually works - spawned team members get full access
14
14
  to all SpecMem MCP tools including team member communication (sayToTeamMember, listenForMessages,
15
15
  sendHeartbeat, getActiveTeamMembers).
16
16
 
@@ -0,0 +1,81 @@
1
+ /**
2
+ * digInTheVault - search the context vault for stashed content
3
+ *
4
+ * when you need to find something in all that stashed content
5
+ * without loading it all into context. BM25-ranked via tsvector.
6
+ *
7
+ * supports:
8
+ * - query search across all vaults (project-scoped)
9
+ * - targeted search within a specific vault_id
10
+ * - full dump of a vault_id (get_all mode)
11
+ * - stats mode (no query = return vault statistics)
12
+ */
13
+
14
+ import { digInTheVault as doTheDig, getFullStash, getVaultStats } from '../../mcp/contextVault.js';
15
+
16
+ export class DigInTheVault {
17
+ name = 'dig_in_the_vault';
18
+ description = 'Search the context vault for previously stashed content. Returns BM25-ranked chunks matching your query. Use vault_id to search a specific stash, or omit to search all. Use get_all:true to retrieve everything from a vault.';
19
+
20
+ inputSchema = {
21
+ type: 'object',
22
+ properties: {
23
+ query: {
24
+ type: 'string',
25
+ description: 'What to search for in the vault'
26
+ },
27
+ vault_id: {
28
+ type: 'string',
29
+ description: 'Search within a specific stash (the id from vault_receipt)'
30
+ },
31
+ limit: {
32
+ type: 'number',
33
+ description: 'Max results to return (default: 10)',
34
+ default: 10
35
+ },
36
+ get_all: {
37
+ type: 'boolean',
38
+ description: 'Retrieve ALL chunks for a vault_id in order (ignores query)',
39
+ default: false
40
+ }
41
+ }
42
+ };
43
+
44
+ async execute(params) {
45
+ const { query, vault_id, limit = 10, get_all = false } = params;
46
+
47
+ try {
48
+ // get_all mode: dump full stash content
49
+ if (get_all && vault_id) {
50
+ const full = await getFullStash(vault_id);
51
+ if (!full) return `<vault_dig ok="false" error="vault ${vault_id} not found or expired"/>`;
52
+ return full;
53
+ }
54
+
55
+ // No query = return vault stats
56
+ if (!query) {
57
+ const stats = await getVaultStats();
58
+ return JSON.stringify(stats, null, 2);
59
+ }
60
+
61
+ // BM25-ranked search
62
+ const results = await doTheDig(query, vault_id || null, limit);
63
+
64
+ if (results.length === 0) {
65
+ return vault_id
66
+ ? `<vault_dig ok="false" vault_id="${vault_id}" query="${query}" error="no matches — try different terms or get_all:true"/>`
67
+ : `<vault_dig ok="false" query="${query}" error="no matches in any vault"/>`;
68
+ }
69
+
70
+ // Format results compactly
71
+ const lines = results.map((r, i) => {
72
+ const header = `── [${i + 1}] vault:${r.vault_id} chunk:${r.chunk_idx} rank:${parseFloat(r.rank).toFixed(3)} ──`;
73
+ return `${header}\n${r.content}`;
74
+ });
75
+
76
+ return lines.join('\n\n');
77
+ } catch (err) {
78
+ return `<vault_dig ok="false" error="${err.message}"/>`;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * stashTheGoods - manually vault content for later retrieval
3
+ *
4
+ * when you got data that's too thicc for the context window
5
+ * but you might need it later, stash it in the vault fr
6
+ *
7
+ * auto-stash handles most cases, but this tool lets claude
8
+ * manually stash anything it wants to keep searchable
9
+ */
10
+
11
+ import { stashTheGoods as doTheStash, formatVaultReceipt } from '../../mcp/contextVault.js';
12
+
13
+ export class StashTheGoods {
14
+ name = 'stash_the_goods';
15
+ description = 'Stash large content in the context vault for token-efficient retrieval later. Content is chunked, indexed with full-text search, and retrievable via dig_in_the_vault. Auto-expires after 24h.';
16
+
17
+ inputSchema = {
18
+ type: 'object',
19
+ properties: {
20
+ content: {
21
+ type: 'string',
22
+ description: 'The content to stash in the vault'
23
+ },
24
+ tool_name: {
25
+ type: 'string',
26
+ description: 'Which tool produced this content (for tracking)'
27
+ },
28
+ tags: {
29
+ type: 'array',
30
+ items: { type: 'string' },
31
+ description: 'Optional tags for categorization'
32
+ }
33
+ },
34
+ required: ['content']
35
+ };
36
+
37
+ async execute(params) {
38
+ const { content, tool_name, tags } = params;
39
+
40
+ if (!content || content.length === 0) {
41
+ return '<vault_receipt ok="false" error="nothing to stash fr"/>';
42
+ }
43
+
44
+ try {
45
+ const result = await doTheStash(content, {
46
+ tool: tool_name || 'manual_stash',
47
+ tags: tags || [],
48
+ manual: true,
49
+ });
50
+
51
+ return formatVaultReceipt(result);
52
+ } catch (err) {
53
+ return `<vault_receipt ok="false" error="${err.message}"/>`;
54
+ }
55
+ }
56
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Team Member Deployer Tool
3
3
  *
4
- * "Skidded" version of Code's Task tool that actually works with MCP
4
+ * "Skidded" version of Code's Agent tool that actually works with MCP
5
5
  * Spawns team members with full SpecMem MCP access
6
6
  *
7
7
  * Now integrates with TeamCommsService for team-based team member coordination:
@@ -23,7 +23,7 @@ import { getAgentsJson, isValidAgentType } from './agentDefinitions.js';
23
23
  /**
24
24
  * Deploy a team member with full SpecMem MCP access
25
25
  *
26
- * This is basically the Task tool but it actually fucking works
26
+ * This is basically the Agent tool but it actually fucking works
27
27
  * because we spawn team members with MCP configured
28
28
  *
29
29
  * Now with team communication support for coordinated multi-team-member work