specmem-hardwicksoftware 3.7.31 → 3.7.32

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to SpecMem - we keep it real with semantic versioning. Deada
4
4
 
5
5
  ---
6
6
 
7
+ ## [3.7.32] - 2026-02-24
8
+
9
+ ### Fixed
10
+ - Sync score `-100%` display bug — `writeSyncScore` now guards against negative values when indexing is pending
11
+ - Concurrent MCP tool call failures — pLimit(2) concurrency cap in mcpProtocolHandler, toolRegistry eviction fix, connectionPool maxConnections reduced 20→6
12
+ - CPU spike on large codebases — fileWatcher batch debounce 500ms, stabilityThreshold 300→500ms
13
+ - `check_sync` returning 0% — `triggerBackgroundIndexing()` now wired into deferred init, syncChecker defensive handling added
14
+ - TUI init flow rendering issues — Blessed screen render guard, pino SonicBoom fd intercept
15
+
16
+ ### Added
17
+ - Auto-resync when sync score drops below 85% — debounced (15min default, `SPECMEM_LOW_SCORE_DEBOUNCE_MS`), configurable threshold via `SPECMEM_LOW_SCORE_THRESHOLD`
18
+ - Offline ML model pipeline — embedding (MiniLM-L6-v2 ONNX quint8) + Argos neural translation bundled via git-lfs, auto-downloaded at init
19
+ - Bash call enforcer hook — agents must call `send_team_message`, `find_code_pointers`, or `drill_down` every 3 Bash calls
20
+ - Compaction proxy daemon — handles session compaction in background process
21
+
22
+ ### Improved
23
+ - Token compression system — improved accuracy, self-healing codebook, round-trip verified
24
+ - Install flow — root no longer required, simplified to 3 steps: install → cd → `specmem init`
25
+ - Reduced npm permission requirements across the board
26
+
27
+ ---
28
+
7
29
  ## [3.7.24] - 2026-02-12
8
30
 
9
31
  ### Added
package/README.md CHANGED
@@ -1,3 +1,15 @@
1
+ <!-- Debian/Ubuntu/Mint/Kali notice -->
2
+ <div align="center">
3
+ <table><tr><td align="center" style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:12px 20px">
4
+ <strong>Debian-based Linux?</strong> &nbsp;
5
+ <img src="https://upload.wikimedia.org/wikipedia/commons/4/4a/Debian-OpenLogo.svg" height="16" alt="Debian"/>
6
+ <img src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Logo-ubuntu_cof-orange-hex.svg" height="16" alt="Ubuntu"/>
7
+ <img src="https://upload.wikimedia.org/wikipedia/commons/3/3f/Linux_Mint_logo_without_wordmark.svg" height="16" alt="Mint"/>
8
+ <img src="https://upload.wikimedia.org/wikipedia/commons/4/4b/Kali_Linux_2.0_wordmark.svg" height="16" alt="Kali"/>
9
+ &nbsp; <code>npm install -g specmem-hardwicksoftware</code> &nbsp;— root no longer required.
10
+ </td></tr></table>
11
+ </div>
12
+
1
13
  <div align="center">
2
14
 
3
15
  <!-- Demo GIF -->
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BASH CALL ENFORCER - PreToolUse Hook
4
+ * =====================================
5
+ * Agents MUST check in with SpecMem every 3 Bash calls.
6
+ *
7
+ * After 3 Bash calls → BLOCKED until they use:
8
+ * - send_team_message (announce progress)
9
+ * - find_code_pointers (semantic code search)
10
+ * - drill_down (dig into memory)
11
+ *
12
+ * This prevents agents from running unlimited shell commands
13
+ * without pausing to share knowledge / use semantic context.
14
+ *
15
+ * Main session = never blocked.
16
+ * Agents only.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const BASH_LIMIT = parseInt(process.env.SPECMEM_BASH_LIMIT || '3', 10);
25
+
26
+ const RESET_TOOLS = [
27
+ 'mcp__specmem__send_team_message',
28
+ 'mcp__specmem__find_code_pointers',
29
+ 'mcp__specmem__drill_down',
30
+ 'mcp__specmem__find_memory',
31
+ 'mcp__specmem__smart_search',
32
+ 'mcp__specmem__broadcast_to_team',
33
+ ];
34
+
35
+ // ── Agent detection: only enforce on full specmem team members ────────────────
36
+ // Pure Bash agents (Task subagent_type:"Bash") don't have MCP tools and cannot
37
+ // satisfy the reset requirement — skip them entirely.
38
+ function isAgent() {
39
+ return process.env.SPECMEM_TEAM_MEMBER === 'true';
40
+ }
41
+
42
+ // ── State helpers ─────────────────────────────────────────────────────────────
43
+ function stateFile() {
44
+ const sid = (process.env.CLAUDE_SESSION_ID || process.env.TASK_ID || 'default')
45
+ .replace(/[^a-zA-Z0-9_-]/g, '_');
46
+ return `/tmp/specmem-bash-enforcer-${sid}.json`;
47
+ }
48
+
49
+ function getState() {
50
+ try {
51
+ const f = stateFile();
52
+ if (fs.existsSync(f)) {
53
+ const d = JSON.parse(fs.readFileSync(f, 'utf8'));
54
+ // Expire after 60 min
55
+ if (d.ts && Date.now() - d.ts < 3_600_000) return d;
56
+ }
57
+ } catch (_) {}
58
+ return { bashCount: 0, needsReset: false, ts: Date.now() };
59
+ }
60
+
61
+ function saveState(state) {
62
+ try {
63
+ state.ts = Date.now();
64
+ fs.writeFileSync(stateFile(), JSON.stringify(state));
65
+ } catch (_) {}
66
+ }
67
+
68
+ // ── Response helpers ──────────────────────────────────────────────────────────
69
+ const deny = (reason) => JSON.stringify({
70
+ hookSpecificOutput: {
71
+ hookEventName: 'PreToolUse',
72
+ permissionDecision: 'deny',
73
+ permissionDecisionReason: reason,
74
+ }
75
+ });
76
+
77
+ const allow = () => JSON.stringify({ continue: true });
78
+
79
+ // ── Timeout guard — never stall the session ───────────────────────────────────
80
+ setTimeout(() => { process.stdout.write(allow()); process.exit(0); }, 400);
81
+
82
+ // ── Main ──────────────────────────────────────────────────────────────────────
83
+ let raw = '';
84
+ process.stdin.setEncoding('utf8');
85
+ process.stdin.on('data', c => { raw += c; });
86
+ process.stdin.on('end', () => {
87
+ try {
88
+ const { tool_name: tool } = JSON.parse(raw);
89
+
90
+ // Reset tools: clear bash counter and allow immediately
91
+ if (RESET_TOOLS.includes(tool)) {
92
+ saveState({ bashCount: 0, needsReset: false, ts: Date.now() });
93
+ process.stdout.write(allow());
94
+ return;
95
+ }
96
+
97
+ // Only enforce on Bash, and only for agents
98
+ if (tool !== 'Bash' || !isAgent()) {
99
+ process.stdout.write(allow());
100
+ return;
101
+ }
102
+
103
+ const state = getState();
104
+
105
+ // Already needs a reset — block this Bash call
106
+ if (state.needsReset) {
107
+ process.stdout.write(deny(
108
+ `[BASH BLOCKED] You've run ${BASH_LIMIT} Bash commands without checking in.\n\n` +
109
+ `Before the next Bash call, you MUST do ONE of:\n` +
110
+ ` • send_team_message({message:"progress update..."}) — share what you found\n` +
111
+ ` • find_code_pointers({query:"..."}) — search the codebase semantically\n` +
112
+ ` • drill_down({drilldownID: N}) — dig into a memory result\n\n` +
113
+ `This keeps you using SpecMem's knowledge, not just raw shell output.`
114
+ ));
115
+ return;
116
+ }
117
+
118
+ state.bashCount = (state.bashCount || 0) + 1;
119
+
120
+ if (state.bashCount >= BASH_LIMIT) {
121
+ state.needsReset = true;
122
+ saveState(state);
123
+ process.stdout.write(deny(
124
+ `[BASH BLOCKED] ${BASH_LIMIT} Bash calls hit — check in with SpecMem first.\n\n` +
125
+ `Do ONE of:\n` +
126
+ ` • send_team_message({message:"what you discovered..."}) — report progress\n` +
127
+ ` • find_code_pointers({query:"..."}) — semantic code search\n` +
128
+ ` • drill_down({drilldownID: N}) — drill into memory\n\n` +
129
+ `After that, Bash is unlocked again (next ${BASH_LIMIT} calls).`
130
+ ));
131
+ return;
132
+ }
133
+
134
+ saveState(state);
135
+ process.stdout.write(allow());
136
+
137
+ } catch (_) {
138
+ process.stdout.write(allow());
139
+ }
140
+ });
@@ -223,6 +223,14 @@
223
223
  "SPECMEM_PROJECT_PATH": "${cwd}"
224
224
  }
225
225
  },
226
+ {
227
+ "type": "command",
228
+ "command": "node /root/.claude/hooks/bash-call-enforcer.cjs",
229
+ "timeout": 2,
230
+ "env": {
231
+ "SPECMEM_PROJECT_PATH": "${cwd}"
232
+ }
233
+ },
226
234
  {
227
235
  "type": "command",
228
236
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -293,6 +301,31 @@
293
301
  "env": {
294
302
  "SPECMEM_PROJECT_PATH": "${cwd}"
295
303
  }
304
+ },
305
+ {
306
+ "type": "command",
307
+ "command": "node /root/.claude/hooks/bash-call-enforcer.cjs",
308
+ "timeout": 2
309
+ }
310
+ ]
311
+ },
312
+ {
313
+ "matcher": "mcp__specmem__send_team_message",
314
+ "hooks": [
315
+ {
316
+ "type": "command",
317
+ "command": "node /root/.claude/hooks/bash-call-enforcer.cjs",
318
+ "timeout": 2
319
+ }
320
+ ]
321
+ },
322
+ {
323
+ "matcher": "mcp__specmem__drill_down",
324
+ "hooks": [
325
+ {
326
+ "type": "command",
327
+ "command": "node /root/.claude/hooks/bash-call-enforcer.cjs",
328
+ "timeout": 2
296
329
  }
297
330
  ]
298
331
  }
@@ -15,6 +15,7 @@ const { spawn } = require('child_process');
15
15
  const net = require('net');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
+ const fs = require('fs');
18
19
 
19
20
  // Import shared path resolution utilities AND Pool
20
21
  const {
@@ -55,6 +56,45 @@ const SPECMEM_HOME = getSpecmemHome();
55
56
  const SPECMEM_PKG = getSpecmemPkg();
56
57
  const SPECMEM_RUN_DIR = expandCwd(process.env.SPECMEM_RUN_DIR) || getProjectSocketDir();
57
58
 
59
+ // Prompt counter — fires on first prompt, then every 3rd (prompt 1, 4, 7, 10, ...)
60
+ // No hard cap — cadence-based injection instead
61
+ const DRILLDOWN_COUNT_FILE = path.join(SPECMEM_RUN_DIR || '/tmp', '.drilldown-prompt-count');
62
+
63
+ function getPromptCount() {
64
+ try {
65
+ if (fs.existsSync(DRILLDOWN_COUNT_FILE)) {
66
+ const data = JSON.parse(fs.readFileSync(DRILLDOWN_COUNT_FILE, 'utf8'));
67
+ // Reset if older than 4 hours
68
+ if (Date.now() - (data.startedAt || 0) > 14400000) return 0;
69
+ return data.count || 0;
70
+ }
71
+ } catch (e) {}
72
+ return 0;
73
+ }
74
+
75
+ function incrementPromptCount() {
76
+ try {
77
+ let data = { count: 0, startedAt: Date.now() };
78
+ if (fs.existsSync(DRILLDOWN_COUNT_FILE)) {
79
+ try {
80
+ data = JSON.parse(fs.readFileSync(DRILLDOWN_COUNT_FILE, 'utf8'));
81
+ if (Date.now() - (data.startedAt || 0) > 14400000) {
82
+ data = { count: 0, startedAt: Date.now() };
83
+ }
84
+ } catch (e) {}
85
+ }
86
+ data.count = (data.count || 0) + 1;
87
+ fs.writeFileSync(DRILLDOWN_COUNT_FILE, JSON.stringify(data));
88
+ } catch (e) {}
89
+ }
90
+
91
+ function shouldFireThisPrompt() {
92
+ const count = getPromptCount(); // 0-indexed: 0 = first prompt
93
+ // Fire on prompt 0 (first), 3, 6, 9, ... (every 3rd starting from first)
94
+ return count % 3 === 0;
95
+ }
96
+
97
+
58
98
  // Project path will be set from 's hook input (cwd field)
59
99
  // Fallback: 1. SPECMEM_PROJECT_PATH env var, 2. process.cwd()
60
100
  let PROJECT_PATH = expandCwd(process.env.SPECMEM_PROJECT_PATH) || process.cwd() || '/';
@@ -62,7 +102,7 @@ let PROJECT_PATH = expandCwd(process.env.SPECMEM_PROJECT_PATH) || process.cwd()
62
102
  // Configuration
63
103
  const CONFIG = {
64
104
  // SpecMem settings
65
- searchLimit: parseInt(process.env.SPECMEM_SEARCH_LIMIT || '5'),
105
+ searchLimit: parseInt(process.env.SPECMEM_SEARCH_LIMIT || '4'),
66
106
  // ACCURACY FIX: Raised threshold from 0.3 to 0.4 to reduce false positives
67
107
  // Local embeddings score lower, but 0.4 filters out noise while keeping relevant results
68
108
  threshold: parseFloat(process.env.SPECMEM_THRESHOLD || '0.4'),
@@ -441,13 +481,18 @@ async function main() {
441
481
  // Debounce: skip if ran within last 5 seconds
442
482
  const DRILLDOWN_DEBOUNCE_FILE = path.join(SPECMEM_RUN_DIR, '.drilldown-debounce');
443
483
  try {
444
- const fs = require('fs');
445
484
  if (fs.existsSync(DRILLDOWN_DEBOUNCE_FILE)) {
446
485
  const last = parseInt(fs.readFileSync(DRILLDOWN_DEBOUNCE_FILE, 'utf8').trim(), 10);
447
486
  if (Date.now() - last < 5000) process.exit(0);
448
487
  }
449
488
  fs.writeFileSync(DRILLDOWN_DEBOUNCE_FILE, String(Date.now()));
450
489
  } catch (e) {}
490
+ // Increment prompt counter first, then check if we should fire this prompt
491
+ incrementPromptCount();
492
+ if (!shouldFireThisPrompt()) {
493
+ process.exit(0);
494
+ }
495
+
451
496
 
452
497
  // Skip task notifications (background agent completions treated as prompts)
453
498
  if (prompt.includes('<task-notification>') || prompt.includes('</task-notification>')) {
@@ -473,6 +518,8 @@ async function main() {
473
518
 
474
519
  // Mark as injected to prevent duplicate injection this session
475
520
  contextDedup.markInjected(PROJECT_PATH, sessionId, prompt);
521
+
522
+
476
523
  }
477
524
  } catch (error) {
478
525
  // Silently fail - don't break the prompt
@@ -15,6 +15,7 @@ const { spawn } = require('child_process');
15
15
  const net = require('net');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
+ const fs = require('fs');
18
19
 
19
20
  // Import shared path resolution utilities AND Pool
20
21
  const {
@@ -55,6 +56,45 @@ const SPECMEM_HOME = getSpecmemHome();
55
56
  const SPECMEM_PKG = getSpecmemPkg();
56
57
  const SPECMEM_RUN_DIR = expandCwd(process.env.SPECMEM_RUN_DIR) || getProjectSocketDir();
57
58
 
59
+ // Prompt counter — fires on first prompt, then every 3rd (prompt 1, 4, 7, 10, ...)
60
+ // No hard cap — cadence-based injection instead
61
+ const DRILLDOWN_COUNT_FILE = path.join(SPECMEM_RUN_DIR || '/tmp', '.drilldown-prompt-count');
62
+
63
+ function getPromptCount() {
64
+ try {
65
+ if (fs.existsSync(DRILLDOWN_COUNT_FILE)) {
66
+ const data = JSON.parse(fs.readFileSync(DRILLDOWN_COUNT_FILE, 'utf8'));
67
+ // Reset if older than 4 hours
68
+ if (Date.now() - (data.startedAt || 0) > 14400000) return 0;
69
+ return data.count || 0;
70
+ }
71
+ } catch (e) {}
72
+ return 0;
73
+ }
74
+
75
+ function incrementPromptCount() {
76
+ try {
77
+ let data = { count: 0, startedAt: Date.now() };
78
+ if (fs.existsSync(DRILLDOWN_COUNT_FILE)) {
79
+ try {
80
+ data = JSON.parse(fs.readFileSync(DRILLDOWN_COUNT_FILE, 'utf8'));
81
+ if (Date.now() - (data.startedAt || 0) > 14400000) {
82
+ data = { count: 0, startedAt: Date.now() };
83
+ }
84
+ } catch (e) {}
85
+ }
86
+ data.count = (data.count || 0) + 1;
87
+ fs.writeFileSync(DRILLDOWN_COUNT_FILE, JSON.stringify(data));
88
+ } catch (e) {}
89
+ }
90
+
91
+ function shouldFireThisPrompt() {
92
+ const count = getPromptCount(); // 0-indexed: 0 = first prompt
93
+ // Fire on prompt 0 (first), 3, 6, 9, ... (every 3rd starting from first)
94
+ return count % 3 === 0;
95
+ }
96
+
97
+
58
98
  // Project path will be set from 's hook input (cwd field)
59
99
  // Fallback: 1. SPECMEM_PROJECT_PATH env var, 2. process.cwd()
60
100
  let PROJECT_PATH = expandCwd(process.env.SPECMEM_PROJECT_PATH) || process.cwd() || '/';
@@ -62,7 +102,7 @@ let PROJECT_PATH = expandCwd(process.env.SPECMEM_PROJECT_PATH) || process.cwd()
62
102
  // Configuration
63
103
  const CONFIG = {
64
104
  // SpecMem settings
65
- searchLimit: parseInt(process.env.SPECMEM_SEARCH_LIMIT || '5'),
105
+ searchLimit: parseInt(process.env.SPECMEM_SEARCH_LIMIT || '4'),
66
106
  // ACCURACY FIX: Raised threshold from 0.3 to 0.4 to reduce false positives
67
107
  // Local embeddings score lower, but 0.4 filters out noise while keeping relevant results
68
108
  threshold: parseFloat(process.env.SPECMEM_THRESHOLD || '0.4'),
@@ -441,13 +481,18 @@ async function main() {
441
481
  // Debounce: skip if ran within last 5 seconds
442
482
  const DRILLDOWN_DEBOUNCE_FILE = path.join(SPECMEM_RUN_DIR, '.drilldown-debounce');
443
483
  try {
444
- const fs = require('fs');
445
484
  if (fs.existsSync(DRILLDOWN_DEBOUNCE_FILE)) {
446
485
  const last = parseInt(fs.readFileSync(DRILLDOWN_DEBOUNCE_FILE, 'utf8').trim(), 10);
447
486
  if (Date.now() - last < 5000) process.exit(0);
448
487
  }
449
488
  fs.writeFileSync(DRILLDOWN_DEBOUNCE_FILE, String(Date.now()));
450
489
  } catch (e) {}
490
+ // Increment prompt counter first, then check if we should fire this prompt
491
+ incrementPromptCount();
492
+ if (!shouldFireThisPrompt()) {
493
+ process.exit(0);
494
+ }
495
+
451
496
 
452
497
  // Skip task notifications (background agent completions treated as prompts)
453
498
  if (prompt.includes('<task-notification>') || prompt.includes('</task-notification>')) {
@@ -473,6 +518,8 @@ async function main() {
473
518
 
474
519
  // Mark as injected to prevent duplicate injection this session
475
520
  contextDedup.markInjected(PROJECT_PATH, sessionId, prompt);
521
+
522
+
476
523
  }
477
524
  } catch (error) {
478
525
  // Silently fail - don't break the prompt