specmem-hardwicksoftware 3.7.42 → 3.7.44

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.
@@ -24,10 +24,21 @@ function isAgent() {
24
24
  if (process.env.CLAUDE_SUBAGENT === '1') return true;
25
25
  if (process.env.CLAUDE_AGENT_ID) return true;
26
26
 
27
+ // CLAUDE_CODE_ENTRYPOINT=task is set by Agent tool when spawning subagents
28
+ if (process.env.CLAUDE_CODE_ENTRYPOINT === 'task') return true;
29
+
27
30
  // Check if this is a team member screen session (has specmem-tm- prefix)
28
- // Regular screens are claude-<project>, not specmem-tm-
31
+ // Regular Claude screens are claude-<project>, not specmem-tm-
29
32
  if (process.env.STY && process.env.STY.includes('specmem-tm-')) return true;
30
33
 
34
+ // Debug: log env vars for troubleshooting
35
+ const relevantEnvVars = Object.keys(process.env)
36
+ .filter(k => k.includes('AGENT') || k.includes('SUBAGENT') || k.includes('TASK') || k.includes('ENTRYPOINT'))
37
+ .reduce((acc, k) => ({ ...acc, [k]: process.env[k] }), {});
38
+ if (Object.keys(relevantEnvVars).length > 0) {
39
+ console.error('[is-agent] Relevant env vars found:', relevantEnvVars);
40
+ }
41
+
31
42
  // Check working directory for agent-specific paths
32
43
  try {
33
44
  const cwd = process.cwd();
@@ -37,7 +48,7 @@ function isAgent() {
37
48
  }
38
49
  } catch (e) {}
39
50
 
40
- // Default: NOT an agent (main session gets unrestricted access)
51
+ // Default: NOT an agent (main session gets unrestricted access)
41
52
  return false;
42
53
  }
43
54
 
@@ -46,7 +57,8 @@ function isAgent() {
46
57
  */
47
58
  function isSubagent() {
48
59
  return process.env.CLAUDE_SUBAGENT === '1' ||
49
- process.env.CLAUDE_AGENT_ID !== undefined;
60
+ process.env.CLAUDE_AGENT_ID !== undefined ||
61
+ process.env.CLAUDE_CODE_ENTRYPOINT === 'task';
50
62
  }
51
63
 
52
64
  /**
@@ -54,6 +66,7 @@ function isSubagent() {
54
66
  */
55
67
  function isTeamMember() {
56
68
  return process.env.SPECMEM_TEAM_MEMBER_ID !== undefined ||
69
+ process.env.CLAUDE_CODE_ENTRYPOINT === 'task' ||
57
70
  (process.env.STY && process.env.STY.includes('specmem-tm-'));
58
71
  }
59
72
 
@@ -76,10 +76,11 @@ try {
76
76
  // ============================================================================
77
77
  // CONFIGURATION
78
78
  // ============================================================================
79
- const MAX_SEARCHES_BEFORE_BLOCK = 2; // Every other search must use find_code_pointers/find_memory
80
- const TEAM_COMMS_CHECK_INTERVAL = 3; // MUST send_team_message every 3 tool usages
81
- const BROADCAST_CHECK_INTERVAL = 5; // MUST read_team_messages w/ include_broadcasts every 5 tool usages
82
- const HELP_CHECK_INTERVAL = 8; // Check help requests every 8 tool usages
79
+ const MAX_SEARCHES_BEFORE_BLOCK = 1; // STRICT: Every 1 Grep/Glob/Read must use find_code_pointers/find_memory
80
+ const TEAM_COMMS_CHECK_INTERVAL = 3; // MUST send_team_message every 3 Grep/Glob/Read (not every tool)
81
+ const BROADCAST_CHECK_INTERVAL = 5; // MUST read_team_messages w/ include_broadcasts every 5 Grep/Glob/Read
82
+ const HELP_CHECK_INTERVAL = 8; // Check help requests every 8 Grep/Glob/Read
83
+ const MEMORY_USAGE_INTERVAL = 3; // MUST use memory tools every 3 Grep/Glob/Read
83
84
 
84
85
  // Tools that count as "announcing"
85
86
  const ANNOUNCE_TOOLS = [
@@ -115,8 +116,8 @@ const HELP_CHECK_TOOLS = [
115
116
  ];
116
117
 
117
118
  // Basic search tools (limited before requiring memory tools)
118
- // NOTE: Read is NOT included agents abuse Read to reset search counters
119
- const BASIC_SEARCH_TOOLS = ['Grep', 'Glob'];
119
+ // NOTE: Read IS included now - agents were abusing it to bypass limits
120
+ const BASIC_SEARCH_TOOLS = ['Grep', 'Glob', 'Read'];
120
121
 
121
122
  // READ + SEARCH combined — forces team msg every 3 reads OR searches
122
123
  const READ_SEARCH_TOOLS = ['Read', 'Grep', 'Glob'];
@@ -184,6 +185,7 @@ function getAgentState(tracking, sessionId) {
184
185
  commsToolCount: 0, // Tool calls since last team comms check (every 4)
185
186
  broadcastToolCount: 0, // Tool calls since last broadcast check (every 5)
186
187
  helpToolUsageCount: 0, // Tool calls since last help check (every 8)
188
+ memoryToolCount: 0, // Tool calls since last memory tool usage (every 3)
187
189
  lastCommsCheck: Date.now(),
188
190
  lastBroadcastCheck: Date.now(),
189
191
  lastHelpCheck: Date.now(),
@@ -258,6 +260,7 @@ function isSpecmemEnabled() {
258
260
  function blockResponse(tool, reason) {
259
261
  // Each denial specifies the EXACT tool to call. Framed as team collaboration,
260
262
  // not enforcement. "team member" language, never "agent".
263
+ console.error(`[TEAM COMMS] BLOCKING: ${tool} - ${reason}`);
261
264
  const msg = `[TEAM COLLABORATION - call ${tool} then retry] ${reason}`;
262
265
  return JSON.stringify({
263
266
  hookSpecificOutput: {
@@ -298,19 +301,24 @@ process.stdin.on('end', () => {
298
301
  const toolName = data.tool_name;
299
302
  const sessionId = data.session_id || 'unknown';
300
303
 
304
+ // DEBUG: Log detection status
305
+ const runningAsAgent = isRunningAsAgent();
306
+ const isSpecmem = isSpecmemProject();
307
+ console.error(`[TEAM COMMS] Tool: ${toolName}, RunningAsAgent: ${runningAsAgent}, IsSpecmemProject: ${isSpecmem}`);
308
+
301
309
  // ========================================================================
302
310
  // AGENT DETECTION - ONLY ENFORCE ON AGENT SESSIONS
303
311
  // Uses env vars (CLAUDE_SUBAGENT, CLAUDE_AGENT_ID, etc.) + subagent tracking
304
312
  // Main Claude window is NEVER blocked, only subagents/team members
305
313
  // ========================================================================
306
- if (!isRunningAsAgent()) {
314
+ if (!runningAsAgent) {
307
315
  // Not an agent - pass through immediately
308
316
  console.log(JSON.stringify({ continue: true }));
309
317
  return;
310
318
  }
311
319
 
312
320
  // Verify this is a specmem-enabled project
313
- if (!isSpecmemProject()) {
321
+ if (!isSpecmem) {
314
322
  console.log(JSON.stringify({ continue: true }));
315
323
  return;
316
324
  }
@@ -386,6 +394,7 @@ process.stdin.on('end', () => {
386
394
  if (MEMORY_TOOLS.includes(toolName)) {
387
395
  state.usedMemoryTools = true;
388
396
  state.searchCount = 0; // Reset search counter — allows next 2 searches
397
+ state.memoryToolCount = 0; // Reset memory tool counter - they just used one!
389
398
  // usedMemoryTools resets to false after 2 more searches (see BASIC_SEARCH_TOOLS block)
390
399
  }
391
400
  // Track Read/Search count for team comms cadence
@@ -430,9 +439,25 @@ process.stdin.on('end', () => {
430
439
  state.commsToolCount = (state.commsToolCount || 0) + 1;
431
440
  state.broadcastToolCount = (state.broadcastToolCount || 0) + 1;
432
441
  state.helpToolUsageCount = (state.helpToolUsageCount || 0) + 1;
442
+ // Count all tools for memory usage enforcement
443
+ state.memoryToolCount = (state.memoryToolCount || 0) + 1;
444
+
445
+ // ========================================================================
446
+ // HARD BLOCK: Must use memory tools every 3 tool usages
447
+ // find_code_pointers, find_memory, drill_down satisfy this
448
+ // ========================================================================
449
+ if (state.memoryToolCount >= MEMORY_USAGE_INTERVAL && !MEMORY_TOOLS.includes(toolName)) {
450
+ state.blockedCount++;
451
+ saveTracking(tracking);
452
+ console.log(blockResponse(
453
+ 'mcp__specmem__find_code_pointers',
454
+ `Use semantic search to find context! Call: find_code_pointers({query:"what you need"}) or find_memory({query:"topic from conversation"})`
455
+ ));
456
+ return;
457
+ }
433
458
 
434
459
  // ========================================================================
435
- // HARD BLOCK: Must send team message every 3 tool usages
460
+ // HARD BLOCK: Must send team message every 4 tool usages
436
461
  // send_team_message() or broadcast_to_team() satisfies this
437
462
  // ========================================================================
438
463
  if (state.commsToolCount >= TEAM_COMMS_CHECK_INTERVAL && !ANNOUNCE_TOOLS.includes(toolName)) {
@@ -824,27 +824,50 @@ export class EmbeddingServerManager extends EventEmitter {
824
824
  }
825
825
  /**
826
826
  * Release the file-based start lock
827
+ * FIX: Use rename-to-temp + unlink for atomic release (prevents race condition)
827
828
  */
828
829
  releaseStartLock() {
829
830
  try {
830
831
  if (existsSync(this.startLockPath)) {
831
832
  // FIX 2.14: Verify ownership before releasing - only the lock owner should release
833
+ // FIX 2.15: Use atomic rename-then-unlink to prevent race conditions
834
+ let isOwner = false;
832
835
  try {
833
836
  const lockContent = readFileSync(this.startLockPath, 'utf8').trim();
834
837
  const parts = lockContent.split(':');
835
838
  const lockPid = parseInt(parts[1], 10);
836
- if (!isNaN(lockPid) && lockPid !== process.pid) {
839
+ if (!isNaN(lockPid) && lockPid === process.pid) {
840
+ isOwner = true;
841
+ } else if (!isNaN(lockPid) && lockPid !== process.pid) {
837
842
  logger.warn({ lockPid, myPid: process.pid }, '[EmbeddingServerManager] Lock release by non-owner - ignoring');
838
843
  return false;
839
844
  }
845
+ // If no valid PID in lock, consider ourselves owner to clean up
846
+ isOwner = true;
840
847
  }
841
848
  catch (readErr) {
842
849
  // Can't verify ownership - proceed with release (lock may be corrupt)
843
850
  logger.debug({ error: readErr }, '[EmbeddingServerManager] Cannot read lock for ownership check, releasing anyway');
851
+ isOwner = true;
852
+ }
853
+
854
+ if (isOwner) {
855
+ // Atomic release: rename to temp path, then unlink
856
+ // This prevents race where another process acquires lock between read and delete
857
+ const tempPath = this.startLockPath + '.releasing.' + process.pid;
858
+ try {
859
+ // First, try atomic rename (will fail if lock was taken)
860
+ renameSync(this.startLockPath, tempPath);
861
+ // If rename succeeded, we own it - now safely unlink
862
+ unlinkSync(tempPath);
863
+ logger.debug('[EmbeddingServerManager] Released start lock (atomic)');
864
+ return true;
865
+ } catch (renameErr) {
866
+ // Rename failed - another process acquired the lock
867
+ logger.debug({ error: renameErr }, '[EmbeddingServerManager] Lock was taken during release, ignoring');
868
+ return false;
869
+ }
844
870
  }
845
- unlinkSync(this.startLockPath);
846
- logger.debug('[EmbeddingServerManager] Released start lock');
847
- return true;
848
871
  }
849
872
  return true; // No lock to release
850
873
  }
@@ -731,10 +731,12 @@ Examples:
731
731
  storage: isDBAvailable() ? 'postgresql' : 'memory'
732
732
  });
733
733
  // Human-readable response
734
+ const typeIcon = { status: '🔄', question: '❓', update: '📝', broadcast: '📢', help_request: '🆘', help_response: '💡' }[type] || '💬';
735
+ const priIcon = { urgent: ' ‼️URGENT', high: ' ❗', normal: '', low: '' }[priority] || '';
734
736
  return {
735
737
  content: [{
736
738
  type: 'text',
737
- text: `sent: ${type} ${channel}\n${message.slice(0, 120)}${message.length > 120 ? '...' : ''}`
739
+ text: `✅ ${typeIcon} Message sent to #${channel}${priIcon}\n "${message.slice(0, 100)}${message.length > 100 ? '...' : ''}"`
738
740
  }]
739
741
  };
740
742
  }
@@ -1091,28 +1093,28 @@ Returns messages sorted by newest first.`;
1091
1093
  help_request: '🆘',
1092
1094
  help_response: '💡'
1093
1095
  };
1094
- const priorityTag = { urgent: '‼️', high: '❗', normal: '', low: '' };
1096
+ const priorityTag = { urgent: ' ‼️URGENT', high: ' ❗', normal: '', low: '' };
1095
1097
  const formatTimeAgo = (date) => {
1096
1098
  const now = Date.now();
1097
1099
  const then = new Date(date).getTime();
1098
1100
  const diffSec = Math.floor((now - then) / 1000);
1099
- if (diffSec < 60) return `${diffSec}s ago`;
1101
+ if (diffSec < 60) return `${diffSec}s`;
1100
1102
  const diffMin = Math.floor(diffSec / 60);
1101
- if (diffMin < 60) return `${diffMin}m ago`;
1103
+ if (diffMin < 60) return `${diffMin}m`;
1102
1104
  const diffHr = Math.floor(diffMin / 60);
1103
- if (diffHr < 24) return `${diffHr}h ago`;
1104
- return `${Math.floor(diffHr / 24)}d ago`;
1105
+ if (diffHr < 24) return `${diffHr}h`;
1106
+ return `${Math.floor(diffHr / 24)}d`;
1105
1107
  };
1106
1108
  const formatMessage = (m) => {
1107
1109
  const emoji = typeEmoji[m.type] || '💬';
1108
- const unread = m.is_unread ? ' ★NEW' : '';
1110
+ const unread = m.is_unread ? ' ' : '';
1109
1111
  const pri = priorityTag[m.priority] || '';
1110
1112
  const time = formatTimeAgo(m.timestamp);
1111
1113
  const from = m.sender_name || m.sender || '?';
1112
1114
  // Content already compressed by verified compressor above
1113
- const content = m.content.slice(0, 400).replace(/\n/g, ' ');
1114
- const truncated = m.content.length > 400 ? '…' : '';
1115
- return ` ${pri}${emoji}${unread} ${from} (${time}): ${content}${truncated}`;
1115
+ const content = m.content.slice(0, 300).replace(/\n/g, ' ');
1116
+ const truncated = m.content.length > 300 ? '…' : '';
1117
+ return ` ${emoji}${unread}${pri} ${from} [${time}]: ${content}${truncated}`;
1116
1118
  };
1117
1119
  // Group messages by channel for sortable output
1118
1120
  const byChannel = {};
@@ -1133,15 +1135,15 @@ Returns messages sorted by newest first.`;
1133
1135
  const msgs = byChannel[ch];
1134
1136
  const chUnread = msgs.filter(m => m.is_unread).length;
1135
1137
  const chLabel = chUnread > 0 ? `#${ch} (${chUnread} new)` : `#${ch}`;
1136
- msgList += `\n── ${chLabel} ──\n`;
1138
+ msgList += `\n┌─ ${chLabel} ─\n`;
1137
1139
  msgList += msgs.map(m => formatMessage(m)).join('\n');
1138
1140
  msgList += '\n';
1139
1141
  }
1140
- const header = `[SPECMEM-TEAM-MESSAGES] ${messages.length} msgs ${unreadCount} unread`;
1141
- const footer = `[/SPECMEM-TEAM-MESSAGES]`;
1142
+ const header = `📬 Team Messages (${messages.length} total, ${unreadCount} unread)`;
1143
+ const footer = '─────────────────────────';
1142
1144
  const output = messages.length > 0
1143
- ? `${header}${msgList}${footer}`
1144
- : `${header}\n No messages\n${footer}`;
1145
+ ? `${header}${msgList}\n${footer}`
1146
+ : `${header}\n\n No messages yet\n${footer}`;
1145
1147
  return { content: [{ type: 'text', text: output }] };
1146
1148
  }
1147
1149
  }
@@ -1260,10 +1262,11 @@ Use cross_project: true for system-wide announcements (use sparingly).`;
1260
1262
  storage: isDBAvailable() ? 'postgresql' : 'memory'
1261
1263
  });
1262
1264
  // Human-readable response
1265
+ const bcastIcon = { status: '🔄', update: '📢', alert: '🚨', milestone: '🎉' }[broadcast_type] || '📣';
1263
1266
  return {
1264
1267
  content: [{
1265
1268
  type: 'text',
1266
- text: `[BROADCAST] ${broadcast_type} sent to team (id: ${messageId.slice(0, 8)})`
1269
+ text: `📣 ${bcastIcon} Broadcast sent to ALL teams\n "${message.slice(0, 80)}${message.length > 80 ? '...' : ''}"`
1267
1270
  }]
1268
1271
  };
1269
1272
  }
@@ -1374,14 +1377,14 @@ Best practice: Always claim before starting work on a task.`;
1374
1377
  warnings,
1375
1378
  storage: isDBAvailable() ? 'postgresql' : 'memory'
1376
1379
  });
1377
- // Ultra-compact return
1380
+ // Human-readable return
1381
+ const fileListText = files && files.length > 0 ? `\n Files: ${files.join(', ')}` : '';
1382
+ const warningText = warnings && warnings.length > 0 ? `\n ⚠️ ${warnings.join(' | ')}` : '';
1378
1383
  return {
1379
- success: true,
1380
- claimId: claimId.slice(0, 8),
1381
- description: stripNewlines(description).slice(0, 50),
1382
- files,
1383
- timestamp: claimedAt,
1384
- warnings
1384
+ content: [{
1385
+ type: 'text',
1386
+ text: `✅ Task claimed: ${description.slice(0, 50)}${description.length > 50 ? '...' : ''}\n ID: ${claimId.slice(0, 8)}${fileListText}${warningText}`
1387
+ }]
1385
1388
  };
1386
1389
  }
1387
1390
  }
@@ -1537,14 +1540,16 @@ Best practice: Release files as you finish them, release entire claim when task
1537
1540
  memberId,
1538
1541
  storage: isDBAvailable() ? 'postgresql' : 'memory'
1539
1542
  });
1540
- // Ultra-compact
1543
+ // Human-readable
1544
+ const filesMsg = releasedFiles.length > 0 ? ` (${releasedFiles.length} files)` : '';
1541
1545
  const message = releasedClaims.length === 1
1542
- ? `Released claim ${releasedClaims[0].slice(0, 8)}`
1543
- : `Released ${releasedClaims.length} claims`;
1546
+ ? `✅ Released: ${releasedClaims[0].slice(0, 8)}${filesMsg}`
1547
+ : `✅ Released ${releasedClaims.length} claims${filesMsg}`;
1544
1548
  return {
1545
- success: true,
1546
- releasedClaims: releasedClaims.map(id => id.slice(0, 8)),
1547
- message
1549
+ content: [{
1550
+ type: 'text',
1551
+ text: message
1552
+ }]
1548
1553
  };
1549
1554
  }
1550
1555
  }
@@ -1737,12 +1742,12 @@ respond using the respond_to_help tool.`;
1737
1742
  skills_needed,
1738
1743
  storage: isDBAvailable() ? 'postgresql' : 'memory'
1739
1744
  });
1740
- // Ultra-compact
1745
+ // Human-readable
1741
1746
  return {
1742
- success: true,
1743
- requestId: requestId.slice(0, 8),
1744
- timestamp: requestedAt,
1745
- message: 'Help request broadcasted to team'
1747
+ content: [{
1748
+ type: 'text',
1749
+ text: `🆘 Help request sent to team\n "${question.slice(0, 60)}${question.length > 60 ? '...' : ''}"`
1750
+ }]
1746
1751
  };
1747
1752
  }
1748
1753
  }
@@ -1859,13 +1864,12 @@ will be notified.`;
1859
1864
  respondedBy,
1860
1865
  storage: isDBAvailable() ? 'postgresql' : 'memory'
1861
1866
  });
1862
- // Ultra-compact
1867
+ // Human-readable
1863
1868
  return {
1864
- success: true,
1865
- responseId: responseId.slice(0, 8),
1866
- requestId: requestId.slice(0, 8),
1867
- timestamp: respondedAt,
1868
- message: `Response sent to ${requester}`
1869
+ content: [{
1870
+ type: 'text',
1871
+ text: `💡 Response sent to @${requester}\n "${response.slice(0, 60)}${response.length > 60 ? '...' : ''}"`
1872
+ }]
1869
1873
  };
1870
1874
  }
1871
1875
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmem-hardwicksoftware",
3
- "version": "3.7.42",
3
+ "version": "3.7.44",
4
4
  "type": "module",
5
5
  "description": "Your Claude Code sessions don't have to start from scratch anymore — SpecMem gives your AI real memory. It won't forget your conversations, your code, or your architecture decisions between sessions. That's the whole point. Semantic code indexing that actually works: TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C, C++, HTML and more. It doesn't just track functions — it gets classes, methods, fields, constants, enums, macros, imports, structs, the whole codebase graph. There's chat memory too, powered by pgvector embeddings. You've also got token compression, team coordination, multi-agent comms, and file watching built in. 74+ MCP tools. Runs on PostgreSQL + Docker. It's kind of a big deal. justcalljon.pro",
6
6
  "main": "dist/index.js",
@@ -9058,6 +9058,18 @@ CREATE INDEX IF NOT EXISTS idx_embedding_queue_project ON embedding_queue (proje
9058
9058
  const claudeDir = path.join(projectPath, '.claude');
9059
9059
  safeMkdir(path.join(claudeDir, 'commands'));
9060
9060
 
9061
+ // Deploy HOW_TO_USE.md to project root
9062
+ const howToUseSrc = path.join(__dirname, '..', 'HOW_TO_USE_SPECMEM_MCP.md');
9063
+ const howToUseDest = path.join(projectPath, 'HOW_TO_USE.md');
9064
+ if (fs.existsSync(howToUseSrc)) {
9065
+ try {
9066
+ fs.copyFileSync(howToUseSrc, howToUseDest);
9067
+ initLog('[SETUP] Deployed HOW_TO_USE.md to project root');
9068
+ } catch (e) {
9069
+ initLog('[SETUP] Failed to deploy HOW_TO_USE.md: ' + e.message);
9070
+ }
9071
+ }
9072
+
9061
9073
  setupUI.setSubProgress(1);
9062
9074
  setupUI.setSubStatus('✓ Project dirs ready');
9063
9075
  await qqms();
@@ -1,6 +1,6 @@
1
1
  ; ============================================
2
2
  ; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
3
- ; Generated by specmem-init at 2026-03-06T01:46:58.539Z
3
+ ; Generated by specmem-init at 2026-03-09T18:56:25.444Z
4
4
  ; Thread counts from model-config.json resourcePool
5
5
  ; ============================================
6
6