specmem-hardwicksoftware 3.7.30 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  3. package/bootstrap.cjs +19 -0
  4. package/claude-hooks/bash-call-enforcer.cjs +140 -0
  5. package/claude-hooks/settings.json +132 -0
  6. package/claude-hooks/specmem-drilldown-hook.cjs +49 -2
  7. package/claude-hooks/specmem-drilldown-hook.js +49 -2
  8. package/claude-hooks/specmem-drilldown-hook.js.bak +495 -0
  9. package/claude-hooks/specmem-precompact.cjs +13 -36
  10. package/claude-hooks/specmem-precompact.js +3 -7
  11. package/claude-hooks/specmem-search-enforcer.cjs +229 -0
  12. package/claude-hooks/specmem-search-tracker.cjs +71 -0
  13. package/claude-hooks/specmem-session-start.cjs +38 -50
  14. package/claude-hooks/specmem-session-start.js +19 -60
  15. package/dist/config.js +11 -16
  16. package/dist/db/connectionPoolGoBrrr.js +3 -3
  17. package/dist/index.js +21 -4
  18. package/dist/mcp/compactionProxy.js +21 -1
  19. package/dist/mcp/embeddingServerManager.js +15 -1
  20. package/dist/mcp/mcpProtocolHandler.js +22 -4
  21. package/dist/mcp/specMemServer.js +16 -3
  22. package/dist/mcp/toolRegistry.js +19 -21
  23. package/dist/tools/goofy/checkSyncStatus.js +14 -7
  24. package/dist/watcher/fileWatcher.js +57 -20
  25. package/dist/watcher/index.js +26 -0
  26. package/dist/watcher/syncChecker.js +11 -7
  27. package/package.json +1 -1
  28. package/scripts/global-postinstall.cjs +7 -2
  29. package/scripts/specmem-init.cjs +5 -0
  30. package/specmem/model-config.json +26 -6
  31. package/specmem/supervisord.conf +1 -1
  32. package/specmem/user-config.json +12 -0
  33. package/svg-sections/readme-install.svg +35 -29
  34. package/svg-sections/readme-whats-new.svg +120 -114
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 -->
package/bootstrap.cjs CHANGED
@@ -4919,6 +4919,25 @@ async function autoInstallThisMf() {
4919
4919
  // Non-fatal - MCP server will retry
4920
4920
  }
4921
4921
 
4922
+ // Acquire socket lock so statusbar/health checks can detect us
4923
+ const projectPath_uf = getProjectPath();
4924
+ const lockAcquired_uf = tryAcquireSocketLock(projectPath_uf);
4925
+ if (lockAcquired_uf) {
4926
+ writeProjectPidFile(projectPath_uf, process.pid);
4927
+ writeInstanceState(projectPath_uf, {
4928
+ pid: process.pid,
4929
+ projectPath: projectPath_uf,
4930
+ projectHash: hashProjectPath(projectPath_uf),
4931
+ startTime: new Date().toISOString(),
4932
+ status: 'running',
4933
+ bootstrapVersion: '1.0.0',
4934
+ mode: 'ultra-fast'
4935
+ });
4936
+ startupLog('Ultra-fast path: socket lock acquired, PID file written');
4937
+ } else {
4938
+ startupLog('Ultra-fast path: could not acquire socket lock (non-fatal)');
4939
+ }
4940
+
4922
4941
  // Start server BEFORE any other operations
4923
4942
  // The server handles its own deferred initialization
4924
4943
  // CRITICAL: startServer() is now async and imports the ES module directly
@@ -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
+ });
@@ -55,6 +55,14 @@
55
55
  {
56
56
  "matcher": "Read",
57
57
  "hooks": [
58
+ {
59
+ "type": "command",
60
+ "command": "node /root/.claude/hooks/specmem-search-enforcer.cjs",
61
+ "timeout": 2,
62
+ "env": {
63
+ "SPECMEM_PROJECT_PATH": "${cwd}"
64
+ }
65
+ },
58
66
  {
59
67
  "type": "command",
60
68
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -84,6 +92,14 @@
84
92
  {
85
93
  "matcher": "Edit",
86
94
  "hooks": [
95
+ {
96
+ "type": "command",
97
+ "command": "node /root/.claude/hooks/specmem-search-enforcer.cjs",
98
+ "timeout": 2,
99
+ "env": {
100
+ "SPECMEM_PROJECT_PATH": "${cwd}"
101
+ }
102
+ },
87
103
  {
88
104
  "type": "command",
89
105
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -97,6 +113,11 @@
97
113
  {
98
114
  "matcher": "Write",
99
115
  "hooks": [
116
+ {
117
+ "type": "command",
118
+ "command": "node /root/.claude/hooks/specmem-search-enforcer.cjs",
119
+ "timeout": 2
120
+ },
100
121
  {
101
122
  "type": "command",
102
123
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -110,6 +131,11 @@
110
131
  {
111
132
  "matcher": "Grep",
112
133
  "hooks": [
134
+ {
135
+ "type": "command",
136
+ "command": "node /root/.claude/hooks/specmem-search-enforcer.cjs",
137
+ "timeout": 2
138
+ },
113
139
  {
114
140
  "type": "command",
115
141
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -147,6 +173,11 @@
147
173
  {
148
174
  "matcher": "Glob",
149
175
  "hooks": [
176
+ {
177
+ "type": "command",
178
+ "command": "node /root/.claude/hooks/specmem-search-enforcer.cjs",
179
+ "timeout": 2
180
+ },
150
181
  {
151
182
  "type": "command",
152
183
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -184,6 +215,22 @@
184
215
  {
185
216
  "matcher": "Bash",
186
217
  "hooks": [
218
+ {
219
+ "type": "command",
220
+ "command": "node /root/.claude/hooks/specmem-search-enforcer.cjs",
221
+ "timeout": 2,
222
+ "env": {
223
+ "SPECMEM_PROJECT_PATH": "${cwd}"
224
+ }
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
+ },
187
234
  {
188
235
  "type": "command",
189
236
  "command": "node /root/.claude/hooks/team-comms-enforcer.cjs",
@@ -254,6 +301,31 @@
254
301
  "env": {
255
302
  "SPECMEM_PROJECT_PATH": "${cwd}"
256
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
257
329
  }
258
330
  ]
259
331
  }
@@ -306,6 +378,66 @@
306
378
  }
307
379
  ],
308
380
  "PostToolUse": [
381
+ {
382
+ "matcher": "Grep",
383
+ "hooks": [
384
+ {
385
+ "type": "command",
386
+ "command": "node /root/.claude/hooks/specmem-search-tracker.cjs",
387
+ "timeout": 5
388
+ }
389
+ ]
390
+ },
391
+ {
392
+ "matcher": "Glob",
393
+ "hooks": [
394
+ {
395
+ "type": "command",
396
+ "command": "node /root/.claude/hooks/specmem-search-tracker.cjs",
397
+ "timeout": 5
398
+ }
399
+ ]
400
+ },
401
+ {
402
+ "matcher": "Read",
403
+ "hooks": [
404
+ {
405
+ "type": "command",
406
+ "command": "node /root/.claude/hooks/specmem-search-tracker.cjs",
407
+ "timeout": 5
408
+ }
409
+ ]
410
+ },
411
+ {
412
+ "matcher": "mcp__specmem__find_memory",
413
+ "hooks": [
414
+ {
415
+ "type": "command",
416
+ "command": "node /root/.claude/hooks/specmem-search-tracker.cjs",
417
+ "timeout": 5
418
+ }
419
+ ]
420
+ },
421
+ {
422
+ "matcher": "mcp__specmem__find_code_pointers",
423
+ "hooks": [
424
+ {
425
+ "type": "command",
426
+ "command": "node /root/.claude/hooks/specmem-search-tracker.cjs",
427
+ "timeout": 5
428
+ }
429
+ ]
430
+ },
431
+ {
432
+ "matcher": "mcp__specmem__drill_down",
433
+ "hooks": [
434
+ {
435
+ "type": "command",
436
+ "command": "node /root/.claude/hooks/specmem-search-tracker.cjs",
437
+ "timeout": 5
438
+ }
439
+ ]
440
+ },
309
441
  {
310
442
  "matcher": "Task",
311
443
  "hooks": [
@@ -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