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 +22 -0
- package/README.md +12 -0
- package/bin/specmem-cli.cjs +18 -33
- package/bootstrap.cjs +5 -1
- package/claude-hooks/agent-loading-hook.js +31 -47
- 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/claude-hooks/subagent-loading-hook.js +1 -1
- package/claude-hooks/team-comms-enforcer.cjs +112 -93
- package/dist/config/configSync.js +4 -1
- package/dist/index.js +44 -1
- package/dist/init/claudeConfigInjector.js +4 -1
- package/dist/installer/autoInstall.js +4 -1
- package/dist/mcp/compactionProxy.js +648 -73
- package/dist/mcp/compactionProxyDaemon.js +18 -4
- package/dist/mcp/specMemServer.js +8 -0
- package/dist/watcher/index.js +40 -0
- package/package.json +1 -1
- package/scripts/deploy-hooks.cjs +4 -1
- package/scripts/specmem-init.cjs +31 -35
- package/scripts/specmem-uninstall.cjs +4 -1
- package/specmem/model-config.json +4 -4
- package/specmem/supervisord.conf +1 -1
- package/svg-sections/readme-install.svg +94 -52
- 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/bin/specmem-cli.cjs
CHANGED
|
@@ -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
|
-
//
|
|
792
|
-
|
|
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:${
|
|
798
|
-
|
|
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
|
-
|
|
811
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
[
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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 || '
|
|
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
|