let-them-talk 3.6.2 → 3.8.0

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/server.js CHANGED
@@ -18,6 +18,15 @@ const PROFILES_FILE = path.join(DATA_DIR, 'profiles.json');
18
18
  const WORKFLOWS_FILE = path.join(DATA_DIR, 'workflows.json');
19
19
  const WORKSPACES_DIR = path.join(DATA_DIR, 'workspaces');
20
20
  const BRANCHES_FILE = path.join(DATA_DIR, 'branches.json');
21
+ const DECISIONS_FILE = path.join(DATA_DIR, 'decisions.json');
22
+ const KB_FILE = path.join(DATA_DIR, 'kb.json');
23
+ const LOCKS_FILE = path.join(DATA_DIR, 'locks.json');
24
+ const PROGRESS_FILE = path.join(DATA_DIR, 'progress.json');
25
+ const VOTES_FILE = path.join(DATA_DIR, 'votes.json');
26
+ const REVIEWS_FILE = path.join(DATA_DIR, 'reviews.json');
27
+ const DEPS_FILE = path.join(DATA_DIR, 'dependencies.json');
28
+ const REPUTATION_FILE = path.join(DATA_DIR, 'reputation.json');
29
+ const COMPRESSED_FILE = path.join(DATA_DIR, 'compressed.json');
21
30
  // Plugins removed in v3.4.3 — unnecessary attack surface, CLIs have their own extension systems
22
31
 
23
32
  // In-memory state for this process
@@ -63,7 +72,13 @@ function isGroupMode() {
63
72
  }
64
73
 
65
74
  function getGroupCooldown() {
66
- return getConfig().group_cooldown || 3000; // default 3s
75
+ // Adaptive cooldown: scales with online agent count — max(500, N * 500)
76
+ // 2 agents = 1s, 3 = 1.5s, 4 = 2s, 6 = 3s, 10 = 5s
77
+ const configured = getConfig().group_cooldown;
78
+ if (configured) return configured; // respect explicit config
79
+ const agents = getAgents();
80
+ const aliveCount = Object.values(agents).filter(a => isPidAlive(a.pid, a.last_activity)).length;
81
+ return Math.max(500, aliveCount * 500);
67
82
  }
68
83
 
69
84
  // --- Managed conversation mode ---
@@ -355,21 +370,33 @@ function autoCompact() {
355
370
 
356
371
  const messages = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
357
372
 
358
- // Collect ALL consumed IDs across all agents
373
+ // Collect consumed IDs for __group__ messages, only check ALIVE agents
374
+ const agents = getAgents();
375
+ const aliveAgentNames = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
359
376
  const allConsumed = new Set();
377
+ const perAgentConsumed = {};
360
378
  if (fs.existsSync(DATA_DIR)) {
361
379
  for (const f of fs.readdirSync(DATA_DIR)) {
362
380
  if (f.startsWith('consumed-') && f.endsWith('.json')) {
381
+ const agentName = f.replace('consumed-', '').replace('.json', '');
363
382
  try {
364
383
  const ids = JSON.parse(fs.readFileSync(path.join(DATA_DIR, f), 'utf8'));
384
+ perAgentConsumed[agentName] = new Set(ids);
365
385
  ids.forEach(id => allConsumed.add(id));
366
386
  } catch {}
367
387
  }
368
388
  }
369
389
  }
370
390
 
371
- // Keep only unconsumed messages (for direct messages, only the recipient consumes)
391
+ // Keep messages that are NOT fully consumed
392
+ // For __group__ messages: consumed when ALL ALIVE agents have consumed it (dead agents don't block)
393
+ // For direct messages: consumed when the recipient has consumed it
372
394
  const active = messages.filter(m => {
395
+ if (m.to === '__group__') {
396
+ // __group__: check if all alive agents (except sender) have consumed
397
+ return !aliveAgentNames.every(n => n === m.from || (perAgentConsumed[n] && perAgentConsumed[n].has(m.id)));
398
+ }
399
+ // Direct: standard check
373
400
  if (!allConsumed.has(m.id)) return true;
374
401
  return false;
375
402
  });
@@ -442,7 +469,9 @@ function getUnconsumedMessages(agentName, fromFilter = null) {
442
469
  const consumed = getConsumedIds(agentName);
443
470
  const perms = getPermissions();
444
471
  return messages.filter(m => {
445
- if (m.to !== agentName) return false;
472
+ if (m.to !== agentName && m.to !== '__group__' && m.to !== '__all__') return false;
473
+ // Skip own group messages
474
+ if (m.to === '__group__' && m.from === agentName) return false;
446
475
  if (consumed.has(m.id)) return false;
447
476
  if (fromFilter && m.from !== fromFilter && !m.system) return false;
448
477
  // Permission check: skip messages from senders this agent can't read
@@ -621,11 +650,69 @@ function toolRegister(name, provider = null) {
621
650
  }
622
651
  }
623
652
  }
653
+ // Clean up file locks held by dead agents
654
+ cleanStaleLocks();
624
655
  } catch {}
625
656
  }, 10000);
626
657
  heartbeatInterval.unref(); // Don't prevent process exit
627
658
 
628
- return { success: true, message: `Registered as Agent ${name} (PID ${process.pid})` };
659
+ // Fire join event + recovery data for returning agents
660
+ const config = getConfig();
661
+ const mode = config.conversation_mode || 'direct';
662
+ const otherAgents = Object.keys(getAgents()).filter(n => n !== name);
663
+
664
+ const result = {
665
+ success: true,
666
+ message: `Registered as Agent ${name} (PID ${process.pid})`,
667
+ conversation_mode: mode,
668
+ agents_online: otherAgents,
669
+ guide: {
670
+ critical_rules: [
671
+ 'AFTER EVERY ACTION YOU TAKE, call listen_group() (group/managed mode) or listen() (direct mode) immediately. This is how you receive messages. If you stop listening, you are invisible to the team.',
672
+ 'Never send multiple messages in a row without calling listen_group() between them — you will miss responses.',
673
+ 'Keep messages concise. 2-3 paragraphs max. No essays.',
674
+ 'When you finish a task, report what you did AND what files you changed, then listen again.',
675
+ ],
676
+ first_steps: mode === 'direct'
677
+ ? '1. Call list_agents() to see who is online. 2. Send a message or call listen() to wait for one.'
678
+ : '1. Call get_briefing() for full project context. 2. Call listen_group() to join the conversation. 3. When you receive messages, respond and immediately call listen_group() again.',
679
+ tool_categories: {
680
+ 'MESSAGING (always use these)': 'send_message, broadcast, listen_group (group/managed), listen (direct), check_messages, get_history, get_summary, handoff, share_file',
681
+ 'TEAM COORDINATION': 'get_briefing (project overview), log_decision / get_decisions (prevent re-debating), kb_write / kb_read (shared knowledge), call_vote / cast_vote (team decisions)',
682
+ 'TASK MANAGEMENT': 'create_task, update_task, list_tasks, declare_dependency, check_dependencies, suggest_task (what should I do next?)',
683
+ 'PROGRESS & QUALITY': 'update_progress / get_progress (feature %), request_review / submit_review (code review), get_reputation (leaderboard)',
684
+ 'FILE SAFETY': 'lock_file / unlock_file (prevent conflicts — ALWAYS lock before editing shared files)',
685
+ 'PROFILES & WORKSPACES': 'update_profile, workspace_write / workspace_read (personal storage)',
686
+ 'MANAGED MODE (if active)': 'claim_manager, yield_floor, set_phase — only the manager uses these',
687
+ },
688
+ patterns: {
689
+ 'Starting work': 'get_briefing → check list_tasks → claim a task with update_task(id, "in_progress") → lock_file → do the work → unlock_file → update_task(id, "done") → listen_group',
690
+ 'Sharing knowledge': 'kb_write("api-schema", "POST /auth → {token}") — so others can kb_read it without asking you',
691
+ 'Making decisions': 'log_decision("Use PostgreSQL", "Better JSON support") — so no one re-debates this later',
692
+ 'Disagreements': 'call_vote("Use Redis for caching?", ["yes", "no"]) — let the team decide democratically',
693
+ 'Code review': 'request_review("src/auth.ts", "Check token expiry logic") — another agent will review and approve/request changes',
694
+ },
695
+ },
696
+ };
697
+
698
+ // Recovery: if this agent has prior data, include it
699
+ const myTasks = getTasks().filter(t => t.assignee === name && t.status !== 'done');
700
+ const myWorkspace = getWorkspace(name);
701
+ const recentHistory = readJsonl(getHistoryFile(currentBranch));
702
+ const myRecentMsgs = recentHistory.filter(m => m.to === name || m.from === name).slice(-5);
703
+
704
+ if (myTasks.length > 0 || Object.keys(myWorkspace).length > 0 || myRecentMsgs.length > 0) {
705
+ result.recovery = {};
706
+ if (myTasks.length > 0) result.recovery.your_active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
707
+ if (Object.keys(myWorkspace).length > 0) result.recovery.your_workspace_keys = Object.keys(myWorkspace);
708
+ if (myRecentMsgs.length > 0) result.recovery.recent_messages = myRecentMsgs.map(m => ({ from: m.from, to: m.to, preview: m.content.substring(0, 100), timestamp: m.timestamp }));
709
+ result.recovery.hint = 'You have prior context from a previous session. Call get_briefing() for a full project summary.';
710
+ }
711
+
712
+ // Notify other agents
713
+ fireEvent('agent_join', { agent: name });
714
+
715
+ return result;
629
716
  } finally {
630
717
  unlockAgentsFile();
631
718
  }
@@ -650,6 +737,10 @@ function setListening(isListening) {
650
737
  const agents = getAgents();
651
738
  if (agents[registeredName]) {
652
739
  agents[registeredName].listening_since = isListening ? new Date().toISOString() : null;
740
+ // Persist last_listened_at so other agents can detect unresponsive agents
741
+ if (isListening) {
742
+ agents[registeredName].last_listened_at = new Date().toISOString();
743
+ }
653
744
  saveAgents(agents);
654
745
  }
655
746
  } catch {}
@@ -672,6 +763,7 @@ function toolListAgents() {
672
763
  status: !alive ? 'dead' : idleSeconds > 60 ? 'sleeping' : 'active',
673
764
  listening_since: info.listening_since || null,
674
765
  is_listening: !!(info.listening_since && alive),
766
+ last_listened_at: info.last_listened_at || null,
675
767
  provider: info.provider || 'unknown',
676
768
  branch: info.branch || 'main',
677
769
  display_name: profile.display_name || name,
@@ -796,13 +888,16 @@ async function toolSendMessage(content, to = null, reply_to = null) {
796
888
  }
797
889
 
798
890
  messageSeq++;
891
+ // In group mode: rewrite to → __group__, original to becomes addressed_to
892
+ const isGroup = isGroupMode() && !isManagedMode();
799
893
  const msg = {
800
894
  id: generateId(),
801
895
  seq: messageSeq,
802
896
  from: registeredName,
803
- to,
897
+ to: isGroup ? '__group__' : to,
804
898
  content,
805
899
  timestamp: new Date().toISOString(),
900
+ ...(isGroup && to && { addressed_to: [to] }),
806
901
  ...(reply_to && { reply_to }),
807
902
  ...(thread_id && { thread_id }),
808
903
  };
@@ -813,18 +908,8 @@ async function toolSendMessage(content, to = null, reply_to = null) {
813
908
  touchActivity();
814
909
  lastSentAt = Date.now();
815
910
 
816
- // In group mode, auto-broadcast: also write to all other agents' queues
817
- // Skip if this message is already a response to a broadcast (prevents cascade)
818
- // NEVER auto-broadcast in managed mode — manager controls communication flow
819
- if (isGroupMode() && !isManagedMode() && !reply_to && !msg.broadcast) {
820
- const otherRecipients = Object.keys(getAgents()).filter(n => n !== registeredName && n !== to);
821
- for (const other of otherRecipients) {
822
- if (!canSendTo(registeredName, other)) continue; // respect permissions
823
- const broadcastMsg = { ...msg, id: generateId(), to: other, broadcast: true, original_to: to };
824
- fs.appendFileSync(getMessagesFile(currentBranch), JSON.stringify(broadcastMsg) + '\n');
825
- fs.appendFileSync(getHistoryFile(currentBranch), JSON.stringify(broadcastMsg) + '\n');
826
- }
827
- }
911
+ // Group mode: O(N) auto-broadcast REMOVED. Messages now use __group__ single-write.
912
+ // The to→__group__ rewrite happens above when the message is created.
828
913
 
829
914
  // Managed mode: auto-advance turns after non-manager sends
830
915
  if (isManagedMode()) {
@@ -875,6 +960,12 @@ async function toolSendMessage(content, to = null, reply_to = null) {
875
960
  result.note = `Agent "${to}" is currently working (not in listen mode). Message queued — they'll see it when they finish their current task and call listen_group().`;
876
961
  }
877
962
 
963
+ // Mode awareness hint: warn if agent seems to be in wrong mode
964
+ const currentMode = getConfig().conversation_mode || 'direct';
965
+ if (currentMode === 'group' || currentMode === 'managed') {
966
+ result.mode_hint = `You're in ${currentMode} mode. Use listen_group() (or listen() — both auto-detect) to stay in the conversation.`;
967
+ }
968
+
878
969
  // Nudge: check if THIS agent has unread messages waiting
879
970
  const myPending = getUnconsumedMessages(registeredName);
880
971
  if (myPending.length > 0) {
@@ -911,6 +1002,32 @@ function toolBroadcast(content) {
911
1002
  }
912
1003
 
913
1004
  ensureDataDir();
1005
+
1006
+ // In group mode: single __group__ write instead of per-agent copies
1007
+ if (isGroupMode() && !isManagedMode()) {
1008
+ messageSeq++;
1009
+ const msg = {
1010
+ id: generateId(),
1011
+ seq: messageSeq,
1012
+ from: registeredName,
1013
+ to: '__group__',
1014
+ content,
1015
+ timestamp: new Date().toISOString(),
1016
+ broadcast: true,
1017
+ };
1018
+ fs.appendFileSync(getMessagesFile(currentBranch), JSON.stringify(msg) + '\n');
1019
+ fs.appendFileSync(getHistoryFile(currentBranch), JSON.stringify(msg) + '\n');
1020
+ touchActivity();
1021
+ lastSentAt = Date.now();
1022
+ const aliveOthers = otherAgents.filter(n => { const a = agents[n]; return isPidAlive(a.pid, a.last_activity); });
1023
+ const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length, sent_to: aliveOthers.map(n => ({ to: n, messageId: msg.id })) };
1024
+ // Nudge for own unread messages
1025
+ const myPending = getUnconsumedMessages(registeredName);
1026
+ if (myPending.length > 0) { result.you_have_messages = myPending.length; result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`; }
1027
+ return result;
1028
+ }
1029
+
1030
+ // Direct/managed mode: per-agent writes (original behavior)
914
1031
  const ids = [];
915
1032
  const skipped = [];
916
1033
  for (const to of otherAgents) {
@@ -1058,6 +1175,12 @@ async function toolListen(from = null) {
1058
1175
  return { error: 'You must call register() first' };
1059
1176
  }
1060
1177
 
1178
+ // Auto-detect group/managed mode and delegate to toolListenGroup
1179
+ // This prevents agents from calling the "wrong" listen function
1180
+ if (isGroupMode() || isManagedMode()) {
1181
+ return toolListenGroup();
1182
+ }
1183
+
1061
1184
  setListening(true);
1062
1185
 
1063
1186
  // Check for existing unconsumed messages first
@@ -1201,6 +1324,11 @@ function toolSetConversationMode(mode) {
1201
1324
  }
1202
1325
  saveConfig(config);
1203
1326
 
1327
+ // Notify all agents about mode change (managed mode already broadcasts above)
1328
+ if (mode !== 'managed') {
1329
+ broadcastSystemMessage(`[MODE] Conversation switched to ${mode} mode by ${registeredName}. ${mode === 'group' ? 'All messages are now shared with everyone.' : 'Messages are now point-to-point.'}`, registeredName);
1330
+ }
1331
+
1204
1332
  const messages = {
1205
1333
  group: 'Group mode enabled. Use listen_group() to receive batched messages. All messages are shared with everyone.',
1206
1334
  direct: 'Direct mode enabled. Use listen() for point-to-point messaging.',
@@ -1343,26 +1471,38 @@ function toolSetPhase(phase) {
1343
1471
  };
1344
1472
  }
1345
1473
 
1346
- async function toolListenGroup(timeout_seconds = 300) {
1474
+ // Deterministic stagger delay based on agent name (500-1500ms)
1475
+ // Same agent always gets the same delay, making response ordering predictable
1476
+ function hashStagger(name) {
1477
+ const hash = name.split('').reduce((h, c) => h + c.charCodeAt(0), 0);
1478
+ return 500 + (hash * 137) % 1000; // 0.5-1.5s range
1479
+ }
1480
+
1481
+ async function toolListenGroup() {
1347
1482
  if (!registeredName) return { error: 'You must call register() first' };
1348
- const timeoutMs = Math.min(Math.max(1, timeout_seconds || 300), 3600) * 1000;
1349
1483
 
1350
- setListening(true);
1484
+ // Auto-detect direct mode and delegate to toolListen (prevents wrong-function bugs)
1485
+ if (!isGroupMode() && !isManagedMode()) {
1486
+ return toolListen();
1487
+ }
1351
1488
 
1352
- // Random stagger to prevent all agents from responding simultaneously (1-3s)
1353
- const stagger = 1000 + Math.random() * 2000;
1354
- await new Promise(r => setTimeout(r, stagger));
1489
+ setListening(true);
1355
1490
 
1356
- const deadline = Date.now() + timeoutMs;
1357
1491
  const consumed = getConsumedIds(registeredName);
1358
1492
 
1359
- while (Date.now() < deadline) {
1360
- // Collect ALL unconsumed messages addressed to us or broadcast
1493
+ // Poll indefinitely (in 5-min chunks to stay within any MCP limits, same as listen())
1494
+ while (true) {
1495
+ const chunkDeadline = Date.now() + 300000;
1496
+
1497
+ while (Date.now() < chunkDeadline) {
1498
+ // Collect ALL unconsumed messages: direct to us, __group__ (everyone), __all__, or system
1361
1499
  const messages = readJsonl(getMessagesFile(currentBranch));
1362
1500
  const batch = [];
1363
1501
  for (const msg of messages) {
1364
1502
  if (consumed.has(msg.id)) continue;
1365
- if (msg.to !== registeredName && msg.to !== '__all__') continue;
1503
+ // Skip own messages in group mode (agent already knows what it sent)
1504
+ if (msg.to === '__group__' && msg.from === registeredName) { consumed.add(msg.id); continue; }
1505
+ if (msg.to !== registeredName && msg.to !== '__all__' && msg.to !== '__group__') continue;
1366
1506
  // Permission check
1367
1507
  const perms = getPermissions();
1368
1508
  if (perms[registeredName] && perms[registeredName].can_read) {
@@ -1379,6 +1519,42 @@ async function toolListenGroup(timeout_seconds = 300) {
1379
1519
  touchActivity();
1380
1520
  setListening(false);
1381
1521
 
1522
+ // Post-receive stagger: deterministic delay based on agent name
1523
+ // Prevents all agents from responding simultaneously to the same batch
1524
+ const staggerMs = hashStagger(registeredName);
1525
+ if (staggerMs > 0) {
1526
+ await new Promise(r => setTimeout(r, staggerMs));
1527
+ }
1528
+
1529
+ // Sort batch by priority: system > threaded replies > direct > broadcast
1530
+ // Within each category, maintain chronological order
1531
+ function messagePriority(m) {
1532
+ if (m.system || m.from === '__system__') return 0;
1533
+ if (m.reply_to || m.thread_id) return 1;
1534
+ if (!m.broadcast) return 2;
1535
+ return 3;
1536
+ }
1537
+ batch.sort((a, b) => {
1538
+ const pa = messagePriority(a), pb = messagePriority(b);
1539
+ if (pa !== pb) return pa - pb;
1540
+ return new Date(a.timestamp) - new Date(b.timestamp);
1541
+ });
1542
+
1543
+ // Build batch summary for triage
1544
+ const summaryCounts = {};
1545
+ for (const m of batch) {
1546
+ const type = m.system || m.from === '__system__' ? 'system'
1547
+ : m.broadcast ? 'broadcast' : (m.reply_to || m.thread_id) ? 'thread' : 'direct';
1548
+ const key = `${m.from}:${type}`;
1549
+ summaryCounts[key] = (summaryCounts[key] || 0) + 1;
1550
+ }
1551
+ const summaryParts = [];
1552
+ for (const [key, count] of Object.entries(summaryCounts)) {
1553
+ const [from, type] = key.split(':');
1554
+ summaryParts.push(`${count} ${type} from ${from}`);
1555
+ }
1556
+ const batchSummary = `${batch.length} messages: ${summaryParts.join(', ')}`;
1557
+
1382
1558
  // Get recent history for context
1383
1559
  const history = readJsonl(getHistoryFile(currentBranch));
1384
1560
  const recentHistory = history.slice(-20).map(m => ({
@@ -1404,14 +1580,33 @@ async function toolListenGroup(timeout_seconds = 300) {
1404
1580
  ...(ageSec > 30 && { delayed: true }),
1405
1581
  ...(m.reply_to && { reply_to: m.reply_to }),
1406
1582
  ...(m.thread_id && { thread_id: m.thread_id }),
1583
+ // addressed_to hint for group messages
1584
+ ...(m.addressed_to && { addressed_to: m.addressed_to }),
1585
+ ...(m.to === '__group__' && {
1586
+ addressed_to_you: !m.addressed_to || m.addressed_to.includes(registeredName),
1587
+ should_respond: !m.addressed_to || m.addressed_to.includes(registeredName),
1588
+ }),
1407
1589
  };
1408
1590
  }),
1409
1591
  message_count: batch.length,
1592
+ batch_summary: batchSummary,
1410
1593
  context: recentHistory,
1411
1594
  agents_online: agentNames.length,
1412
1595
  agents_silent: silent,
1413
1596
  agents_status: agentNames.reduce(function(acc, n) {
1414
- acc[n] = agents[n].listening_since ? 'listening' : 'working';
1597
+ if (agents[n].listening_since) {
1598
+ acc[n] = 'listening';
1599
+ } else {
1600
+ // Check for unresponsive: not listening, >2min since last listen, has pending messages
1601
+ const lastListened = agents[n].last_listened_at;
1602
+ const sinceLastListen = lastListened ? Date.now() - new Date(lastListened).getTime() : Infinity;
1603
+ const pendingForAgent = getUnconsumedMessages(n);
1604
+ if (sinceLastListen > 120000 && pendingForAgent.length > 0) {
1605
+ acc[n] = 'unresponsive';
1606
+ } else {
1607
+ acc[n] = 'working';
1608
+ }
1609
+ }
1415
1610
  return acc;
1416
1611
  }, {}),
1417
1612
  hint: silent.length > 0
@@ -1455,15 +1650,8 @@ async function toolListenGroup(timeout_seconds = 300) {
1455
1650
 
1456
1651
  await adaptiveSleep(0);
1457
1652
  }
1458
-
1459
- setListening(false);
1460
- return {
1461
- timeout: true,
1462
- retry: true,
1463
- message: 'No messages yet. Call listen_group() again immediately to keep listening. Do NOT stop — you must stay in the conversation.',
1464
- messages: [],
1465
- message_count: 0,
1466
- };
1653
+ // No message in this 5-min chunk — loop again (stay listening forever)
1654
+ }
1467
1655
  }
1468
1656
 
1469
1657
  function toolGetHistory(limit = 50, thread_id = null) {
@@ -1718,6 +1906,23 @@ function toolUpdateTask(taskId, status, notes = null) {
1718
1906
  saveTasks(tasks);
1719
1907
  touchActivity();
1720
1908
 
1909
+ // Event hooks: task completion
1910
+ if (status === 'done') {
1911
+ fireEvent('task_complete', { title: task.title, created_by: task.created_by });
1912
+ // Check if this resolves any dependencies
1913
+ const deps = getDeps();
1914
+ for (const dep of deps) {
1915
+ if (dep.depends_on === taskId && !dep.resolved) {
1916
+ dep.resolved = true;
1917
+ const blockedTask = tasks.find(t => t.id === dep.task_id);
1918
+ if (blockedTask && blockedTask.assignee) {
1919
+ fireEvent('dependency_met', { task_title: task.title, notify: blockedTask.assignee });
1920
+ }
1921
+ }
1922
+ }
1923
+ writeJsonFile(DEPS_FILE, deps);
1924
+ }
1925
+
1721
1926
  return { success: true, task_id: task.id, status: task.status, title: task.title };
1722
1927
  }
1723
1928
 
@@ -1800,8 +2005,8 @@ function toolReset() {
1800
2005
  }
1801
2006
  }
1802
2007
  }
1803
- // Remove profiles, workflows, branches, permissions, read receipts
1804
- for (const f of [PROFILES_FILE, WORKFLOWS_FILE, BRANCHES_FILE, PERMISSIONS_FILE, READ_RECEIPTS_FILE, CONFIG_FILE]) {
2008
+ // Remove profiles, workflows, branches, permissions, read receipts, and new ecosystem files
2009
+ for (const f of [PROFILES_FILE, WORKFLOWS_FILE, BRANCHES_FILE, PERMISSIONS_FILE, READ_RECEIPTS_FILE, CONFIG_FILE, DECISIONS_FILE, KB_FILE, LOCKS_FILE, PROGRESS_FILE, VOTES_FILE, REVIEWS_FILE, DEPS_FILE, REPUTATION_FILE, COMPRESSED_FILE]) {
1805
2010
  if (fs.existsSync(f)) fs.unlinkSync(f);
1806
2011
  }
1807
2012
  // Remove workspaces dir
@@ -2131,10 +2336,659 @@ function toolListBranches() {
2131
2336
  return { branches: result, current: currentBranch };
2132
2337
  }
2133
2338
 
2339
+ // --- Tier 1: Briefing, File Locking, Decisions, Recovery ---
2340
+
2341
+ // Helpers for new data files
2342
+ function readJsonFile(file) { if (!fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } }
2343
+ function writeJsonFile(file, data) { ensureDataDir(); fs.writeFileSync(file, JSON.stringify(data, null, 2)); }
2344
+
2345
+ function getDecisions() { return readJsonFile(DECISIONS_FILE) || []; }
2346
+ function getKB() { return readJsonFile(KB_FILE) || {}; }
2347
+ function getLocks() { return readJsonFile(LOCKS_FILE) || {}; }
2348
+ function getProgressData() { return readJsonFile(PROGRESS_FILE) || {}; }
2349
+ function getVotes() { return readJsonFile(VOTES_FILE) || []; }
2350
+ function getReviews() { return readJsonFile(REVIEWS_FILE) || []; }
2351
+ function getDeps() { return readJsonFile(DEPS_FILE) || []; }
2352
+
2353
+ // Auto-cleanup dead agent locks (called from heartbeat)
2354
+ function cleanStaleLocks() {
2355
+ const locks = getLocks();
2356
+ const agents = getAgents();
2357
+ let changed = false;
2358
+ for (const [filePath, lock] of Object.entries(locks)) {
2359
+ if (!agents[lock.agent] || !isPidAlive(agents[lock.agent].pid, agents[lock.agent].last_activity)) {
2360
+ delete locks[filePath];
2361
+ changed = true;
2362
+ }
2363
+ }
2364
+ if (changed) writeJsonFile(LOCKS_FILE, locks);
2365
+ }
2366
+
2367
+ // Event hook: fire system messages based on events
2368
+ function fireEvent(eventName, data) {
2369
+ const agents = getAgents();
2370
+ const aliveAgents = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
2371
+
2372
+ switch (eventName) {
2373
+ case 'agent_join': {
2374
+ // Notify existing agents
2375
+ for (const name of aliveAgents) {
2376
+ if (name === data.agent) continue;
2377
+ sendSystemMessage(name, `[EVENT] ${data.agent} has joined the team. They are now online.`);
2378
+ }
2379
+ break;
2380
+ }
2381
+ case 'task_complete': {
2382
+ // Notify task creator
2383
+ if (data.created_by && data.created_by !== registeredName && agents[data.created_by]) {
2384
+ sendSystemMessage(data.created_by, `[EVENT] Task "${data.title}" completed by ${registeredName}.`);
2385
+ }
2386
+ // Check if all tasks done
2387
+ const allTasks = getTasks();
2388
+ const pending = allTasks.filter(t => t.status !== 'done');
2389
+ if (pending.length === 0 && allTasks.length > 0) {
2390
+ broadcastSystemMessage(`[EVENT] All ${allTasks.length} tasks are complete! Consider starting a review phase.`);
2391
+ }
2392
+ break;
2393
+ }
2394
+ case 'dependency_met': {
2395
+ if (data.notify && agents[data.notify]) {
2396
+ sendSystemMessage(data.notify, `[EVENT] Dependency resolved: "${data.task_title}" is done. You can now proceed with your blocked task.`);
2397
+ }
2398
+ break;
2399
+ }
2400
+ }
2401
+ }
2402
+
2403
+ function toolGetGuide() {
2404
+ if (!registeredName) return { error: 'You must call register() first' };
2405
+ const config = getConfig();
2406
+ const mode = config.conversation_mode || 'direct';
2407
+ return {
2408
+ your_name: registeredName,
2409
+ conversation_mode: mode,
2410
+ critical_rules: [
2411
+ 'AFTER EVERY ACTION, call listen_group() (group/managed) or listen() (direct). This is how you receive messages.',
2412
+ 'Never send multiple messages without listening between them.',
2413
+ 'Keep messages concise — 2-3 paragraphs max.',
2414
+ 'When you finish a task, report what you did + files changed, then listen again.',
2415
+ 'ALWAYS lock_file() before editing shared files, unlock_file() when done.',
2416
+ 'Use log_decision() for any team decisions so they are not re-debated.',
2417
+ 'Use kb_write() to share knowledge (API specs, conventions) so others can read without asking.',
2418
+ ],
2419
+ tool_categories: {
2420
+ 'MESSAGING': 'send_message, broadcast, listen_group, listen, check_messages, get_history, get_summary, handoff, share_file',
2421
+ 'COORDINATION': 'get_briefing, log_decision, get_decisions, kb_write, kb_read, kb_list, call_vote, cast_vote, vote_status',
2422
+ 'TASKS': 'create_task, update_task, list_tasks, declare_dependency, check_dependencies, suggest_task',
2423
+ 'QUALITY': 'update_progress, get_progress, request_review, submit_review, get_reputation',
2424
+ 'SAFETY': 'lock_file, unlock_file',
2425
+ 'MANAGED MODE': 'claim_manager, yield_floor, set_phase (manager only)',
2426
+ },
2427
+ workflow: '1. get_briefing → 2. check list_tasks/suggest_task → 3. claim task → 4. lock_file → 5. do work → 6. unlock_file → 7. update_task done → 8. listen_group',
2428
+ };
2429
+ }
2430
+
2431
+ function toolGetBriefing() {
2432
+ if (!registeredName) return { error: 'You must call register() first' };
2433
+
2434
+ const agents = getAgents();
2435
+ const profiles = getProfiles();
2436
+ const tasks = getTasks();
2437
+ const decisions = getDecisions();
2438
+ const kb = getKB();
2439
+ const progress = getProgressData();
2440
+ const history = readJsonl(getHistoryFile(currentBranch));
2441
+ const locks = getLocks();
2442
+ const config = getConfig();
2443
+
2444
+ // Agent roster
2445
+ const roster = {};
2446
+ for (const [name, info] of Object.entries(agents)) {
2447
+ const alive = isPidAlive(info.pid, info.last_activity);
2448
+ const profile = profiles[name] || {};
2449
+ roster[name] = {
2450
+ status: !alive ? 'offline' : info.listening_since ? 'listening' : 'working',
2451
+ role: profile.role || '',
2452
+ provider: info.provider || 'unknown',
2453
+ };
2454
+ }
2455
+
2456
+ // Recent messages summary (last 15)
2457
+ const recentMsgs = history.slice(-15).map(m => ({
2458
+ from: m.from, to: m.to,
2459
+ preview: m.content.substring(0, 150),
2460
+ timestamp: m.timestamp,
2461
+ }));
2462
+
2463
+ // Active tasks
2464
+ const activeTasks = tasks.filter(t => t.status !== 'done').map(t => ({
2465
+ id: t.id, title: t.title, status: t.status, assignee: t.assignee, created_by: t.created_by,
2466
+ }));
2467
+ const doneTasks = tasks.filter(t => t.status === 'done').length;
2468
+
2469
+ // Locked files
2470
+ const lockedFiles = {};
2471
+ for (const [fp, lock] of Object.entries(locks)) {
2472
+ lockedFiles[fp] = { locked_by: lock.agent, since: lock.since };
2473
+ }
2474
+
2475
+ // Project files summary (scan cwd for key files)
2476
+ const projectFiles = [];
2477
+ try {
2478
+ const cwd = process.cwd();
2479
+ const scan = function(dir, depth) {
2480
+ if (depth > 2) return;
2481
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
2482
+ for (const e of entries) {
2483
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue;
2484
+ const rel = path.relative(cwd, path.join(dir, e.name));
2485
+ if (e.isDirectory()) { projectFiles.push(rel + '/'); scan(path.join(dir, e.name), depth + 1); }
2486
+ else if (e.isFile()) projectFiles.push(rel);
2487
+ }
2488
+ };
2489
+ scan(cwd, 0);
2490
+ } catch {}
2491
+
2492
+ return {
2493
+ briefing: true,
2494
+ conversation_mode: config.conversation_mode || 'direct',
2495
+ agents: roster,
2496
+ your_name: registeredName,
2497
+ total_messages: history.length,
2498
+ recent_messages: recentMsgs,
2499
+ tasks: { active: activeTasks, completed_count: doneTasks, total: tasks.length },
2500
+ decisions: decisions.slice(-10),
2501
+ knowledge_base_keys: Object.keys(kb),
2502
+ locked_files: lockedFiles,
2503
+ progress,
2504
+ project_files: projectFiles.slice(0, 80),
2505
+ hint: 'You are now fully briefed. Check active tasks, read recent messages for context, and start contributing.',
2506
+ };
2507
+ }
2508
+
2509
+ function toolLockFile(filePath) {
2510
+ if (!registeredName) return { error: 'You must call register() first' };
2511
+ if (typeof filePath !== 'string' || filePath.length < 1 || filePath.length > 200) return { error: 'Invalid file path' };
2512
+
2513
+ const normalized = filePath.replace(/\\/g, '/');
2514
+ const locks = getLocks();
2515
+
2516
+ if (locks[normalized]) {
2517
+ const holder = locks[normalized].agent;
2518
+ if (holder === registeredName) return { success: true, message: 'You already hold this lock.', file: normalized };
2519
+ // Check if holder is still alive
2520
+ const agents = getAgents();
2521
+ if (agents[holder] && isPidAlive(agents[holder].pid, agents[holder].last_activity)) {
2522
+ return { error: `File "${normalized}" is locked by ${holder} since ${locks[normalized].since}. Wait for them to unlock it or message them.` };
2523
+ }
2524
+ // Dead holder — take over
2525
+ }
2526
+
2527
+ locks[normalized] = { agent: registeredName, since: new Date().toISOString() };
2528
+ writeJsonFile(LOCKS_FILE, locks);
2529
+ touchActivity();
2530
+ return { success: true, file: normalized, message: `File locked. Other agents cannot edit "${normalized}" until you call unlock_file().` };
2531
+ }
2532
+
2533
+ function toolUnlockFile(filePath) {
2534
+ if (!registeredName) return { error: 'You must call register() first' };
2535
+ const normalized = (filePath || '').replace(/\\/g, '/');
2536
+ const locks = getLocks();
2537
+
2538
+ if (!filePath) {
2539
+ // Unlock ALL files held by this agent
2540
+ let count = 0;
2541
+ for (const [fp, lock] of Object.entries(locks)) {
2542
+ if (lock.agent === registeredName) { delete locks[fp]; count++; }
2543
+ }
2544
+ writeJsonFile(LOCKS_FILE, locks);
2545
+ return { success: true, unlocked: count, message: `Unlocked ${count} file(s).` };
2546
+ }
2547
+
2548
+ if (!locks[normalized]) return { success: true, message: 'File was not locked.' };
2549
+ if (locks[normalized].agent !== registeredName) return { error: `File is locked by ${locks[normalized].agent}, not you.` };
2550
+
2551
+ delete locks[normalized];
2552
+ writeJsonFile(LOCKS_FILE, locks);
2553
+ return { success: true, file: normalized, message: 'File unlocked.' };
2554
+ }
2555
+
2556
+ function toolLogDecision(decision, reasoning, topic) {
2557
+ if (!registeredName) return { error: 'You must call register() first' };
2558
+ if (typeof decision !== 'string' || decision.length < 1 || decision.length > 500) return { error: 'Decision must be 1-500 chars' };
2559
+
2560
+ const decisions = getDecisions();
2561
+ const entry = {
2562
+ id: 'dec_' + generateId(),
2563
+ decision,
2564
+ reasoning: (reasoning || '').substring(0, 1000),
2565
+ topic: (topic || 'general').substring(0, 50),
2566
+ decided_by: registeredName,
2567
+ decided_at: new Date().toISOString(),
2568
+ };
2569
+ decisions.push(entry);
2570
+ if (decisions.length > 200) decisions.splice(0, decisions.length - 200); // cap
2571
+ writeJsonFile(DECISIONS_FILE, decisions);
2572
+ touchActivity();
2573
+ return { success: true, decision_id: entry.id, message: 'Decision logged. Other agents can see it via get_decisions() or get_briefing().' };
2574
+ }
2575
+
2576
+ function toolGetDecisions(topic) {
2577
+ let decisions = getDecisions();
2578
+ if (topic) decisions = decisions.filter(d => d.topic === topic);
2579
+ return { count: decisions.length, decisions: decisions.slice(-30) };
2580
+ }
2581
+
2582
+ // --- Tier 2: Knowledge Base, Progress, Event hooks ---
2583
+
2584
+ function toolKBWrite(key, content) {
2585
+ if (!registeredName) return { error: 'You must call register() first' };
2586
+ if (typeof key !== 'string' || key.length < 1 || key.length > 50) return { error: 'Key must be 1-50 chars' };
2587
+ if (!/^[a-zA-Z0-9_\-\.]+$/.test(key)) return { error: 'Key must be alphanumeric/underscore/hyphen/dot' };
2588
+ if (typeof content !== 'string' || Buffer.byteLength(content, 'utf8') > 102400) return { error: 'Content exceeds 100KB' };
2589
+
2590
+ const kb = getKB();
2591
+ kb[key] = { content, updated_by: registeredName, updated_at: new Date().toISOString() };
2592
+ if (Object.keys(kb).length > 100) return { error: 'Knowledge base full (max 100 keys)' };
2593
+ writeJsonFile(KB_FILE, kb);
2594
+ touchActivity();
2595
+ return { success: true, key, size: content.length, total_keys: Object.keys(kb).length };
2596
+ }
2597
+
2598
+ function toolKBRead(key) {
2599
+ const kb = getKB();
2600
+ if (key) {
2601
+ if (!kb[key]) return { error: `Key "${key}" not found in knowledge base` };
2602
+ return { key, content: kb[key].content, updated_by: kb[key].updated_by, updated_at: kb[key].updated_at };
2603
+ }
2604
+ // Return all entries
2605
+ const entries = {};
2606
+ for (const [k, v] of Object.entries(kb)) {
2607
+ entries[k] = { content: v.content, updated_by: v.updated_by, updated_at: v.updated_at };
2608
+ }
2609
+ return { entries, total_keys: Object.keys(kb).length };
2610
+ }
2611
+
2612
+ function toolKBList() {
2613
+ const kb = getKB();
2614
+ return {
2615
+ keys: Object.keys(kb).map(k => ({ key: k, updated_by: kb[k].updated_by, updated_at: kb[k].updated_at, size: kb[k].content.length })),
2616
+ total: Object.keys(kb).length,
2617
+ };
2618
+ }
2619
+
2620
+ function toolUpdateProgress(feature, percent, notes) {
2621
+ if (!registeredName) return { error: 'You must call register() first' };
2622
+ if (typeof feature !== 'string' || feature.length < 1 || feature.length > 100) return { error: 'Feature name must be 1-100 chars' };
2623
+ if (typeof percent !== 'number' || percent < 0 || percent > 100) return { error: 'Percent must be 0-100' };
2624
+
2625
+ const progress = getProgressData();
2626
+ progress[feature] = {
2627
+ percent,
2628
+ notes: (notes || '').substring(0, 500),
2629
+ updated_by: registeredName,
2630
+ updated_at: new Date().toISOString(),
2631
+ };
2632
+ writeJsonFile(PROGRESS_FILE, progress);
2633
+ touchActivity();
2634
+ return { success: true, feature, percent, message: `Progress updated: ${feature} is ${percent}% complete.` };
2635
+ }
2636
+
2637
+ function toolGetProgress() {
2638
+ const progress = getProgressData();
2639
+ const features = Object.entries(progress).map(([name, p]) => ({
2640
+ feature: name, percent: p.percent, notes: p.notes, updated_by: p.updated_by, updated_at: p.updated_at,
2641
+ }));
2642
+ const avg = features.length > 0 ? Math.round(features.reduce((s, f) => s + f.percent, 0) / features.length) : 0;
2643
+ return { features, overall_percent: avg, feature_count: features.length };
2644
+ }
2645
+
2646
+ // --- Tier 3: Voting, Code Review, Dependencies ---
2647
+
2648
+ function toolCallVote(question, options) {
2649
+ if (!registeredName) return { error: 'You must call register() first' };
2650
+ if (typeof question !== 'string' || question.length < 1 || question.length > 200) return { error: 'Question must be 1-200 chars' };
2651
+ if (!Array.isArray(options) || options.length < 2 || options.length > 10) return { error: 'Need 2-10 options' };
2652
+
2653
+ const votes = getVotes();
2654
+ const vote = {
2655
+ id: 'vote_' + generateId(),
2656
+ question,
2657
+ options: options.map(o => String(o).substring(0, 50)),
2658
+ votes: {},
2659
+ status: 'open',
2660
+ created_by: registeredName,
2661
+ created_at: new Date().toISOString(),
2662
+ };
2663
+ votes.push(vote);
2664
+ writeJsonFile(VOTES_FILE, votes);
2665
+
2666
+ // Notify all agents
2667
+ broadcastSystemMessage(`[VOTE] ${registeredName} started a vote: "${question}" — Options: ${vote.options.join(', ')}. Call cast_vote("${vote.id}", "your_choice") to vote.`, registeredName);
2668
+ touchActivity();
2669
+ return { success: true, vote_id: vote.id, question, options: vote.options, message: 'Vote created. All agents have been notified.' };
2670
+ }
2671
+
2672
+ function toolCastVote(voteId, choice) {
2673
+ if (!registeredName) return { error: 'You must call register() first' };
2674
+
2675
+ const votes = getVotes();
2676
+ const vote = votes.find(v => v.id === voteId);
2677
+ if (!vote) return { error: `Vote not found: ${voteId}` };
2678
+ if (vote.status !== 'open') return { error: 'Vote is already closed.' };
2679
+ if (!vote.options.includes(choice)) return { error: `Invalid choice. Options: ${vote.options.join(', ')}` };
2680
+
2681
+ vote.votes[registeredName] = { choice, voted_at: new Date().toISOString() };
2682
+
2683
+ // Check if all online agents have voted
2684
+ const agents = getAgents();
2685
+ const onlineAgents = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
2686
+ const allVoted = onlineAgents.every(n => vote.votes[n]);
2687
+
2688
+ if (allVoted) {
2689
+ vote.status = 'closed';
2690
+ vote.closed_at = new Date().toISOString();
2691
+ // Count results
2692
+ const results = {};
2693
+ for (const opt of vote.options) results[opt] = 0;
2694
+ for (const v of Object.values(vote.votes)) results[v.choice]++;
2695
+ vote.results = results;
2696
+ const winner = Object.entries(results).sort((a, b) => b[1] - a[1])[0];
2697
+ broadcastSystemMessage(`[VOTE RESULT] "${vote.question}" — Winner: ${winner[0]} (${winner[1]} votes). Full results: ${JSON.stringify(results)}`);
2698
+ }
2699
+
2700
+ writeJsonFile(VOTES_FILE, votes);
2701
+ touchActivity();
2702
+ return { success: true, vote_id: voteId, your_vote: choice, status: vote.status, votes_cast: Object.keys(vote.votes).length, agents_online: onlineAgents.length };
2703
+ }
2704
+
2705
+ function toolVoteStatus(voteId) {
2706
+ const votes = getVotes();
2707
+ if (voteId) {
2708
+ const vote = votes.find(v => v.id === voteId);
2709
+ if (!vote) return { error: `Vote not found: ${voteId}` };
2710
+ return { vote };
2711
+ }
2712
+ return { votes: votes.map(v => ({ id: v.id, question: v.question, status: v.status, votes_cast: Object.keys(v.votes).length, results: v.results || null })) };
2713
+ }
2714
+
2715
+ function toolRequestReview(filePath, description) {
2716
+ if (!registeredName) return { error: 'You must call register() first' };
2717
+ if (typeof filePath !== 'string' || filePath.length < 1) return { error: 'File path required' };
2718
+
2719
+ const reviews = getReviews();
2720
+ const review = {
2721
+ id: 'rev_' + generateId(),
2722
+ file: filePath.replace(/\\/g, '/'),
2723
+ description: (description || '').substring(0, 500),
2724
+ status: 'pending',
2725
+ requested_by: registeredName,
2726
+ requested_at: new Date().toISOString(),
2727
+ reviewer: null,
2728
+ feedback: null,
2729
+ };
2730
+ reviews.push(review);
2731
+ writeJsonFile(REVIEWS_FILE, reviews);
2732
+
2733
+ // Notify all other agents
2734
+ broadcastSystemMessage(`[REVIEW] ${registeredName} requests review of "${review.file}": ${review.description || 'No description'}. Call submit_review("${review.id}", "approved"/"changes_requested", "your feedback") to review.`, registeredName);
2735
+ touchActivity();
2736
+ return { success: true, review_id: review.id, file: review.file, message: 'Review requested. Team has been notified.' };
2737
+ }
2738
+
2739
+ function toolSubmitReview(reviewId, status, feedback) {
2740
+ if (!registeredName) return { error: 'You must call register() first' };
2741
+
2742
+ const validStatuses = ['approved', 'changes_requested'];
2743
+ if (!validStatuses.includes(status)) return { error: `Status must be: ${validStatuses.join(' or ')}` };
2744
+
2745
+ const reviews = getReviews();
2746
+ const review = reviews.find(r => r.id === reviewId);
2747
+ if (!review) return { error: `Review not found: ${reviewId}` };
2748
+ if (review.requested_by === registeredName) return { error: 'Cannot review your own code.' };
2749
+
2750
+ review.status = status;
2751
+ review.reviewer = registeredName;
2752
+ review.feedback = (feedback || '').substring(0, 2000);
2753
+ review.reviewed_at = new Date().toISOString();
2754
+ writeJsonFile(REVIEWS_FILE, reviews);
2755
+
2756
+ // Notify requester
2757
+ const agents = getAgents();
2758
+ if (agents[review.requested_by]) {
2759
+ sendSystemMessage(review.requested_by, `[REVIEW] ${registeredName} ${status === 'approved' ? 'approved' : 'requested changes on'} "${review.file}": ${review.feedback || 'No feedback'}`);
2760
+ }
2761
+ touchActivity();
2762
+ return { success: true, review_id: reviewId, status, message: `Review submitted: ${status}` };
2763
+ }
2764
+
2765
+ function toolDeclareDependency(taskId, dependsOnTaskId) {
2766
+ if (!registeredName) return { error: 'You must call register() first' };
2767
+
2768
+ const tasks = getTasks();
2769
+ const task = tasks.find(t => t.id === taskId);
2770
+ const depTask = tasks.find(t => t.id === dependsOnTaskId);
2771
+ if (!task) return { error: `Task not found: ${taskId}` };
2772
+ if (!depTask) return { error: `Dependency task not found: ${dependsOnTaskId}` };
2773
+
2774
+ const deps = getDeps();
2775
+ deps.push({
2776
+ id: 'dep_' + generateId(),
2777
+ task_id: taskId,
2778
+ depends_on: dependsOnTaskId,
2779
+ declared_by: registeredName,
2780
+ declared_at: new Date().toISOString(),
2781
+ resolved: depTask.status === 'done',
2782
+ });
2783
+ writeJsonFile(DEPS_FILE, deps);
2784
+ touchActivity();
2785
+
2786
+ if (depTask.status === 'done') {
2787
+ return { success: true, message: `Dependency declared but already resolved — "${depTask.title}" is done. You can proceed.` };
2788
+ }
2789
+ return { success: true, message: `Dependency declared: "${task.title}" is blocked until "${depTask.title}" is done. You'll be notified when it completes.` };
2790
+ }
2791
+
2792
+ function toolCheckDependencies(taskId) {
2793
+ const deps = getDeps();
2794
+ const tasks = getTasks();
2795
+
2796
+ if (taskId) {
2797
+ const taskDeps = deps.filter(d => d.task_id === taskId);
2798
+ return {
2799
+ task_id: taskId,
2800
+ dependencies: taskDeps.map(d => {
2801
+ const t = tasks.find(t2 => t2.id === d.depends_on);
2802
+ return { depends_on: d.depends_on, title: t ? t.title : 'unknown', status: t ? t.status : 'unknown', resolved: t ? t.status === 'done' : false };
2803
+ }),
2804
+ };
2805
+ }
2806
+ // All unresolved deps
2807
+ const unresolved = deps.filter(d => {
2808
+ const t = tasks.find(t2 => t2.id === d.depends_on);
2809
+ return t && t.status !== 'done';
2810
+ });
2811
+ return { unresolved_count: unresolved.length, unresolved: unresolved.map(d => ({ task_id: d.task_id, blocked_by: d.depends_on })) };
2812
+ }
2813
+
2814
+ // --- Conversation Compression ---
2815
+
2816
+ function getCompressed() { return readJsonFile(COMPRESSED_FILE) || { segments: [], last_compressed_at: null }; }
2817
+
2818
+ // Compress old messages into summary segments
2819
+ // Keeps last 20 verbatim, groups older messages into topic summaries
2820
+ function autoCompress() {
2821
+ const history = readJsonl(getHistoryFile(currentBranch));
2822
+ if (history.length <= 50) return; // only compress when conversation is long
2823
+
2824
+ const compressed = getCompressed();
2825
+ const cutoff = history.length - 20; // keep last 20 verbatim
2826
+ const toCompress = history.slice(compressed.segments.length > 0 ? compressed.segments.reduce((s, seg) => s + seg.message_count, 0) : 0, cutoff);
2827
+ if (toCompress.length < 10) return; // not enough new messages to compress
2828
+
2829
+ // Group messages into chunks of ~10 and create summaries
2830
+ const chunkSize = 10;
2831
+ for (let i = 0; i < toCompress.length; i += chunkSize) {
2832
+ const chunk = toCompress.slice(i, i + chunkSize);
2833
+ const speakers = [...new Set(chunk.map(m => m.from))];
2834
+ const topics = chunk.map(m => {
2835
+ const preview = m.content.substring(0, 80).replace(/\n/g, ' ');
2836
+ return `${m.from}: ${preview}`;
2837
+ });
2838
+ const segment = {
2839
+ id: 'seg_' + generateId(),
2840
+ from_time: chunk[0].timestamp,
2841
+ to_time: chunk[chunk.length - 1].timestamp,
2842
+ message_count: chunk.length,
2843
+ speakers,
2844
+ summary: topics.join(' | '),
2845
+ first_msg_id: chunk[0].id,
2846
+ last_msg_id: chunk[chunk.length - 1].id,
2847
+ };
2848
+ compressed.segments.push(segment);
2849
+ }
2850
+
2851
+ // Cap segments at 100
2852
+ if (compressed.segments.length > 100) compressed.segments = compressed.segments.slice(-100);
2853
+ compressed.last_compressed_at = new Date().toISOString();
2854
+ compressed.total_original_messages = history.length;
2855
+ writeJsonFile(COMPRESSED_FILE, compressed);
2856
+ }
2857
+
2858
+ function toolGetCompressedHistory() {
2859
+ if (!registeredName) return { error: 'You must call register() first' };
2860
+
2861
+ const compressed = getCompressed();
2862
+ const history = readJsonl(getHistoryFile(currentBranch));
2863
+ const recent = history.slice(-20);
2864
+
2865
+ return {
2866
+ compressed_segments: compressed.segments.slice(-20).map(s => ({
2867
+ time_range: s.from_time + ' to ' + s.to_time,
2868
+ speakers: s.speakers,
2869
+ message_count: s.message_count,
2870
+ summary: s.summary,
2871
+ })),
2872
+ recent_messages: recent.map(m => ({
2873
+ id: m.id, from: m.from, to: m.to,
2874
+ content: m.content.substring(0, 300),
2875
+ timestamp: m.timestamp,
2876
+ })),
2877
+ total_messages: history.length,
2878
+ compressed_count: compressed.segments.reduce((s, seg) => s + seg.message_count, 0),
2879
+ recent_count: recent.length,
2880
+ hint: 'Compressed segments summarize older messages. Recent messages are shown verbatim.',
2881
+ };
2882
+ }
2883
+
2884
+ // --- Agent Reputation ---
2885
+
2886
+ function getReputation() { return readJsonFile(REPUTATION_FILE) || {}; }
2887
+
2888
+ function trackReputation(agent, action) {
2889
+ const rep = getReputation();
2890
+ if (!rep[agent]) {
2891
+ rep[agent] = {
2892
+ tasks_completed: 0, tasks_created: 0, reviews_done: 0, reviews_requested: 0,
2893
+ bugs_found: 0, messages_sent: 0, decisions_made: 0, votes_cast: 0,
2894
+ kb_contributions: 0, files_shared: 0, first_seen: new Date().toISOString(),
2895
+ last_active: new Date().toISOString(), strengths: [],
2896
+ };
2897
+ }
2898
+ const r = rep[agent];
2899
+ r.last_active = new Date().toISOString();
2900
+
2901
+ switch (action) {
2902
+ case 'task_complete': r.tasks_completed++; break;
2903
+ case 'task_create': r.tasks_created++; break;
2904
+ case 'review_submit': r.reviews_done++; break;
2905
+ case 'review_request': r.reviews_requested++; break;
2906
+ case 'message_send': r.messages_sent++; break;
2907
+ case 'decision_log': r.decisions_made++; break;
2908
+ case 'vote_cast': r.votes_cast++; break;
2909
+ case 'kb_write': r.kb_contributions++; break;
2910
+ case 'file_share': r.files_shared++; break;
2911
+ case 'bug_found': r.bugs_found++; break;
2912
+ }
2913
+
2914
+ // Auto-detect strengths based on stats
2915
+ r.strengths = [];
2916
+ if (r.tasks_completed >= 3) r.strengths.push('productive');
2917
+ if (r.reviews_done >= 2) r.strengths.push('reviewer');
2918
+ if (r.decisions_made >= 2) r.strengths.push('decision-maker');
2919
+ if (r.kb_contributions >= 3) r.strengths.push('documenter');
2920
+ if (r.tasks_created >= 3) r.strengths.push('organizer');
2921
+ if (r.bugs_found >= 2) r.strengths.push('bug-hunter');
2922
+
2923
+ writeJsonFile(REPUTATION_FILE, rep);
2924
+ }
2925
+
2926
+ function toolGetReputation(agent) {
2927
+ const rep = getReputation();
2928
+
2929
+ if (agent) {
2930
+ if (!rep[agent]) return { agent, message: 'No reputation data yet for this agent.' };
2931
+ return { agent, reputation: rep[agent] };
2932
+ }
2933
+
2934
+ // All agents with ranking
2935
+ const leaderboard = Object.entries(rep).map(([name, r]) => ({
2936
+ agent: name,
2937
+ score: r.tasks_completed * 10 + r.reviews_done * 5 + r.decisions_made * 3 + r.kb_contributions * 2 + r.bugs_found * 8,
2938
+ tasks_completed: r.tasks_completed,
2939
+ reviews_done: r.reviews_done,
2940
+ strengths: r.strengths,
2941
+ last_active: r.last_active,
2942
+ })).sort((a, b) => b.score - a.score);
2943
+
2944
+ return { leaderboard, total_agents: leaderboard.length };
2945
+ }
2946
+
2947
+ function toolSuggestTask() {
2948
+ if (!registeredName) return { error: 'You must call register() first' };
2949
+
2950
+ const rep = getReputation();
2951
+ const myRep = rep[registeredName];
2952
+ const tasks = getTasks();
2953
+ const pendingTasks = tasks.filter(t => t.status === 'pending' && !t.assignee);
2954
+ const unassignedTasks = tasks.filter(t => t.status === 'pending');
2955
+
2956
+ if (pendingTasks.length === 0 && unassignedTasks.length === 0) {
2957
+ // Check reviews
2958
+ const reviews = getReviews();
2959
+ const pendingReviews = reviews.filter(r => r.status === 'pending' && r.requested_by !== registeredName);
2960
+ if (pendingReviews.length > 0) {
2961
+ return { suggestion: 'review', review_id: pendingReviews[0].id, file: pendingReviews[0].file, message: `No pending tasks, but there's a code review waiting: "${pendingReviews[0].file}". Call submit_review() to review it.` };
2962
+ }
2963
+ // Check deps
2964
+ const deps = getDeps();
2965
+ const unresolved = deps.filter(d => !d.resolved);
2966
+ if (unresolved.length > 0) {
2967
+ return { suggestion: 'unblock', message: `No tasks available, but ${unresolved.length} task(s) are blocked by dependencies. Check if you can help resolve them.` };
2968
+ }
2969
+ return { suggestion: 'none', message: 'No pending tasks, reviews, or blocked items. Ask the team what needs doing next.' };
2970
+ }
2971
+
2972
+ // Suggest based on reputation strengths
2973
+ let suggested = pendingTasks[0] || unassignedTasks[0];
2974
+ if (myRep && myRep.strengths.includes('reviewer')) {
2975
+ const reviews = getReviews().filter(r => r.status === 'pending' && r.requested_by !== registeredName);
2976
+ if (reviews.length > 0) return { suggestion: 'review', review_id: reviews[0].id, file: reviews[0].file, message: `Based on your strengths (reviewer), review "${reviews[0].file}".` };
2977
+ }
2978
+
2979
+ return {
2980
+ suggestion: 'task',
2981
+ task_id: suggested.id,
2982
+ title: suggested.title,
2983
+ description: suggested.description,
2984
+ message: `Suggested: "${suggested.title}". Call update_task("${suggested.id}", "in_progress") to claim it.`,
2985
+ };
2986
+ }
2987
+
2134
2988
  // --- MCP Server setup ---
2135
2989
 
2136
2990
  const server = new Server(
2137
- { name: 'agent-bridge', version: '3.6.2' },
2991
+ { name: 'agent-bridge', version: '3.8.0' },
2138
2992
  { capabilities: { tools: {} } }
2139
2993
  );
2140
2994
 
@@ -2143,7 +2997,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2143
2997
  tools: [
2144
2998
  {
2145
2999
  name: 'register',
2146
- description: 'Register this agent\'s identity (any name, e.g. "A", "Coder", "Reviewer"). Must be called before any other tool.',
3000
+ description: 'Register this agent\'s identity. Must be called first. Returns a collaboration guide with all tool categories, critical rules, and workflow patterns READ IT CAREFULLY before doing anything else. Then call get_briefing() for project context, then listen_group() to join the conversation.',
2147
3001
  inputSchema: {
2148
3002
  type: 'object',
2149
3003
  properties: {
@@ -2222,7 +3076,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2222
3076
  },
2223
3077
  {
2224
3078
  name: 'listen',
2225
- description: 'Listen for messages indefinitely. Unlike wait_for_reply, this never times out it blocks until a message arrives. The agent should call listen() after finishing any task to stay available. After receiving a message, process it, respond, then call listen() again.',
3079
+ description: 'Listen for messages indefinitely. Auto-detects conversation mode: in group/managed mode, behaves like listen_group() (returns batched messages with agent statuses). In direct mode, returns one message at a time. Either listen() or listen_group() works in any mode they auto-delegate to the correct behavior.',
2226
3080
  inputSchema: {
2227
3081
  type: 'object',
2228
3082
  properties: {
@@ -2545,14 +3399,127 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2545
3399
  },
2546
3400
  {
2547
3401
  name: 'listen_group',
2548
- description: 'Listen for messages in group or managed conversation mode. Returns ALL unconsumed messages as a batch, plus conversation context and hints. IMPORTANT: After processing messages and responding, you MUST call listen_group() again immediately. If it times out with retry:true, call it again. Never stop listening — this is how you stay in the conversation.',
3402
+ description: 'Listen for messages in group or managed conversation mode. Auto-detects mode: in direct mode, behaves like listen(). Returns ALL unconsumed messages as a sorted batch (system > threaded > direct > broadcast), plus batch_summary, agent statuses, and hints. Either listen() or listen_group() works in any mode they auto-delegate. Call again immediately after responding.',
2549
3403
  inputSchema: {
2550
3404
  type: 'object',
2551
- properties: {
2552
- timeout_seconds: { type: 'number', description: 'Max seconds to wait for messages (default 300)' },
2553
- },
3405
+ properties: {},
2554
3406
  },
2555
3407
  },
3408
+ // --- Briefing & Recovery ---
3409
+ {
3410
+ name: 'get_guide',
3411
+ description: 'Get the collaboration guide — all tool categories, critical rules, and workflow patterns. Call this if you are unsure how to use the tools or need a refresher on best practices.',
3412
+ inputSchema: { type: 'object', properties: {} },
3413
+ },
3414
+ {
3415
+ name: 'get_briefing',
3416
+ description: 'Get a full project briefing: who is online, active tasks, recent decisions, knowledge base, locked files, progress, and project files. Call this when joining a project or after being away. One call = fully onboarded.',
3417
+ inputSchema: { type: 'object', properties: {} },
3418
+ },
3419
+ // --- File Locking ---
3420
+ {
3421
+ name: 'lock_file',
3422
+ description: 'Lock a file for exclusive editing. Other agents will be warned if they try to edit it. Call unlock_file() when done. Locks auto-release if you disconnect.',
3423
+ inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Relative path to the file to lock' } }, required: ['file_path'] },
3424
+ },
3425
+ {
3426
+ name: 'unlock_file',
3427
+ description: 'Unlock a file you previously locked. Omit file_path to unlock all your files.',
3428
+ inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to unlock (optional — omit to unlock all)' } } },
3429
+ },
3430
+ // --- Decision Log ---
3431
+ {
3432
+ name: 'log_decision',
3433
+ description: 'Log a team decision so it persists and other agents can reference it. Prevents re-debating the same choices.',
3434
+ inputSchema: { type: 'object', properties: { decision: { type: 'string', description: 'The decision made (max 500 chars)' }, reasoning: { type: 'string', description: 'Why this was decided (optional, max 1000 chars)' }, topic: { type: 'string', description: 'Category like "architecture", "tech-stack", "design" (optional)' } }, required: ['decision'] },
3435
+ },
3436
+ {
3437
+ name: 'get_decisions',
3438
+ description: 'Get all logged decisions, optionally filtered by topic.',
3439
+ inputSchema: { type: 'object', properties: { topic: { type: 'string', description: 'Filter by topic (optional)' } } },
3440
+ },
3441
+ // --- Knowledge Base ---
3442
+ {
3443
+ name: 'kb_write',
3444
+ description: 'Write to the shared team knowledge base. Any agent can read, any agent can write. Use for API specs, conventions, shared data.',
3445
+ inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key name (1-50 alphanumeric chars)' }, content: { type: 'string', description: 'Content (max 100KB)' } }, required: ['key', 'content'] },
3446
+ },
3447
+ {
3448
+ name: 'kb_read',
3449
+ description: 'Read from the shared knowledge base. Omit key to read all entries.',
3450
+ inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key to read (optional — omit for all)' } } },
3451
+ },
3452
+ {
3453
+ name: 'kb_list',
3454
+ description: 'List all keys in the shared knowledge base with metadata.',
3455
+ inputSchema: { type: 'object', properties: {} },
3456
+ },
3457
+ // --- Progress Tracking ---
3458
+ {
3459
+ name: 'update_progress',
3460
+ description: 'Update feature-level progress. Higher level than tasks — tracks overall feature completion percentage.',
3461
+ inputSchema: { type: 'object', properties: { feature: { type: 'string', description: 'Feature name (max 100 chars)' }, percent: { type: 'number', description: 'Completion percentage 0-100' }, notes: { type: 'string', description: 'Progress notes (optional)' } }, required: ['feature', 'percent'] },
3462
+ },
3463
+ {
3464
+ name: 'get_progress',
3465
+ description: 'Get progress on all features with completion percentages and overall project progress.',
3466
+ inputSchema: { type: 'object', properties: {} },
3467
+ },
3468
+ // --- Voting ---
3469
+ {
3470
+ name: 'call_vote',
3471
+ description: 'Start a vote for the team to decide something. All online agents are notified and can cast their vote.',
3472
+ inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'The question to vote on' }, options: { type: 'array', items: { type: 'string' }, description: 'Array of 2-10 options to choose from' } }, required: ['question', 'options'] },
3473
+ },
3474
+ {
3475
+ name: 'cast_vote',
3476
+ description: 'Cast your vote on an open vote. Vote auto-resolves when all online agents have voted.',
3477
+ inputSchema: { type: 'object', properties: { vote_id: { type: 'string', description: 'Vote ID' }, choice: { type: 'string', description: 'Your choice (must match one of the options)' } }, required: ['vote_id', 'choice'] },
3478
+ },
3479
+ {
3480
+ name: 'vote_status',
3481
+ description: 'Check status of a specific vote or all votes.',
3482
+ inputSchema: { type: 'object', properties: { vote_id: { type: 'string', description: 'Vote ID (optional — omit for all)' } } },
3483
+ },
3484
+ // --- Code Review ---
3485
+ {
3486
+ name: 'request_review',
3487
+ description: 'Request a code review from the team. Creates a review request and notifies all agents.',
3488
+ inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to review' }, description: { type: 'string', description: 'What to focus on in the review' } }, required: ['file_path'] },
3489
+ },
3490
+ {
3491
+ name: 'submit_review',
3492
+ description: 'Submit a code review — approve or request changes with feedback.',
3493
+ inputSchema: { type: 'object', properties: { review_id: { type: 'string', description: 'Review ID' }, status: { type: 'string', enum: ['approved', 'changes_requested'], description: 'Review result' }, feedback: { type: 'string', description: 'Your review feedback (max 2000 chars)' } }, required: ['review_id', 'status'] },
3494
+ },
3495
+ // --- Dependencies ---
3496
+ {
3497
+ name: 'declare_dependency',
3498
+ description: 'Declare that a task depends on another task. You will be notified when the dependency is complete.',
3499
+ inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Your task that is blocked' }, depends_on: { type: 'string', description: 'Task ID that must complete first' } }, required: ['task_id', 'depends_on'] },
3500
+ },
3501
+ {
3502
+ name: 'check_dependencies',
3503
+ description: 'Check dependency status for a task or all unresolved dependencies.',
3504
+ inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to check (optional — omit for all unresolved)' } } },
3505
+ },
3506
+ // --- Conversation Compression ---
3507
+ {
3508
+ name: 'get_compressed_history',
3509
+ description: 'Get conversation history with automatic compression. Old messages are summarized into segments, recent messages shown verbatim. Use this when the conversation is long and you need to catch up without overflowing your context.',
3510
+ inputSchema: { type: 'object', properties: {} },
3511
+ },
3512
+ // --- Reputation ---
3513
+ {
3514
+ name: 'get_reputation',
3515
+ description: 'View agent reputation — tasks completed, reviews done, bugs found, strengths. Shows leaderboard when called without agent name.',
3516
+ inputSchema: { type: 'object', properties: { agent: { type: 'string', description: 'Agent name (optional — omit for leaderboard)' } } },
3517
+ },
3518
+ {
3519
+ name: 'suggest_task',
3520
+ description: 'Get a task suggestion based on your strengths, pending tasks, open reviews, and blocked dependencies. Helps you find the most useful thing to do next.',
3521
+ inputSchema: { type: 'object', properties: {} },
3522
+ },
2556
3523
  // --- Managed mode tools ---
2557
3524
  {
2558
3525
  name: 'claim_manager',
@@ -2681,7 +3648,70 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2681
3648
  result = toolSetConversationMode(args.mode);
2682
3649
  break;
2683
3650
  case 'listen_group':
2684
- result = await toolListenGroup(args?.timeout_seconds);
3651
+ result = await toolListenGroup();
3652
+ break;
3653
+ case 'get_guide':
3654
+ result = toolGetGuide();
3655
+ break;
3656
+ case 'get_briefing':
3657
+ result = toolGetBriefing();
3658
+ break;
3659
+ case 'lock_file':
3660
+ result = toolLockFile(args.file_path);
3661
+ break;
3662
+ case 'unlock_file':
3663
+ result = toolUnlockFile(args?.file_path);
3664
+ break;
3665
+ case 'log_decision':
3666
+ result = toolLogDecision(args.decision, args?.reasoning, args?.topic);
3667
+ break;
3668
+ case 'get_decisions':
3669
+ result = toolGetDecisions(args?.topic);
3670
+ break;
3671
+ case 'kb_write':
3672
+ result = toolKBWrite(args.key, args.content);
3673
+ break;
3674
+ case 'kb_read':
3675
+ result = toolKBRead(args?.key);
3676
+ break;
3677
+ case 'kb_list':
3678
+ result = toolKBList();
3679
+ break;
3680
+ case 'update_progress':
3681
+ result = toolUpdateProgress(args.feature, args.percent, args?.notes);
3682
+ break;
3683
+ case 'get_progress':
3684
+ result = toolGetProgress();
3685
+ break;
3686
+ case 'call_vote':
3687
+ result = toolCallVote(args.question, args.options);
3688
+ break;
3689
+ case 'cast_vote':
3690
+ result = toolCastVote(args.vote_id, args.choice);
3691
+ break;
3692
+ case 'vote_status':
3693
+ result = toolVoteStatus(args?.vote_id);
3694
+ break;
3695
+ case 'request_review':
3696
+ result = toolRequestReview(args.file_path, args?.description);
3697
+ break;
3698
+ case 'submit_review':
3699
+ result = toolSubmitReview(args.review_id, args.status, args?.feedback);
3700
+ break;
3701
+ case 'declare_dependency':
3702
+ result = toolDeclareDependency(args.task_id, args.depends_on);
3703
+ break;
3704
+ case 'check_dependencies':
3705
+ result = toolCheckDependencies(args?.task_id);
3706
+ break;
3707
+ case 'get_compressed_history':
3708
+ result = toolGetCompressedHistory();
3709
+ break;
3710
+ case 'get_reputation':
3711
+ result = toolGetReputation(args?.agent);
3712
+ break;
3713
+ case 'suggest_task':
3714
+ result = toolSuggestTask();
2685
3715
  break;
2686
3716
  case 'claim_manager':
2687
3717
  result = toolClaimManager();
@@ -2706,19 +3736,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2706
3736
  };
2707
3737
  }
2708
3738
 
2709
- // Global hook: on non-listen tools, check for pending messages and nudge the agent
2710
- // This catches agents who are mid-work and have messages piling up
3739
+ // Global hook: on non-listen tools, check for pending messages and nudge with escalating urgency
2711
3740
  const listenTools = ['listen', 'listen_group', 'listen_codex', 'wait_for_reply', 'check_messages'];
2712
3741
  if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
2713
3742
  try {
2714
3743
  const pending = getUnconsumedMessages(registeredName);
2715
3744
  if (pending.length > 0 && !result.you_have_messages) {
2716
3745
  result._pending_messages = pending.length;
2717
- result._nudge = `You have ${pending.length} unread message(s) from the team. Finish your current task quickly, then call listen_group() to read them.`;
3746
+ // Escalate urgency based on oldest pending message age
3747
+ const oldestAge = pending.reduce((max, m) => {
3748
+ const age = Date.now() - new Date(m.timestamp).getTime();
3749
+ return age > max ? age : max;
3750
+ }, 0);
3751
+ const ageSec = Math.round(oldestAge / 1000);
3752
+ if (ageSec > 120) {
3753
+ result._nudge = `CRITICAL: ${pending.length} message(s) waiting ${Math.round(ageSec / 60)}+ min. Team is likely blocked on you. Call listen_group() NOW.`;
3754
+ } else if (ageSec > 30) {
3755
+ result._nudge = `URGENT: ${pending.length} message(s) waiting ${ageSec}s. Team may be blocked. Call listen_group() soon.`;
3756
+ } else {
3757
+ result._nudge = `You have ${pending.length} unread message(s). Call listen_group() after this to read them.`;
3758
+ }
2718
3759
  }
2719
3760
  } catch {}
2720
3761
  }
2721
3762
 
3763
+ // Global hook: reputation tracking
3764
+ if (registeredName && result.success) {
3765
+ try {
3766
+ const repMap = {
3767
+ 'send_message': 'message_send', 'broadcast': 'message_send',
3768
+ 'create_task': 'task_create', 'share_file': 'file_share',
3769
+ 'log_decision': 'decision_log', 'cast_vote': 'vote_cast',
3770
+ 'kb_write': 'kb_write', 'request_review': 'review_request',
3771
+ 'submit_review': 'review_submit',
3772
+ };
3773
+ if (repMap[name]) trackReputation(registeredName, repMap[name]);
3774
+ // Track task completion specifically
3775
+ if (name === 'update_task' && args?.status === 'done') trackReputation(registeredName, 'task_complete');
3776
+ } catch {}
3777
+ }
3778
+
3779
+ // Global hook: auto-compress conversation periodically
3780
+ if (name === 'send_message' || name === 'broadcast') {
3781
+ try { autoCompress(); } catch {}
3782
+ }
3783
+
2722
3784
  return {
2723
3785
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
2724
3786
  };
@@ -2751,7 +3813,7 @@ async function main() {
2751
3813
  ensureDataDir();
2752
3814
  const transport = new StdioServerTransport();
2753
3815
  await server.connect(transport);
2754
- console.error('Agent Bridge MCP server v3.6.2 running (32 tools)');
3816
+ console.error('Agent Bridge MCP server v3.8.0 running (53 tools)');
2755
3817
  }
2756
3818
 
2757
3819
  main().catch(console.error);