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 +22 -0
- package/README.md +12 -0
- package/claude-hooks/bash-call-enforcer.cjs +140 -0
- package/claude-hooks/settings.json +33 -0
- package/claude-hooks/specmem-drilldown-hook.cjs +49 -2
- package/claude-hooks/specmem-drilldown-hook.js +49 -2
- package/claude-hooks/specmem-drilldown-hook.js.bak +495 -0
- package/claude-hooks/specmem-precompact.cjs +13 -36
- package/claude-hooks/specmem-precompact.js +3 -7
- package/claude-hooks/specmem-session-start.cjs +38 -50
- package/claude-hooks/specmem-session-start.js +19 -60
- package/dist/mcp/compactionProxy.js +21 -1
- package/dist/watcher/index.js +26 -0
- package/package.json +1 -1
- package/specmem/model-config.json +2 -2
- package/specmem/supervisord.conf +1 -1
- package/svg-sections/readme-install.svg +35 -29
- 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>
|
|
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
|
+
<code>npm install -g specmem-hardwicksoftware</code> — 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 || '
|
|
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 || '
|
|
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
|