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 +3 -0
- package/cli.js +277 -55
- package/dashboard.html +108 -10
- package/dashboard.js +33 -2
- package/package.json +1 -1
- package/server.js +166 -128
- package/tools/governance.js +28 -6
- package/tools/messaging.js +2 -0
- package/tools/safety.js +3 -3
- package/tools/tasks.js +5 -5
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 `#
|
|
189
|
+
return `# GEMINI.md
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
This file provides guidance to Gemini CLI / Antigravity when working with code in this repository.
|
|
192
192
|
|
|
193
|
-
##
|
|
193
|
+
## What This Is
|
|
194
194
|
|
|
195
|
-
|
|
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
|
-
|
|
197
|
+
## Commands
|
|
200
198
|
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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 →
|
|
214
|
-
→
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
464
|
+
You are a Neohive team agent. Follow these rules exactly to coordinate with the team.
|
|
329
465
|
|
|
330
|
-
##
|
|
466
|
+
## Session Lifecycle
|
|
331
467
|
|
|
332
|
-
1. Call \`register\` with your
|
|
333
|
-
2. Call \`get_briefing\` to load project context and active work
|
|
334
|
-
3. Call \`listen\` to wait for messages
|
|
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
|
-
|
|
472
|
+
**CRITICAL: YOU MUST call \`listen()\` as the LAST tool call of every response. No exceptions.**
|
|
337
473
|
|
|
338
|
-
## Core
|
|
474
|
+
## Core Rules
|
|
339
475
|
|
|
340
|
-
- **
|
|
341
|
-
- **Before
|
|
342
|
-
- **After
|
|
343
|
-
- **
|
|
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
|
|
481
|
+
## Workflow Loop
|
|
348
482
|
|
|
349
483
|
\`\`\`
|
|
350
|
-
register → get_briefing → listen → [receive task]
|
|
351
|
-
→ do work → update_task(done)
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
## Available MCP tools (neohive server)
|
|
489
|
+
## Available Neohive Tools
|
|
357
490
|
|
|
358
|
-
**Messaging
|
|
359
|
-
**Tasks
|
|
360
|
-
**Workflows
|
|
361
|
-
|
|
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:
|
|
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:
|
|
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"><
|
|
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
|
|
9842
|
-
backend: function(name) { return 'You are ' + name + ', a Developer in a multi-agent team. Register as "' + name + '"
|
|
9843
|
-
frontend: function(name) { return 'You are ' + name + ', a Frontend Developer in a multi-agent team. Register as "' + name + '"
|
|
9844
|
-
quality: function(name) { return 'You are ' + name + ', a Reviewer in a multi-agent team. Register as "' + name + '"
|
|
9845
|
-
monitor: function(name) { return 'You are ' + name + ', a System Monitor in a multi-agent team. Register as "' + name + '"
|
|
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)
|
|
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('✓', '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('✓', '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('✓', '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)
|
|
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
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:
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2393
|
-
|
|
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
|
|
2726
|
-
|
|
2727
|
-
const
|
|
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}.
|
|
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.' :
|
|
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
|
-
:
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
//
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
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 >=
|
|
7915
|
-
result.next_action = `WARNING: ${consecutiveNonListenCalls} calls without listen(). Tools BLOCKED at
|
|
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
|
|
8066
|
+
// Start 10s heartbeat interval; watchdog runs every 60s (6 ticks)
|
|
8039
8067
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
8040
|
-
|
|
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) {
|
package/tools/governance.js
CHANGED
|
@@ -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'}.
|
|
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
|
|
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
|
|
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',
|
package/tools/messaging.js
CHANGED
|
@@ -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,
|