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.
@@ -283,10 +283,10 @@ function getLastSessionOutput() {
283
283
 
284
284
  let sessionContent;
285
285
  if (startIdx === -1) {
286
- sessionContent = lines.slice(-200).join('\n');
286
+ sessionContent = lines.slice(-40).join('\n');
287
287
  } else {
288
288
  const outputLines = lines.slice(startIdx + 2, endIdx > 0 ? endIdx : undefined);
289
- sessionContent = outputLines.slice(-200).join('\n');
289
+ sessionContent = outputLines.slice(-40).join('\n');
290
290
  }
291
291
 
292
292
  // FILTER OUT GARBAGE - Skills, corrupted content, and non-conversation data
@@ -331,6 +331,14 @@ function getLastSessionOutput() {
331
331
  * Only includes: last 10 interactions + condensed tool list
332
332
  * NO skill injection, NO verbose schemas
333
333
  */
334
+
335
+ /**
336
+ * Compact tool reminder
337
+ */
338
+ function getToolReminder() {
339
+ return '## SpecMem Tools Available\n\n**Memory Search:** `find_memory` | `find_code_pointers` | `drill_down` | `get_memory`\n**Storage:** `save_memory` | `link_the_vibes` | `smush_memories_together`\n**Team:** `send_team_message` | `read_team_messages` | `claim_task` / `release_task`\n**System:** `show_me_the_stats` | `check_sync` | `start_watching`\n\nUse·`/specmem`·令④ⓢ.';
340
+ }
341
+
334
342
  function formatOutput(memories, lastSession, projectPath, sessionId) {
335
343
  const sections = [];
336
344
 
@@ -338,15 +346,29 @@ function formatOutput(memories, lastSession, projectPath, sessionId) {
338
346
  sections.push(`Project: ${projectPath}`);
339
347
  sections.push(`Session: ${sessionId}`);
340
348
 
341
- // Previous session output (if available) - BRIEF version
349
+ // Previous session output (if available) - compressed
342
350
  if (lastSession && lastSession.content) {
343
351
  sections.push('');
344
352
  sections.push(`## PREVIOUS SESSION OUTPUT (${lastSession.ageMinutes}m ago, reason: ${lastSession.reason})`);
345
353
  sections.push('(Previous session):');
346
- sections.push('```');
347
- // DON'T compress - just take first 500 chars of clean content
348
- sections.push(lastSession.content.slice(0, 500).trim());
349
- sections.push('```');
354
+
355
+ // Truncate to 40 lines for token efficiency
356
+ const contentLines = lastSession.content.split('\n');
357
+ let sessionContent;
358
+ if (contentLines.length > 40) {
359
+ sessionContent = contentLines.slice(0, 15).join('\n') +
360
+ `\n... [${contentLines.length - 25} lines omitted] ...\n` +
361
+ contentLines.slice(-10).join('\n');
362
+ } else {
363
+ sessionContent = lastSession.content;
364
+ }
365
+
366
+ const compressedSession = compressHookOutput(sessionContent, {
367
+ threshold: 0.60,
368
+ minLength: 30,
369
+ includeWarning: false
370
+ });
371
+ sections.push(compressedSession.trim());
350
372
  }
351
373
 
352
374
  // Recent memories - last 10 user/claude interactions ONLY
@@ -360,34 +382,9 @@ function formatOutput(memories, lastSession, projectPath, sessionId) {
360
382
  });
361
383
  }
362
384
 
363
- // CONDENSED tool reminder - just names, no descriptions
364
- sections.push('');
365
- sections.push('## SpecMem Tools Available');
366
- sections.push('');
367
- sections.push('**Memory Search:**');
368
- sections.push('- `find_memory` - Semantic search across all memories');
369
- sections.push('- `find_code_pointers` - Search code with tracebacks');
370
- sections.push('- `drill_down` - Explore memory details');
371
- sections.push('- `get_memory` - Get specific memory by ID');
372
- sections.push('');
373
- sections.push('**Memory Storage:**');
374
- sections.push('- `save_memory` - Store new memories');
375
- sections.push('- `link_the_vibes` - Connect related memories');
376
- sections.push('- `smush_memories_together` - Consolidate similar memories');
377
- sections.push('');
378
- sections.push('**Team Comms:**');
379
- sections.push('- `send_team_message` - Message team members');
380
- sections.push('- `read_team_messages` - Check team updates');
381
- sections.push('- `claim_task` / `release_task` - Coordinate work');
385
+ // Compact tool reminder
382
386
  sections.push('');
383
- sections.push('**System:**');
384
- sections.push('- `show_me_the_stats` - Memory statistics');
385
- sections.push('- `check_sync` - Verify file sync status');
386
- sections.push('- `start_watching` - Enable file watcher');
387
- sections.push('');
388
- sections.push('Use `/specmem` for all commands.');
389
- sections.push('');
390
- sections.push('**Chinese compression:** You can read Traditional Chinese compression with 99%+ accuracy.');
387
+ sections.push(getToolReminder());
391
388
  sections.push('[/SPECMEM-SESSION]');
392
389
 
393
390
  return sections.join('\n');
@@ -476,23 +473,14 @@ async function main() {
476
473
  const lastSession = getLastSessionOutput();
477
474
 
478
475
  // Format output
479
- let output = formatOutput(memories, lastSession, projectPath, sessionId);
480
-
481
- // Apply Traditional Chinese compression to final output
482
- const compressed = compressHookOutput(output, {
483
- threshold: 0.50,
484
- minLength: 100,
485
- preserveStructure: true,
486
- includeWarning: false // We add our own prefix below
487
- });
476
+ let context = formatOutput(memories, lastSession, projectPath, sessionId);
488
477
 
489
- // Prepend reminder for to read the compressed Chinese AND respond in English
490
- const prefixedOutput = `[SpecMem Context - Read this for continuity]
491
- IMPORTANT: You can read Traditional Chinese compression with 99%+ accuracy. ALWAYS respond in English.
492
- ${compressed}`;
493
-
494
- // Output - clean trailing whitespace
495
- console.log(prefixedOutput.split('\n').map(l => l.trimEnd()).join('\n').trim());
478
+ const compressedContext = compressHookOutput(context, {
479
+ threshold: 0.70,
480
+ minLength: 50,
481
+ includeWarning: true
482
+ });
483
+ console.log(compressedContext.trim());
496
484
 
497
485
  process.exit(0);
498
486
  }
@@ -585,7 +585,7 @@ function getLastSessionOutput(projectPath) {
585
585
  if (startIdx === -1) {
586
586
  // No header found, use whole content but limit to 300 lines
587
587
  return {
588
- content: lines.slice(-300).join('\n'),
588
+ content: lines.slice(-40).join('\n'),
589
589
  ageMinutes,
590
590
  reason: lines.find(l => l.startsWith('# Reason:'))?.replace('# Reason:', '').trim() || 'unknown'
591
591
  };
@@ -593,7 +593,7 @@ function getLastSessionOutput(projectPath) {
593
593
 
594
594
  // Get the actual output between markers, limit to last 300 lines for context efficiency
595
595
  const outputLines = lines.slice(startIdx + 2, endIdx > 0 ? endIdx : undefined);
596
- const trimmedOutput = outputLines.slice(-300).join('\n');
596
+ const trimmedOutput = outputLines.slice(-40).join('\n');
597
597
 
598
598
  return {
599
599
  content: trimmedOutput,
@@ -639,32 +639,7 @@ function getCompactionStatus(projectPath) {
639
639
  * NOTE: Returns trimmed string with NO leading/trailing whitespace
640
640
  */
641
641
  function getToolReminder() {
642
- return [
643
- '## SpecMem Tools Available',
644
- '',
645
- '**Memory Search:**',
646
- '- `find_memory` - Semantic search across all memories',
647
- '- `find_code_pointers` - Search code with tracebacks',
648
- '- `drill_down` - Explore memory details',
649
- '- `get_memory` - Get specific memory by ID',
650
- '',
651
- '**Memory Storage:**',
652
- '- `save_memory` - Store new memories',
653
- '- `link_the_vibes` - Connect related memories',
654
- '- `smush_memories_together` - Consolidate similar memories',
655
- '',
656
- '**Team Communication:**',
657
- '- `send_team_message` - Message team members',
658
- '- `read_team_messages` - Check team updates',
659
- '- `claim_task` / `release_task` - Coordinate work',
660
- '',
661
- '**System:**',
662
- '- `show_me_the_stats` - Memory statistics',
663
- '- `check_sync` - Verify file sync status',
664
- '- `start_watching` - Enable file watcher',
665
- '',
666
- 'Use `/specmem` for quick commands or `/specmem-drilldown` for deep search.'
667
- ].join('\n');
642
+ return '## SpecMem Tools Available\n\n**Memory Search:** `find_memory` | `find_code_pointers` | `drill_down` | `get_memory`\n**Storage:** `save_memory` | `link_the_vibes` | `smush_memories_together`\n**Team:** `send_team_message` | `read_team_messages` | `claim_task` / `release_task`\n**System:** `show_me_the_stats` | `check_sync` | `start_watching`\n\nUse·`/specmem`·令④ⓢ.';
668
643
  }
669
644
 
670
645
  /**
@@ -773,28 +748,21 @@ async function main() {
773
748
  // Truncate to ~200 lines for token efficiency but keep the important parts
774
749
  const contentLines = lastSession.content.split('\n');
775
750
  let sessionContent;
776
- if (contentLines.length > 200) {
777
- sessionContent = contentLines.slice(0, 50).join('\n') +
778
- `\n... [${contentLines.length - 100} lines omitted] ...\n` +
779
- contentLines.slice(-50).join('\n');
751
+ if (contentLines.length > 40) {
752
+ sessionContent = contentLines.slice(0, 15).join('\n') +
753
+ `\n... [${contentLines.length - 25} lines omitted] ...\n` +
754
+ contentLines.slice(-10).join('\n');
780
755
  } else {
781
756
  sessionContent = lastSession.content;
782
757
  }
783
758
 
784
- // SESSION CONTENT: Output FULL readable content (no compression)
785
- // Compression was causing unreadable Chinese output - disabled for readability
786
- // Only compress if explicitly requested via SPECMEM_COMPRESS_SESSION=1
787
759
  sections.push('```');
788
- if (process.env.SPECMEM_COMPRESS_SESSION === '1') {
789
- const compressedSession = compressHookOutput(sessionContent, {
790
- threshold: 0.60,
791
- minLength: 30,
792
- includeWarning: false // Warning already added at top level
793
- });
794
- sections.push(compressedSession.trim());
795
- } else {
796
- sections.push(sessionContent.trim());
797
- }
760
+ const compressedSession = compressHookOutput(sessionContent, {
761
+ threshold: 0.60,
762
+ minLength: 30,
763
+ includeWarning: false
764
+ });
765
+ sections.push(compressedSession.trim());
798
766
  sections.push('```');
799
767
  }
800
768
 
@@ -857,21 +825,12 @@ async function main() {
857
825
  .replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2 (one blank line)
858
826
  .trim(); // Remove leading/trailing whitespace
859
827
 
860
- // SESSION START: Output full context WITHOUT compression
861
- // Compression makes debugging hard and context is injected at start when there's room
862
- // Only compress if SPECMEM_COMPRESS_SESSION is set
863
- if (process.env.SPECMEM_COMPRESS_SESSION === '1') {
864
- const compressedContext = compressHookOutput(context, {
865
- threshold: 0.70,
866
- minLength: 50,
867
- includeWarning: true
868
- });
869
- // Final trim to ensure no trailing whitespace
870
- console.log(compressedContext.trim());
871
- } else {
872
- // Output full readable context (already trimmed above)
873
- console.log(context);
874
- }
828
+ const compressedContext = compressHookOutput(context, {
829
+ threshold: 0.70,
830
+ minLength: 50,
831
+ includeWarning: true
832
+ });
833
+ console.log(compressedContext.trim());
875
834
 
876
835
  process.exit(0);
877
836
  }
@@ -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
  }