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 +29 -0
- package/README.md +86 -2
- package/cli.js +1 -1
- package/dashboard.html +2 -2
- package/office/agents.js +8 -5
- package/office/animation.js +1 -1
- package/office/campus-env.js +41 -19
- package/office/monitors.js +10 -1
- package/package.json +1 -1
- package/server.js +876 -24
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
|
-
- **
|
|
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 (
|
|
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.
|
|
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
|
|
6861
|
+
// All 52 Tools
|
|
6862
6862
|
'<div class="docs-section">' +
|
|
6863
|
-
'<h3>All
|
|
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
|
-
|
|
163
|
-
desk.screenMat.
|
|
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
|
|
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
|
|
package/office/animation.js
CHANGED
|
@@ -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.
|
|
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;
|
package/office/campus-env.js
CHANGED
|
@@ -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
|
-
// ---
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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,
|
|
461
|
+
logoLabel.position.set(0, 4.3, lz + 1.6);
|
|
461
462
|
group.add(logoLabel);
|
|
462
|
-
|
|
463
|
-
|
|
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,
|
|
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
|
-
//
|
|
1245
|
-
var
|
|
1246
|
-
var
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
S.furnitureGroup.add(
|
|
1250
|
-
var
|
|
1251
|
-
new THREE.MeshStandardMaterial({ color:
|
|
1252
|
-
|
|
1253
|
-
S.furnitureGroup.add(
|
|
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');
|
package/office/monitors.js
CHANGED
|
@@ -37,7 +37,16 @@ export function updateMonitorScreen(deskIdx, agentName, time) {
|
|
|
37
37
|
var agentInfo = (window.cachedAgents || {})[agentName] || {};
|
|
38
38
|
var lines = [];
|
|
39
39
|
|
|
40
|
-
|
|
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
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
3606
|
+
console.error('Agent Bridge MCP server v3.7.0 running (52 tools)');
|
|
2755
3607
|
}
|
|
2756
3608
|
|
|
2757
3609
|
main().catch(console.error);
|