specmem-hardwicksoftware 3.7.32 → 3.7.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to SpecMem - we keep it real with semantic versioning. Deada
4
4
 
5
5
  ---
6
6
 
7
+ ## [3.7.34] - 2026-02-26
8
+
9
+ ### Fixed
10
+ - System reminder dedup bug — `String.replace(string, '')` only removes first occurrence; duplicate identical reminders in same message block now fully nuked via `while(includes)` loop
11
+ - Edit tool stripping bug — `smartStripEdit(block)` was passing the whole tool_use block instead of `block.input`, causing null return and no stripping of Edit payloads in old messages
12
+
13
+ ---
14
+
15
+ ## [3.7.33] - 2026-02-26
16
+
17
+ ### Fixed
18
+ - Token compaction proxy reliability — resolved connection drops and socket failures under sustained load
19
+ - Multi-project registry for translation socket resolution — daemon now iterates registered projects to find active socket instead of hardcoded path
20
+ - System reminder stripping — strips redundant `<system-reminder>` blocks (keeps first occurrence), reduces token overhead per request
21
+ - Compaction proxy minimum translate length removed — all content now eligible for compression regardless of size
22
+
23
+ ### Improved
24
+ - Token usage efficiency — advanced stenographic compression now applied to all tool results and system prompts, measurably lower token consumption per session
25
+ - Agent behavior modification hooks — agents now properly communicate via team messaging tools and use SpecMem tool calls as expected
26
+ - Tool call enforcement — bash call enforcer upgraded with better debouncing and team comms validation
27
+ - Team communications enforcer — improved reliability of inter-agent message routing
28
+ - Multi-project resource sharing — compaction daemon tracks project registry with cache invalidation on changes
29
+ - README updated with new "How to Install on Linux Systems" SVG featuring Debian, Ubuntu, Ubuntu LTS, Mint, and Kali Linux
30
+
31
+ ### Added
32
+ - Project registry system in compaction proxy — tracks all registered projects for shared translation memory and synonym caches
33
+ - Dynamic translate socket resolution across registered projects (sorted by last-seen)
34
+ - `SYSTEM_REMINDER_STRIPPING` live config toggle
35
+ - New skill commands for agent orchestration and autoclaude workflows
36
+
37
+ ---
38
+
7
39
  ## [3.7.32] - 2026-02-24
8
40
 
9
41
  ### Fixed
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ## Token compression currently broken - please run specmem proxy off before running specmem init - until this is fixed!
2
+
1
3
  <!-- Debian/Ubuntu/Mint/Kali notice -->
2
4
  <div align="center">
3
5
  <table><tr><td align="center" style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:12px 20px">
@@ -12,8 +14,8 @@
12
14
 
13
15
  <div align="center">
14
16
 
15
- <!-- Demo GIF -->
16
- <img src="./IMG_4365.gif" alt="SpecMem Demo" width="400">
17
+ <!-- How to Install SVG -->
18
+ <img src="./svg-sections/readme-how-to-install.svg" alt="How to Install SpecMem on Linux" width="800">
17
19
 
18
20
  <br/>
19
21
  <br/>
@@ -779,7 +779,7 @@ function manageProxy(args) {
779
779
  switch (sub) {
780
780
  case 'disable':
781
781
  case 'off': {
782
- // Create disabled flag
782
+ // Create disabled flag — daemon stays running but in passthrough mode
783
783
  try {
784
784
  if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
785
785
  fs.writeFileSync(disabledFile, new Date().toISOString(), { mode: 0o644 });
@@ -788,40 +788,17 @@ function manageProxy(args) {
788
788
  process.exit(1);
789
789
  }
790
790
 
791
- // Remove port file so wrapper won't try
792
- try { if (fs.existsSync(portFile)) fs.unlinkSync(portFile); } catch (e) { /* ok */ }
793
-
794
- // Kill the running proxy if it's up
795
- const port = process.env.COMPACTION_PROXY_PORT || '4080';
791
+ // Switch the running proxy to paused (passthrough) mode — don't kill it
792
+ const disablePort = process.env.COMPACTION_PROXY_PORT || '4080';
796
793
  try {
797
- execSync(`curl -sf http://127.0.0.1:${port}/health`, { timeout: 1000, stdio: 'pipe' });
798
- // Proxy is running find and kill it
799
- try {
800
- const pid = execSync(`lsof -ti tcp:${port} -s tcp:listen 2>/dev/null`, { encoding: 'utf8' }).trim();
801
- if (pid) {
802
- process.kill(parseInt(pid), 'SIGTERM');
803
- console.log(`${GREEN}✓${RESET} Killed running proxy (PID ${pid})`);
804
- }
805
- } catch (e) { /* couldn't find pid, that's ok */ }
794
+ execSync(`curl -sf -X POST http://127.0.0.1:${disablePort}/pause`, { timeout: 2000, stdio: 'pipe' });
795
+ console.log(`${GREEN}✓${RESET} Proxy switched to ${YELLOW}passthrough${RESET} mode`);
806
796
  } catch (e) {
807
- // Proxy not running
797
+ // Proxy not running — that's fine, flag will keep next start paused
808
798
  }
809
799
 
810
- // Remove the bashrc block that sets ANTHROPIC_BASE_URL so new shells don't get it
811
- try {
812
- const bashrc = path.join(os.homedir(), '.bashrc');
813
- if (fs.existsSync(bashrc)) {
814
- const content = fs.readFileSync(bashrc, 'utf8');
815
- const cleaned = content.replace(/\n?# specmem-proxy-env\n(?:# [^\n]*\n)*if \[ -f "\$HOME\/\.claude\/\.compaction-proxy-port" \];[\s\S]*?fi\n?/g, '\n');
816
- if (cleaned !== content) {
817
- fs.writeFileSync(bashrc, cleaned);
818
- console.log(`${GREEN}✓${RESET} Removed proxy env from .bashrc`);
819
- }
820
- }
821
- } catch (e) { /* non-fatal */ }
822
-
823
- console.log(`${GREEN}✓${RESET} Compaction proxy ${RED}disabled${RESET}`);
824
- console.log(` ${DIM}Proxy won't start on next session${RESET}`);
800
+ console.log(`${GREEN}✓${RESET} Compaction proxy ${RED}disabled${RESET} (passthrough traffic still flows)`);
801
+ console.log(` ${DIM}Proxy stays running but passes all requests through untouched${RESET}`);
825
802
  console.log(`\n ${DIM}Re-enable with: ${CYAN}specmem proxy enable${RESET}`);
826
803
  break;
827
804
  }
@@ -831,9 +808,17 @@ function manageProxy(args) {
831
808
  // Remove disabled flag
832
809
  try { if (fs.existsSync(disabledFile)) fs.unlinkSync(disabledFile); } catch (e) { /* ok */ }
833
810
 
811
+ // Unpause the running proxy — activate compression immediately
812
+ const enablePort = process.env.COMPACTION_PROXY_PORT || '4080';
813
+ try {
814
+ execSync(`curl -sf -X POST http://127.0.0.1:${enablePort}/resume`, { timeout: 2000, stdio: 'pipe' });
815
+ console.log(`${GREEN}✓${RESET} Proxy compression ${GREEN}activated${RESET}`);
816
+ } catch (e) {
817
+ // Proxy not running — will start active on next session
818
+ }
819
+
834
820
  console.log(`${GREEN}✓${RESET} Compaction proxy ${GREEN}enabled${RESET}`);
835
- console.log(` ${DIM}Proxy will start on next Claude session${RESET}`);
836
- console.log(` ${DIM}Or restart Claude now to activate immediately${RESET}`);
821
+ console.log(` ${DIM}Compression active immediately (no restart needed)${RESET}`);
837
822
  break;
838
823
  }
839
824
 
package/bootstrap.cjs CHANGED
@@ -955,7 +955,11 @@ function syncProjectConfigs() {
955
955
  // Backup and write
956
956
  const backupPath = `${claudeJsonPath}.backup.${Date.now()}`;
957
957
  fs.copyFileSync(claudeJsonPath, backupPath);
958
- fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
958
+ // ATOMIC WRITE: write to temp file then rename to prevent corruption
959
+ // fs.writeFileSync truncates first, so Claude Code can read a partial file mid-write
960
+ const tmpPath = claudeJsonPath + '.tmp.' + process.pid;
961
+ fs.writeFileSync(tmpPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
962
+ fs.renameSync(tmpPath, claudeJsonPath);
959
963
  startupLog(`CONFIG SYNC: Fixed ${fixedProjects.length} stale project configs: ${fixedProjects.join(', ')}`);
960
964
 
961
965
  // Clean up old backups (keep last 3)
@@ -734,53 +734,37 @@ async function loadSkills() {
734
734
  function getMinimalTeamContext() {
735
735
  return `
736
736
 
737
- [MANDATORY-FIRST-ACTION]
738
- Before doing ANYTHING else, you MUST announce yourself:
739
- send_team_message({type:"status", message:"Starting: [your task summary]"})
740
- If you skip this, ALL your tool calls will be BLOCKED. No exceptions.
741
- [/MANDATORY-FIRST-ACTION]
742
-
743
- [TEAM CONTEXT]
744
- You are a developer on a software development team. The senior engineer on this team is skeptical of AI assistance, so you must perform your tasks efficiently and communicate well with your fellow developers.
745
-
746
- ⚠️ CRITICAL: YOU MUST USE MCP TOOLS TO COMPLETE YOUR TASK ⚠️
747
- There is NO alternative path. The system ENFORCES MCP tool usage:
748
- - You CANNOT proceed without announcing via send_team_message FIRST
749
- - You CANNOT write code without claiming files via claim_task
750
- - You CANNOT search more than 3 times without using find_memory/find_code_pointers
751
- - Every 4 tool calls, you WILL BE BLOCKED until you check read_team_messages (team comms)
752
- - Every 5 tool calls, you WILL BE BLOCKED until you check read_team_messages with include_broadcasts:true
753
- - Every 8 tool calls, you WILL BE BLOCKED until you check get_team_status for help requests
754
- - These counters apply to ALL tool calls, including memory tools and searches - no exceptions
755
- If you want to complete your goal, you MUST use MCP tools. There is no workaround.
756
-
757
- TEAM COMMUNICATION (mcp__specmem__ prefix):
758
- - send_team_message({message, channel:"main"|"swarm-1".."swarm-5", type:"status"|"question"|"update", priority})
759
- - read_team_messages({limit:5, include_swarms:true}) - MANDATORY every 4 tool calls!
760
- - read_team_messages({include_broadcasts:true, include_swarms:true, limit:10}) - MANDATORY every 5 calls!
761
- - broadcast_to_team({message, broadcast_type:"status"|"progress"|"announcement", priority})
762
- - claim_task({description, files:["path1","path2"]}) - REQUIRED before editing
763
- - release_task({claimId:"all"|"<id>"}) - release when done
764
- - get_team_status({}) - see active claims, help requests, team activity
765
- - request_help({question, context, skills_needed:["skill1"]}) - ASK OTHER SWARMS FOR HELP!
766
- - respond_to_help({requestId, response}) - HELP OTHER SWARMS! Helping hands make the world go round!
767
-
768
- MEMORY TOOLS (MANDATORY - enforced by hooks):
769
- - find_memory({query, limit:10}) - semantic search for past conversations and decisions
770
- - find_code_pointers({query, limit:10, includeTracebacks:true}) - semantic code search with callers/callees
771
- - save_memory({content, importance, memoryType, tags}) - save important findings
772
- - drill_down({drilldownID}) - get full context on a memory result
773
- - getMemoryFull({id}) - get full memory with live code
774
-
775
- WORKFLOW (enforced - you cannot skip steps):
776
- 1. START: send_team_message({type:"status", message:"Starting: [task]"})
777
- 2. CLAIM: claim_task({description, files}) - REQUIRED before any writes
778
- 3. SEARCH: find_memory/find_code_pointers FIRST, then Grep/Glob if needed
779
- 4. EVERY 4 CALLS: read_team_messages({include_swarms:true, limit:5}) - MANDATORY, you WILL be blocked
780
- 5. EVERY 5 CALLS: read_team_messages({include_broadcasts:true, include_swarms:true, limit:10}) - MANDATORY
781
- 6. EVERY 8 CALLS: get_team_status() - check if anyone needs help!
782
- 6. DONE: release_task({claimId:"all"}), send completion status
783
- [/TEAM CONTEXT]`;
737
+ [TEAM COLLABORATION]
738
+ Welcome! You're joining a team of developers working on this project together. To keep things running smoothly and avoid stepping on each other's toes, here's how the team stays coordinated:
739
+
740
+ GETTING STARTED:
741
+ First, let the team know you're here and what you'll be working on:
742
+ send_team_message({type:"status", message:"Starting: [your task summary]"})
743
+
744
+ SEARCHING FOR CODE & CONTEXT:
745
+ The project has semantic search tools that are much more effective than grep — use these first:
746
+ find_code_pointers({query:"what you're looking for"}) semantic code search with tracebacks, shows who calls what
747
+ find_memory({query:"topic"}) finds past discussions, decisions, and context from the user and other team members
748
+ drill_down({drilldownID: N}) expand a result from find_code_pointers or find_memory for full detail
749
+ If semantic search doesn't cover it, Grep/Glob are still available as a fallback.
750
+
751
+ EDITING FILES:
752
+ To avoid merge conflicts with other team members:
753
+ 1. claim_task({description:"what you're doing", files:["path/to/file"]}) claim before editing
754
+ 2. Make your edits
755
+ 3. release_task({claimId:"all"}) release so others can work on those files
756
+ If a file is already claimed by someone else, coordinate via send_team_message first.
757
+
758
+ STAYING IN SYNC:
759
+ Check in with the team periodically other team members may have updates that affect your work:
760
+ read_team_messages({include_swarms:true, limit:5}) see recent team activity
761
+ get_team_status() — see who's working on what, and if anyone needs help
762
+ respond_to_help({requestId, response}) if you can help a teammate, do it!
763
+
764
+ WHEN YOU'RE DONE:
765
+ release_task({claimId:"all"}) free your claimed files
766
+ send_team_message({type:"status", message:"Completed: [summary of what you did]"})
767
+ [/TEAM COLLABORATION]`;
784
768
  }
785
769
 
786
770
  // ============================================================================
@@ -141,7 +141,7 @@ function handleSubagentStart(data) {
141
141
  // CRITICAL: The announcement requirement is MANDATORY and ENFORCED by team-comms-enforcer.cjs
142
142
  // Make it PROMINENT and FIRST in the context so agents don't miss it
143
143
  const shortPurpose = purpose.slice(0, 30).replace(/"/g, "'");
144
- const loadingContext = '[AGENT-' + agentNum + '-MANDATORY-STARTUP] YOUR FIRST ACTION MUST BE: send_team_message({type:"status", message:"Starting: ' + shortPurpose + '"}) | WARNING: You WILL BE BLOCKED from ALL other tools until you announce via send_team_message. This is ENFORCED by hooks - no bypass possible. | Available MCP tools: send_team_message, read_team_messages, claim_task, release_task, get_team_status, find_memory, find_code_pointers | When done: send_team_message({type:"status", message:"Completed: [summary]"})';
144
+ const loadingContext = '[TEAM-MEMBER-' + agentNum + '] Hey! First, let the team know you are here: send_team_message({type:"status", message:"Starting: ' + shortPurpose + '"}) | For code search use find_code_pointers(), for past discussions use find_memory(), for detail use drill_down() | Before editing: claim_task(), after editing: release_task() | When done: send_team_message({type:"status", message:"Completed: [summary]"})';
145
145
 
146
146
  // suppressOutput: true = Context gets processed but doesn't spam terminal
147
147
  // The agent will output its OWN progress via team messages
@@ -127,7 +127,7 @@ const WRITE_TOOLS = ['Edit', 'Write', 'NotebookEdit'];
127
127
  // - Task: can spawn sub-agents to bypass limits
128
128
  const FULL_COMPLIANCE_TOOLS = ['Bash', 'Task'];
129
129
 
130
- // Tools that are always allowed (reading team state + cross-swarm help)
130
+ // Tools that are always allowed (reading team state + cross-swarm help + research)
131
131
  const ALWAYS_ALLOWED = [
132
132
  'mcp__specmem__read_team_messages',
133
133
  'mcp__specmem__get_team_status',
@@ -144,7 +144,12 @@ const ALWAYS_ALLOWED = [
144
144
  'mcp__specmem__save_memory',
145
145
  // Cross-swarm help - helping hands make the world go round!
146
146
  'mcp__specmem__request_help',
147
- 'mcp__specmem__respond_to_help'
147
+ 'mcp__specmem__respond_to_help',
148
+ // Research tools — read-only, never block these
149
+ 'WebFetch',
150
+ 'WebSearch',
151
+ 'ToolSearch',
152
+ 'Read',
148
153
  ];
149
154
 
150
155
  // ============================================================================
@@ -214,25 +219,36 @@ function isSpecmemProject() {
214
219
  */
215
220
  function isRunningAsAgent() {
216
221
  // Method 1: Environment variable detection (most reliable)
217
- // ONLY enforce for deployed team members, NOT built-in subagents (Explore, Plan, etc.)
218
- // Built-in subagents have CLAUDE_SUBAGENT=1 but no MCP tools, so enforcement blocks them permanently.
222
+ // Deployed team members always enforce
219
223
  if (isTeamMemberFn()) return true;
220
224
 
221
- // Method 2: Check subagent tracking (from subagent-loading-hook.js SubagentStart)
222
- try {
223
- const agentsFile = `${PROJECT_TMP_DIR}/agents.json`;
224
- if (fs.existsSync(agentsFile)) {
225
- const data = JSON.parse(fs.readFileSync(agentsFile, 'utf8'));
226
- const now = Date.now();
227
- for (const agent of Object.values(data.agents || {})) {
228
- // Agent started within last 10 min AND has no endTime = still running
229
- if (!agent.endTime && agent.startTime && (now - agent.startTime < 600000)) {
230
- return true;
225
+ // Method 2: General-purpose subagents (CLAUDE_SUBAGENT=1)
226
+ // These DO have MCP tools and SHOULD be enforced.
227
+ // Exclude Explore/Plan agents — they don't have MCP tools and can't comply.
228
+ // We check agents.json to see if the active subagent has MCP tools.
229
+ if (process.env.CLAUDE_SUBAGENT === '1' || process.env.CLAUDE_AGENT_ID) {
230
+ try {
231
+ const agentsFile = `${PROJECT_TMP_DIR}/agents.json`;
232
+ if (fs.existsSync(agentsFile)) {
233
+ const data = JSON.parse(fs.readFileSync(agentsFile, 'utf8'));
234
+ const now = Date.now();
235
+ for (const agent of Object.values(data.agents || {})) {
236
+ // Active agent (started within 10 min, no endTime)
237
+ if (!agent.endTime && agent.startTime && (now - agent.startTime < 600000)) {
238
+ // Check if this agent has MCP tools (general-purpose agents do)
239
+ const tools = agent.tools || [];
240
+ const hasMcpTools = tools.some(t => t.startsWith('mcp__specmem__'));
241
+ if (hasMcpTools) return true;
242
+ }
231
243
  }
232
244
  }
233
- }
234
- } catch {}
245
+ } catch {}
246
+ // No agents.json or no MCP tools found — this is likely Explore/Plan, skip enforcement
247
+ return false;
248
+ }
235
249
 
250
+ // Method 3: Check subagent tracking as fallback (parent context seeing active agents)
251
+ // This does NOT enforce on the parent — only on processes with CLAUDE_SUBAGENT=1
236
252
  return false;
237
253
  }
238
254
 
@@ -255,12 +271,15 @@ function isSpecmemEnabled() {
255
271
  // ============================================================================
256
272
  // BLOCK RESPONSE BUILDERS
257
273
  // ============================================================================
258
- function blockResponse(reason) {
274
+ function blockResponse(tool, reason) {
275
+ // Each denial specifies the EXACT tool to call. Framed as team collaboration,
276
+ // not enforcement. "team member" language, never "agent".
277
+ const msg = `[TEAM COLLABORATION - call ${tool} then retry] ${reason}`;
259
278
  return JSON.stringify({
260
279
  hookSpecificOutput: {
261
280
  hookEventName: 'PreToolUse',
262
281
  permissionDecision: 'deny',
263
- permissionDecisionReason: reason
282
+ permissionDecisionReason: msg
264
283
  }
265
284
  });
266
285
  }
@@ -397,12 +416,8 @@ process.stdin.on('end', () => {
397
416
  state.blockedCount++;
398
417
  saveTracking(tracking);
399
418
  console.log(blockResponse(
400
- `[BLOCKED] You MUST ANNOUNCE yourself first!\n\n` +
401
- `This is your MANDATORY FIRST ACTION before any other tool:\n\n` +
402
- `send_team_message({type:"status", message:"Starting: [describe your task]"})\n\n` +
403
- `Note: If you were assigned to a specific channel (e.g. swarm-1, swarm-2), use that channel.\n` +
404
- `Otherwise, the message will go to the main channel.\n\n` +
405
- `After announcing, you can proceed with other tools.`
419
+ 'mcp__specmem__send_team_message',
420
+ `Let other team members know what you're working on so nobody duplicates effort. Call: send_team_message({type:"status", message:"Starting: [your task]"})`
406
421
  ));
407
422
  return;
408
423
  }
@@ -424,10 +439,8 @@ process.stdin.on('end', () => {
424
439
  state.blockedCount++;
425
440
  saveTracking(tracking);
426
441
  console.log(blockResponse(
427
- `[BLOCKED] MANDATORY team comms check! (${state.commsToolCount} tools since last check)\n\n` +
428
- `REQUIRED: read_team_messages({include_swarms: true, limit: 5})\n\n` +
429
- `You MUST check team messages every 4 tool calls. This is non-negotiable.\n` +
430
- `Other agents may have critical updates for you. CHECK NOW.`
442
+ 'mcp__specmem__read_team_messages',
443
+ `Quick check-in — other team members may have updates that affect your work. Call: read_team_messages({include_swarms: true, limit: 5})`
431
444
  ));
432
445
  return;
433
446
  }
@@ -441,10 +454,8 @@ process.stdin.on('end', () => {
441
454
  state.blockedCount++;
442
455
  saveTracking(tracking);
443
456
  console.log(blockResponse(
444
- `[BLOCKED] MANDATORY broadcast check! (${state.broadcastToolCount} tools since last broadcast check)\n\n` +
445
- `REQUIRED: read_team_messages({include_broadcasts: true, include_swarms: true, limit: 10})\n\n` +
446
- `You MUST check broadcasts every 5 tool calls. This is non-negotiable.\n` +
447
- `Team-wide announcements and status updates require your attention. CHECK NOW.`
457
+ 'mcp__specmem__read_team_messages',
458
+ `Time to check for team-wide announcements — there may be important updates. Call: read_team_messages({include_broadcasts: true, include_swarms: true, limit: 10})`
448
459
  ));
449
460
  return;
450
461
  }
@@ -457,11 +468,8 @@ process.stdin.on('end', () => {
457
468
  state.blockedCount++;
458
469
  saveTracking(tracking);
459
470
  console.log(blockResponse(
460
- `[BLOCKED] Time to check if anyone needs help! (${state.helpToolUsageCount} tools since last check)\n\n` +
461
- `REQUIRED: get_team_status()\n\n` +
462
- `This shows open help requests from ALL swarms. Helping hands make the world go round!\n` +
463
- `If you see a request you can help with, use respond_to_help().\n` +
464
- `After checking, you can continue working.`
471
+ 'mcp__specmem__get_team_status',
472
+ `Quick check — another team member might need a hand. Call: get_team_status() — if someone needs help, respond_to_help().`
465
473
  ));
466
474
  return;
467
475
  }
@@ -480,7 +488,7 @@ process.stdin.on('end', () => {
480
488
  // ========================================================================
481
489
  if (state.commsToolCount === TEAM_COMMS_CHECK_INTERVAL - 1) {
482
490
  console.log(allowWithReminder(
483
- `[HEADS UP] Next tool call will require team comms check. Do it now: read_team_messages({include_swarms: true, limit: 5})`
491
+ `Heads up good time to check in with the team: read_team_messages({include_swarms: true, limit: 5})`
484
492
  ));
485
493
  // Don't return - continue to other checks
486
494
  }
@@ -499,12 +507,8 @@ process.stdin.on('end', () => {
499
507
  state.blockedCount++;
500
508
  saveTracking(tracking);
501
509
  console.log(blockResponse(
502
- `[BLOCKED] ${state.searchCount} searches without using memory tools!\n\n` +
503
- `YOU MUST USE THESE FIRST:\n` +
504
- `• find_memory({query:"your search"}) - semantic memory search\n` +
505
- `• find_code_pointers({query:"your search"}) - semantic code search with tracebacks\n\n` +
506
- `These are MORE POWERFUL than Grep/Glob. USE THEM NOW.\n` +
507
- `This resets every ${MAX_SEARCHES_BEFORE_BLOCK} searches — you must keep using them.`
510
+ 'mcp__specmem__find_code_pointers',
511
+ `Grep/Glob haven't found what you need — try semantic search instead, it's much more effective. Call: find_code_pointers({query:"what you're looking for"}) — if you need more detail on a result, use drill_down(). To find what another team member or the user said, use find_memory({query:"topic"}).`
508
512
  ));
509
513
  return;
510
514
  }
@@ -513,7 +517,7 @@ process.stdin.on('end', () => {
513
517
  if (state.searchCount === MAX_SEARCHES_BEFORE_BLOCK) {
514
518
  saveTracking(tracking);
515
519
  console.log(allowWithReminder(
516
- `[WARNING] Last search before BLOCK! Use find_memory() or find_code_pointers() NOW or next search will be blocked.`
520
+ `Tip: find_code_pointers() and find_memory() are much faster for semantic searches try them next.`
517
521
  ));
518
522
  return;
519
523
  }
@@ -532,50 +536,63 @@ process.stdin.on('end', () => {
532
536
  const toolInput = data.tool_input || {};
533
537
  const filePath = toolInput.file_path || '';
534
538
 
535
- if (!state.claimed) {
536
- issues.push(`claim_task({description:"what you're doing", files:["${filePath || 'file.ts'}"]}) - CLAIM FIRST`);
537
- }
538
- if (!state.usedMemoryTools) {
539
- issues.push(`find_memory() or find_code_pointers() to understand the code first`);
540
- }
541
-
542
- // Check file claims — agent must have THIS file in their own claim
543
- // Uses GLOBAL claims file so cross-channel/cross-swarm agents see each other
539
+ // Check file claims — to avoid conflicts with other team members
540
+ let fileInOtherClaim = false;
541
+ let otherClaimDesc = '';
542
+ let fileInOwnClaim = false;
544
543
  if (filePath && state.claimed) {
545
544
  try {
546
545
  let claims = {};
547
546
  if (fs.existsSync(GLOBAL_CLAIMS_FILE)) {
548
547
  claims = JSON.parse(fs.readFileSync(GLOBAL_CLAIMS_FILE, 'utf8'));
549
548
  }
550
- let fileInOwnClaim = false;
551
- let fileInOtherClaim = false;
552
- let otherClaimDesc = '';
553
- for (const [claimId, claim] of Object.entries(claims)) {
554
- if (claim.files && claim.files.some(f => filePath.endsWith(f) || f.endsWith(filePath) || f === filePath)) {
555
- if (claim.sessionId === sessionId || claim.agentId === sessionId) {
556
- fileInOwnClaim = true;
557
- } else {
558
- fileInOtherClaim = true;
559
- otherClaimDesc = claim.description || claim.agentId || 'another agent';
560
- }
549
+ for (const [claimId, claim] of Object.entries(claims)) {
550
+ if (claim.files && claim.files.some(f => filePath.endsWith(f) || f.endsWith(filePath) || f === filePath)) {
551
+ if (claim.sessionId === sessionId || claim.agentId === sessionId) {
552
+ fileInOwnClaim = true;
553
+ } else {
554
+ fileInOtherClaim = true;
555
+ otherClaimDesc = claim.description || claim.agentId || 'another team member';
561
556
  }
562
557
  }
563
- if (fileInOtherClaim) {
564
- issues.push(`File "${filePath}" is claimed by: ${otherClaimDesc}\n Wait for them to release_task() or coordinate via send_team_message()`);
565
- } else if (!fileInOwnClaim) {
566
- issues.push(`File "${filePath}" is NOT in your claim!\n Update your claim: claim_task({description:"your task", files:["${filePath}"]})`);
567
- }
558
+ }
568
559
  } catch (e) {}
569
560
  }
570
561
 
571
- if (issues.length > 0) {
562
+ // Determine the specific blocker and give a targeted message
563
+ if (fileInOtherClaim) {
572
564
  state.blockedCount++;
573
565
  saveTracking(tracking);
574
566
  console.log(blockResponse(
575
- `[BLOCKED] Cannot ${toolName} without proper preparation!\n\n` +
576
- `YOU MUST DO THESE FIRST:\n` +
577
- issues.map((i, idx) => `${idx + 1}. ${i}`).join('\n') + `\n\n` +
578
- `Then retry your ${toolName}.`
567
+ 'mcp__specmem__send_team_message',
568
+ `"${filePath}" is currently being edited by ${otherClaimDesc}. To avoid merge conflicts, coordinate with them first. Call: send_team_message({type:"status", message:"Waiting on ${filePath} — is it free yet?"})`
569
+ ));
570
+ return;
571
+ }
572
+ if (!state.claimed) {
573
+ state.blockedCount++;
574
+ saveTracking(tracking);
575
+ console.log(blockResponse(
576
+ 'mcp__specmem__claim_task',
577
+ `To prevent edit conflicts with other team members, claim the file before editing. Call: claim_task({description:"${(filePath || 'editing').slice(0, 40)}", files:["${filePath || 'file'}"]})`
578
+ ));
579
+ return;
580
+ }
581
+ if (!fileInOwnClaim && filePath) {
582
+ state.blockedCount++;
583
+ saveTracking(tracking);
584
+ console.log(blockResponse(
585
+ 'mcp__specmem__claim_task',
586
+ `"${filePath}" isn't in your current claim. Update it so other team members know you're working here. Call: claim_task({description:"your task", files:["${filePath}"]})`
587
+ ));
588
+ return;
589
+ }
590
+ if (!state.usedMemoryTools) {
591
+ state.blockedCount++;
592
+ saveTracking(tracking);
593
+ console.log(blockResponse(
594
+ 'mcp__specmem__find_code_pointers',
595
+ `Before editing, check if there's existing context about this code. Call: find_code_pointers({query:"${(filePath || 'what you are editing').slice(0, 50)}"}) — or find_memory() to see what the user/team discussed about it.`
579
596
  ));
580
597
  return;
581
598
  }
@@ -601,11 +618,8 @@ process.stdin.on('end', () => {
601
618
  state.blockedCount++;
602
619
  saveTracking(tracking);
603
620
  console.log(blockResponse(
604
- `[BLOCKED] You edited a file but didn't release your claim!\n\n` +
605
- `File edited: ${state.editedFiles[state.editedFiles.length - 1]}\n\n` +
606
- `REQUIRED: release_task({claimId:"${state.currentClaimId || 'your-claim-id'}"})\n\n` +
607
- `Protocol: claim_task → Edit/Write → release_task (every time).\n` +
608
- `Release now, then claim_task again for your next edit.`
621
+ 'mcp__specmem__release_task',
622
+ `You're done editing ${state.editedFiles[state.editedFiles.length - 1]} — release the claim so other team members can work on it. Call: release_task({claimId:"${state.currentClaimId || 'your-claim-id'}"})`
609
623
  ));
610
624
  return;
611
625
  }
@@ -618,26 +632,31 @@ process.stdin.on('end', () => {
618
632
  if (FULL_COMPLIANCE_TOOLS.includes(toolName)) {
619
633
  const issues = [];
620
634
 
635
+ // Give the FIRST missing step as a specific tool call
621
636
  if (!state.announced) {
622
- issues.push(`send_team_message({message:"Starting: [task]", type:"status"}) - ANNOUNCE FIRST`);
637
+ state.blockedCount++;
638
+ saveTracking(tracking);
639
+ console.log(blockResponse(
640
+ 'mcp__specmem__send_team_message',
641
+ `Let the team know what you're working on first. Call: send_team_message({type:"status", message:"Starting: [your task]"})`
642
+ ));
643
+ return;
623
644
  }
624
645
  if (!state.claimed) {
625
- issues.push(`claim_task({description:"what you're doing"}) - CLAIM YOUR WORK`);
646
+ state.blockedCount++;
647
+ saveTracking(tracking);
648
+ console.log(blockResponse(
649
+ 'mcp__specmem__claim_task',
650
+ `Claim your task so other team members don't overlap. Call: claim_task({description:"what you're doing"})`
651
+ ));
652
+ return;
626
653
  }
627
654
  if (!state.usedMemoryTools) {
628
- issues.push(`find_memory() or find_code_pointers() - USE SEMANTIC SEARCH FIRST`);
629
- }
630
-
631
- if (issues.length > 0) {
632
655
  state.blockedCount++;
633
656
  saveTracking(tracking);
634
- const toolType = toolName === 'Task' ? 'sub-agents' : 'Bash';
635
657
  console.log(blockResponse(
636
- `[BLOCKED] ${toolName} requires FULL protocol compliance!\n\n` +
637
- `Nice try - no bypassing through ${toolType}.\n\n` +
638
- `YOU MUST DO ALL OF THESE FIRST:\n` +
639
- issues.map((i, idx) => `${idx + 1}. ${i}`).join('\n') + `\n\n` +
640
- `NO SHORTCUTS. Follow the protocol.`
658
+ 'mcp__specmem__find_code_pointers',
659
+ `Check existing context first it'll save time. Call: find_code_pointers({query:"what you need"}) or find_memory({query:"topic"})`
641
660
  ));
642
661
  return;
643
662
  }
@@ -654,7 +673,7 @@ process.stdin.on('end', () => {
654
673
 
655
674
  if (!state.usedMemoryTools && state.searchCount > 0) {
656
675
  console.log(allowWithReminder(
657
- `[REMINDER] Use find_memory() and find_code_pointers() for better results!`
676
+ `Tip: find_code_pointers() gives semantic code search with tracebacks, and find_memory() finds what the user or team discussed — both faster than grep.`
658
677
  ));
659
678
  return;
660
679
  }
@@ -321,7 +321,10 @@ function syncProjectConfigs() {
321
321
  catch {
322
322
  // Ignore cleanup errors
323
323
  }
324
- fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
324
+ // ATOMIC WRITE: write to temp file then rename to prevent corruption
325
+ const tmpPath = claudeJsonPath + '.tmp.' + process.pid;
326
+ fs.writeFileSync(tmpPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
327
+ fs.renameSync(tmpPath, claudeJsonPath);
325
328
  logger.info({ projectsFixed }, '[ConfigSync] Project-level configs updated');
326
329
  return { fixed: true, projectsFixed };
327
330
  }