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.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -0
- package/bootstrap.cjs +19 -0
- package/claude-hooks/bash-call-enforcer.cjs +140 -0
- package/claude-hooks/settings.json +132 -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-search-enforcer.cjs +229 -0
- package/claude-hooks/specmem-search-tracker.cjs +71 -0
- package/claude-hooks/specmem-session-start.cjs +38 -50
- package/claude-hooks/specmem-session-start.js +19 -60
- package/dist/config.js +11 -16
- package/dist/db/connectionPoolGoBrrr.js +3 -3
- package/dist/index.js +21 -4
- package/dist/mcp/compactionProxy.js +21 -1
- package/dist/mcp/embeddingServerManager.js +15 -1
- package/dist/mcp/mcpProtocolHandler.js +22 -4
- package/dist/mcp/specMemServer.js +16 -3
- package/dist/mcp/toolRegistry.js +19 -21
- package/dist/tools/goofy/checkSyncStatus.js +14 -7
- package/dist/watcher/fileWatcher.js +57 -20
- package/dist/watcher/index.js +26 -0
- package/dist/watcher/syncChecker.js +11 -7
- package/package.json +1 -1
- package/scripts/global-postinstall.cjs +7 -2
- package/scripts/specmem-init.cjs +5 -0
- package/specmem/model-config.json +26 -6
- package/specmem/supervisord.conf +1 -1
- package/specmem/user-config.json +12 -0
- 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 -->
|
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 || '
|
|
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
|