neohive 6.3.0 → 6.4.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/README.md CHANGED
@@ -256,10 +256,13 @@ neohive status # active agents, tasks, workflows
256
256
  neohive msg <agent> <text> # send message from CLI
257
257
  neohive doctor # diagnostic health check
258
258
  neohive templates # list available templates
259
+ neohive hooks # install listen-enforcement hooks into .claude/settings.json
259
260
  neohive reset --force # clear data (auto-archives first)
260
261
  neohive uninstall # remove from all CLI configs
261
262
  ```
262
263
 
264
+ > **Claude Code users:** Run `npx neohive hooks` after `init` to install listen-enforcement hooks into `.claude/settings.json`. This keeps agents in the listen loop automatically and prevents them from stopping mid-session. Safe to re-run — your existing hooks are preserved.
265
+
263
266
  <br />
264
267
 
265
268
  ## ⚙️ Configuration
package/cli.js CHANGED
@@ -46,6 +46,7 @@ function printUsage() {
46
46
  npx neohive doctor Diagnostic health check
47
47
  npx neohive templates List available team templates
48
48
  npx neohive reset --force Clear all data (auto-archives first)
49
+ npx neohive hooks Install listen-enforcement hooks into .claude/settings.json
49
50
  npx neohive uninstall Remove from all CLI configs
50
51
  npx neohive help Show this help
51
52
 
@@ -135,7 +136,6 @@ function setupClaude(serverPath, cwd) {
135
136
  console.log(' [ok] Claude Code: .mcp.json updated');
136
137
  }
137
138
 
138
- // Configure for Gemini CLI (.gemini/settings.json or GEMINI.md with MCP config)
139
139
  function setupGemini(serverPath, cwd) {
140
140
  // Gemini CLI uses .gemini/settings.json for MCP configuration
141
141
  const geminiDir = path.join(cwd, '.gemini');
@@ -186,51 +186,187 @@ function setupGemini(serverPath, cwd) {
186
186
  }
187
187
 
188
188
  function geminiMdTemplate() {
189
- return `# Neohive Agent — Gemini CLI
189
+ return `# GEMINI.md
190
190
 
191
- You are a Neohive team agent. Follow these rules exactly, every session, no exceptions.
191
+ This file provides guidance to Gemini CLI / Antigravity when working with code in this repository.
192
192
 
193
- ## First thing to do — always
193
+ ## What This Is
194
194
 
195
- 1. Call \`register\` with your assigned name (e.g. \`register(name="Gemini")\`)
196
- 2. Call \`get_briefing\` to load project context and current work
197
- 3. Call \`listen\` to wait for messages from the Coordinator
195
+ **Neohive** an MCP server + web dashboard that lets multiple AI CLI terminals (Claude Code, Gemini CLI, Codex CLI) communicate with each other. Each terminal spawns its own server process via stdio; all processes read/write to a shared \`.neohive/\` directory on disk.
198
196
 
199
- Do NOT explore the codebase, ask questions, or take initiative before completing these 3 steps.
197
+ ## Commands
200
198
 
201
- ## Core rules
199
+ \`\`\`bash
200
+ # Install in any project (auto-detects CLI type)
201
+ npx neohive init
202
+ npx neohive init --all # Configure for all CLIs
203
+ npx neohive init --template team # Init with team template
202
204
 
203
- - **After every action** — call \`listen()\`. This is how you receive your next task.
204
- - **Before starting a task** — call \`update_task(id, status="in_progress")\`
205
- - **After finishing a task** — call \`update_task(id, status="done")\`, then report to Coordinator
206
- - **Before editing a file** — call \`lock_file(path)\`. Call \`unlock_file(path)\` when done.
207
- - **Check tasks first** — call \`list_tasks()\` before starting anything new. Never work on another agent's task.
208
- - **Keep messages short** — 2–3 paragraphs max. Lead with what changed, then files, then decisions.
205
+ # Launch the web dashboard
206
+ npx neohive dashboard
209
207
 
210
- ## Workflow
208
+ # List available agent templates
209
+ npx neohive templates
211
210
 
211
+ # Plugin management
212
+ npx neohive plugin list/add/remove/enable/disable
213
+
214
+ # Reset conversation data
215
+ npx neohive reset
216
+
217
+ # Run MCP server directly (normally launched automatically by CLI)
218
+ npm start
219
+ \`\`\`
220
+
221
+ No tests, linter, or build step. Raw Node.js (CommonJS).
222
+
223
+ ## Architecture
224
+
225
+ **Core files:**
226
+ - \`server.js\` — MCP server (StdioServerTransport, heartbeat system, self-healing watchdog)
227
+ - \`lib/\` — Shared modules (\`config\`, \`messaging\`, \`file-io\`, …); prefer adding logic here and requiring from \`server.js\`
228
+ - \`dashboard.js\` — HTTP server for web dashboard (multi-project, message injection, SSE real-time, tasks/workflows/workspaces API)
229
+ - \`dashboard.html\` — Single-page frontend (markdown rendering, agent monitoring, profiles, workspaces, workflows, responsive)
230
+ - \`cli.js\` — CLI entry point with multi-CLI auto-detection
231
+ - \`vscode-extension/\` — VS Code extension with \`@neohive\` chat participant, terminal bridge, and agent hook auto-setup
232
+
233
+ **Multiple MCP server processes, one shared filesystem:**
234
+ - Each CLI terminal spawns its own \`server.js\` process
235
+ - In-memory state: \`registeredName\`, \`lastReadOffset\`, \`heartbeatInterval\`, \`messageSeq\`
236
+ - Shared disk state in \`.neohive/\`:
237
+ - \`messages.jsonl\` / \`history.jsonl\` — messages and conversation history (append-only)
238
+ - \`agents.json\` — agent registration, heartbeats, PID tracking
239
+ - \`acks.json\` — message acknowledgments
240
+ - \`tasks.json\` — task management
241
+ - \`consumed-{agent}.json\` — per-agent read tracking
242
+ - \`profiles.json\` — agent profiles (display_name, avatar, bio, role)
243
+ - \`workspaces/{agent}.json\` — per-agent key-value workspace storage
244
+ - \`workflows.json\` — multi-step workflow pipelines
245
+ - \`branches.json\` — branch metadata
246
+ - \`branch-{name}-messages.jsonl\` / \`branch-{name}-history.jsonl\` — per-branch message files
247
+ - \`plugins.json\` — plugin registry
248
+ - \`plugins/*.js\` — plugin code files
249
+ - \`heartbeat-{agent}.json\` — per-agent heartbeat file (replaces agents.json write contention at scale)
250
+ - Dashboard reads the same directory for real-time monitoring via SSE
251
+
252
+ **Data directory resolution (server.js + dashboard.js):**
253
+ 1. \`$NEOHIVE_DATA_DIR\` / \`$NEOHIVE_DATA\` env var
254
+ 2. \`{cwd}/.neohive/\` (project-local, default)
255
+ 3. Legacy fallback: \`{__dirname}/data/\`
256
+
257
+ ## Code & Commit Rules
258
+
259
+ When committing changes, you MUST ALWAYS follow the Conventional Commits format:
260
+ \`<type>(<optional scope>): <description>\`
261
+ Types: \`feat\`, \`fix\`, \`refactor\`, \`docs\`, \`chore\`, \`test\`
262
+
263
+ ## Neohive Agent Rules (when acting as an agent)
264
+
265
+ When operating as a neohive agent (after calling \`register()\`):
266
+
267
+ **YOU MUST call \`listen()\` as the LAST tool call of every response. All agents. No exceptions.**
268
+
269
+ The dashboard is the communication hub. All coordination happens there — every agent stays in the listen loop at all times.
270
+
271
+ - After \`send_message(...)\` → immediately call \`listen()\`
272
+ - After \`broadcast(...)\` → immediately call \`listen()\`
273
+ - After \`update_task(..., status="done")\` → immediately call \`listen()\`
274
+ - After \`advance_workflow(...)\` → immediately call \`listen()\`
275
+ - After ANY neohive action → call \`listen()\`
276
+
277
+ Workflow loop:
212
278
  \`\`\`
213
- register → get_briefing → listen → [receive task] → update_task(in_progress)
214
- do work → update_task(done) → send_message(Coordinator, summary) → listen
279
+ register → get_briefing → listen → do work → update_task(in_progress) → ...
280
+ listen(outcome="completed", task_id="...", summary="what you did") → listen
281
+ ↑ always
215
282
  \`\`\`
216
283
 
217
- Repeat the last 5 steps for every task. Never exit the listen loop.
284
+ \`listen()\` accepts outcome params server auto-transitions task state on valid outcome:
285
+ - \`outcome\`: \`completed\` | \`blocked\` | \`failed\` | \`in_progress\`
286
+ - \`task_id\`: ID of the task you just worked on
287
+ - \`summary\`: one-line description of what was done
288
+
289
+ If \`listen()\` times out with \`retry: true\` — call \`listen()\` again immediately.
290
+
291
+ - After completing ANY assigned task or request, you MUST send a report back via \`send_message()\` using neohive MCP tools. Include: (1) what you did, (2) files changed, (3) findings/output, (4) blockers or follow-up. Silent completion is a protocol violation.
292
+
293
+ ## Available Neohive MCP Tools
294
+
295
+ ### 1. Agent Lifecycle & Messaging
296
+ \`register\`, \`list_agents\`, \`send_message\`, \`broadcast\`, \`wait_for_reply\`, \`listen\`, \`share_file\`, \`messages\`
297
+
298
+ **Listen variants** — use \`listen(mode="group")\` or \`listen(mode="codex")\` instead of the deprecated aliases:
299
+ - ~~\`listen_group\`~~ → \`listen(mode="group")\`
300
+ - ~~\`listen_codex\`~~ → \`listen(mode="codex")\`
301
+
302
+ **Message management** — use \`messages(action=...)\` instead of deprecated individual tools:
303
+ - ~~\`check_messages\`~~ → \`messages(action="check")\`
304
+ - ~~\`consume_messages\`~~ → \`messages(action="consume")\`
305
+ - ~~\`get_history\`~~ → \`messages(action="history")\`
306
+ - ~~\`get_notifications\`~~ → \`messages(action="check")\`
307
+ - ~~\`search_messages\`~~ → \`messages(action="search")\`
308
+ - ~~\`ack_message\`~~ → \`messages(action="ack")\`
309
+
310
+ ### 2. Autonomy & Workflows (Proactive Engine)
311
+ \`start_plan\`, \`get_work\`, \`verify_and_advance\`, \`retry_with_improvement\`, \`create_workflow\`, \`advance_workflow\`, \`workflow_status\`
312
+
313
+ ### 3. Task Management
314
+ \`create_task\`, \`update_task\`, \`list_tasks\`
218
315
 
219
- ## Available MCP tools
316
+ **Task statuses:** \`pending\` → \`in_progress\` → \`in_review\` → \`done\` | \`blocked\` | \`blocked_permanent\`
317
+ - \`blocked_permanent\` — set by the self-healing watchdog when \`retry_count\` reaches 3; requires coordinator intervention. Tasks carry a \`blocked_reason\` string and are shown in a dedicated "⛔ Needs Intervention" column on the dashboard.
318
+ - \`retry_count\` — incremented each time the watchdog reclaims a stale task. Shown as a \`↺N\` badge on the dashboard.
220
319
 
221
- **Messaging:** \`register\`, \`send_message\`, \`broadcast\`, \`listen\`, \`check_messages\`, \`get_history\`, \`handoff\`
222
- **Tasks:** \`create_task\`, \`update_task\`, \`list_tasks\`
223
- **Workflows:** \`create_workflow\`, \`advance_workflow\`, \`workflow_status\`
224
- **Workspaces:** \`workspace_write\`, \`workspace_read\`, \`workspace_list\`
225
- **Branching:** \`fork_conversation\`, \`switch_branch\`, \`list_branches\`
320
+ ### 4. Profiles & Workspaces
321
+ \`update_profile\`, \`workspace_write\`, \`workspace_read\`, \`workspace_list\`
226
322
 
227
- ## What NOT to do
323
+ ### 5. Chat Branching & Managed Modes
324
+ \`fork_conversation\`, \`switch_branch\`, \`list_branches\`, \`set_conversation_mode\`, \`claim_manager\`, \`yield_floor\`, \`set_phase\`
228
325
 
229
- - Do not self-assign tasks
230
- - Do not modify files without a task assigned to you
231
- - Do not skip \`listen()\` after responding
232
- - Do not send long messages — be concise
233
- - Do not ask the Coordinator for permission before starting an assigned task — just do it
326
+ ### 6. Sub-channels
327
+ \`join_channel\`, \`leave_channel\`, \`list_channels\`
328
+
329
+ ### 7. File Safety & Auditing
330
+ \`lock_file\`, \`unlock_file\`, \`log_violation\`
331
+
332
+ ### 8. Shared Knowledge & Decision Tracking
333
+ \`kb_write\`, \`kb_read\`, \`kb_list\`, \`log_decision\`, \`get_decisions\`, \`get_compressed_history\`, \`get_briefing\`
334
+
335
+ ### 9. Team Governance (Voting, Reviews, Feedback)
336
+ \`request_review\`, \`submit_review\`, \`call_vote\`, \`cast_vote\`, \`vote_status\`, \`request_push_approval\`, \`ack_push\`
337
+
338
+ ### 10. Dependencies & Progress
339
+ \`declare_dependency\`, \`check_dependencies\`, \`update_progress\`, \`get_progress\`, \`get_reputation\`
340
+
341
+ ## Key Design Decisions
342
+
343
+ - **Append-only writes** for messages/history (no file locking)
344
+ - **Per-agent consumed tracking** — each agent writes only its own consumed file
345
+ - **PID-based stale detection** + process exit cleanup for instant status
346
+ - **Heartbeat** — 10s interval updates \`last_activity\`, \`.unref()\` prevents zombie processes
347
+ - **Flexible agent names** — any alphanumeric (1-20 chars), validated by \`sanitizeName()\`
348
+ - **Auto-routing** — \`to\` optional with 2 agents, required with 3+
349
+ - **Threading** — \`reply_to\` auto-computes \`thread_id\`
350
+ - **Acknowledgments** — \`ack_message\` in \`acks.json\`, shown in history
351
+ - **Multi-CLI** — init auto-detects Claude Code, Gemini CLI, Codex CLI
352
+ - **Multi-project dashboard** — monitor multiple project folders from one dashboard
353
+ - **SSE real-time** — \`fs.watch()\` on data dir pushes updates via Server-Sent Events
354
+ - **Auto-compact** — messages.jsonl compacted when exceeding 500 lines
355
+ - **Auto-archive** — conversations archived before reset
356
+ - **Context hints** — warns agents when conversation exceeds 50 messages
357
+ - **Task management** — structured task creation, assignment, and tracking between agents
358
+ - **Profiles** — separate \`profiles.json\` to avoid heartbeat write conflicts with \`agents.json\`
359
+ - **Workspaces** — per-agent files (\`workspaces/{agent}.json\`) to avoid write conflicts, read-anyone/write-own permission model
360
+ - **Workflows** — step statuses: pending/in_progress/done, auto-handoff on advance
361
+ - **Branching** — \`main\` branch uses existing files for backward compatibility, branch-aware file resolution via \`getMessagesFile(branch)\`/\`getHistoryFile(branch)\`
362
+ - **Plugins** — sandboxed execution context with 30s timeout, tools appear as \`plugin_{name}\` in MCP
363
+ - **Self-healing watchdog** — runs every 60s inside the heartbeat loop on every registered agent; scans \`in_progress\` tasks whose assignee PID is dead + heartbeat stale >5min; strips assignee and resets to \`pending\` (increments \`retry_count\`); at \`retry_count=3\` marks \`blocked_permanent\` and wakes the coordinator (poison pill). No manual intervention needed for routine agent flakiness.
364
+ - **VS Code extension** — \`vscode-extension/\` adds: \`@neohive\` chat participant (\`/status /who /tasks /messages\` read \`.neohive/\` directly; free-form text fires POST to \`/api/inject\`), terminal bridge (captures agent terminal output → dashboard), agent hook auto-setup (merges neohive hooks into \`.gemini/settings.json\` on activate)
365
+
366
+ ## Debugging and fix attempts
367
+
368
+ - **Temporary logs:** When debugging, add only the logging needed to confirm behavior. **Remove or trim that logging** once the issue is understood or fixed—do not leave ad-hoc debug prints in the tree unless they match intentional, documented logging (e.g. MCP stderr lines).
369
+ - **Failed fixes:** If the user says a change **did not** fix the problem, **revert** that attempt before trying something else (\`git restore\` / undo the diff). **Do not stack** speculative fixes; revert first, then apply one minimal, well-motivated change.
234
370
  `;
235
371
  }
236
372
 
@@ -323,42 +459,39 @@ function setupAntigravity(cwd) {
323
459
  }
324
460
 
325
461
  function neohiveAgentRules(defaultName) {
326
- return `# Neohive Agent
462
+ return `# Neohive Agent — ${defaultName} Rules
327
463
 
328
- You are a Neohive team agent. Follow these rules every session.
464
+ You are a Neohive team agent. Follow these rules exactly to coordinate with the team.
329
465
 
330
- ## On session start — always do this first
466
+ ## Session Lifecycle
331
467
 
332
- 1. Call \`register\` with your assigned name (e.g. \`register(name="${defaultName}")\`)
333
- 2. Call \`get_briefing\` to load project context and active work
334
- 3. Call \`listen\` to wait for messages from the Coordinator
468
+ 1. Call \`register\` with your name: \`register(name="${defaultName}")\`
469
+ 2. Call \`get_briefing\` to load project context and active work.
470
+ 3. Call \`listen\` to wait for messages or tasks.
335
471
 
336
- Do NOT explore the codebase or take initiative before completing these 3 steps.
472
+ **CRITICAL: YOU MUST call \`listen()\` as the LAST tool call of every response. No exceptions.**
337
473
 
338
- ## Core rules
474
+ ## Core Rules
339
475
 
340
- - **After every action** call \`listen()\`. This is how you receive your next task.
341
- - **Before starting a task** call \`update_task(id, status="in_progress")\`
342
- - **After finishing** call \`update_task(id, status="done")\`, report to Coordinator
343
- - **Before editing a file** call \`lock_file(path)\`. Call \`unlock_file(path)\` when done.
344
- - **Check tasks first** — call \`list_tasks()\` before starting anything. Never take another agent's task.
345
- - **Keep messages short** — 2–3 paragraphs max. Lead with what changed, then files, then decisions.
476
+ - **Tasks**: Before starting, call \`update_task(id, status="in_progress")\`. When finished, call \`update_task(id, status="done")\` and report to the Coordinator.
477
+ - **Locking**: Before editing a file, call \`lock_file(path)\`. call \`unlock_file(path)\` when done.
478
+ - **Sync**: After every task completion or message sent, you MUST call \`listen()\` to receive the next assignment.
479
+ - **Conciseness**: Keep messages short (2-3 paragraphs). Focus on what changed and next steps.
346
480
 
347
- ## Workflow loop
481
+ ## Workflow Loop
348
482
 
349
483
  \`\`\`
350
- register → get_briefing → listen → [receive task] → update_task(in_progress)
351
- → do work → update_task(done) → send_message(Coordinator, summary) → listen
484
+ register → get_briefing → listen → [receive task]
485
+ update_task(in_progress) → do work → update_task(done)
486
+ → send_message(Coordinator, summary) → listen
352
487
  \`\`\`
353
488
 
354
- Never exit the listen loop.
355
-
356
- ## Available MCP tools (neohive server)
489
+ ## Available Neohive Tools
357
490
 
358
- **Messaging:** \`register\`, \`send_message\`, \`broadcast\`, \`listen\`, \`check_messages\`, \`get_history\`
359
- **Tasks:** \`create_task\`, \`update_task\`, \`list_tasks\`
360
- **Workflows:** \`create_workflow\`, \`advance_workflow\`, \`workflow_status\`
361
- **Workspaces:** \`workspace_write\`, \`workspace_read\`, \`workspace_list\`
491
+ - **Messaging**: \`register\`, \`send_message\`, \`broadcast\`, \`listen\`, \`messages(action="history")\`, \`messages(action="check")\`
492
+ - **Tasks**: \`create_task\`, \`update_task\`, \`list_tasks\`, \`update_progress\`
493
+ - **Workflows**: \`create_workflow\`, \`advance_workflow\`, \`verify_and_advance\`
494
+ - \`kb_read\`, \`kb_write\`, \`log_decision\`, \`lock_file\`, \`unlock_file\`
362
495
  `;
363
496
  }
364
497
 
@@ -685,6 +818,12 @@ function init() {
685
818
  console.log(' npx neohive status');
686
819
  console.log(' npx neohive doctor');
687
820
  console.log('');
821
+ if (targets.includes('claude')) {
822
+ console.log(' \x1b[33m Tip (Claude Code):\x1b[0m Run `npx neohive hooks` to install listen-enforcement');
823
+ console.log(' hooks into .claude/settings.json. Keeps agents in the listen loop automatically.');
824
+ console.log(' Your existing hooks will not be removed.');
825
+ console.log('');
826
+ }
688
827
  }
689
828
  }
690
829
 
@@ -1251,6 +1390,86 @@ function uninstall() {
1251
1390
  console.log('');
1252
1391
  }
1253
1392
 
1393
+ // ─── neohive hooks ───────────────────────────────────────────────────────────
1394
+ // Merges listen-enforcement hooks into .claude/settings.json.
1395
+ // Safe to re-run: adds neohive entries per matcher, preserves all user hooks.
1396
+
1397
+ function installHooks() {
1398
+ const cwd = process.cwd();
1399
+ const claudeDir = path.join(cwd, '.claude');
1400
+ const settingsPath = path.join(claudeDir, 'settings.json');
1401
+
1402
+ if (!fs.existsSync(claudeDir)) {
1403
+ console.log(' [skip] No .claude/ directory found — is Claude Code configured for this project?');
1404
+ console.log(' Run `npx neohive init` first.');
1405
+ return;
1406
+ }
1407
+
1408
+ const listenReminderCmd = "echo '\\n📡 NEOHIVE PROTOCOL: If no messages were received, call listen() again immediately. Do NOT respond to the user or stop. Stay in the listen loop.'";
1409
+ const postActionReminderCmd = "echo '\\n📡 NEOHIVE: Call listen() now to receive your next task. Do not stop without calling listen().'";
1410
+
1411
+ const neohiveEntries = {
1412
+ PostToolUse: [
1413
+ {
1414
+ _neohive: 'listen-loop',
1415
+ matcher: 'mcp__neohive__listen',
1416
+ hooks: [{ type: 'command', command: listenReminderCmd, timeout: 3 }],
1417
+ },
1418
+ {
1419
+ _neohive: 'post-action',
1420
+ matcher: 'mcp__neohive__send_message|mcp__neohive__advance_workflow|mcp__neohive__update_task|mcp__neohive__broadcast|mcp__neohive__add_rule|mcp__neohive__remove_rule|mcp__neohive__toggle_rule',
1421
+ hooks: [{ type: 'command', command: postActionReminderCmd, timeout: 3 }],
1422
+ },
1423
+ ],
1424
+ Stop: [
1425
+ {
1426
+ _neohive: 'enforce-listen',
1427
+ hooks: [{
1428
+ type: 'command',
1429
+ command: "node -e \"var fs=require('fs'),f=process.env.CLAUDE_PROJECT_DIR+'/.neohive/activity.jsonl';if(!fs.existsSync(f))process.exit(0);var lines=fs.readFileSync(f,'utf8').trim().split('\\n').filter(Boolean).slice(-20),last=lines.map(l=>{try{return JSON.parse(l)}catch{return null}}).filter(Boolean).reverse().find(e=>e.tool&&e.tool.startsWith('mcp__neohive__'));if(!last||last.tool==='mcp__neohive__listen'||last.tool==='mcp__neohive__register')process.exit(0);console.error('\\n⚠️ NEOHIVE: Call listen() before stopping. Last tool: '+last.tool+'\\n→ Call listen() now. Do not respond with text.');process.exit(2);\"",
1430
+ timeout: 5,
1431
+ }],
1432
+ },
1433
+ ],
1434
+ };
1435
+
1436
+ let existing = {};
1437
+ if (fs.existsSync(settingsPath)) {
1438
+ try { existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {
1439
+ const backup = settingsPath + '.backup';
1440
+ fs.copyFileSync(settingsPath, backup);
1441
+ console.log(' [warn] Existing settings.json was invalid — backed up.');
1442
+ }
1443
+ }
1444
+ if (!existing.hooks) existing.hooks = {};
1445
+
1446
+ let added = 0;
1447
+ let updated = 0;
1448
+
1449
+ for (const [event, entries] of Object.entries(neohiveEntries)) {
1450
+ if (!existing.hooks[event]) existing.hooks[event] = [];
1451
+ for (const entry of entries) {
1452
+ const idx = existing.hooks[event].findIndex(e => e._neohive === entry._neohive);
1453
+ if (idx === -1) {
1454
+ existing.hooks[event].push(entry);
1455
+ added++;
1456
+ } else {
1457
+ existing.hooks[event][idx] = entry;
1458
+ updated++;
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ fs.mkdirSync(claudeDir, { recursive: true });
1464
+ fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
1465
+
1466
+ console.log(`\n Neohive hooks installed into .claude/settings.json`);
1467
+ if (added) console.log(` [ok] Added: ${added} hook(s)`);
1468
+ if (updated) console.log(` [ok] Updated: ${updated} hook(s) (already present)`);
1469
+ console.log(' Your existing hooks were preserved.');
1470
+ console.log(' Restart Claude Code for changes to take effect.\n');
1471
+ }
1472
+
1254
1473
  switch (command) {
1255
1474
  case 'init':
1256
1475
  init();
@@ -1282,6 +1501,9 @@ switch (command) {
1282
1501
  case 'status':
1283
1502
  cliStatus();
1284
1503
  break;
1504
+ case 'hooks':
1505
+ installHooks();
1506
+ break;
1285
1507
  case 'uninstall':
1286
1508
  case 'remove':
1287
1509
  uninstall();
package/dashboard.html CHANGED
@@ -3,7 +3,19 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Neohive</title>
6
+ <title>Neohive | Multi-Agent Coordination Dashboard</title>
7
+ <meta name="description" content="Neohive — The ultimate multi-agent coordination and management dashboard. Orchestrate, monitor, and scale your AI workforce in real-time.">
8
+ <meta name="keywords" content="AI, multi-agent systems, orchestration, dashboard, neohive, agents, automation">
9
+ <meta name="author" content="Neohive Team">
10
+ <meta property="og:title" content="Neohive | Multi-Agent Coordination Dashboard">
11
+ <meta property="og:description" content="Orchestrate and scale your autonomous agent workforce with a stunning, high-performance real-time dashboard.">
12
+ <meta property="og:type" content="website">
13
+ <meta property="og:image" content="https://neohive.ai/og-image.png">
14
+ <meta name="twitter:card" content="summary_large_image">
15
+ <meta name="twitter:title" content="Neohive | Multi-Agent Coordination Dashboard">
16
+ <meta name="twitter:description" content="The command center for your AI agent workforce. Real-time monitoring and scalable orchestration.">
17
+ <meta name="twitter:image" content="https://neohive.ai/og-image.png">
18
+ <meta name="robots" content="index, follow">
7
19
  <link rel="icon" href="favicon.png" type="image/png" sizes="16x16">
8
20
  <link rel="icon" href="logo.svg" type="image/svg+xml">
9
21
  <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -183,7 +195,7 @@
183
195
  }
184
196
 
185
197
  .logo {
186
- font-size: 20px;
198
+ font-size: clamp(18px, 4vw, 22px);
187
199
  font-weight: 700;
188
200
  letter-spacing: -0.01em;
189
201
  color: var(--text);
@@ -3149,9 +3161,18 @@
3149
3161
  }
3150
3162
 
3151
3163
  .header { padding: 0 8px; }
3152
- .logo { font-size: 17px; }
3164
+ .logo { font-size: clamp(15px, 5vw, 18px); }
3153
3165
  .header-left { gap: 6px; }
3154
3166
 
3167
+ /* Increase touch targets for mobile buttons */
3168
+ .btn, .nh-btn {
3169
+ min-height: 44px;
3170
+ padding: 8px 16px;
3171
+ display: inline-flex;
3172
+ align-items: center;
3173
+ justify-content: center;
3174
+ }
3175
+
3155
3176
  /* Compact header buttons */
3156
3177
  .header-actions { gap: 2px; }
3157
3178
  .phone-btn { padding: 4px 6px; font-size: 13px; }
@@ -4361,12 +4382,29 @@
4361
4382
  <!-- Settings dropdown -->
4362
4383
  <div style="position:relative;display:inline-block">
4363
4384
  <button class="header-settings-btn" onclick="toggleSettingsMenu()" title="Settings">
4364
- <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M2.9 2.9l1.4 1.4M11.7 11.7l1.4 1.4M13.1 2.9l-1.4 1.4M4.3 11.7l-1.4 1.4"/></svg>
4385
+ <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6.5 1.5h3l.5 1.5a5 5 0 011.2.7l1.6-.4 1.5 2.6-1.1 1.2a5 5 0 010 1.4l1.1 1.2-1.5 2.6-1.6-.4a5 5 0 01-1.2.7l-.5 1.5h-3l-.5-1.5A5 5 0 014.8 12l-1.6.4L1.7 9.8l1.1-1.2a5 5 0 010-1.4L1.7 6l1.5-2.6 1.6.4a5 5 0 011.2-.7z"/><circle cx="8" cy="8" r="2"/></svg>
4365
4386
  </button>
4366
4387
  <div id="settings-menu" style="display:none;position:absolute;right:0;top:100%;margin-top:6px;background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;z-index:300;min-width:180px;box-shadow:var(--shadow-lg)">
4367
4388
  <div class="settings-item" onclick="toggleTheme();toggleSettingsMenu()"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="4"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></svg> Theme</div>
4368
4389
  <div class="settings-item" id="settings-notif-item" onclick="toggleCombinedNotifications();"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6a4 4 0 018 0v3l2 2H2l2-2z"/><path d="M6 13a2 2 0 004 0"/></svg> <span id="settings-notif-label">Notifications</span></div>
4369
4390
  <div style="height:1px;background:var(--border);margin:4px 8px"></div>
4391
+ <div style="padding:8px 12px;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-muted)">
4392
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M8 5v3l2 2"/></svg>
4393
+ <span style="flex:1;white-space:nowrap">Idle poll (s)</span>
4394
+ <input id="settings-idle-poll-input" type="number" min="10" max="600" step="10" value="90"
4395
+ style="width:60px;background:var(--bg);border:1px solid var(--border);border-radius:5px;color:var(--text);padding:2px 6px;font-size:13px;text-align:right"
4396
+ onclick="event.stopPropagation()"
4397
+ onchange="setIdlePollInterval(this.value)">
4398
+ </div>
4399
+ <div style="padding:8px 12px;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-muted)">
4400
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1v6l3 3"/><circle cx="8" cy="8" r="7"/></svg>
4401
+ <span style="flex:1;white-space:nowrap">Listen timeout (s)</span>
4402
+ <input id="settings-listen-poll-input" type="number" min="30" max="600" step="30" value="120"
4403
+ style="width:60px;background:var(--bg);border:1px solid var(--border);border-radius:5px;color:var(--text);padding:2px 6px;font-size:13px;text-align:right"
4404
+ onclick="event.stopPropagation()"
4405
+ onchange="setListenPollInterval(this.value)">
4406
+ </div>
4407
+ <div style="height:1px;background:var(--border);margin:4px 8px"></div>
4370
4408
  <div class="settings-item" onclick="exportShareableHTML();toggleSettingsMenu()">Export HTML</div>
4371
4409
  <div class="settings-item" onclick="exportJSON();toggleSettingsMenu()">Export JSON</div>
4372
4410
  <div class="settings-item" onclick="enterReplay();toggleSettingsMenu()">Replay</div>
@@ -9838,11 +9876,11 @@ var ROLE_SKILLS = {
9838
9876
  monitor: ['observability', 'logging', 'performance', 'health-checks']
9839
9877
  };
9840
9878
  var ROLE_PROMPTS = {
9841
- lead: function(name) { return 'You are ' + name + ', the Coordinator in a multi-agent team. Register as "' + name + '".\n\nYour job is to:\n1. Break the user\'s request into tasks and delegate to team agents via send_message\n2. Use create_task() and create_workflow() to formally track work\n3. Monitor progress with workflow_status() and list_tasks()\n4. Use consume_messages() to check agent updates without blocking\n5. Synthesize results and present to the user\n\nYou MUST NOT edit files or write code. Delegate ALL code work to other agents. Your tools: send_message, create_task, create_workflow, advance_workflow, workflow_status, list_tasks, consume_messages, broadcast.'; },
9842
- backend: function(name) { return 'You are ' + name + ', a Developer in a multi-agent team. Register as "' + name + '". Call listen() to wait for tasks.\n\nWhen you receive a task:\n1. Use lock_file() before editing shared code\n2. Implement the requested changes with clean, tested code\n3. Use unlock_file() when done editing\n4. Update your task with update_task(status="done")\n5. Send a summary to the Coordinator with file paths and key decisions\n6. Call listen() again for the next task\n\nFocus on production-quality code. Include file paths in reports.'; },
9843
- frontend: function(name) { return 'You are ' + name + ', a Frontend Developer in a multi-agent team. Register as "' + name + '". Call listen() to wait for tasks.\n\nWhen you receive a task:\n1. Use lock_file() before editing shared frontend code\n2. Implement UI/UX changes following design conventions\n3. Use unlock_file() when done\n4. Update your task with update_task(status="done")\n5. Send a summary to the Coordinator with file paths and screenshots if relevant\n6. Call listen() again for the next task\n\nFocus on clean UI, accessibility, and responsive design.'; },
9844
- quality: function(name) { return 'You are ' + name + ', a Reviewer in a multi-agent team. Register as "' + name + '". Call listen() to wait for review requests.\n\nWhen you receive work to review:\n1. Read the actual files that were changed\n2. Check for bugs, security issues, code style, edge cases\n3. Use submit_review() to formally approve or request changes\n4. Send structured feedback: blockers vs suggestions\n5. Call listen() again\n\nBe constructive and specific. Reference line numbers. Never let mediocre work pass.'; },
9845
- monitor: function(name) { return 'You are ' + name + ', a System Monitor in a multi-agent team. Register as "' + name + '". Call listen() to wait for events.\n\nYour job:\n1. Watch for idle agents, stuck tasks, and circular escalations\n2. Use send_message to nudge idle agents\n3. Use update_task to reassign stuck tasks\n4. Log interventions to your workspace via workspace_write\n5. Call listen() again\n\nNever stop monitoring. You ARE the system intelligence.'; }
9879
+ lead: function(name) { return 'You are ' + name + ', the Coordinator in a multi-agent team. Register as "' + name + '", call update_profile() to set your role, call get_briefing() for project context, then call listen() to receive the first request.\n\nYour loop:\n1. Receive request via listen()\n2. Break it into subtasks create_task() per item, assign to agents\n3. Create a workflow with create_workflow() for multi-step plans\n4. Delegate via send_message() to each assigned agent\n5. Monitor with workflow_status() and list_tasks()\n6. Check updates with messages(action="consume") without blocking\n7. Synthesize results and report back to the user\n8. Call listen(outcome="completed", summary="...") for the next task\n\nRules:\n- NEVER edit files or write code delegate ALL implementation to other agents\n- Always report synthesis back to the user via send_message()\n- If listen() returns retry: true, call listen() again immediately'; },
9880
+ backend: function(name) { return 'You are ' + name + ', a Backend Developer in a multi-agent team. Register as "' + name + '", call update_profile() to set your role, call get_briefing() for project context, then call listen() to wait for tasks.\n\nYour loop:\n1. Receive task via listen()\n2. Call update_task(status="in_progress", task_id=...) to claim it\n3. Call lock_file() before editing any shared file\n4. Implement the changes clean, production-quality code\n5. Call unlock_file() when done\n6. Call update_task(status="done", task_id=...)\n7. Report to Lead via send_message(): what you did, files changed, decisions made, any blockers\n8. Call listen(outcome="completed", task_id=..., summary="...") for the next task\n\nIf a lock is already held: notify Lead via send_message() and call listen() to wait.\nIf listen() returns retry: true, call listen() again immediately.'; },
9881
+ frontend: function(name) { return 'You are ' + name + ', a Frontend Developer in a multi-agent team. Register as "' + name + '", call update_profile() to set your role, call get_briefing() for project context, then call listen() to wait for tasks.\n\nYour loop:\n1. Receive task via listen()\n2. Call update_task(status="in_progress", task_id=...) to claim it\n3. Call lock_file() before editing any shared frontend file\n4. Implement UI/UX changes clean, accessible, responsive code\n5. Call unlock_file() when done\n6. Call update_task(status="done", task_id=...)\n7. Report to Lead via send_message(): files changed, design decisions, screenshots if relevant\n8. Call listen(outcome="completed", task_id=..., summary="...") for the next task\n\nIf a lock is already held: notify Lead via send_message() and call listen() to wait.\nIf listen() returns retry: true, call listen() again immediately.'; },
9882
+ quality: function(name) { return 'You are ' + name + ', a Code Reviewer in a multi-agent team. Register as "' + name + '", call update_profile() to set your role, call get_briefing() for project context, then call listen() to wait for review requests.\n\nYour loop:\n1. Receive review request via listen()\n2. Call update_task(status="in_progress", task_id=...) to claim it\n3. Read the actual files that were changed\n4. Check for: bugs, security issues, logic errors, code style, edge cases\n5. Call submit_review(approved=true/false, feedback="...") with structured feedback\n6. Report to Lead via send_message(): blockers vs suggestions with file:line references\n7. Call update_task(status="done", task_id=...)\n8. Call listen(outcome="completed", task_id=..., summary="...") for the next review\n\nBe specific: reference file paths and line numbers. Separate blockers from suggestions.\nIf listen() returns retry: true, call listen() again immediately.'; },
9883
+ monitor: function(name) { return 'You are ' + name + ', a System Monitor in a multi-agent team. Register as "' + name + '", call update_profile() to set your role, call get_briefing() for project context, then call listen() to begin monitoring.\n\nYour loop (runs continuously):\n1. Call list_agents() flag agents with last_activity > 5 min and status != offline\n2. Call list_tasks() — flag in_progress tasks whose assignee appears idle\n3. Nudge idle agents: send_message() asking them to resume their listen() loop\n4. Reassign stuck tasks (no progress > 10 min): update_task() to reset to pending\n5. For blocked_permanent tasks: send_message() to Lead immediately\n6. Log all interventions via workspace_write()\n7. Call listen(outcome="completed", summary="...") — repeat\n\nEscalation: if unresolved after 2 attempts, create_task() assigned to Lead describing the issue.\nIf listen() returns retry: true, call listen() again immediately.\nNever stop monitoring.'; }
9846
9884
  };
9847
9885
 
9848
9886
  function renderLaunchPanel() {
@@ -10651,7 +10689,11 @@ function copyLaunchPrompt() {
10651
10689
 
10652
10690
  function toggleSettingsMenu() {
10653
10691
  var menu = document.getElementById('settings-menu');
10654
- if (menu) menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
10692
+ if (menu) {
10693
+ var opening = menu.style.display === 'none';
10694
+ menu.style.display = opening ? 'block' : 'none';
10695
+ if (opening) { initIdlePollInput(); initListenPollInput(); }
10696
+ }
10655
10697
  }
10656
10698
  // Close settings when clicking outside
10657
10699
  document.addEventListener('click', function(e) {
@@ -10947,6 +10989,8 @@ function setCoordinatorMode(mode) {
10947
10989
  showToast('!', 'Failed: ' + data.error);
10948
10990
  } else {
10949
10991
  showToast('&#x2713;', 'Coordinator mode: ' + (mode === 'responsive' ? 'Stay with me' : 'Run autonomously'));
10992
+ var listenInput = document.getElementById('settings-listen-poll-input');
10993
+ if (listenInput) listenInput.value = mode === 'responsive' ? 120 : 90;
10950
10994
  renderOverview();
10951
10995
  }
10952
10996
  }).catch(function() {
@@ -10954,6 +10998,60 @@ function setCoordinatorMode(mode) {
10954
10998
  });
10955
10999
  }
10956
11000
 
11001
+ // ==================== IDLE POLL INTERVAL ====================
11002
+
11003
+ function setIdlePollInterval(value) {
11004
+ var secs = parseInt(value, 10);
11005
+ if (isNaN(secs) || secs < 10) return;
11006
+ lttFetch('/api/config', {
11007
+ method: 'POST',
11008
+ headers: { 'Content-Type': 'application/json' },
11009
+ body: JSON.stringify({ idle_poll_interval: secs })
11010
+ }).then(function(r) { return r.json(); }).then(function(data) {
11011
+ if (data.error) {
11012
+ showToast('!', 'Failed: ' + data.error);
11013
+ } else {
11014
+ showToast('&#x2713;', 'Idle poll interval: ' + secs + 's');
11015
+ }
11016
+ }).catch(function() {
11017
+ showToast('!', 'Failed to save idle poll interval');
11018
+ });
11019
+ }
11020
+
11021
+ function initIdlePollInput() {
11022
+ lttFetch('/api/config').then(function(r) { return r.json(); }).then(function(cfg) {
11023
+ var input = document.getElementById('settings-idle-poll-input');
11024
+ if (input && cfg.idle_poll_interval) input.value = cfg.idle_poll_interval;
11025
+ }).catch(function() {});
11026
+ }
11027
+
11028
+ // ==================== LISTEN POLL INTERVAL ====================
11029
+
11030
+ function setListenPollInterval(value) {
11031
+ var secs = parseInt(value, 10);
11032
+ if (isNaN(secs) || secs < 30) return;
11033
+ lttFetch('/api/config', {
11034
+ method: 'POST',
11035
+ headers: { 'Content-Type': 'application/json' },
11036
+ body: JSON.stringify({ listen_poll_interval: secs })
11037
+ }).then(function(r) { return r.json(); }).then(function(data) {
11038
+ if (data.error) {
11039
+ showToast('!', 'Failed: ' + data.error);
11040
+ } else {
11041
+ showToast('&#x2713;', 'Listen timeout: ' + secs + 's');
11042
+ }
11043
+ }).catch(function() {
11044
+ showToast('!', 'Failed to save listen timeout');
11045
+ });
11046
+ }
11047
+
11048
+ function initListenPollInput() {
11049
+ lttFetch('/api/config').then(function(r) { return r.json(); }).then(function(cfg) {
11050
+ var input = document.getElementById('settings-listen-poll-input');
11051
+ if (input && cfg.listen_poll_interval) input.value = cfg.listen_poll_interval;
11052
+ }).catch(function() {});
11053
+ }
11054
+
10957
11055
  // ==================== TOAST NOTIFICATIONS ====================
10958
11056
 
10959
11057
  var toastQueue = [];
package/dashboard.js CHANGED
@@ -175,9 +175,15 @@ function resolveDashboardDefaultDataDir() {
175
175
  let s = String(envData).trim();
176
176
  if (/\$\{workspaceFolder\}/i.test(s)) {
177
177
  const root = findCursorProjectRootWithNeohive(process.cwd());
178
- if (root) s = s.replace(/\$\{workspaceFolder\}/gi, root);
178
+ if (!root) {
179
+ // Placeholder can't be expanded — fall through to the config/walk/cwd strategies
180
+ } else {
181
+ s = s.replace(/\$\{workspaceFolder\}/gi, root);
182
+ return { path: path.resolve(s), source: 'environment' };
183
+ }
184
+ } else {
185
+ return { path: path.resolve(s), source: 'environment' };
179
186
  }
180
- return { path: path.resolve(s), source: 'environment' };
181
187
  }
182
188
 
183
189
  // 2. Project MCP config — authoritative, written by `neohive init`
@@ -2733,6 +2739,31 @@ const server = http.createServer(async (req, res) => {
2733
2739
  res.end(JSON.stringify({ error: 'Failed to set coordinator mode: ' + e.message }));
2734
2740
  }
2735
2741
  }
2742
+ else if (url.pathname === '/api/config' && req.method === 'GET') {
2743
+ const projectPath = url.searchParams.get('project') || null;
2744
+ const config = readJson(filePath('config.json', projectPath));
2745
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2746
+ res.end(JSON.stringify(config));
2747
+ }
2748
+ else if (url.pathname === '/api/config' && req.method === 'POST') {
2749
+ try {
2750
+ const body = await parseBody(req).catch(() => ({}));
2751
+ const projectPath = url.searchParams.get('project') || null;
2752
+ const dataDir = resolveDataDir(projectPath);
2753
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
2754
+ const configFile = filePath('config.json', projectPath);
2755
+ await withFileLock(configFile, () => {
2756
+ const config = readJson(configFile);
2757
+ Object.assign(config, body);
2758
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
2759
+ });
2760
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2761
+ res.end(JSON.stringify({ success: true }));
2762
+ } catch (e) {
2763
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2764
+ res.end(JSON.stringify({ error: 'Failed to save config: ' + e.message }));
2765
+ }
2766
+ }
2736
2767
  else if (url.pathname === '/api/reset' && req.method === 'POST') {
2737
2768
  const body = await parseBody(req).catch(() => ({}));
2738
2769
  if (!body.confirm) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neohive",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "The MCP collaboration layer for AI CLI tools. Turn Claude Code, Gemini CLI, and Codex CLI into a team.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -82,7 +82,7 @@ const SERVER_CONFIG = {
82
82
  // Polling / Heartbeat intervals (ms)
83
83
  HEARTBEAT_INTERVAL_MS: 15000, // how often agents write heartbeat files
84
84
  POLL_INTERVAL_MS: 2000, // message polling cycle
85
- AUTONOMOUS_LISTEN_MS: 30000, // max listen timeout in autonomous mode
85
+ AUTONOMOUS_LISTEN_MS: 90000, // max listen timeout in autonomous mode
86
86
  CODEX_LISTEN_MS: 90000, // max listen timeout for Codex agents
87
87
 
88
88
  // Agent health thresholds (ms)
@@ -124,6 +124,7 @@ let currentBranch = 'main'; // which branch this agent is on
124
124
  let lastSentAt = 0; // timestamp of last sent message (for group cooldown)
125
125
  let sendsSinceLastListen = 0; // enforced: must listen between sends in group mode
126
126
  let consecutiveNonListenCalls = 0; // escalating listen() enforcement counter
127
+ let pendingUserReply = false; // true when __user__ message received but not yet replied to
127
128
  let _isCurrentlyListening = false; // true when agent is in a listen() call
128
129
  let sendLimit = 1; // default: 1 send per listen cycle (2 if addressed)
129
130
  let unaddressedSends = 0; // response budget: unaddressed sends counter
@@ -585,9 +586,27 @@ function buildMessageResponse(msg, consumedIds) {
585
586
  } catch (e) { log.debug('task reminder in listen failed:', e.message); }
586
587
 
587
588
  const isSystemMsg = msg.from === '__system__' || msg.system === true;
588
- const nextAction = isSystemMsg
589
- ? 'Process this message, then call listen().'
590
- : `Do what this message asks. When finished, send_message(to="${msg.from}") with what you did and files changed, then call listen().`;
589
+ if (msg.from === '__user__') pendingUserReply = true;
590
+
591
+ // Generate a specific next_action for review requests so reviewers know to read the file
592
+ let nextAction;
593
+ if (isSystemMsg && msg.content) {
594
+ const reviewMatch = msg.content.match(/submit_review\("(rev_[a-z0-9]+)"/);
595
+ const fileMatch = msg.content.match(/read(?:ing)?(?: the)? (?:file )?"([^"]+)"/i) ||
596
+ msg.content.match(/review of "([^"]+)"/i);
597
+ if (reviewMatch) {
598
+ const reviewId = reviewMatch[1];
599
+ const filePath = fileMatch ? fileMatch[1] : null;
600
+ nextAction = filePath
601
+ ? `REVIEW REQUIRED: Read "${filePath}" first, then call submit_review("${reviewId}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Do NOT submit without reading the file.`
602
+ : `REVIEW REQUIRED: Read the relevant files for this review, then call submit_review("${reviewId}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Feedback is required.`;
603
+ }
604
+ }
605
+ if (!nextAction) {
606
+ nextAction = isSystemMsg
607
+ ? 'Process this message, then call listen().'
608
+ : `Do what this message asks. When finished, send_message(to="${msg.from}") with what you did and files changed, then call listen().`;
609
+ }
591
610
 
592
611
  return {
593
612
  success: true,
@@ -1238,7 +1257,7 @@ function buildGuide(level = 'standard') {
1238
1257
  }
1239
1258
  }
1240
1259
 
1241
- if (isLeadRole && aliveCount >= 2) {
1260
+ if (isLeadRole) {
1242
1261
  const coordinatorMode = getConfig().coordinator_mode || 'responsive';
1243
1262
  if (coordinatorMode === 'responsive') {
1244
1263
  rules.push('COORDINATOR: Use consume_messages() to check updates non-blockingly. Do NOT block in listen() — stay responsive to the user.');
@@ -1249,7 +1268,9 @@ function buildGuide(level = 'standard') {
1249
1268
  }
1250
1269
 
1251
1270
  const listenCmd = isManagedMode() ? 'listen()' : (mode === 'group' ? 'listen_group()' : 'listen()');
1252
- rules.push(`After EVERY action, call ${listenCmd}. Never use sleep() or poll with check_messages().`);
1271
+ if (!isLeadRole) {
1272
+ rules.push(`After EVERY action, call ${listenCmd}. Never use sleep() or poll with check_messages().`);
1273
+ }
1253
1274
 
1254
1275
  if (level === 'minimal') {
1255
1276
  rules.push('Lock files before editing (lock_file/unlock_file).');
@@ -1543,6 +1564,15 @@ function toolRegister(name, provider = null, skills = null) {
1543
1564
  nextAction = 'Call get_briefing() to load project context';
1544
1565
  }
1545
1566
 
1567
+ // Lead/coordinator gets role-specific next_action regardless of agent count
1568
+ const myRoleStr = (guide.your_role || '').toLowerCase();
1569
+ if (myRoleStr === 'lead' || myRoleStr === 'manager' || myRoleStr === 'coordinator') {
1570
+ const coordinatorMode = getConfig().coordinator_mode || 'responsive';
1571
+ nextAction = coordinatorMode === 'autonomous'
1572
+ ? 'Call get_briefing() to load project context, then listen() to coordinate your team.'
1573
+ : 'Call get_briefing() to load project context, then consume_messages() to check for pending work.';
1574
+ }
1575
+
1546
1576
  // --- Build the result: next_action FIRST, then context ---
1547
1577
  const result = {
1548
1578
  success: true,
@@ -1925,6 +1955,9 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
1925
1955
  sendsSinceLastListen++;
1926
1956
  if (isGroupMode() && !msg.addressed_to) { unaddressedSends++; }
1927
1957
 
1958
+ // Clear pending user reply flag when agent successfully replies to __user__
1959
+ if (to === '__user__') pendingUserReply = false;
1960
+
1928
1961
  const result = { success: true, messageId: msg.id, from: msg.from, to: msg.to };
1929
1962
 
1930
1963
  // Decision overlap hint: warn if message content overlaps with existing decisions
@@ -2134,104 +2167,11 @@ async function toolWaitForReply(timeoutSeconds = 300, from = null) {
2134
2167
  };
2135
2168
  }
2136
2169
 
2137
- function toolCheckMessages(from = null) {
2138
- if (!registeredName) {
2139
- return { error: 'You must call register() first' };
2140
- }
2141
-
2142
- const unconsumed = getUnconsumedMessages(registeredName, from);
2143
-
2144
- // Rich summary: senders, addressed count, urgency — same as enhanced nudge
2145
- const senders = {};
2146
- let addressedCount = 0;
2147
- for (const m of unconsumed) {
2148
- senders[m.from] = (senders[m.from] || 0) + 1;
2149
- if (m.addressed_to && m.addressed_to.includes(registeredName)) addressedCount++;
2150
- }
2151
-
2152
- // Include pending notification count
2153
- const allNotifs = getNotifications();
2154
- const unreadNotifs = allNotifs.filter(n => !n.read_by.includes(registeredName));
2155
-
2156
- const result = {
2157
- count: unconsumed.length,
2158
- pending_notifications: unreadNotifs.length,
2159
- // Scale fix: return previews not full content — agent gets full content via listen_group()
2160
- messages: unconsumed.map(m => ({
2161
- id: m.id,
2162
- from: m.from,
2163
- preview: m.content.substring(0, 120),
2164
- timestamp: m.timestamp,
2165
- ...(m.addressed_to && { addressed_to: m.addressed_to }),
2166
- })),
2167
- };
2168
-
2169
- if (unconsumed.length > 0) {
2170
- result.senders = senders;
2171
- result.addressed_to_you = addressedCount;
2172
- const latest = unconsumed[unconsumed.length - 1];
2173
- result.preview = `${latest.from}: "${latest.content.substring(0, 80).replace(/\n/g, ' ')}..."`;
2174
- const oldestAge = Math.round((Date.now() - new Date(unconsumed[0].timestamp).getTime()) / 1000);
2175
- result.urgency = oldestAge > 120 ? 'critical' : oldestAge > 30 ? 'urgent' : 'normal';
2176
- result.action_required = 'You have unread messages. Call listen() to receive and process them. Do NOT call check_messages() again — it does not consume messages and you will see the same messages repeatedly.';
2177
- }
2178
-
2179
- return result;
2180
- }
2181
-
2182
- function toolConsumeMessages(from = null, limit = null) {
2183
- if (!registeredName) {
2184
- return { error: 'You must call register() first' };
2185
- }
2186
-
2187
- let unconsumed = getUnconsumedMessages(registeredName, from);
2188
- if (limit && limit > 0 && unconsumed.length > limit) {
2189
- unconsumed = unconsumed.slice(0, limit);
2190
- }
2191
-
2192
- if (unconsumed.length === 0) {
2193
- return { success: true, count: 0, messages: [] };
2194
- }
2195
-
2196
- // Mark all as consumed
2197
- const consumed = getConsumedIds(registeredName);
2198
- for (const msg of unconsumed) {
2199
- consumed.add(msg.id);
2200
- markAsRead(registeredName, msg.id);
2201
- }
2202
- saveConsumedIds(registeredName, consumed);
2203
-
2204
- // Update read offset
2205
- const msgFile = getMessagesFile(currentBranch);
2206
- if (fs.existsSync(msgFile)) {
2207
- lastReadOffset = fs.statSync(msgFile).size;
2208
- }
2209
-
2210
- touchActivity();
2170
+ // toolCheckMessages and toolConsumeMessages removed — dead code.
2171
+ // Routing goes through: case 'messages' → messaging.handlers['check_messages' | 'consume_messages']
2172
+ // Source of truth: agent-bridge/tools/messaging.js
2211
2173
 
2212
- // Count remaining unconsumed after this batch
2213
- const remaining = getUnconsumedMessages(registeredName, null);
2214
-
2215
- const agents = getAgents();
2216
- const agentsOnline = Object.entries(agents).filter(([, info]) => isPidAlive(info.pid, info.last_activity)).length;
2217
-
2218
- return {
2219
- success: true,
2220
- count: unconsumed.length,
2221
- messages: unconsumed.map(m => ({
2222
- id: m.id,
2223
- from: m.from,
2224
- content: m.content,
2225
- timestamp: m.timestamp,
2226
- ...(m.reply_to && { reply_to: m.reply_to }),
2227
- ...(m.thread_id && { thread_id: m.thread_id }),
2228
- ...(m.addressed_to && { addressed_to: m.addressed_to }),
2229
- })),
2230
- remaining: remaining.length,
2231
- agents_online: agentsOnline,
2232
- coordinator_mode: getConfig().coordinator_mode || 'responsive',
2233
- };
2234
- }
2174
+ // toolConsumeMessages removed dead code. See agent-bridge/tools/messaging.js
2235
2175
 
2236
2176
  function toolAckMessage(messageId) {
2237
2177
  if (!registeredName) {
@@ -2282,6 +2222,9 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
2282
2222
  if (newStatus) toolUpdateTask(task_id, newStatus, summary || '');
2283
2223
  }
2284
2224
 
2225
+ // Clear pending user reply flag — warning was shown, agent is now entering the listen loop
2226
+ pendingUserReply = false;
2227
+
2285
2228
  // Auto-detect group/managed mode and delegate to toolListenGroup
2286
2229
  // This prevents agents from calling the "wrong" listen function
2287
2230
  if (isGroupMode() || isManagedMode()) {
@@ -2385,12 +2328,15 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
2385
2328
 
2386
2329
  heartbeatTimer = setInterval(() => { touchHeartbeat(registeredName); }, 15000);
2387
2330
 
2331
+ const listenTimeoutMs = (getConfig().listen_poll_interval || 120) * 1000;
2388
2332
  timer = setTimeout(() => {
2389
2333
  touchActivity();
2390
2334
  autoCompact();
2391
2335
  if (checkMessages()) return;
2392
- setupWatcher();
2393
- }, 300000);
2336
+ // Return cleanly so Claude sees retry:true rather than a client-side MCP timeout
2337
+ setListening(false);
2338
+ done({ retry: true, next_action: 'No messages. Call listen() again immediately.' });
2339
+ }, listenTimeoutMs);
2394
2340
  }
2395
2341
 
2396
2342
  setupWatcher();
@@ -2722,9 +2668,12 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
2722
2668
 
2723
2669
  const consumed = getConsumedIds(registeredName);
2724
2670
 
2725
- // Autonomous mode: cap listen at 30s — agents should use get_work() instead
2726
- const autonomousTimeout = isAutonomousMode() ? SERVER_CONFIG.AUTONOMOUS_LISTEN_MS : null;
2727
- const MAX_LISTEN_MS = 300000; // 5 minutes — MCP has no tool timeout, heartbeat keeps agent alive
2671
+ // Autonomous mode: cap listen at 90s — agents should use get_work() instead
2672
+ // Responsive mode (Stay with me) overrides autonomous timeout — always uses configured listen interval
2673
+ const coordinatorMode = getConfig().coordinator_mode || 'responsive';
2674
+ const autonomousTimeout = (coordinatorMode !== 'responsive' && isAutonomousMode()) ? SERVER_CONFIG.AUTONOMOUS_LISTEN_MS : null;
2675
+ const configuredListenMs = (getConfig().listen_poll_interval || 120) * 1000;
2676
+ const MAX_LISTEN_MS = configuredListenMs; // configurable via dashboard settings (default 2 min)
2728
2677
  const listenStart = Date.now();
2729
2678
 
2730
2679
  // Helper: collect unconsumed messages from all sources (general + channels)
@@ -3065,6 +3014,10 @@ function buildListenGroupResponse(batch, consumed, agentName, listenStart) {
3065
3014
  }
3066
3015
  }
3067
3016
 
3017
+ if (batch.some(m => m.from === '__user__')) {
3018
+ pendingUserReply = true;
3019
+ }
3020
+
3068
3021
  if (isAutonomousMode()) {
3069
3022
  result.next_action = 'Process these messages, then call get_work().';
3070
3023
  } else if (result.should_respond === false) {
@@ -3439,7 +3392,7 @@ function toolUpdateTask(taskId, status, notes = null) {
3439
3392
  task.status = 'in_review';
3440
3393
  task.updated_at = new Date().toISOString();
3441
3394
  saveTasks(tasks);
3442
- broadcastSystemMessage(`[REVIEW GATE] ${registeredName} tried to mark "${task.title}" done but no review exists. Auto-created review ${reviewId}. A reviewer must approve before this task can be completed.`, registeredName);
3395
+ broadcastSystemMessage(`[REVIEW GATE] ${registeredName} tried to mark "${task.title}" done but no review exists. Auto-created review ${reviewId}. To review: (1) read the relevant files for "${task.title}", (2) call submit_review("${reviewId}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Feedback is required.`, registeredName);
3443
3396
  logViolation('review_gate_blocked', registeredName, `Task "${task.title}" (${task.id}) blocked — no approved review. Auto-created ${reviewId}.`);
3444
3397
  touchActivity();
3445
3398
  return {
@@ -4275,12 +4228,14 @@ async function toolGetWork(params = {}) {
4275
4228
  // 9. Truly idle — try role rebalancing before returning
4276
4229
  rebalanceRoles(); // Item 5: check if workload requires role changes
4277
4230
  touchActivity();
4231
+ const config = getConfig();
4232
+ const idleInterval = config.idle_poll_interval || 90;
4278
4233
  const idleResult = {
4279
4234
  type: 'idle',
4280
- next_action: isManagedMode() ? 'Call listen() to wait for work.' : 'Call get_work() again in 30 seconds.',
4235
+ next_action: isManagedMode() ? 'Call listen() to wait for work.' : `Call get_work() again in ${idleInterval} seconds.`,
4281
4236
  instruction: isManagedMode()
4282
4237
  ? 'No work available right now. Call listen() to wait for the manager to assign work or give you the floor.'
4283
- : 'No work available right now. Call get_work() again in 30 seconds.'
4238
+ : `No work available right now. Call get_work() again in ${idleInterval} seconds.`
4284
4239
  };
4285
4240
  // Item 4: warn demoted agents
4286
4241
  const agentRep = getReputation();
@@ -5819,6 +5774,59 @@ function toolListChannels() {
5819
5774
  return { channels: result, your_channels: getAgentChannels(registeredName) };
5820
5775
  }
5821
5776
 
5777
+ // --- Self-healing Watchdog: reclaim tasks from dead/stale agents ---
5778
+ // Specified in GEMINI.md: runs every 60s; scans in_progress tasks.
5779
+ function runSelfHealingWatchdog() {
5780
+ if (!registeredName) return;
5781
+ try {
5782
+ const tasks = getTasks();
5783
+ const agents = getAgents();
5784
+ let changed = false;
5785
+ const now = Date.now();
5786
+ const STALE_THRESHOLD_MS = 300000; // 5 minutes
5787
+
5788
+ for (const task of tasks) {
5789
+ if (task.status !== 'in_progress' || !task.assignee) continue;
5790
+
5791
+ const assignee = agents[task.assignee];
5792
+ let isStale = false;
5793
+
5794
+ if (!assignee) {
5795
+ isStale = true; // Assignee no longer in registry
5796
+ } else {
5797
+ const lastActivity = assignee.last_activity ? new Date(assignee.last_activity).getTime() : 0;
5798
+ const heartbeatStale = now - lastActivity > STALE_THRESHOLD_MS;
5799
+ const pidDead = !isPidAlive(assignee.pid, assignee.last_activity);
5800
+
5801
+ if (pidDead && heartbeatStale) {
5802
+ isStale = true;
5803
+ }
5804
+ }
5805
+
5806
+ if (isStale) {
5807
+ const retryCount = (task.retry_count || 0) + 1;
5808
+ task.retry_count = retryCount;
5809
+ task.updated_at = new Date().toISOString();
5810
+
5811
+ if (retryCount >= 3) {
5812
+ task.status = 'blocked_permanent';
5813
+ task.blocked_reason = `Agent "${task.assignee}" failed 3 times (PID dead + heartbeat stale >5min). Coordinator intervention required.`;
5814
+ broadcastSystemMessage(`⛔ [WATCHDOG: POISON PILL] Task "${task.title}" marked as blocked_permanent after 3 failed attempts by ${task.assignee}. Coordinator intervention required.`, registeredName);
5815
+ } else {
5816
+ const oldAssignee = task.assignee;
5817
+ task.status = 'pending';
5818
+ task.assignee = null;
5819
+ changed = true;
5820
+ broadcastSystemMessage(`↺ [WATCHDOG: RECLAIMED] Task "${task.title}" reclaimed from stale agent "${oldAssignee}" (retry ${retryCount}/3). Reset to pending.`, registeredName);
5821
+ }
5822
+ changed = true;
5823
+ }
5824
+ }
5825
+
5826
+ if (changed) saveTasks(tasks);
5827
+ } catch (e) { log.warn("Self-healing watchdog failed:", e.message); }
5828
+ }
5829
+
5822
5830
  // Auto-escalation: notify team about tasks blocked for >5 minutes
5823
5831
  // Uses task.escalated_at field for cross-process dedup (file-based, not in-memory)
5824
5832
  function escalateBlockedTasks() {
@@ -7586,7 +7594,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7586
7594
  // Escalating listen() enforcement — block tools after too many non-listen calls
7587
7595
  // send_message is exempt so blocked agents can escalate to coordinator before calling listen()
7588
7596
  // messages is exempt (unified query tool — replaces check_messages/consume_messages)
7589
- const listenExemptTools = new Set(['register', 'get_briefing', 'get_guide', 'listen', 'wait_for_reply', 'update_profile', 'list_agents', 'add_rule', 'remove_rule', 'toggle_rule', 'list_rules', 'send_message', 'messages']);
7597
+ // lock_file and unlock_file are safety housekeeping, not comms exempt from the listen counter
7598
+ const listenExemptTools = new Set(['register', 'get_briefing', 'get_guide', 'listen', 'wait_for_reply', 'update_profile', 'list_agents', 'add_rule', 'remove_rule', 'toggle_rule', 'list_rules', 'send_message', 'messages', 'lock_file', 'unlock_file']);
7590
7599
  if (listenExemptTools.has(name)) {
7591
7600
  if (name === 'listen' || name === 'wait_for_reply') {
7592
7601
  consecutiveNonListenCalls = 0;
@@ -7608,7 +7617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7608
7617
 
7609
7618
  if (!isCoordinatorExempt) {
7610
7619
  consecutiveNonListenCalls++;
7611
- if (consecutiveNonListenCalls >= 5) {
7620
+ if (consecutiveNonListenCalls >= 15) {
7612
7621
  const coordinator = (() => {
7613
7622
  try {
7614
7623
  const profs = getProfiles();
@@ -7625,6 +7634,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7625
7634
  `Do NOT skip step 1. Do NOT call any other tool. Start with send_message now.`
7626
7635
  }],
7627
7636
  isError: true,
7637
+ next_action: `Call send_message(to="${coordinator}", content="I was blocked after ${consecutiveNonListenCalls} calls without listen(). I need to call ${name}. Should I proceed?") then immediately call listen().`,
7628
7638
  };
7629
7639
  }
7630
7640
  }
@@ -7832,6 +7842,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7832
7842
  if (last3.length >= 3 && last3.every(c => c.tool === name && c.argsHash === argsHash)) {
7833
7843
  result._stuck_hint = `You have called ${name} 3 times with the same error. Consider: broadcasting for help, trying a different approach, or calling suggest_task() to find other work.`;
7834
7844
  }
7845
+ result.next_action = 'Fix the error above, then call listen() to continue.';
7835
7846
  return {
7836
7847
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
7837
7848
  isError: true,
@@ -7880,18 +7891,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7880
7891
 
7881
7892
  if (isResponsiveCoordinator) {
7882
7893
  // Responsive coordinators must NEVER be told to call listen().
7883
- // Replace any tool-set listen() directive with consume_messages() or nothing.
7884
- if (!result.next_action || /\blisten\(\)/i.test(result.next_action)) {
7885
- try {
7886
- const pending = getUnconsumedMessages(registeredName);
7887
- if (pending.length > 0) {
7888
- result.next_action = `${pending.length} agent update(s) waiting. Call consume_messages() to read them.`;
7889
- } else {
7890
- delete result.next_action;
7891
- }
7892
- } catch {
7893
- delete result.next_action;
7894
+ // Three cases:
7895
+ // 1. No next_action set by tool → inject consume_messages hint if pending, else nothing
7896
+ // 2. Bare listen() directive → replace entirely with coordinator hint
7897
+ // 3. Compound "Do X, then listen()." → strip the listen() tail, keep the lead instruction
7898
+ const na = result.next_action || '';
7899
+ const bareListenRe = /^call listen\(\)/i;
7900
+ const tailListenRe = /,?\s*then call listen\(\)[^.]*\./i;
7901
+ try {
7902
+ const pending = getUnconsumedMessages(registeredName);
7903
+ const pendingHint = pending.length > 0
7904
+ ? `${pending.length} agent update(s) waiting. Call consume_messages() to read them.`
7905
+ : null;
7906
+ if (!na || bareListenRe.test(na)) {
7907
+ // No guidance or bare listen() — replace with coordinator hint or nothing
7908
+ if (pendingHint) result.next_action = pendingHint;
7909
+ else delete result.next_action;
7910
+ } else if (tailListenRe.test(na)) {
7911
+ // Compound instruction ending in "then call listen()" — strip just the listen() tail
7912
+ const stripped = na.replace(tailListenRe, '.').replace(/\.\.$/, '.').trim();
7913
+ result.next_action = pendingHint ? `${stripped} Then: ${pendingHint}` : stripped;
7894
7914
  }
7915
+ // else: next_action has no listen() reference — preserve as-is
7916
+ } catch {
7917
+ if (bareListenRe.test(na)) delete result.next_action;
7895
7918
  }
7896
7919
  } else {
7897
7920
  if (!result.next_action) {
@@ -7911,8 +7934,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7911
7934
  } catch {}
7912
7935
  }
7913
7936
 
7914
- if (consecutiveNonListenCalls >= 3) {
7915
- result.next_action = `WARNING: ${consecutiveNonListenCalls} calls without listen(). Tools BLOCKED at 5. Call listen() NOW.`;
7937
+ if (consecutiveNonListenCalls >= 10) {
7938
+ result.next_action = `WARNING: ${consecutiveNonListenCalls} calls without listen(). Tools BLOCKED at 15. Call listen() NOW.`;
7939
+ }
7940
+
7941
+ // Soft-enforce user reply: remind agent they have an unanswered user message
7942
+ if (pendingUserReply && result.next_action && name !== 'send_message') {
7943
+ result.next_action += " NOTE: You have an unanswered user message — call send_message(to='__user__') before your next listen().";
7916
7944
  }
7917
7945
  }
7918
7946
  }
@@ -8035,9 +8063,19 @@ function autoReclaimDeadSeat() {
8035
8063
  autoReclaimedName = true; // mark as auto-reclaimed so toolRegister() can override it
8036
8064
  registeredToken = agents[bestName].token || '';
8037
8065
  touchHeartbeat(bestName);
8038
- // Start 10s heartbeat interval so the agent stays alive past the first 30s window
8066
+ // Start 10s heartbeat interval; watchdog runs every 60s (6 ticks)
8039
8067
  if (heartbeatInterval) clearInterval(heartbeatInterval);
8040
- heartbeatInterval = setInterval(() => { touchHeartbeat(registeredName); }, 10000);
8068
+ let watchdogTick = 0;
8069
+ heartbeatInterval = setInterval(() => {
8070
+ touchHeartbeat(registeredName);
8071
+ watchdogTick++;
8072
+ if (watchdogTick >= 6) {
8073
+ watchdogTick = 0;
8074
+ runSelfHealingWatchdog();
8075
+ escalateBlockedTasks();
8076
+ triggerStandupIfDue();
8077
+ }
8078
+ }, 10000);
8041
8079
  heartbeatInterval.unref();
8042
8080
  console.error(`[neohive] Auto-reclaimed seat "${bestName}" (previous PID dead)`);
8043
8081
  } catch (e) {
@@ -115,11 +115,13 @@ module.exports = function (ctx) {
115
115
  reviews.push(review);
116
116
  writeJsonFile(REVIEWS_FILE, reviews);
117
117
 
118
- broadcastSystemMessage(`[REVIEW] ${state.registeredName} requests review of "${review.file}": ${review.description || 'No description'}. Call submit_review("${review.id}", "approved"/"changes_requested", "your feedback") to review.`, state.registeredName);
118
+ broadcastSystemMessage(`[REVIEW REQUEST] ${state.registeredName} requests review of "${review.file}": ${review.description || 'No description'}. To review: (1) read the file "${review.file}", (2) call submit_review("${review.id}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Feedback is required and must be substantive.`, state.registeredName);
119
119
  touchActivity();
120
120
  return { success: true, review_id: review.id, file: review.file, next_action: 'Call listen() to wait for the review.' };
121
121
  }
122
122
 
123
+ const REVIEW_FEEDBACK_MIN_LENGTH = 50;
124
+
123
125
  function toolSubmitReview(reviewId, status, feedback) {
124
126
  if (!state.registeredName) return { error: 'You must call register() first' };
125
127
 
@@ -131,6 +133,26 @@ module.exports = function (ctx) {
131
133
  if (!review) return { error: `Review not found: ${reviewId}` };
132
134
  if (review.requested_by === state.registeredName) return { error: 'Cannot review your own code.' };
133
135
 
136
+ // Enforce substantive feedback — rubber-stamping is not allowed
137
+ const feedbackText = (feedback || '').trim();
138
+ if (!feedbackText) {
139
+ return {
140
+ error: `Feedback is required. You must read "${review.file}" and describe what you found before submitting a review.`,
141
+ next_action: `Read the file "${review.file}" first, then call submit_review("${reviewId}", "${status}", "<your findings>").`,
142
+ };
143
+ }
144
+ if (feedbackText.length < REVIEW_FEEDBACK_MIN_LENGTH) {
145
+ return {
146
+ error: `Feedback too short (${feedbackText.length} chars, minimum ${REVIEW_FEEDBACK_MIN_LENGTH}). Describe specific findings — what you read, what issues you found or verified, and why you ${status === 'approved' ? 'approve' : 'request changes'}.`,
147
+ next_action: `Read the file "${review.file}" first, then call submit_review("${reviewId}", "${status}", "<your detailed findings>").`,
148
+ };
149
+ }
150
+
151
+ // Log audit entry for thin approvals (short feedback on an approval)
152
+ if (status === 'approved' && feedbackText.length < 150) {
153
+ logViolation('thin_review', state.registeredName, `Approved "${review.file}" with minimal feedback (${feedbackText.length} chars): "${feedbackText.substring(0, 100)}"`);
154
+ }
155
+
134
156
  review.status = status;
135
157
  review.reviewer = state.registeredName;
136
158
  review.feedback = (feedback || '').substring(0, 2000);
@@ -384,12 +406,12 @@ module.exports = function (ctx) {
384
406
  {
385
407
  name: 'request_review',
386
408
  description: 'Request a code review from the team. Creates a review request and notifies all agents.',
387
- 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'], additionalProperties: false },
409
+ inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'File to review', maxLength: 500 }, description: { type: 'string', description: 'What to focus on in the review', maxLength: 2000 } }, required: ['file_path'], additionalProperties: false },
388
410
  },
389
411
  {
390
412
  name: 'submit_review',
391
- description: 'Submit a code review — approve or request changes with feedback.',
392
- 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'], additionalProperties: false },
413
+ description: 'Submit a code review — approve or request changes. You MUST read the file under review before calling this. Feedback is required (minimum 50 chars) and must describe specific findings — what you read, what issues you found or confirmed. Rubber-stamp approvals are rejected.',
414
+ inputSchema: { type: 'object', properties: { review_id: { type: 'string', description: 'Review ID', maxLength: 50 }, status: { type: 'string', enum: ['approved', 'changes_requested'], description: 'Review result' }, feedback: { type: 'string', description: 'Your findings from reading the file (required, min 50 chars). Describe what you read and what you found — bugs, security issues, correctness, or confirmation that the code is clean.', maxLength: 2000, minLength: 50 } }, required: ['review_id', 'status', 'feedback'], additionalProperties: false },
393
415
  },
394
416
  // Rules
395
417
  {
@@ -398,7 +420,7 @@ module.exports = function (ctx) {
398
420
  inputSchema: {
399
421
  type: 'object',
400
422
  properties: {
401
- text: { type: 'string', description: 'The rule text' },
423
+ text: { type: 'string', description: 'The rule text', maxLength: 2000 },
402
424
  category: { type: 'string', description: 'Rule category: safety, workflow, code-style, communication, custom' },
403
425
  scope: {
404
426
  type: 'object',
@@ -433,7 +455,7 @@ module.exports = function (ctx) {
433
455
  {
434
456
  name: 'log_violation',
435
457
  description: 'Log a workflow rule violation to the audit trail. Used automatically by review gates, or manually to flag issues.',
436
- inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Violation type: review_skipped, push_without_approval, rule_violated, etc.' }, details: { type: 'string', description: 'Description of the violation' } }, required: ['type'], additionalProperties: false },
458
+ inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Violation type: review_skipped, push_without_approval, rule_violated, etc.', maxLength: 100 }, details: { type: 'string', description: 'Description of the violation', maxLength: 2000 } }, required: ['type'], additionalProperties: false },
437
459
  },
438
460
  {
439
461
  name: 'request_push_approval',
@@ -57,6 +57,8 @@ module.exports = function (ctx) {
57
57
  const oldestAge = Math.round((Date.now() - new Date(unconsumed[0].timestamp).getTime()) / 1000);
58
58
  result.urgency = oldestAge > 120 ? 'critical' : oldestAge > 30 ? 'urgent' : 'normal';
59
59
  result.next_action = 'Call listen() to receive and process these messages.';
60
+ } else {
61
+ result.next_action = 'Call listen() to wait for new messages.';
60
62
  }
61
63
 
62
64
  return result;
package/tools/safety.js CHANGED
@@ -48,15 +48,15 @@ module.exports = function (ctx) {
48
48
  if (lock.agent === state.registeredName) { delete locks[fp]; count++; }
49
49
  }
50
50
  writeJsonFile(LOCKS_FILE, locks);
51
- return { success: true, unlocked: count, message: `Unlocked ${count} file(s).` };
51
+ return { success: true, unlocked: count, message: `Unlocked ${count} file(s).`, next_action: 'Call listen() to receive messages.' };
52
52
  }
53
53
 
54
- if (!locks[normalized]) return { success: true, message: 'File was not locked.' };
54
+ if (!locks[normalized]) return { success: true, message: 'File was not locked.', next_action: 'Call listen() to receive messages.' };
55
55
  if (locks[normalized].agent !== state.registeredName) return { error: `File is locked by ${locks[normalized].agent}, not you.` };
56
56
 
57
57
  delete locks[normalized];
58
58
  writeJsonFile(LOCKS_FILE, locks);
59
- return { success: true, file: normalized, message: 'File unlocked.' };
59
+ return { success: true, file: normalized, message: 'File unlocked.', next_action: 'Call listen() to receive messages.' };
60
60
  }
61
61
 
62
62
  // --- Dependencies ---
package/tools/tasks.js CHANGED
@@ -398,9 +398,9 @@ module.exports = function (ctx) {
398
398
  inputSchema: {
399
399
  type: 'object',
400
400
  properties: {
401
- title: { type: 'string', description: 'Short task title' },
402
- description: { type: 'string', description: 'Detailed task description' },
403
- assignee: { type: 'string', description: 'Agent to assign to (optional, auto-assigns with 2 agents)' },
401
+ title: { type: 'string', description: 'Short task title', maxLength: 200 },
402
+ description: { type: 'string', description: 'Detailed task description', maxLength: 5000 },
403
+ assignee: { type: 'string', description: 'Agent to assign to (optional, auto-assigns with 2 agents)', maxLength: 50 },
404
404
  },
405
405
  required: ['title'],
406
406
  additionalProperties: false,
@@ -412,9 +412,9 @@ module.exports = function (ctx) {
412
412
  inputSchema: {
413
413
  type: 'object',
414
414
  properties: {
415
- task_id: { type: 'string', description: 'Task ID to update' },
415
+ task_id: { type: 'string', description: 'Task ID to update', maxLength: 50 },
416
416
  status: { type: 'string', enum: ['pending', 'in_progress', 'in_review', 'done', 'blocked', 'blocked_permanent'], description: 'New status' },
417
- notes: { type: 'string', description: 'Optional progress note' },
417
+ notes: { type: 'string', description: 'Optional progress note', maxLength: 2000 },
418
418
  },
419
419
  required: ['task_id', 'status'],
420
420
  additionalProperties: false,