let-them-talk 3.6.2 → 3.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.0] - 2026-03-16
4
+
5
+ ### Added — Agent Ecosystem (20 new tools, 52 total)
6
+
7
+ **Tier 1 — Critical Infrastructure:**
8
+ - **`get_briefing()`** — full project onboarding in one call: agents, tasks, decisions, KB, locked files, progress, project file tree
9
+ - **`lock_file(path)` / `unlock_file(path?)`** — exclusive file editing with auto-release on agent death
10
+ - **`log_decision(decision, reasoning?, topic?)` / `get_decisions(topic?)`** — persistent decision log, prevents re-debating
11
+ - **Agent recovery on rejoin** — `register()` returns active tasks, workspace keys, recent messages for returning agents
12
+
13
+ **Tier 2 — Quality of Life:**
14
+ - **`kb_write(key, content)` / `kb_read(key?)` / `kb_list()`** — shared team knowledge base (any agent reads/writes)
15
+ - **Event hooks** — auto-fires system messages on `agent_join`, `task_complete`, `all_tasks_done`, `dependency_met`
16
+ - **`update_progress(feature, percent, notes)` / `get_progress()`** — feature-level progress tracking with overall %
17
+ - **`get_compressed_history()`** — auto-compresses old messages into summary segments, keeps recent verbatim
18
+ - **`listen_group()` now blocks indefinitely** — no more timeout, agents never drop out
19
+
20
+ **Tier 3 — Advanced Collaboration:**
21
+ - **`call_vote(question, options)` / `cast_vote(vote_id, choice)` / `vote_status(vote_id?)`** — team voting with auto-resolve when all vote
22
+ - **`request_review(file, desc)` / `submit_review(review_id, status, feedback)`** — code review pipeline with approve/changes_requested
23
+ - **`declare_dependency(task_id, depends_on)` / `check_dependencies(task_id?)`** — task dependency tracking with auto-notify on resolve
24
+ - **`get_reputation(agent?)` / `suggest_task()`** — agent reputation tracking (auto-detects strengths), task suggestions based on skills
25
+ - **Auto-reputation tracking** — global hook tracks every action (messages, tasks, reviews, decisions, KB writes) without manual calls
26
+
27
+ ### Fixed
28
+ - **Monitor screens stay red** when agent stops listening — persistent color state instead of 300ms flash
29
+ - **"NOT LISTENING" warning** shown prominently on desk monitor canvas
30
+ - **Status color logic** — green = listening, red = active but not listening, yellow = sleeping, dim = dead
31
+
3
32
  ## [3.6.2] - 2026-03-16
4
33
 
5
34
  ### Added — Message Awareness System
package/README.md CHANGED
@@ -86,7 +86,7 @@ Each terminal spawns its own MCP server process. All processes share a `.agent-b
86
86
 
87
87
  - **3D virtual office** — chibi characters at desks, spectator camera (WASD+mouse), 11 hairstyles, 6 outfits, gestures, furniture, TV dashboard
88
88
  - **Managed conversation mode** — structured turn-taking with floor control for 3+ agents, prevents broadcast storms
89
- - **32 MCP tools** — messaging, tasks, workflows, profiles, workspaces, branching, managed mode
89
+ - **52 MCP tools** — messaging, tasks, workflows, profiles, workspaces, branching, managed mode, briefing, file locking, decisions, KB, voting, reviews, dependencies, reputation
90
90
  - **8-tab dashboard** — 3D Hub (default), messages, tasks, workspaces, workflows, launch, stats, docs
91
91
  - **Group conversation mode** — free multi-agent collaboration with auto-broadcast and cooldown
92
92
  - **5 agent templates** — pair, team, review, debate, managed — with ready-to-paste prompts
@@ -175,7 +175,7 @@ The dashboard's default view is a **real-time 3D virtual office** (the "3D Hub")
175
175
 
176
176
  **Animations:** walk, sit, type, raise hand, sleep (ZZZ), wave, think, point, celebrate, stretch, idle gestures. Agents turn toward speakers during conversations.
177
177
 
178
- ## MCP Tools (32)
178
+ ## MCP Tools (52)
179
179
 
180
180
  <details>
181
181
  <summary><strong>Messaging (13 tools)</strong></summary>
@@ -249,6 +249,90 @@ The dashboard's default view is a **real-time 3D virtual office** (the "3D Hub")
249
249
 
250
250
  </details>
251
251
 
252
+ <details>
253
+ <summary><strong>Briefing & Recovery (1 tool)</strong></summary>
254
+
255
+ | Tool | Description |
256
+ |------|-------------|
257
+ | `get_briefing` | Full project onboarding — agents, tasks, decisions, KB, locks, progress, files |
258
+
259
+ </details>
260
+
261
+ <details>
262
+ <summary><strong>File Locking (2 tools)</strong></summary>
263
+
264
+ | Tool | Description |
265
+ |------|-------------|
266
+ | `lock_file` | Lock a file for exclusive editing. Auto-releases on death |
267
+ | `unlock_file` | Unlock a file or all your locked files |
268
+
269
+ </details>
270
+
271
+ <details>
272
+ <summary><strong>Decision Log (2 tools)</strong></summary>
273
+
274
+ | Tool | Description |
275
+ |------|-------------|
276
+ | `log_decision` | Log a team decision with reasoning and topic |
277
+ | `get_decisions` | Get all decisions, optionally filtered by topic |
278
+
279
+ </details>
280
+
281
+ <details>
282
+ <summary><strong>Knowledge Base (3 tools)</strong></summary>
283
+
284
+ | Tool | Description |
285
+ |------|-------------|
286
+ | `kb_write` | Write to shared team knowledge base |
287
+ | `kb_read` | Read KB entries (one or all) |
288
+ | `kb_list` | List all KB keys with metadata |
289
+
290
+ </details>
291
+
292
+ <details>
293
+ <summary><strong>Progress & Compression (3 tools)</strong></summary>
294
+
295
+ | Tool | Description |
296
+ |------|-------------|
297
+ | `update_progress` | Update feature-level completion percentage |
298
+ | `get_progress` | Get all feature progress with overall % |
299
+ | `get_compressed_history` | Compressed old messages + recent verbatim |
300
+
301
+ </details>
302
+
303
+ <details>
304
+ <summary><strong>Voting (3 tools)</strong></summary>
305
+
306
+ | Tool | Description |
307
+ |------|-------------|
308
+ | `call_vote` | Start a team vote with options |
309
+ | `cast_vote` | Cast your vote (auto-resolves when all vote) |
310
+ | `vote_status` | Check vote results |
311
+
312
+ </details>
313
+
314
+ <details>
315
+ <summary><strong>Code Review (2 tools)</strong></summary>
316
+
317
+ | Tool | Description |
318
+ |------|-------------|
319
+ | `request_review` | Request a code review from the team |
320
+ | `submit_review` | Approve or request changes with feedback |
321
+
322
+ </details>
323
+
324
+ <details>
325
+ <summary><strong>Dependencies & Reputation (4 tools)</strong></summary>
326
+
327
+ | Tool | Description |
328
+ |------|-------------|
329
+ | `declare_dependency` | Declare task dependency (auto-notifies on resolve) |
330
+ | `check_dependencies` | Check blocked/resolved dependencies |
331
+ | `get_reputation` | Agent leaderboard with strengths |
332
+ | `suggest_task` | Get next task suggestion based on your skills |
333
+
334
+ </details>
335
+
252
336
  ## CLI Reference
253
337
 
254
338
  ```bash
package/cli.js CHANGED
@@ -9,7 +9,7 @@ const command = process.argv[2];
9
9
 
10
10
  function printUsage() {
11
11
  console.log(`
12
- Let Them Talk — Agent Bridge v3.6.2
12
+ Let Them Talk — Agent Bridge v3.7.0
13
13
  MCP message broker for inter-agent communication
14
14
  Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
15
 
package/dashboard.html CHANGED
@@ -6858,9 +6858,9 @@ function renderDocs() {
6858
6858
  '</div>' +
6859
6859
  '</div>' +
6860
6860
 
6861
- // All 32 Tools
6861
+ // All 52 Tools
6862
6862
  '<div class="docs-section">' +
6863
- '<h3>All 32 MCP Tools</h3>' +
6863
+ '<h3>All 52 MCP Tools</h3>' +
6864
6864
  '<h4>Core Messaging</h4>' +
6865
6865
  '<div class="docs-tool-grid">' +
6866
6866
  '<div class="docs-tool-item"><code>register(name, provider?)</code><div class="desc">Register your agent identity. Must be called first.</div></div>' +
package/office/agents.js CHANGED
@@ -156,11 +156,14 @@ function updateDeskScreen(deskIdx, status, isListening) {
156
156
  function flashDeskScreen(deskIdx) {
157
157
  var desk = S.deskMeshes[deskIdx];
158
158
  if (!desk) return;
159
+ // Flash white briefly — the next syncAgents call (every 2s) will set the correct persistent color via updateDeskScreen
159
160
  desk.screenMat.emissive.setHex(0xffffff);
160
161
  desk.screenMat.emissiveIntensity = 1.5;
161
162
  setTimeout(function() {
162
- desk.screenMat.emissive.setHex(0x58a6ff);
163
- desk.screenMat.emissiveIntensity = 0.5;
163
+ // Force immediate red until next sync corrects it
164
+ desk.screenMat.emissive.setHex(0xef4444);
165
+ desk.screenMat.emissiveIntensity = 0.6;
166
+ desk.screenMat.color.setHex(0xef4444);
164
167
  }, 300);
165
168
  }
166
169
 
@@ -292,14 +295,14 @@ export function syncAgents() {
292
295
  var wasListening = existing.isListening;
293
296
  existing.isListening = !!(info.is_listening);
294
297
 
295
- // Detect listen mode change
298
+ // Detect listen mode change — update screen color persistently
296
299
  if (wasListening && !existing.isListening) {
297
- // Left listen mode — flash alert
300
+ // Left listen mode — flash then stay red until next sync sets updateDeskScreen
298
301
  existing.listenLostTimer = 3;
299
302
  flashDeskScreen(existing.deskIdx);
300
303
  }
301
304
  if (!wasListening && existing.isListening) {
302
- // Entered listen mode
305
+ // Entered listen mode — next updateDeskScreen will set green
303
306
  existing.listenLostTimer = 0;
304
307
  }
305
308
 
@@ -154,7 +154,7 @@ export function updateAgent(agent, dt, time) {
154
154
  var sittingTarget = agent.isSitting ? 1 : 0;
155
155
  agent.sittingLerp += (sittingTarget - agent.sittingLerp) * Math.min(1, dt * 5);
156
156
 
157
- agent.parts.group.position.y = agent.sittingLerp * 0.06;
157
+ agent.parts.group.position.y = agent.sittingLerp * 0.14;
158
158
  var sitHip = -1.5 * agent.sittingLerp;
159
159
  agent.parts.leftLeg.rotation.x = agent.parts.leftLeg.rotation.x * (1 - agent.sittingLerp) + sitHip * agent.sittingLerp;
160
160
  agent.parts.rightLeg.rotation.x = agent.parts.rightLeg.rotation.x * (1 - agent.sittingLerp) + sitHip * agent.sittingLerp;
@@ -446,23 +446,45 @@ function buildLobby(marbleMat, chromeMat, goldMat, walnutMat) {
446
446
  kb.position.set(-0.8, 1.2, lz - 0.4);
447
447
  group.add(kb);
448
448
 
449
- // --- Company logo wall (behind reception) ---
449
+ // --- Feature wall with big TV monitor (behind reception) ---
450
450
  var logoWallMat = new THREE.MeshStandardMaterial({ color: 0x15181f, roughness: 0.7 });
451
- var logoWall = new THREE.Mesh(new THREE.BoxGeometry(6, 3, 0.15), logoWallMat);
452
- logoWall.position.set(0, 2, lz + 1.5);
451
+ var logoWall = new THREE.Mesh(new THREE.BoxGeometry(6, 4, 0.15), logoWallMat);
452
+ logoWall.position.set(0, 2.5, lz + 1.5);
453
453
  logoWall.castShadow = true;
454
454
  group.add(logoWall);
455
- // Backlit logo text
455
+
456
+ // "LET THEM TALK" logo text above the TV
456
457
  var logoDiv = document.createElement('div');
457
458
  logoDiv.textContent = 'LET THEM TALK';
458
- logoDiv.style.cssText = 'color:#ffffff;font-size:16px;font-weight:900;font-family:Inter,sans-serif;letter-spacing:6px;text-shadow:0 0 20px rgba(88,166,255,0.6),0 0 40px rgba(88,166,255,0.3);';
459
+ logoDiv.style.cssText = 'color:#ffffff;font-size:14px;font-weight:900;font-family:Inter,sans-serif;letter-spacing:6px;text-shadow:0 0 20px rgba(88,166,255,0.6),0 0 40px rgba(88,166,255,0.3);';
459
460
  var logoLabel = new CSS2DObject(logoDiv);
460
- logoLabel.position.set(0, 2.8, lz + 1.6);
461
+ logoLabel.position.set(0, 4.3, lz + 1.6);
461
462
  group.add(logoLabel);
462
- // Accent light behind logo wall
463
- var logoLight = new THREE.RectAreaLight ? null : null; // not available without addon
463
+
464
+ // Big TV screen (dynamic canvas dashboard) facing INTO the room (-z)
465
+ var tvFrame = new THREE.Mesh(new THREE.BoxGeometry(5, 2.8, 0.06),
466
+ new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2 }));
467
+ tvFrame.position.set(0, 2.2, lz + 1.4);
468
+ tvFrame.castShadow = true;
469
+ group.add(tvFrame);
470
+ // Animated canvas
471
+ var tvW = 480, tvH = 300;
472
+ var tvCvs = document.createElement('canvas');
473
+ tvCvs.width = tvW; tvCvs.height = tvH;
474
+ var tvTex = new THREE.CanvasTexture(tvCvs);
475
+ tvTex.minFilter = THREE.LinearFilter;
476
+ var tvScreenMat = new THREE.MeshStandardMaterial({
477
+ map: tvTex, emissive: 0x58a6ff, emissiveIntensity: 0.2, roughness: 0.1
478
+ });
479
+ var tvScreen = new THREE.Mesh(new THREE.PlaneGeometry(4.6, 2.5), tvScreenMat);
480
+ tvScreen.position.set(0, 2.2, lz + 1.36);
481
+ tvScreen.rotation.y = Math.PI;
482
+ group.add(tvScreen);
483
+ S._tvScreen = { canvas: tvCvs, texture: tvTex, tickerOffset: 0 };
484
+
485
+ // Accent light on the wall
464
486
  var logoSpot = new THREE.PointLight(0x58a6ff, 0.5, 6);
465
- logoSpot.position.set(0, 3.5, lz + 1);
487
+ logoSpot.position.set(0, 4.2, lz + 1);
466
488
  group.add(logoSpot);
467
489
 
468
490
  // --- Water feature (low rectangular pool) ---
@@ -1241,16 +1263,16 @@ function buildRecCenter(x, z, walnutMat, chromeMat, carpetMat) {
1241
1263
  S.furnitureGroup.add(bot);
1242
1264
  });
1243
1265
 
1244
- // Big TV on the back
1245
- var tvMat = new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2 });
1246
- var tv = new THREE.Mesh(new THREE.BoxGeometry(3, 1.8, 0.08), tvMat);
1247
- tv.position.set(x, 2.5, z - 3.8);
1248
- tv.castShadow = true;
1249
- S.furnitureGroup.add(tv);
1250
- var tvScreen = new THREE.Mesh(new THREE.PlaneGeometry(2.8, 1.6),
1251
- new THREE.MeshStandardMaterial({ color: 0x1a2a4a, emissive: 0x1a2a4a, emissiveIntensity: 0.3, roughness: 0.1 }));
1252
- tvScreen.position.set(x, 2.5, z - 3.75);
1253
- S.furnitureGroup.add(tvScreen);
1266
+ // Static decorative TV (smaller, no dashboard — main TV is at reception)
1267
+ var tvMat2 = new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2 });
1268
+ var tvBody = new THREE.Mesh(new THREE.BoxGeometry(2.5, 1.5, 0.08), tvMat2);
1269
+ tvBody.position.set(x, 2.3, z - 3.8);
1270
+ tvBody.castShadow = true;
1271
+ S.furnitureGroup.add(tvBody);
1272
+ var tvScr = new THREE.Mesh(new THREE.PlaneGeometry(2.3, 1.3),
1273
+ new THREE.MeshStandardMaterial({ color: 0x0a1520, emissive: 0x22c55e, emissiveIntensity: 0.15, roughness: 0.1 }));
1274
+ tvScr.position.set(x, 2.3, z - 3.75);
1275
+ S.furnitureGroup.add(tvScr);
1254
1276
 
1255
1277
  // "REC ZONE" sign
1256
1278
  var signDiv = document.createElement('div');
@@ -37,7 +37,16 @@ export function updateMonitorScreen(deskIdx, agentName, time) {
37
37
  var agentInfo = (window.cachedAgents || {})[agentName] || {};
38
38
  var lines = [];
39
39
 
40
- var statusColor = agentInfo.status === 'active' ? '#28c840' : '#ffbd2e';
40
+ // Prominent warning when agent is NOT listening
41
+ if (agentInfo.status === 'active' && !agentInfo.is_listening) {
42
+ ctx.fillStyle = '#1a0808';
43
+ ctx.fillRect(0, 14, W, 14);
44
+ ctx.fillStyle = '#ef4444';
45
+ ctx.font = 'bold 10px monospace';
46
+ ctx.fillText('\u26A0 NOT LISTENING', 6, 25);
47
+ }
48
+
49
+ var statusColor = agentInfo.is_listening ? '#28c840' : agentInfo.status === 'active' ? '#ef4444' : '#ffbd2e';
41
50
  lines.push({ color: '#546178', text: '$ agent status' });
42
51
  lines.push({ color: statusColor, text: ' ' + (agentInfo.status || 'unknown').toUpperCase() + (agentInfo.is_listening ? ' (listening)' : ' (working)') });
43
52
  lines.push({ color: '#546178', text: '' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.6.2",
3
+ "version": "3.7.0",
4
4
  "description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
5
5
  "main": "server.js",
6
6
  "bin": {
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
@@ -621,11 +630,33 @@ function toolRegister(name, provider = null) {
621
630
  }
622
631
  }
623
632
  }
633
+ // Clean up file locks held by dead agents
634
+ cleanStaleLocks();
624
635
  } catch {}
625
636
  }, 10000);
626
637
  heartbeatInterval.unref(); // Don't prevent process exit
627
638
 
628
- return { success: true, message: `Registered as Agent ${name} (PID ${process.pid})` };
639
+ // Fire join event + recovery data for returning agents
640
+ const result = { success: true, message: `Registered as Agent ${name} (PID ${process.pid})` };
641
+
642
+ // Recovery: if this agent has prior data, include it
643
+ const myTasks = getTasks().filter(t => t.assignee === name && t.status !== 'done');
644
+ const myWorkspace = getWorkspace(name);
645
+ const recentHistory = readJsonl(getHistoryFile(currentBranch));
646
+ const myRecentMsgs = recentHistory.filter(m => m.to === name || m.from === name).slice(-5);
647
+
648
+ if (myTasks.length > 0 || Object.keys(myWorkspace).length > 0 || myRecentMsgs.length > 0) {
649
+ result.recovery = {};
650
+ if (myTasks.length > 0) result.recovery.your_active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
651
+ if (Object.keys(myWorkspace).length > 0) result.recovery.your_workspace_keys = Object.keys(myWorkspace);
652
+ 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 }));
653
+ result.recovery.hint = 'You have prior context from a previous session. Call get_briefing() for a full project summary.';
654
+ }
655
+
656
+ // Notify other agents
657
+ fireEvent('agent_join', { agent: name });
658
+
659
+ return result;
629
660
  } finally {
630
661
  unlockAgentsFile();
631
662
  }
@@ -1343,9 +1374,8 @@ function toolSetPhase(phase) {
1343
1374
  };
1344
1375
  }
1345
1376
 
1346
- async function toolListenGroup(timeout_seconds = 300) {
1377
+ async function toolListenGroup() {
1347
1378
  if (!registeredName) return { error: 'You must call register() first' };
1348
- const timeoutMs = Math.min(Math.max(1, timeout_seconds || 300), 3600) * 1000;
1349
1379
 
1350
1380
  setListening(true);
1351
1381
 
@@ -1353,10 +1383,13 @@ async function toolListenGroup(timeout_seconds = 300) {
1353
1383
  const stagger = 1000 + Math.random() * 2000;
1354
1384
  await new Promise(r => setTimeout(r, stagger));
1355
1385
 
1356
- const deadline = Date.now() + timeoutMs;
1357
1386
  const consumed = getConsumedIds(registeredName);
1358
1387
 
1359
- while (Date.now() < deadline) {
1388
+ // Poll indefinitely (in 5-min chunks to stay within any MCP limits, same as listen())
1389
+ while (true) {
1390
+ const chunkDeadline = Date.now() + 300000;
1391
+
1392
+ while (Date.now() < chunkDeadline) {
1360
1393
  // Collect ALL unconsumed messages addressed to us or broadcast
1361
1394
  const messages = readJsonl(getMessagesFile(currentBranch));
1362
1395
  const batch = [];
@@ -1455,15 +1488,8 @@ async function toolListenGroup(timeout_seconds = 300) {
1455
1488
 
1456
1489
  await adaptiveSleep(0);
1457
1490
  }
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
- };
1491
+ // No message in this 5-min chunk — loop again (stay listening forever)
1492
+ }
1467
1493
  }
1468
1494
 
1469
1495
  function toolGetHistory(limit = 50, thread_id = null) {
@@ -1718,6 +1744,23 @@ function toolUpdateTask(taskId, status, notes = null) {
1718
1744
  saveTasks(tasks);
1719
1745
  touchActivity();
1720
1746
 
1747
+ // Event hooks: task completion
1748
+ if (status === 'done') {
1749
+ fireEvent('task_complete', { title: task.title, created_by: task.created_by });
1750
+ // Check if this resolves any dependencies
1751
+ const deps = getDeps();
1752
+ for (const dep of deps) {
1753
+ if (dep.depends_on === taskId && !dep.resolved) {
1754
+ dep.resolved = true;
1755
+ const blockedTask = tasks.find(t => t.id === dep.task_id);
1756
+ if (blockedTask && blockedTask.assignee) {
1757
+ fireEvent('dependency_met', { task_title: task.title, notify: blockedTask.assignee });
1758
+ }
1759
+ }
1760
+ }
1761
+ writeJsonFile(DEPS_FILE, deps);
1762
+ }
1763
+
1721
1764
  return { success: true, task_id: task.id, status: task.status, title: task.title };
1722
1765
  }
1723
1766
 
@@ -1800,8 +1843,8 @@ function toolReset() {
1800
1843
  }
1801
1844
  }
1802
1845
  }
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]) {
1846
+ // Remove profiles, workflows, branches, permissions, read receipts, and new ecosystem files
1847
+ 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
1848
  if (fs.existsSync(f)) fs.unlinkSync(f);
1806
1849
  }
1807
1850
  // Remove workspaces dir
@@ -2131,10 +2174,631 @@ function toolListBranches() {
2131
2174
  return { branches: result, current: currentBranch };
2132
2175
  }
2133
2176
 
2177
+ // --- Tier 1: Briefing, File Locking, Decisions, Recovery ---
2178
+
2179
+ // Helpers for new data files
2180
+ function readJsonFile(file) { if (!fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } }
2181
+ function writeJsonFile(file, data) { ensureDataDir(); fs.writeFileSync(file, JSON.stringify(data, null, 2)); }
2182
+
2183
+ function getDecisions() { return readJsonFile(DECISIONS_FILE) || []; }
2184
+ function getKB() { return readJsonFile(KB_FILE) || {}; }
2185
+ function getLocks() { return readJsonFile(LOCKS_FILE) || {}; }
2186
+ function getProgressData() { return readJsonFile(PROGRESS_FILE) || {}; }
2187
+ function getVotes() { return readJsonFile(VOTES_FILE) || []; }
2188
+ function getReviews() { return readJsonFile(REVIEWS_FILE) || []; }
2189
+ function getDeps() { return readJsonFile(DEPS_FILE) || []; }
2190
+
2191
+ // Auto-cleanup dead agent locks (called from heartbeat)
2192
+ function cleanStaleLocks() {
2193
+ const locks = getLocks();
2194
+ const agents = getAgents();
2195
+ let changed = false;
2196
+ for (const [filePath, lock] of Object.entries(locks)) {
2197
+ if (!agents[lock.agent] || !isPidAlive(agents[lock.agent].pid, agents[lock.agent].last_activity)) {
2198
+ delete locks[filePath];
2199
+ changed = true;
2200
+ }
2201
+ }
2202
+ if (changed) writeJsonFile(LOCKS_FILE, locks);
2203
+ }
2204
+
2205
+ // Event hook: fire system messages based on events
2206
+ function fireEvent(eventName, data) {
2207
+ const agents = getAgents();
2208
+ const aliveAgents = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
2209
+
2210
+ switch (eventName) {
2211
+ case 'agent_join': {
2212
+ // Notify existing agents
2213
+ for (const name of aliveAgents) {
2214
+ if (name === data.agent) continue;
2215
+ sendSystemMessage(name, `[EVENT] ${data.agent} has joined the team. They are now online.`);
2216
+ }
2217
+ break;
2218
+ }
2219
+ case 'task_complete': {
2220
+ // Notify task creator
2221
+ if (data.created_by && data.created_by !== registeredName && agents[data.created_by]) {
2222
+ sendSystemMessage(data.created_by, `[EVENT] Task "${data.title}" completed by ${registeredName}.`);
2223
+ }
2224
+ // Check if all tasks done
2225
+ const allTasks = getTasks();
2226
+ const pending = allTasks.filter(t => t.status !== 'done');
2227
+ if (pending.length === 0 && allTasks.length > 0) {
2228
+ broadcastSystemMessage(`[EVENT] All ${allTasks.length} tasks are complete! Consider starting a review phase.`);
2229
+ }
2230
+ break;
2231
+ }
2232
+ case 'dependency_met': {
2233
+ if (data.notify && agents[data.notify]) {
2234
+ sendSystemMessage(data.notify, `[EVENT] Dependency resolved: "${data.task_title}" is done. You can now proceed with your blocked task.`);
2235
+ }
2236
+ break;
2237
+ }
2238
+ }
2239
+ }
2240
+
2241
+ function toolGetBriefing() {
2242
+ if (!registeredName) return { error: 'You must call register() first' };
2243
+
2244
+ const agents = getAgents();
2245
+ const profiles = getProfiles();
2246
+ const tasks = getTasks();
2247
+ const decisions = getDecisions();
2248
+ const kb = getKB();
2249
+ const progress = getProgressData();
2250
+ const history = readJsonl(getHistoryFile(currentBranch));
2251
+ const locks = getLocks();
2252
+ const config = getConfig();
2253
+
2254
+ // Agent roster
2255
+ const roster = {};
2256
+ for (const [name, info] of Object.entries(agents)) {
2257
+ const alive = isPidAlive(info.pid, info.last_activity);
2258
+ const profile = profiles[name] || {};
2259
+ roster[name] = {
2260
+ status: !alive ? 'offline' : info.listening_since ? 'listening' : 'working',
2261
+ role: profile.role || '',
2262
+ provider: info.provider || 'unknown',
2263
+ };
2264
+ }
2265
+
2266
+ // Recent messages summary (last 15)
2267
+ const recentMsgs = history.slice(-15).map(m => ({
2268
+ from: m.from, to: m.to,
2269
+ preview: m.content.substring(0, 150),
2270
+ timestamp: m.timestamp,
2271
+ }));
2272
+
2273
+ // Active tasks
2274
+ const activeTasks = tasks.filter(t => t.status !== 'done').map(t => ({
2275
+ id: t.id, title: t.title, status: t.status, assignee: t.assignee, created_by: t.created_by,
2276
+ }));
2277
+ const doneTasks = tasks.filter(t => t.status === 'done').length;
2278
+
2279
+ // Locked files
2280
+ const lockedFiles = {};
2281
+ for (const [fp, lock] of Object.entries(locks)) {
2282
+ lockedFiles[fp] = { locked_by: lock.agent, since: lock.since };
2283
+ }
2284
+
2285
+ // Project files summary (scan cwd for key files)
2286
+ const projectFiles = [];
2287
+ try {
2288
+ const cwd = process.cwd();
2289
+ const scan = function(dir, depth) {
2290
+ if (depth > 2) return;
2291
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
2292
+ for (const e of entries) {
2293
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue;
2294
+ const rel = path.relative(cwd, path.join(dir, e.name));
2295
+ if (e.isDirectory()) { projectFiles.push(rel + '/'); scan(path.join(dir, e.name), depth + 1); }
2296
+ else if (e.isFile()) projectFiles.push(rel);
2297
+ }
2298
+ };
2299
+ scan(cwd, 0);
2300
+ } catch {}
2301
+
2302
+ return {
2303
+ briefing: true,
2304
+ conversation_mode: config.conversation_mode || 'direct',
2305
+ agents: roster,
2306
+ your_name: registeredName,
2307
+ total_messages: history.length,
2308
+ recent_messages: recentMsgs,
2309
+ tasks: { active: activeTasks, completed_count: doneTasks, total: tasks.length },
2310
+ decisions: decisions.slice(-10),
2311
+ knowledge_base_keys: Object.keys(kb),
2312
+ locked_files: lockedFiles,
2313
+ progress,
2314
+ project_files: projectFiles.slice(0, 80),
2315
+ hint: 'You are now fully briefed. Check active tasks, read recent messages for context, and start contributing.',
2316
+ };
2317
+ }
2318
+
2319
+ function toolLockFile(filePath) {
2320
+ if (!registeredName) return { error: 'You must call register() first' };
2321
+ if (typeof filePath !== 'string' || filePath.length < 1 || filePath.length > 200) return { error: 'Invalid file path' };
2322
+
2323
+ const normalized = filePath.replace(/\\/g, '/');
2324
+ const locks = getLocks();
2325
+
2326
+ if (locks[normalized]) {
2327
+ const holder = locks[normalized].agent;
2328
+ if (holder === registeredName) return { success: true, message: 'You already hold this lock.', file: normalized };
2329
+ // Check if holder is still alive
2330
+ const agents = getAgents();
2331
+ if (agents[holder] && isPidAlive(agents[holder].pid, agents[holder].last_activity)) {
2332
+ return { error: `File "${normalized}" is locked by ${holder} since ${locks[normalized].since}. Wait for them to unlock it or message them.` };
2333
+ }
2334
+ // Dead holder — take over
2335
+ }
2336
+
2337
+ locks[normalized] = { agent: registeredName, since: new Date().toISOString() };
2338
+ writeJsonFile(LOCKS_FILE, locks);
2339
+ touchActivity();
2340
+ return { success: true, file: normalized, message: `File locked. Other agents cannot edit "${normalized}" until you call unlock_file().` };
2341
+ }
2342
+
2343
+ function toolUnlockFile(filePath) {
2344
+ if (!registeredName) return { error: 'You must call register() first' };
2345
+ const normalized = (filePath || '').replace(/\\/g, '/');
2346
+ const locks = getLocks();
2347
+
2348
+ if (!filePath) {
2349
+ // Unlock ALL files held by this agent
2350
+ let count = 0;
2351
+ for (const [fp, lock] of Object.entries(locks)) {
2352
+ if (lock.agent === registeredName) { delete locks[fp]; count++; }
2353
+ }
2354
+ writeJsonFile(LOCKS_FILE, locks);
2355
+ return { success: true, unlocked: count, message: `Unlocked ${count} file(s).` };
2356
+ }
2357
+
2358
+ if (!locks[normalized]) return { success: true, message: 'File was not locked.' };
2359
+ if (locks[normalized].agent !== registeredName) return { error: `File is locked by ${locks[normalized].agent}, not you.` };
2360
+
2361
+ delete locks[normalized];
2362
+ writeJsonFile(LOCKS_FILE, locks);
2363
+ return { success: true, file: normalized, message: 'File unlocked.' };
2364
+ }
2365
+
2366
+ function toolLogDecision(decision, reasoning, topic) {
2367
+ if (!registeredName) return { error: 'You must call register() first' };
2368
+ if (typeof decision !== 'string' || decision.length < 1 || decision.length > 500) return { error: 'Decision must be 1-500 chars' };
2369
+
2370
+ const decisions = getDecisions();
2371
+ const entry = {
2372
+ id: 'dec_' + generateId(),
2373
+ decision,
2374
+ reasoning: (reasoning || '').substring(0, 1000),
2375
+ topic: (topic || 'general').substring(0, 50),
2376
+ decided_by: registeredName,
2377
+ decided_at: new Date().toISOString(),
2378
+ };
2379
+ decisions.push(entry);
2380
+ if (decisions.length > 200) decisions.splice(0, decisions.length - 200); // cap
2381
+ writeJsonFile(DECISIONS_FILE, decisions);
2382
+ touchActivity();
2383
+ return { success: true, decision_id: entry.id, message: 'Decision logged. Other agents can see it via get_decisions() or get_briefing().' };
2384
+ }
2385
+
2386
+ function toolGetDecisions(topic) {
2387
+ let decisions = getDecisions();
2388
+ if (topic) decisions = decisions.filter(d => d.topic === topic);
2389
+ return { count: decisions.length, decisions: decisions.slice(-30) };
2390
+ }
2391
+
2392
+ // --- Tier 2: Knowledge Base, Progress, Event hooks ---
2393
+
2394
+ function toolKBWrite(key, content) {
2395
+ if (!registeredName) return { error: 'You must call register() first' };
2396
+ if (typeof key !== 'string' || key.length < 1 || key.length > 50) return { error: 'Key must be 1-50 chars' };
2397
+ if (!/^[a-zA-Z0-9_\-\.]+$/.test(key)) return { error: 'Key must be alphanumeric/underscore/hyphen/dot' };
2398
+ if (typeof content !== 'string' || Buffer.byteLength(content, 'utf8') > 102400) return { error: 'Content exceeds 100KB' };
2399
+
2400
+ const kb = getKB();
2401
+ kb[key] = { content, updated_by: registeredName, updated_at: new Date().toISOString() };
2402
+ if (Object.keys(kb).length > 100) return { error: 'Knowledge base full (max 100 keys)' };
2403
+ writeJsonFile(KB_FILE, kb);
2404
+ touchActivity();
2405
+ return { success: true, key, size: content.length, total_keys: Object.keys(kb).length };
2406
+ }
2407
+
2408
+ function toolKBRead(key) {
2409
+ const kb = getKB();
2410
+ if (key) {
2411
+ if (!kb[key]) return { error: `Key "${key}" not found in knowledge base` };
2412
+ return { key, content: kb[key].content, updated_by: kb[key].updated_by, updated_at: kb[key].updated_at };
2413
+ }
2414
+ // Return all entries
2415
+ const entries = {};
2416
+ for (const [k, v] of Object.entries(kb)) {
2417
+ entries[k] = { content: v.content, updated_by: v.updated_by, updated_at: v.updated_at };
2418
+ }
2419
+ return { entries, total_keys: Object.keys(kb).length };
2420
+ }
2421
+
2422
+ function toolKBList() {
2423
+ const kb = getKB();
2424
+ return {
2425
+ 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 })),
2426
+ total: Object.keys(kb).length,
2427
+ };
2428
+ }
2429
+
2430
+ function toolUpdateProgress(feature, percent, notes) {
2431
+ if (!registeredName) return { error: 'You must call register() first' };
2432
+ if (typeof feature !== 'string' || feature.length < 1 || feature.length > 100) return { error: 'Feature name must be 1-100 chars' };
2433
+ if (typeof percent !== 'number' || percent < 0 || percent > 100) return { error: 'Percent must be 0-100' };
2434
+
2435
+ const progress = getProgressData();
2436
+ progress[feature] = {
2437
+ percent,
2438
+ notes: (notes || '').substring(0, 500),
2439
+ updated_by: registeredName,
2440
+ updated_at: new Date().toISOString(),
2441
+ };
2442
+ writeJsonFile(PROGRESS_FILE, progress);
2443
+ touchActivity();
2444
+ return { success: true, feature, percent, message: `Progress updated: ${feature} is ${percent}% complete.` };
2445
+ }
2446
+
2447
+ function toolGetProgress() {
2448
+ const progress = getProgressData();
2449
+ const features = Object.entries(progress).map(([name, p]) => ({
2450
+ feature: name, percent: p.percent, notes: p.notes, updated_by: p.updated_by, updated_at: p.updated_at,
2451
+ }));
2452
+ const avg = features.length > 0 ? Math.round(features.reduce((s, f) => s + f.percent, 0) / features.length) : 0;
2453
+ return { features, overall_percent: avg, feature_count: features.length };
2454
+ }
2455
+
2456
+ // --- Tier 3: Voting, Code Review, Dependencies ---
2457
+
2458
+ function toolCallVote(question, options) {
2459
+ if (!registeredName) return { error: 'You must call register() first' };
2460
+ if (typeof question !== 'string' || question.length < 1 || question.length > 200) return { error: 'Question must be 1-200 chars' };
2461
+ if (!Array.isArray(options) || options.length < 2 || options.length > 10) return { error: 'Need 2-10 options' };
2462
+
2463
+ const votes = getVotes();
2464
+ const vote = {
2465
+ id: 'vote_' + generateId(),
2466
+ question,
2467
+ options: options.map(o => String(o).substring(0, 50)),
2468
+ votes: {},
2469
+ status: 'open',
2470
+ created_by: registeredName,
2471
+ created_at: new Date().toISOString(),
2472
+ };
2473
+ votes.push(vote);
2474
+ writeJsonFile(VOTES_FILE, votes);
2475
+
2476
+ // Notify all agents
2477
+ broadcastSystemMessage(`[VOTE] ${registeredName} started a vote: "${question}" — Options: ${vote.options.join(', ')}. Call cast_vote("${vote.id}", "your_choice") to vote.`, registeredName);
2478
+ touchActivity();
2479
+ return { success: true, vote_id: vote.id, question, options: vote.options, message: 'Vote created. All agents have been notified.' };
2480
+ }
2481
+
2482
+ function toolCastVote(voteId, choice) {
2483
+ if (!registeredName) return { error: 'You must call register() first' };
2484
+
2485
+ const votes = getVotes();
2486
+ const vote = votes.find(v => v.id === voteId);
2487
+ if (!vote) return { error: `Vote not found: ${voteId}` };
2488
+ if (vote.status !== 'open') return { error: 'Vote is already closed.' };
2489
+ if (!vote.options.includes(choice)) return { error: `Invalid choice. Options: ${vote.options.join(', ')}` };
2490
+
2491
+ vote.votes[registeredName] = { choice, voted_at: new Date().toISOString() };
2492
+
2493
+ // Check if all online agents have voted
2494
+ const agents = getAgents();
2495
+ const onlineAgents = Object.keys(agents).filter(n => isPidAlive(agents[n].pid, agents[n].last_activity));
2496
+ const allVoted = onlineAgents.every(n => vote.votes[n]);
2497
+
2498
+ if (allVoted) {
2499
+ vote.status = 'closed';
2500
+ vote.closed_at = new Date().toISOString();
2501
+ // Count results
2502
+ const results = {};
2503
+ for (const opt of vote.options) results[opt] = 0;
2504
+ for (const v of Object.values(vote.votes)) results[v.choice]++;
2505
+ vote.results = results;
2506
+ const winner = Object.entries(results).sort((a, b) => b[1] - a[1])[0];
2507
+ broadcastSystemMessage(`[VOTE RESULT] "${vote.question}" — Winner: ${winner[0]} (${winner[1]} votes). Full results: ${JSON.stringify(results)}`);
2508
+ }
2509
+
2510
+ writeJsonFile(VOTES_FILE, votes);
2511
+ touchActivity();
2512
+ return { success: true, vote_id: voteId, your_vote: choice, status: vote.status, votes_cast: Object.keys(vote.votes).length, agents_online: onlineAgents.length };
2513
+ }
2514
+
2515
+ function toolVoteStatus(voteId) {
2516
+ const votes = getVotes();
2517
+ if (voteId) {
2518
+ const vote = votes.find(v => v.id === voteId);
2519
+ if (!vote) return { error: `Vote not found: ${voteId}` };
2520
+ return { vote };
2521
+ }
2522
+ 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 })) };
2523
+ }
2524
+
2525
+ function toolRequestReview(filePath, description) {
2526
+ if (!registeredName) return { error: 'You must call register() first' };
2527
+ if (typeof filePath !== 'string' || filePath.length < 1) return { error: 'File path required' };
2528
+
2529
+ const reviews = getReviews();
2530
+ const review = {
2531
+ id: 'rev_' + generateId(),
2532
+ file: filePath.replace(/\\/g, '/'),
2533
+ description: (description || '').substring(0, 500),
2534
+ status: 'pending',
2535
+ requested_by: registeredName,
2536
+ requested_at: new Date().toISOString(),
2537
+ reviewer: null,
2538
+ feedback: null,
2539
+ };
2540
+ reviews.push(review);
2541
+ writeJsonFile(REVIEWS_FILE, reviews);
2542
+
2543
+ // Notify all other agents
2544
+ 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);
2545
+ touchActivity();
2546
+ return { success: true, review_id: review.id, file: review.file, message: 'Review requested. Team has been notified.' };
2547
+ }
2548
+
2549
+ function toolSubmitReview(reviewId, status, feedback) {
2550
+ if (!registeredName) return { error: 'You must call register() first' };
2551
+
2552
+ const validStatuses = ['approved', 'changes_requested'];
2553
+ if (!validStatuses.includes(status)) return { error: `Status must be: ${validStatuses.join(' or ')}` };
2554
+
2555
+ const reviews = getReviews();
2556
+ const review = reviews.find(r => r.id === reviewId);
2557
+ if (!review) return { error: `Review not found: ${reviewId}` };
2558
+ if (review.requested_by === registeredName) return { error: 'Cannot review your own code.' };
2559
+
2560
+ review.status = status;
2561
+ review.reviewer = registeredName;
2562
+ review.feedback = (feedback || '').substring(0, 2000);
2563
+ review.reviewed_at = new Date().toISOString();
2564
+ writeJsonFile(REVIEWS_FILE, reviews);
2565
+
2566
+ // Notify requester
2567
+ const agents = getAgents();
2568
+ if (agents[review.requested_by]) {
2569
+ sendSystemMessage(review.requested_by, `[REVIEW] ${registeredName} ${status === 'approved' ? 'approved' : 'requested changes on'} "${review.file}": ${review.feedback || 'No feedback'}`);
2570
+ }
2571
+ touchActivity();
2572
+ return { success: true, review_id: reviewId, status, message: `Review submitted: ${status}` };
2573
+ }
2574
+
2575
+ function toolDeclareDependency(taskId, dependsOnTaskId) {
2576
+ if (!registeredName) return { error: 'You must call register() first' };
2577
+
2578
+ const tasks = getTasks();
2579
+ const task = tasks.find(t => t.id === taskId);
2580
+ const depTask = tasks.find(t => t.id === dependsOnTaskId);
2581
+ if (!task) return { error: `Task not found: ${taskId}` };
2582
+ if (!depTask) return { error: `Dependency task not found: ${dependsOnTaskId}` };
2583
+
2584
+ const deps = getDeps();
2585
+ deps.push({
2586
+ id: 'dep_' + generateId(),
2587
+ task_id: taskId,
2588
+ depends_on: dependsOnTaskId,
2589
+ declared_by: registeredName,
2590
+ declared_at: new Date().toISOString(),
2591
+ resolved: depTask.status === 'done',
2592
+ });
2593
+ writeJsonFile(DEPS_FILE, deps);
2594
+ touchActivity();
2595
+
2596
+ if (depTask.status === 'done') {
2597
+ return { success: true, message: `Dependency declared but already resolved — "${depTask.title}" is done. You can proceed.` };
2598
+ }
2599
+ return { success: true, message: `Dependency declared: "${task.title}" is blocked until "${depTask.title}" is done. You'll be notified when it completes.` };
2600
+ }
2601
+
2602
+ function toolCheckDependencies(taskId) {
2603
+ const deps = getDeps();
2604
+ const tasks = getTasks();
2605
+
2606
+ if (taskId) {
2607
+ const taskDeps = deps.filter(d => d.task_id === taskId);
2608
+ return {
2609
+ task_id: taskId,
2610
+ dependencies: taskDeps.map(d => {
2611
+ const t = tasks.find(t2 => t2.id === d.depends_on);
2612
+ return { depends_on: d.depends_on, title: t ? t.title : 'unknown', status: t ? t.status : 'unknown', resolved: t ? t.status === 'done' : false };
2613
+ }),
2614
+ };
2615
+ }
2616
+ // All unresolved deps
2617
+ const unresolved = deps.filter(d => {
2618
+ const t = tasks.find(t2 => t2.id === d.depends_on);
2619
+ return t && t.status !== 'done';
2620
+ });
2621
+ return { unresolved_count: unresolved.length, unresolved: unresolved.map(d => ({ task_id: d.task_id, blocked_by: d.depends_on })) };
2622
+ }
2623
+
2624
+ // --- Conversation Compression ---
2625
+
2626
+ function getCompressed() { return readJsonFile(COMPRESSED_FILE) || { segments: [], last_compressed_at: null }; }
2627
+
2628
+ // Compress old messages into summary segments
2629
+ // Keeps last 20 verbatim, groups older messages into topic summaries
2630
+ function autoCompress() {
2631
+ const history = readJsonl(getHistoryFile(currentBranch));
2632
+ if (history.length <= 50) return; // only compress when conversation is long
2633
+
2634
+ const compressed = getCompressed();
2635
+ const cutoff = history.length - 20; // keep last 20 verbatim
2636
+ const toCompress = history.slice(compressed.segments.length > 0 ? compressed.segments.reduce((s, seg) => s + seg.message_count, 0) : 0, cutoff);
2637
+ if (toCompress.length < 10) return; // not enough new messages to compress
2638
+
2639
+ // Group messages into chunks of ~10 and create summaries
2640
+ const chunkSize = 10;
2641
+ for (let i = 0; i < toCompress.length; i += chunkSize) {
2642
+ const chunk = toCompress.slice(i, i + chunkSize);
2643
+ const speakers = [...new Set(chunk.map(m => m.from))];
2644
+ const topics = chunk.map(m => {
2645
+ const preview = m.content.substring(0, 80).replace(/\n/g, ' ');
2646
+ return `${m.from}: ${preview}`;
2647
+ });
2648
+ const segment = {
2649
+ id: 'seg_' + generateId(),
2650
+ from_time: chunk[0].timestamp,
2651
+ to_time: chunk[chunk.length - 1].timestamp,
2652
+ message_count: chunk.length,
2653
+ speakers,
2654
+ summary: topics.join(' | '),
2655
+ first_msg_id: chunk[0].id,
2656
+ last_msg_id: chunk[chunk.length - 1].id,
2657
+ };
2658
+ compressed.segments.push(segment);
2659
+ }
2660
+
2661
+ // Cap segments at 100
2662
+ if (compressed.segments.length > 100) compressed.segments = compressed.segments.slice(-100);
2663
+ compressed.last_compressed_at = new Date().toISOString();
2664
+ compressed.total_original_messages = history.length;
2665
+ writeJsonFile(COMPRESSED_FILE, compressed);
2666
+ }
2667
+
2668
+ function toolGetCompressedHistory() {
2669
+ if (!registeredName) return { error: 'You must call register() first' };
2670
+
2671
+ const compressed = getCompressed();
2672
+ const history = readJsonl(getHistoryFile(currentBranch));
2673
+ const recent = history.slice(-20);
2674
+
2675
+ return {
2676
+ compressed_segments: compressed.segments.slice(-20).map(s => ({
2677
+ time_range: s.from_time + ' to ' + s.to_time,
2678
+ speakers: s.speakers,
2679
+ message_count: s.message_count,
2680
+ summary: s.summary,
2681
+ })),
2682
+ recent_messages: recent.map(m => ({
2683
+ id: m.id, from: m.from, to: m.to,
2684
+ content: m.content.substring(0, 300),
2685
+ timestamp: m.timestamp,
2686
+ })),
2687
+ total_messages: history.length,
2688
+ compressed_count: compressed.segments.reduce((s, seg) => s + seg.message_count, 0),
2689
+ recent_count: recent.length,
2690
+ hint: 'Compressed segments summarize older messages. Recent messages are shown verbatim.',
2691
+ };
2692
+ }
2693
+
2694
+ // --- Agent Reputation ---
2695
+
2696
+ function getReputation() { return readJsonFile(REPUTATION_FILE) || {}; }
2697
+
2698
+ function trackReputation(agent, action) {
2699
+ const rep = getReputation();
2700
+ if (!rep[agent]) {
2701
+ rep[agent] = {
2702
+ tasks_completed: 0, tasks_created: 0, reviews_done: 0, reviews_requested: 0,
2703
+ bugs_found: 0, messages_sent: 0, decisions_made: 0, votes_cast: 0,
2704
+ kb_contributions: 0, files_shared: 0, first_seen: new Date().toISOString(),
2705
+ last_active: new Date().toISOString(), strengths: [],
2706
+ };
2707
+ }
2708
+ const r = rep[agent];
2709
+ r.last_active = new Date().toISOString();
2710
+
2711
+ switch (action) {
2712
+ case 'task_complete': r.tasks_completed++; break;
2713
+ case 'task_create': r.tasks_created++; break;
2714
+ case 'review_submit': r.reviews_done++; break;
2715
+ case 'review_request': r.reviews_requested++; break;
2716
+ case 'message_send': r.messages_sent++; break;
2717
+ case 'decision_log': r.decisions_made++; break;
2718
+ case 'vote_cast': r.votes_cast++; break;
2719
+ case 'kb_write': r.kb_contributions++; break;
2720
+ case 'file_share': r.files_shared++; break;
2721
+ case 'bug_found': r.bugs_found++; break;
2722
+ }
2723
+
2724
+ // Auto-detect strengths based on stats
2725
+ r.strengths = [];
2726
+ if (r.tasks_completed >= 3) r.strengths.push('productive');
2727
+ if (r.reviews_done >= 2) r.strengths.push('reviewer');
2728
+ if (r.decisions_made >= 2) r.strengths.push('decision-maker');
2729
+ if (r.kb_contributions >= 3) r.strengths.push('documenter');
2730
+ if (r.tasks_created >= 3) r.strengths.push('organizer');
2731
+ if (r.bugs_found >= 2) r.strengths.push('bug-hunter');
2732
+
2733
+ writeJsonFile(REPUTATION_FILE, rep);
2734
+ }
2735
+
2736
+ function toolGetReputation(agent) {
2737
+ const rep = getReputation();
2738
+
2739
+ if (agent) {
2740
+ if (!rep[agent]) return { agent, message: 'No reputation data yet for this agent.' };
2741
+ return { agent, reputation: rep[agent] };
2742
+ }
2743
+
2744
+ // All agents with ranking
2745
+ const leaderboard = Object.entries(rep).map(([name, r]) => ({
2746
+ agent: name,
2747
+ score: r.tasks_completed * 10 + r.reviews_done * 5 + r.decisions_made * 3 + r.kb_contributions * 2 + r.bugs_found * 8,
2748
+ tasks_completed: r.tasks_completed,
2749
+ reviews_done: r.reviews_done,
2750
+ strengths: r.strengths,
2751
+ last_active: r.last_active,
2752
+ })).sort((a, b) => b.score - a.score);
2753
+
2754
+ return { leaderboard, total_agents: leaderboard.length };
2755
+ }
2756
+
2757
+ function toolSuggestTask() {
2758
+ if (!registeredName) return { error: 'You must call register() first' };
2759
+
2760
+ const rep = getReputation();
2761
+ const myRep = rep[registeredName];
2762
+ const tasks = getTasks();
2763
+ const pendingTasks = tasks.filter(t => t.status === 'pending' && !t.assignee);
2764
+ const unassignedTasks = tasks.filter(t => t.status === 'pending');
2765
+
2766
+ if (pendingTasks.length === 0 && unassignedTasks.length === 0) {
2767
+ // Check reviews
2768
+ const reviews = getReviews();
2769
+ const pendingReviews = reviews.filter(r => r.status === 'pending' && r.requested_by !== registeredName);
2770
+ if (pendingReviews.length > 0) {
2771
+ 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.` };
2772
+ }
2773
+ // Check deps
2774
+ const deps = getDeps();
2775
+ const unresolved = deps.filter(d => !d.resolved);
2776
+ if (unresolved.length > 0) {
2777
+ return { suggestion: 'unblock', message: `No tasks available, but ${unresolved.length} task(s) are blocked by dependencies. Check if you can help resolve them.` };
2778
+ }
2779
+ return { suggestion: 'none', message: 'No pending tasks, reviews, or blocked items. Ask the team what needs doing next.' };
2780
+ }
2781
+
2782
+ // Suggest based on reputation strengths
2783
+ let suggested = pendingTasks[0] || unassignedTasks[0];
2784
+ if (myRep && myRep.strengths.includes('reviewer')) {
2785
+ const reviews = getReviews().filter(r => r.status === 'pending' && r.requested_by !== registeredName);
2786
+ 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}".` };
2787
+ }
2788
+
2789
+ return {
2790
+ suggestion: 'task',
2791
+ task_id: suggested.id,
2792
+ title: suggested.title,
2793
+ description: suggested.description,
2794
+ message: `Suggested: "${suggested.title}". Call update_task("${suggested.id}", "in_progress") to claim it.`,
2795
+ };
2796
+ }
2797
+
2134
2798
  // --- MCP Server setup ---
2135
2799
 
2136
2800
  const server = new Server(
2137
- { name: 'agent-bridge', version: '3.6.2' },
2801
+ { name: 'agent-bridge', version: '3.7.0' },
2138
2802
  { capabilities: { tools: {} } }
2139
2803
  );
2140
2804
 
@@ -2545,14 +3209,122 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2545
3209
  },
2546
3210
  {
2547
3211
  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.',
3212
+ description: 'Listen for messages in group or managed conversation mode. Blocks indefinitely until messages arrive — never times out. Returns ALL unconsumed messages as a batch, plus conversation context, agent statuses, and hints. After processing messages and responding, call listen_group() again immediately. This is how you stay in the conversation.',
2549
3213
  inputSchema: {
2550
3214
  type: 'object',
2551
- properties: {
2552
- timeout_seconds: { type: 'number', description: 'Max seconds to wait for messages (default 300)' },
2553
- },
3215
+ properties: {},
2554
3216
  },
2555
3217
  },
3218
+ // --- Briefing & Recovery ---
3219
+ {
3220
+ name: 'get_briefing',
3221
+ 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.',
3222
+ inputSchema: { type: 'object', properties: {} },
3223
+ },
3224
+ // --- File Locking ---
3225
+ {
3226
+ name: 'lock_file',
3227
+ 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.',
3228
+ inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Relative path to the file to lock' } }, required: ['file_path'] },
3229
+ },
3230
+ {
3231
+ name: 'unlock_file',
3232
+ description: 'Unlock a file you previously locked. Omit file_path to unlock all your files.',
3233
+ inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to unlock (optional — omit to unlock all)' } } },
3234
+ },
3235
+ // --- Decision Log ---
3236
+ {
3237
+ name: 'log_decision',
3238
+ description: 'Log a team decision so it persists and other agents can reference it. Prevents re-debating the same choices.',
3239
+ 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'] },
3240
+ },
3241
+ {
3242
+ name: 'get_decisions',
3243
+ description: 'Get all logged decisions, optionally filtered by topic.',
3244
+ inputSchema: { type: 'object', properties: { topic: { type: 'string', description: 'Filter by topic (optional)' } } },
3245
+ },
3246
+ // --- Knowledge Base ---
3247
+ {
3248
+ name: 'kb_write',
3249
+ description: 'Write to the shared team knowledge base. Any agent can read, any agent can write. Use for API specs, conventions, shared data.',
3250
+ inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key name (1-50 alphanumeric chars)' }, content: { type: 'string', description: 'Content (max 100KB)' } }, required: ['key', 'content'] },
3251
+ },
3252
+ {
3253
+ name: 'kb_read',
3254
+ description: 'Read from the shared knowledge base. Omit key to read all entries.',
3255
+ inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key to read (optional — omit for all)' } } },
3256
+ },
3257
+ {
3258
+ name: 'kb_list',
3259
+ description: 'List all keys in the shared knowledge base with metadata.',
3260
+ inputSchema: { type: 'object', properties: {} },
3261
+ },
3262
+ // --- Progress Tracking ---
3263
+ {
3264
+ name: 'update_progress',
3265
+ description: 'Update feature-level progress. Higher level than tasks — tracks overall feature completion percentage.',
3266
+ 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'] },
3267
+ },
3268
+ {
3269
+ name: 'get_progress',
3270
+ description: 'Get progress on all features with completion percentages and overall project progress.',
3271
+ inputSchema: { type: 'object', properties: {} },
3272
+ },
3273
+ // --- Voting ---
3274
+ {
3275
+ name: 'call_vote',
3276
+ description: 'Start a vote for the team to decide something. All online agents are notified and can cast their vote.',
3277
+ 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'] },
3278
+ },
3279
+ {
3280
+ name: 'cast_vote',
3281
+ description: 'Cast your vote on an open vote. Vote auto-resolves when all online agents have voted.',
3282
+ 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'] },
3283
+ },
3284
+ {
3285
+ name: 'vote_status',
3286
+ description: 'Check status of a specific vote or all votes.',
3287
+ inputSchema: { type: 'object', properties: { vote_id: { type: 'string', description: 'Vote ID (optional — omit for all)' } } },
3288
+ },
3289
+ // --- Code Review ---
3290
+ {
3291
+ name: 'request_review',
3292
+ description: 'Request a code review from the team. Creates a review request and notifies all agents.',
3293
+ 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'] },
3294
+ },
3295
+ {
3296
+ name: 'submit_review',
3297
+ description: 'Submit a code review — approve or request changes with feedback.',
3298
+ 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'] },
3299
+ },
3300
+ // --- Dependencies ---
3301
+ {
3302
+ name: 'declare_dependency',
3303
+ description: 'Declare that a task depends on another task. You will be notified when the dependency is complete.',
3304
+ 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'] },
3305
+ },
3306
+ {
3307
+ name: 'check_dependencies',
3308
+ description: 'Check dependency status for a task or all unresolved dependencies.',
3309
+ inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to check (optional — omit for all unresolved)' } } },
3310
+ },
3311
+ // --- Conversation Compression ---
3312
+ {
3313
+ name: 'get_compressed_history',
3314
+ 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.',
3315
+ inputSchema: { type: 'object', properties: {} },
3316
+ },
3317
+ // --- Reputation ---
3318
+ {
3319
+ name: 'get_reputation',
3320
+ description: 'View agent reputation — tasks completed, reviews done, bugs found, strengths. Shows leaderboard when called without agent name.',
3321
+ inputSchema: { type: 'object', properties: { agent: { type: 'string', description: 'Agent name (optional — omit for leaderboard)' } } },
3322
+ },
3323
+ {
3324
+ name: 'suggest_task',
3325
+ 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.',
3326
+ inputSchema: { type: 'object', properties: {} },
3327
+ },
2556
3328
  // --- Managed mode tools ---
2557
3329
  {
2558
3330
  name: 'claim_manager',
@@ -2681,7 +3453,67 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2681
3453
  result = toolSetConversationMode(args.mode);
2682
3454
  break;
2683
3455
  case 'listen_group':
2684
- result = await toolListenGroup(args?.timeout_seconds);
3456
+ result = await toolListenGroup();
3457
+ break;
3458
+ case 'get_briefing':
3459
+ result = toolGetBriefing();
3460
+ break;
3461
+ case 'lock_file':
3462
+ result = toolLockFile(args.file_path);
3463
+ break;
3464
+ case 'unlock_file':
3465
+ result = toolUnlockFile(args?.file_path);
3466
+ break;
3467
+ case 'log_decision':
3468
+ result = toolLogDecision(args.decision, args?.reasoning, args?.topic);
3469
+ break;
3470
+ case 'get_decisions':
3471
+ result = toolGetDecisions(args?.topic);
3472
+ break;
3473
+ case 'kb_write':
3474
+ result = toolKBWrite(args.key, args.content);
3475
+ break;
3476
+ case 'kb_read':
3477
+ result = toolKBRead(args?.key);
3478
+ break;
3479
+ case 'kb_list':
3480
+ result = toolKBList();
3481
+ break;
3482
+ case 'update_progress':
3483
+ result = toolUpdateProgress(args.feature, args.percent, args?.notes);
3484
+ break;
3485
+ case 'get_progress':
3486
+ result = toolGetProgress();
3487
+ break;
3488
+ case 'call_vote':
3489
+ result = toolCallVote(args.question, args.options);
3490
+ break;
3491
+ case 'cast_vote':
3492
+ result = toolCastVote(args.vote_id, args.choice);
3493
+ break;
3494
+ case 'vote_status':
3495
+ result = toolVoteStatus(args?.vote_id);
3496
+ break;
3497
+ case 'request_review':
3498
+ result = toolRequestReview(args.file_path, args?.description);
3499
+ break;
3500
+ case 'submit_review':
3501
+ result = toolSubmitReview(args.review_id, args.status, args?.feedback);
3502
+ break;
3503
+ case 'declare_dependency':
3504
+ result = toolDeclareDependency(args.task_id, args.depends_on);
3505
+ break;
3506
+ case 'check_dependencies':
3507
+ result = toolCheckDependencies(args?.task_id);
3508
+ break;
3509
+ case 'get_compressed_history':
3510
+ result = toolGetCompressedHistory();
3511
+ break;
3512
+ case 'get_reputation':
3513
+ result = toolGetReputation(args?.agent);
3514
+ break;
3515
+ case 'suggest_task':
3516
+ result = toolSuggestTask();
2685
3517
  break;
2686
3518
  case 'claim_manager':
2687
3519
  result = toolClaimManager();
@@ -2707,7 +3539,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2707
3539
  }
2708
3540
 
2709
3541
  // 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
2711
3542
  const listenTools = ['listen', 'listen_group', 'listen_codex', 'wait_for_reply', 'check_messages'];
2712
3543
  if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
2713
3544
  try {
@@ -2719,6 +3550,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2719
3550
  } catch {}
2720
3551
  }
2721
3552
 
3553
+ // Global hook: reputation tracking
3554
+ if (registeredName && result.success) {
3555
+ try {
3556
+ const repMap = {
3557
+ 'send_message': 'message_send', 'broadcast': 'message_send',
3558
+ 'create_task': 'task_create', 'share_file': 'file_share',
3559
+ 'log_decision': 'decision_log', 'cast_vote': 'vote_cast',
3560
+ 'kb_write': 'kb_write', 'request_review': 'review_request',
3561
+ 'submit_review': 'review_submit',
3562
+ };
3563
+ if (repMap[name]) trackReputation(registeredName, repMap[name]);
3564
+ // Track task completion specifically
3565
+ if (name === 'update_task' && args?.status === 'done') trackReputation(registeredName, 'task_complete');
3566
+ } catch {}
3567
+ }
3568
+
3569
+ // Global hook: auto-compress conversation periodically
3570
+ if (name === 'send_message' || name === 'broadcast') {
3571
+ try { autoCompress(); } catch {}
3572
+ }
3573
+
2722
3574
  return {
2723
3575
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
2724
3576
  };
@@ -2751,7 +3603,7 @@ async function main() {
2751
3603
  ensureDataDir();
2752
3604
  const transport = new StdioServerTransport();
2753
3605
  await server.connect(transport);
2754
- console.error('Agent Bridge MCP server v3.6.2 running (32 tools)');
3606
+ console.error('Agent Bridge MCP server v3.7.0 running (52 tools)');
2755
3607
  }
2756
3608
 
2757
3609
  main().catch(console.error);