specmem-hardwicksoftware 3.7.31 → 3.7.33

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 -->
@@ -779,7 +779,7 @@ function manageProxy(args) {
779
779
  switch (sub) {
780
780
  case 'disable':
781
781
  case 'off': {
782
- // Create disabled flag
782
+ // Create disabled flag — daemon stays running but in passthrough mode
783
783
  try {
784
784
  if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
785
785
  fs.writeFileSync(disabledFile, new Date().toISOString(), { mode: 0o644 });
@@ -788,40 +788,17 @@ function manageProxy(args) {
788
788
  process.exit(1);
789
789
  }
790
790
 
791
- // Remove port file so wrapper won't try
792
- try { if (fs.existsSync(portFile)) fs.unlinkSync(portFile); } catch (e) { /* ok */ }
793
-
794
- // Kill the running proxy if it's up
795
- const port = process.env.COMPACTION_PROXY_PORT || '4080';
791
+ // Switch the running proxy to paused (passthrough) mode — don't kill it
792
+ const disablePort = process.env.COMPACTION_PROXY_PORT || '4080';
796
793
  try {
797
- execSync(`curl -sf http://127.0.0.1:${port}/health`, { timeout: 1000, stdio: 'pipe' });
798
- // Proxy is running find and kill it
799
- try {
800
- const pid = execSync(`lsof -ti tcp:${port} -s tcp:listen 2>/dev/null`, { encoding: 'utf8' }).trim();
801
- if (pid) {
802
- process.kill(parseInt(pid), 'SIGTERM');
803
- console.log(`${GREEN}✓${RESET} Killed running proxy (PID ${pid})`);
804
- }
805
- } catch (e) { /* couldn't find pid, that's ok */ }
794
+ execSync(`curl -sf -X POST http://127.0.0.1:${disablePort}/pause`, { timeout: 2000, stdio: 'pipe' });
795
+ console.log(`${GREEN}✓${RESET} Proxy switched to ${YELLOW}passthrough${RESET} mode`);
806
796
  } catch (e) {
807
- // Proxy not running
797
+ // Proxy not running — that's fine, flag will keep next start paused
808
798
  }
809
799
 
810
- // Remove the bashrc block that sets ANTHROPIC_BASE_URL so new shells don't get it
811
- try {
812
- const bashrc = path.join(os.homedir(), '.bashrc');
813
- if (fs.existsSync(bashrc)) {
814
- const content = fs.readFileSync(bashrc, 'utf8');
815
- const cleaned = content.replace(/\n?# specmem-proxy-env\n(?:# [^\n]*\n)*if \[ -f "\$HOME\/\.claude\/\.compaction-proxy-port" \];[\s\S]*?fi\n?/g, '\n');
816
- if (cleaned !== content) {
817
- fs.writeFileSync(bashrc, cleaned);
818
- console.log(`${GREEN}✓${RESET} Removed proxy env from .bashrc`);
819
- }
820
- }
821
- } catch (e) { /* non-fatal */ }
822
-
823
- console.log(`${GREEN}✓${RESET} Compaction proxy ${RED}disabled${RESET}`);
824
- console.log(` ${DIM}Proxy won't start on next session${RESET}`);
800
+ console.log(`${GREEN}✓${RESET} Compaction proxy ${RED}disabled${RESET} (passthrough traffic still flows)`);
801
+ console.log(` ${DIM}Proxy stays running but passes all requests through untouched${RESET}`);
825
802
  console.log(`\n ${DIM}Re-enable with: ${CYAN}specmem proxy enable${RESET}`);
826
803
  break;
827
804
  }
@@ -831,9 +808,17 @@ function manageProxy(args) {
831
808
  // Remove disabled flag
832
809
  try { if (fs.existsSync(disabledFile)) fs.unlinkSync(disabledFile); } catch (e) { /* ok */ }
833
810
 
811
+ // Unpause the running proxy — activate compression immediately
812
+ const enablePort = process.env.COMPACTION_PROXY_PORT || '4080';
813
+ try {
814
+ execSync(`curl -sf -X POST http://127.0.0.1:${enablePort}/resume`, { timeout: 2000, stdio: 'pipe' });
815
+ console.log(`${GREEN}✓${RESET} Proxy compression ${GREEN}activated${RESET}`);
816
+ } catch (e) {
817
+ // Proxy not running — will start active on next session
818
+ }
819
+
834
820
  console.log(`${GREEN}✓${RESET} Compaction proxy ${GREEN}enabled${RESET}`);
835
- console.log(` ${DIM}Proxy will start on next Claude session${RESET}`);
836
- console.log(` ${DIM}Or restart Claude now to activate immediately${RESET}`);
821
+ console.log(` ${DIM}Compression active immediately (no restart needed)${RESET}`);
837
822
  break;
838
823
  }
839
824
 
package/bootstrap.cjs CHANGED
@@ -955,7 +955,11 @@ function syncProjectConfigs() {
955
955
  // Backup and write
956
956
  const backupPath = `${claudeJsonPath}.backup.${Date.now()}`;
957
957
  fs.copyFileSync(claudeJsonPath, backupPath);
958
- fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
958
+ // ATOMIC WRITE: write to temp file then rename to prevent corruption
959
+ // fs.writeFileSync truncates first, so Claude Code can read a partial file mid-write
960
+ const tmpPath = claudeJsonPath + '.tmp.' + process.pid;
961
+ fs.writeFileSync(tmpPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
962
+ fs.renameSync(tmpPath, claudeJsonPath);
959
963
  startupLog(`CONFIG SYNC: Fixed ${fixedProjects.length} stale project configs: ${fixedProjects.join(', ')}`);
960
964
 
961
965
  // Clean up old backups (keep last 3)
@@ -734,53 +734,37 @@ async function loadSkills() {
734
734
  function getMinimalTeamContext() {
735
735
  return `
736
736
 
737
- [MANDATORY-FIRST-ACTION]
738
- Before doing ANYTHING else, you MUST announce yourself:
739
- send_team_message({type:"status", message:"Starting: [your task summary]"})
740
- If you skip this, ALL your tool calls will be BLOCKED. No exceptions.
741
- [/MANDATORY-FIRST-ACTION]
742
-
743
- [TEAM CONTEXT]
744
- You are a developer on a software development team. The senior engineer on this team is skeptical of AI assistance, so you must perform your tasks efficiently and communicate well with your fellow developers.
745
-
746
- ⚠️ CRITICAL: YOU MUST USE MCP TOOLS TO COMPLETE YOUR TASK ⚠️
747
- There is NO alternative path. The system ENFORCES MCP tool usage:
748
- - You CANNOT proceed without announcing via send_team_message FIRST
749
- - You CANNOT write code without claiming files via claim_task
750
- - You CANNOT search more than 3 times without using find_memory/find_code_pointers
751
- - Every 4 tool calls, you WILL BE BLOCKED until you check read_team_messages (team comms)
752
- - Every 5 tool calls, you WILL BE BLOCKED until you check read_team_messages with include_broadcasts:true
753
- - Every 8 tool calls, you WILL BE BLOCKED until you check get_team_status for help requests
754
- - These counters apply to ALL tool calls, including memory tools and searches - no exceptions
755
- If you want to complete your goal, you MUST use MCP tools. There is no workaround.
756
-
757
- TEAM COMMUNICATION (mcp__specmem__ prefix):
758
- - send_team_message({message, channel:"main"|"swarm-1".."swarm-5", type:"status"|"question"|"update", priority})
759
- - read_team_messages({limit:5, include_swarms:true}) - MANDATORY every 4 tool calls!
760
- - read_team_messages({include_broadcasts:true, include_swarms:true, limit:10}) - MANDATORY every 5 calls!
761
- - broadcast_to_team({message, broadcast_type:"status"|"progress"|"announcement", priority})
762
- - claim_task({description, files:["path1","path2"]}) - REQUIRED before editing
763
- - release_task({claimId:"all"|"<id>"}) - release when done
764
- - get_team_status({}) - see active claims, help requests, team activity
765
- - request_help({question, context, skills_needed:["skill1"]}) - ASK OTHER SWARMS FOR HELP!
766
- - respond_to_help({requestId, response}) - HELP OTHER SWARMS! Helping hands make the world go round!
767
-
768
- MEMORY TOOLS (MANDATORY - enforced by hooks):
769
- - find_memory({query, limit:10}) - semantic search for past conversations and decisions
770
- - find_code_pointers({query, limit:10, includeTracebacks:true}) - semantic code search with callers/callees
771
- - save_memory({content, importance, memoryType, tags}) - save important findings
772
- - drill_down({drilldownID}) - get full context on a memory result
773
- - getMemoryFull({id}) - get full memory with live code
774
-
775
- WORKFLOW (enforced - you cannot skip steps):
776
- 1. START: send_team_message({type:"status", message:"Starting: [task]"})
777
- 2. CLAIM: claim_task({description, files}) - REQUIRED before any writes
778
- 3. SEARCH: find_memory/find_code_pointers FIRST, then Grep/Glob if needed
779
- 4. EVERY 4 CALLS: read_team_messages({include_swarms:true, limit:5}) - MANDATORY, you WILL be blocked
780
- 5. EVERY 5 CALLS: read_team_messages({include_broadcasts:true, include_swarms:true, limit:10}) - MANDATORY
781
- 6. EVERY 8 CALLS: get_team_status() - check if anyone needs help!
782
- 6. DONE: release_task({claimId:"all"}), send completion status
783
- [/TEAM CONTEXT]`;
737
+ [TEAM COLLABORATION]
738
+ Welcome! You're joining a team of developers working on this project together. To keep things running smoothly and avoid stepping on each other's toes, here's how the team stays coordinated:
739
+
740
+ GETTING STARTED:
741
+ First, let the team know you're here and what you'll be working on:
742
+ send_team_message({type:"status", message:"Starting: [your task summary]"})
743
+
744
+ SEARCHING FOR CODE & CONTEXT:
745
+ The project has semantic search tools that are much more effective than grep — use these first:
746
+ find_code_pointers({query:"what you're looking for"}) semantic code search with tracebacks, shows who calls what
747
+ find_memory({query:"topic"}) finds past discussions, decisions, and context from the user and other team members
748
+ drill_down({drilldownID: N}) expand a result from find_code_pointers or find_memory for full detail
749
+ If semantic search doesn't cover it, Grep/Glob are still available as a fallback.
750
+
751
+ EDITING FILES:
752
+ To avoid merge conflicts with other team members:
753
+ 1. claim_task({description:"what you're doing", files:["path/to/file"]}) claim before editing
754
+ 2. Make your edits
755
+ 3. release_task({claimId:"all"}) release so others can work on those files
756
+ If a file is already claimed by someone else, coordinate via send_team_message first.
757
+
758
+ STAYING IN SYNC:
759
+ Check in with the team periodically other team members may have updates that affect your work:
760
+ read_team_messages({include_swarms:true, limit:5}) see recent team activity
761
+ get_team_status() — see who's working on what, and if anyone needs help
762
+ respond_to_help({requestId, response}) if you can help a teammate, do it!
763
+
764
+ WHEN YOU'RE DONE:
765
+ release_task({claimId:"all"}) free your claimed files
766
+ send_team_message({type:"status", message:"Completed: [summary of what you did]"})
767
+ [/TEAM COLLABORATION]`;
784
768
  }
785
769
 
786
770
  // ============================================================================
@@ -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