let-them-talk 3.6.1 → 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 +46 -0
- package/README.md +86 -2
- package/cli.js +1 -1
- package/dashboard.html +12 -2
- package/office/agents.js +94 -17
- package/office/animation.js +35 -1
- package/office/campus-env.js +1483 -0
- package/office/environment.js +10 -0
- package/office/index.js +46 -16
- package/office/monitors.js +10 -1
- package/office/navigation.js +263 -0
- package/office/scene.js +3 -3
- package/office/spectator-camera.js +1 -1
- package/office/state.js +1 -1
- package/package.json +1 -1
- package/server.js +928 -23
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
|
}
|
|
@@ -871,6 +902,15 @@ async function toolSendMessage(content, to = null, reply_to = null) {
|
|
|
871
902
|
if (currentBranch !== 'main') result.branch = currentBranch;
|
|
872
903
|
if (!recipientAlive) {
|
|
873
904
|
result.warning = `Agent "${to}" appears offline (PID not running). Message queued but may not be received until they reconnect.`;
|
|
905
|
+
} else if (agents[to] && !agents[to].listening_since) {
|
|
906
|
+
result.note = `Agent "${to}" is currently working (not in listen mode). Message queued — they'll see it when they finish their current task and call listen_group().`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Nudge: check if THIS agent has unread messages waiting
|
|
910
|
+
const myPending = getUnconsumedMessages(registeredName);
|
|
911
|
+
if (myPending.length > 0) {
|
|
912
|
+
result.you_have_messages = myPending.length;
|
|
913
|
+
result.urgent = `You have ${myPending.length} unread message(s) waiting. Call listen_group() after this to read them.`;
|
|
874
914
|
}
|
|
875
915
|
return result;
|
|
876
916
|
}
|
|
@@ -925,6 +965,19 @@ function toolBroadcast(content) {
|
|
|
925
965
|
|
|
926
966
|
const result = { success: true, sent_to: ids, recipient_count: ids.length };
|
|
927
967
|
if (skipped.length > 0) result.skipped = skipped;
|
|
968
|
+
// Show which recipients are busy vs listening
|
|
969
|
+
const agentsNow = getAgents();
|
|
970
|
+
const busy = ids.filter(function(i) { return agentsNow[i.to] && !agentsNow[i.to].listening_since; }).map(function(i) { return i.to; });
|
|
971
|
+
if (busy.length > 0) {
|
|
972
|
+
result.busy_agents = busy;
|
|
973
|
+
result.note = busy.join(', ') + (busy.length === 1 ? ' is' : ' are') + ' currently working (not listening). Messages queued.';
|
|
974
|
+
}
|
|
975
|
+
// Nudge for own unread messages
|
|
976
|
+
const myPending = getUnconsumedMessages(registeredName);
|
|
977
|
+
if (myPending.length > 0) {
|
|
978
|
+
result.you_have_messages = myPending.length;
|
|
979
|
+
result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`;
|
|
980
|
+
}
|
|
928
981
|
return result;
|
|
929
982
|
}
|
|
930
983
|
|
|
@@ -1321,9 +1374,8 @@ function toolSetPhase(phase) {
|
|
|
1321
1374
|
};
|
|
1322
1375
|
}
|
|
1323
1376
|
|
|
1324
|
-
async function toolListenGroup(
|
|
1377
|
+
async function toolListenGroup() {
|
|
1325
1378
|
if (!registeredName) return { error: 'You must call register() first' };
|
|
1326
|
-
const timeoutMs = Math.min(Math.max(1, timeout_seconds || 300), 3600) * 1000;
|
|
1327
1379
|
|
|
1328
1380
|
setListening(true);
|
|
1329
1381
|
|
|
@@ -1331,10 +1383,13 @@ async function toolListenGroup(timeout_seconds = 300) {
|
|
|
1331
1383
|
const stagger = 1000 + Math.random() * 2000;
|
|
1332
1384
|
await new Promise(r => setTimeout(r, stagger));
|
|
1333
1385
|
|
|
1334
|
-
const deadline = Date.now() + timeoutMs;
|
|
1335
1386
|
const consumed = getConsumedIds(registeredName);
|
|
1336
1387
|
|
|
1337
|
-
|
|
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) {
|
|
1338
1393
|
// Collect ALL unconsumed messages addressed to us or broadcast
|
|
1339
1394
|
const messages = readJsonl(getMessagesFile(currentBranch));
|
|
1340
1395
|
const batch = [];
|
|
@@ -1370,17 +1425,28 @@ async function toolListenGroup(timeout_seconds = 300) {
|
|
|
1370
1425
|
const recentSpeakers = new Set(history.slice(-10).map(m => m.from));
|
|
1371
1426
|
const silent = agentNames.filter(n => !recentSpeakers.has(n) && n !== registeredName);
|
|
1372
1427
|
|
|
1428
|
+
const now = Date.now();
|
|
1373
1429
|
const result = {
|
|
1374
|
-
messages: batch.map(m =>
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1430
|
+
messages: batch.map(m => {
|
|
1431
|
+
const ageMs = now - new Date(m.timestamp).getTime();
|
|
1432
|
+
const ageSec = Math.round(ageMs / 1000);
|
|
1433
|
+
return {
|
|
1434
|
+
id: m.id, from: m.from, to: m.to, content: m.content,
|
|
1435
|
+
timestamp: m.timestamp,
|
|
1436
|
+
age_seconds: ageSec,
|
|
1437
|
+
...(ageSec > 30 && { delayed: true }),
|
|
1438
|
+
...(m.reply_to && { reply_to: m.reply_to }),
|
|
1439
|
+
...(m.thread_id && { thread_id: m.thread_id }),
|
|
1440
|
+
};
|
|
1441
|
+
}),
|
|
1380
1442
|
message_count: batch.length,
|
|
1381
1443
|
context: recentHistory,
|
|
1382
1444
|
agents_online: agentNames.length,
|
|
1383
1445
|
agents_silent: silent,
|
|
1446
|
+
agents_status: agentNames.reduce(function(acc, n) {
|
|
1447
|
+
acc[n] = agents[n].listening_since ? 'listening' : 'working';
|
|
1448
|
+
return acc;
|
|
1449
|
+
}, {}),
|
|
1384
1450
|
hint: silent.length > 0
|
|
1385
1451
|
? `${silent.join(', ')} haven't spoken recently. Consider addressing them.`
|
|
1386
1452
|
: 'All agents are active in the conversation.',
|
|
@@ -1416,14 +1482,14 @@ async function toolListenGroup(timeout_seconds = 300) {
|
|
|
1416
1482
|
}
|
|
1417
1483
|
}
|
|
1418
1484
|
|
|
1485
|
+
result.next_action = 'After processing these messages and sending your response, call listen_group() again immediately. Never stop listening.';
|
|
1419
1486
|
return result;
|
|
1420
1487
|
}
|
|
1421
1488
|
|
|
1422
1489
|
await adaptiveSleep(0);
|
|
1423
1490
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
return { timeout: true, message: 'No messages received within timeout.', messages: [], message_count: 0 };
|
|
1491
|
+
// No message in this 5-min chunk — loop again (stay listening forever)
|
|
1492
|
+
}
|
|
1427
1493
|
}
|
|
1428
1494
|
|
|
1429
1495
|
function toolGetHistory(limit = 50, thread_id = null) {
|
|
@@ -1678,6 +1744,23 @@ function toolUpdateTask(taskId, status, notes = null) {
|
|
|
1678
1744
|
saveTasks(tasks);
|
|
1679
1745
|
touchActivity();
|
|
1680
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
|
+
|
|
1681
1764
|
return { success: true, task_id: task.id, status: task.status, title: task.title };
|
|
1682
1765
|
}
|
|
1683
1766
|
|
|
@@ -1760,8 +1843,8 @@ function toolReset() {
|
|
|
1760
1843
|
}
|
|
1761
1844
|
}
|
|
1762
1845
|
}
|
|
1763
|
-
// Remove profiles, workflows, branches, permissions, read receipts
|
|
1764
|
-
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]) {
|
|
1765
1848
|
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
1766
1849
|
}
|
|
1767
1850
|
// Remove workspaces dir
|
|
@@ -2091,10 +2174,631 @@ function toolListBranches() {
|
|
|
2091
2174
|
return { branches: result, current: currentBranch };
|
|
2092
2175
|
}
|
|
2093
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
|
+
|
|
2094
2798
|
// --- MCP Server setup ---
|
|
2095
2799
|
|
|
2096
2800
|
const server = new Server(
|
|
2097
|
-
{ name: 'agent-bridge', version: '3.
|
|
2801
|
+
{ name: 'agent-bridge', version: '3.7.0' },
|
|
2098
2802
|
{ capabilities: { tools: {} } }
|
|
2099
2803
|
);
|
|
2100
2804
|
|
|
@@ -2505,14 +3209,122 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2505
3209
|
},
|
|
2506
3210
|
{
|
|
2507
3211
|
name: 'listen_group',
|
|
2508
|
-
description: 'Listen for messages in group or managed conversation mode. Returns ALL unconsumed messages as a batch
|
|
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.',
|
|
2509
3213
|
inputSchema: {
|
|
2510
3214
|
type: 'object',
|
|
2511
|
-
properties: {
|
|
2512
|
-
timeout_seconds: { type: 'number', description: 'Max seconds to wait for messages (default 300)' },
|
|
2513
|
-
},
|
|
3215
|
+
properties: {},
|
|
2514
3216
|
},
|
|
2515
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
|
+
},
|
|
2516
3328
|
// --- Managed mode tools ---
|
|
2517
3329
|
{
|
|
2518
3330
|
name: 'claim_manager',
|
|
@@ -2641,7 +3453,67 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2641
3453
|
result = toolSetConversationMode(args.mode);
|
|
2642
3454
|
break;
|
|
2643
3455
|
case 'listen_group':
|
|
2644
|
-
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();
|
|
2645
3517
|
break;
|
|
2646
3518
|
case 'claim_manager':
|
|
2647
3519
|
result = toolClaimManager();
|
|
@@ -2666,6 +3538,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2666
3538
|
};
|
|
2667
3539
|
}
|
|
2668
3540
|
|
|
3541
|
+
// Global hook: on non-listen tools, check for pending messages and nudge the agent
|
|
3542
|
+
const listenTools = ['listen', 'listen_group', 'listen_codex', 'wait_for_reply', 'check_messages'];
|
|
3543
|
+
if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
|
|
3544
|
+
try {
|
|
3545
|
+
const pending = getUnconsumedMessages(registeredName);
|
|
3546
|
+
if (pending.length > 0 && !result.you_have_messages) {
|
|
3547
|
+
result._pending_messages = pending.length;
|
|
3548
|
+
result._nudge = `You have ${pending.length} unread message(s) from the team. Finish your current task quickly, then call listen_group() to read them.`;
|
|
3549
|
+
}
|
|
3550
|
+
} catch {}
|
|
3551
|
+
}
|
|
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
|
+
|
|
2669
3574
|
return {
|
|
2670
3575
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
2671
3576
|
};
|
|
@@ -2698,7 +3603,7 @@ async function main() {
|
|
|
2698
3603
|
ensureDataDir();
|
|
2699
3604
|
const transport = new StdioServerTransport();
|
|
2700
3605
|
await server.connect(transport);
|
|
2701
|
-
console.error('Agent Bridge MCP server v3.
|
|
3606
|
+
console.error('Agent Bridge MCP server v3.7.0 running (52 tools)');
|
|
2702
3607
|
}
|
|
2703
3608
|
|
|
2704
3609
|
main().catch(console.error);
|